Merge branch 'aureplop/kfet-auth' into aureplop/kfet-auth_cms

+ Move migrations.
+ Update tests to use new url names and new permissions.
This commit is contained in:
Aurélien Delobelle 2017-10-24 19:41:45 +02:00
commit c524da22fe
25 changed files with 3039 additions and 112 deletions

View file

@ -20,7 +20,6 @@ variables:
# psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD
cache:
paths:
- vendor/python
@ -31,12 +30,12 @@ before_script:
- mkdir -p vendor/{python,pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt
- redis-cli config set requirepass $REDIS_PASSWD || true
- pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt
test:
stage: test
script:
- python manage.py test -v3
- python manage.py test

View file

@ -5,17 +5,34 @@ from django.db import migrations, models
from django.conf import settings
from django.utils import timezone
def forwards_func(apps, schema_editor):
def fill_tirage_fields(apps, schema_editor):
"""
Create a `Tirage` to fill new field `tirage` of `Participant`
and `Spectacle` already existing.
"""
Participant = apps.get_model("bda", "Participant")
Spectacle = apps.get_model("bda", "Spectacle")
Tirage = apps.get_model("bda", "Tirage")
db_alias = schema_editor.connection.alias
Tirage.objects.using(db_alias).bulk_create([
Tirage(
id=1,
title="Tirage de test (migration)",
active=False,
ouverture=timezone.now(),
fermeture=timezone.now()),
])
# These querysets only contains instances not linked to any `Tirage`.
participants = Participant.objects.filter(tirage=None)
spectacles = Spectacle.objects.filter(tirage=None)
if not participants.count() and not spectacles.count():
# No need to create a "trash" tirage.
return
tirage = Tirage.objects.create(
title="Tirage de test (migration)",
active=False,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
participants.update(tirage=tirage)
spectacles.update(tirage=tirage)
class Migration(migrations.Migration):
@ -35,22 +52,33 @@ class Migration(migrations.Migration):
('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')),
],
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
migrations.AlterField(
model_name='participant',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
),
# Create fields `spectacle` for `Participant` and `Spectacle` models.
# These fields are not nullable, but we first create them as nullable
# to give a default value for existing instances of these models.
migrations.AddField(
model_name='participant',
name='tirage',
field=models.ForeignKey(default=1, to='bda.Tirage'),
preserve_default=False,
field=models.ForeignKey(to='bda.Tirage', null=True),
),
migrations.AddField(
model_name='spectacle',
name='tirage',
field=models.ForeignKey(default=1, to='bda.Tirage'),
preserve_default=False,
field=models.ForeignKey(to='bda.Tirage', null=True),
),
migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop),
migrations.AlterField(
model_name='participant',
name='tirage',
field=models.ForeignKey(to='bda.Tirage'),
),
migrations.AlterField(
model_name='spectacle',
name='tirage',
field=models.ForeignKey(to='bda.Tirage'),
),
]

View file

@ -782,9 +782,9 @@ def catalogue(request, request_type):
.select_related('location')
.prefetch_related('quote_set')
)
if categories_id:
if categories_id and 0 not in categories_id:
shows_qs = shows_qs.filter(category__id__in=categories_id)
if locations_id:
if locations_id and 0 not in locations_id:
shows_qs = shows_qs.filter(location__id__in=locations_id)
# On convertit les descriptions à envoyer en une liste facilement

View file

@ -45,6 +45,7 @@ RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
BASE_DIR = os.path.dirname(

View file

@ -58,7 +58,7 @@ def autocomplete(request):
)
# Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'):
if getattr(settings, 'LDAP_SERVER_URL', None):
# Fetching
ldap_query = '(&{:s})'.format(''.join(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit)

View file

@ -577,7 +577,7 @@ def export_members(request):
writer = unicodecsv.writer(response)
for profile in CofProfile.objects.filter(is_cof=True).all():
user = profile.user
bits = [profile.id, user.username, user.first_name, user.last_name,
bits = [user.id, user.username, user.first_name, user.last_name,
user.email, profile.phone, profile.occupation,
profile.departement, profile.type_cotiz]
writer.writerow([str(bit) for bit in bits])
@ -596,7 +596,7 @@ def csv_export_mega(filename, qs):
comments = "---".join(
[comment.content for comment in reg.comments.all()])
bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.id,
profile.phone, user.id,
profile.comments if profile.comments else "", comments]
writer.writerow([str(bit) for bit in bits])

View file

@ -14,7 +14,7 @@ from kfet.models import Account
from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME
from .backends import AccountBackend, GenericBackend
from .fields import GroupsField, CorePermissionsField
from .forms import GroupForm, UserGroupForm
from .forms import UserGroupForm
from .middleware import TemporaryAuthMiddleware
from .models import GenericTeamToken, Group, Permission
from .utils import get_kfet_generic_user
@ -85,6 +85,7 @@ class GroupFormTests(TestCase):
content_type=ot_ct, codename='cool')
def test_creation(self):
from .forms import GroupForm
data = {
'name': 'Another Group',
'permissions': [self.kf_perm1.pk],
@ -101,6 +102,7 @@ class GroupFormTests(TestCase):
Non-kfet permissions of Group are kept when the form is submitted.
Regression test for #168.
"""
from .forms import GroupForm
self.kf_group.permissions.add(self.ot_perm)
selected = [self.kf_perm1, self.kf_perm2]

View file

@ -76,7 +76,7 @@ def account_create(request):
queries['users_notcof'].values_list('username', flat=True))
# Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'):
if getattr(settings, 'LDAP_SERVER_URL', None):
# Fetching
ldap_query = '(&{:s})'.format(''.join(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=word)
@ -106,6 +106,7 @@ def account_create(request):
return render(request, "kfet/account_create_autocomplete.html", data)
@teamkfet_required
def account_search(request):
if "q" not in request.GET:
raise Http404

View file

@ -53,7 +53,7 @@
"kfetpage"
],
"owner": [
"root"
"kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@ -83,7 +83,7 @@
"kfetpage"
],
"owner": [
"root"
"kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@ -113,7 +113,7 @@
"kfetpage"
],
"owner": [
"root"
"kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@ -143,7 +143,7 @@
"kfetpage"
],
"owner": [
"root"
"kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@ -173,7 +173,7 @@
"kfetpage"
],
"owner": [
"root"
"kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@ -203,7 +203,7 @@
"kfetpage"
],
"owner": [
"root"
"kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@ -233,7 +233,7 @@
"page"
],
"owner": [
"root"
"kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@ -263,7 +263,7 @@
"kfetpage"
],
"owner": [
"root"
"kfet_genericteam"
],
"expired": false,
"first_published_at": "2017-05-28T04:20:00.000Z",
@ -681,7 +681,7 @@
"fields": {
"created_at": "2017-05-30T04:20:00.000Z",
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"collection": 2,
"title": "K-F\u00eat - Plan d'acc\u00e8s",
@ -694,7 +694,7 @@
"fields": {
"created_at": "2017-05-30T04:20:00.000Z",
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"collection": 2,
"title": "K-F\u00eat - Demande d'autorisation",
@ -707,7 +707,7 @@
"fields": {
"created_at": "2017-05-30T04:20:00.000Z",
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"collection": 2,
"title": "K-F\u00eat - Trait\u00e9 de Flipper Th\u00e9orique",
@ -730,7 +730,7 @@
"title": "K-F\u00eat - Amazon Hunt",
"width": 200,
"uploaded_by_user": [
"root"
"kfet_genericteam"
]
}
},
@ -750,7 +750,7 @@
"title": "K-F\u00eat - Fun Machine",
"width": 200,
"uploaded_by_user": [
"root"
"kfet_genericteam"
]
}
},
@ -767,7 +767,7 @@
"title": "Hugo Manet",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -787,7 +787,7 @@
"title": "Lisa Gourdon",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -807,7 +807,7 @@
"title": "Pierre Quesselaire",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -827,7 +827,7 @@
"title": "Thibault Scoquard",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -847,7 +847,7 @@
"title": "Arnaud Fanthomme",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -867,7 +867,7 @@
"title": "Vincent Balerdi",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -887,7 +887,7 @@
"title": "Nathana\u00ebl Willaime",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -907,7 +907,7 @@
"title": "\u00c9lisabeth Miller",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -927,7 +927,7 @@
"title": "Arthur Lesage",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -947,7 +947,7 @@
"title": "Sarah Asset",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -967,7 +967,7 @@
"title": "Alexandre Legrand",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -987,7 +987,7 @@
"title": "\u00c9tienne Baudel",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1007,7 +1007,7 @@
"title": "Marine Snape",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1027,7 +1027,7 @@
"title": "Anatole Gosset",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1047,7 +1047,7 @@
"title": "Jacko Rastikian",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1067,7 +1067,7 @@
"title": "Alexandre Jannaud",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1087,7 +1087,7 @@
"title": "Aur\u00e9lien Delobelle",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1107,7 +1107,7 @@
"title": "Sylvain Douteau",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1127,7 +1127,7 @@
"title": "Rapha\u00ebl Lescanne",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1147,7 +1147,7 @@
"title": "Romain Gourvil",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1167,7 +1167,7 @@
"title": "Marie Labeye",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1187,7 +1187,7 @@
"title": "Oscar Blumberg",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1207,7 +1207,7 @@
"title": "Za\u00efd Allybokus",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1227,7 +1227,7 @@
"title": "Damien Garreau",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1247,7 +1247,7 @@
"title": "Andr\u00e9a Londono-Lopez",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1267,7 +1267,7 @@
"title": "Tristan Roussel",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1287,7 +1287,7 @@
"title": "Guillaume Vernade",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1307,7 +1307,7 @@
"title": "Lucas Mercier",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1327,7 +1327,7 @@
"title": "Fran\u00e7ois Maillot",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},
@ -1347,7 +1347,7 @@
"title": "Fabrice Catoire",
"collection": 2,
"uploaded_by_user": [
"root"
"kfet_genericteam"
],
"created_at": "2017-05-30T04:20:00.000Z"
},

View file

@ -123,7 +123,6 @@ class UserRestrictTeamForm(UserForm):
fields = ['first_name', 'last_name', 'email']
class AccountNegativeForm(forms.ModelForm):
class Meta:
model = AccountNegative

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0056_change_account_meta'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'), ('can_force_close', 'Fermer manuellement la K-Fêt'), ('see_config', 'Voir la configuration K-Fêt'), ('change_config', 'Modifier la configuration K-Fêt'))},
),
]

View file

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('kfet', '0057_add_perms_config'),
('kfet', '0056_change_account_meta'),
('kfet', '0054_update_promos'),
]

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0059_create_generic'),
]
operations = [
migrations.AlterField(
model_name='supplier',
name='address',
field=models.TextField(verbose_name='adresse', blank=True),
),
migrations.AlterField(
model_name='supplier',
name='articles',
field=models.ManyToManyField(verbose_name='articles vendus', through='kfet.SupplierArticle', related_name='suppliers', to='kfet.Article'),
),
migrations.AlterField(
model_name='supplier',
name='comment',
field=models.TextField(verbose_name='commentaire', blank=True),
),
migrations.AlterField(
model_name='supplier',
name='email',
field=models.EmailField(max_length=254, verbose_name='adresse mail', blank=True),
),
migrations.AlterField(
model_name='supplier',
name='phone',
field=models.CharField(max_length=20, verbose_name='téléphone', blank=True),
),
]

View file

@ -7,7 +7,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0059_create_generic'),
('kfet', '0060_amend_supplier'),
]
operations = [

View file

@ -89,7 +89,7 @@ class Migration(migrations.Migration):
Data migration which performs permissions cleaning.
"""
dependencies = [
('kfet', '0060_change_models_opts'),
('kfet', '0061_change_models_opts'),
('auth', '0006_require_contenttypes_0002'),
('contenttypes', '0002_remove_content_type_name'),
]

View file

@ -96,6 +96,8 @@ class Account(models.Model):
('special_add_account',
"Créer un compte avec une balance initiale"),
('can_force_close', "Fermer manuellement la K-Fêt"),
('see_config', "Voir la configuration K-Fêt"),
('change_config', "Modifier la configuration K-Fêt"),
)
def __str__(self):
@ -581,17 +583,19 @@ class InventoryArticle(models.Model):
self.stock_error = self.stock_new - self.stock_old
super(InventoryArticle, self).save(*args, **kwargs)
@python_2_unicode_compatible
class Supplier(models.Model):
articles = models.ManyToManyField(
Article,
through = 'SupplierArticle',
related_name = "suppliers")
name = models.CharField("nom", max_length = 45)
address = models.TextField("adresse")
email = models.EmailField("adresse mail")
phone = models.CharField("téléphone", max_length = 10)
comment = models.TextField("commentaire")
verbose_name=_("articles vendus"),
through='SupplierArticle',
related_name='suppliers',
)
name = models.CharField(_("nom"), max_length=45)
address = models.TextField(_("adresse"), blank=True)
email = models.EmailField(_("adresse mail"), blank=True)
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
comment = models.TextField(_("commentaire"), blank=True)
class Meta:
verbose_name = _("Fournisseur")
@ -601,6 +605,7 @@ class Supplier(models.Model):
def __str__(self):
return self.name
class SupplierArticle(models.Model):
supplier = models.ForeignKey(
Supplier, on_delete = models.PROTECT)

View file

@ -1,5 +1,6 @@
import json
from datetime import timedelta
from unittest import mock
from django.contrib.auth.models import AnonymousUser, Permission, User
from django.test import Client
@ -118,6 +119,11 @@ class OpenKfetViewsTest(ChannelTestCase):
"""OpenKfet views unit-tests suite."""
def setUp(self):
# Need this (and here) because of '<client>.login' in setUp
patcher_messages = mock.patch('gestioncof.signals.messages')
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
# get some permissions
perms = {
'kfet.is_team': Permission.objects.get(codename='is_team'),
@ -194,7 +200,8 @@ class OpenKfetConsumerTest(ChannelTestCase):
OpenKfetConsumer.group_send('kfet.open.team', {'test': 'plop'})
self.assertIsNone(c.receive())
def test_team_user(self):
@mock.patch('gestioncof.signals.messages')
def test_team_user(self, mock_messages):
"""Team user is added to kfet.open.team group."""
# setup team user and its client
t = User.objects.create_user('team', '', 'team')
@ -224,6 +231,11 @@ class OpenKfetScenarioTest(ChannelTestCase):
"""OpenKfet functionnal tests suite."""
def setUp(self):
# Need this (and here) because of '<client>.login' in setUp
patcher_messages = mock.patch('gestioncof.signals.messages')
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
# anonymous client (for views)
self.c = Client()
# anonymous client (for websockets)

View file

@ -0,0 +1,95 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from gestioncof.models import CofProfile
from ..models import Account
from .testcases import TestCaseMixin
from .utils import (
create_user, create_team, create_root, get_perms, user_add_perms,
)
User = get_user_model()
class UserHelpersTests(TestCaseMixin, TestCase):
def test_create_user(self):
"""create_user creates a basic user and its account."""
u = create_user()
a = u.profile.account_kfet
self.assertInstanceExpected(u, {
'get_full_name': 'first last',
'username': 'user',
})
self.assertFalse(u.user_permissions.exists())
self.assertEqual('000', a.trigramme)
def test_create_team(self):
u = create_team()
a = u.profile.account_kfet
self.assertInstanceExpected(u, {
'get_full_name': 'team member',
'username': 'team',
})
self.assertTrue(u.has_perm('kfet.is_team'))
self.assertEqual('100', a.trigramme)
def test_create_root(self):
u = create_root()
a = u.profile.account_kfet
self.assertInstanceExpected(u, {
'get_full_name': 'super user',
'username': 'root',
'is_superuser': True,
'is_staff': True,
})
self.assertEqual('200', a.trigramme)
class PermHelpersTest(TestCaseMixin, TestCase):
def setUp(self):
cts = ContentType.objects.get_for_models(Account, CofProfile)
self.perm1 = Permission.objects.create(
content_type=cts[Account],
codename='test_perm',
name='Perm for test',
)
self.perm2 = Permission.objects.create(
content_type=cts[CofProfile],
codename='another_test_perm',
name='Another one',
)
self.perm_team = Permission.objects.get(
content_type__app_label='kfet',
codename='is_team',
)
def test_get_perms(self):
perms = get_perms('kfet.test_perm', 'gestioncof.another_test_perm')
self.assertDictEqual(perms, {
'kfet.test_perm': self.perm1,
'gestioncof.another_test_perm': self.perm2,
})
def test_user_add_perms(self):
user = User.objects.create_user(username='user', password='user')
user.user_permissions.add(self.perm1)
user_add_perms(user, ['kfet.is_team', 'gestioncof.another_test_perm'])
self.assertQuerysetEqual(
user.user_permissions.all(),
map(repr, [self.perm1, self.perm2, self.perm_team]),
ordered=False,
)

File diff suppressed because it is too large Load diff

353
kfet/tests/testcases.py Normal file
View file

@ -0,0 +1,353 @@
from unittest import mock
from urllib.parse import parse_qs, urlparse
from django.core.urlresolvers import reverse
from django.http import QueryDict
from django.test import Client
from django.utils import timezone
from django.utils.functional import cached_property
from .utils import create_root, create_team, create_user
class TestCaseMixin:
"""Extends TestCase for kfet application tests."""
def assertForbidden(self, response):
"""
Test that the response (retrieved with a Client) is a denial of access.
The response should verify one of the following:
- its HTTP response code is 403,
- it redirects to the login page with a GET parameter named 'next'
whose value is the url of the requested page.
"""
request = response.wsgi_request
try:
try:
# Is this an HTTP Forbidden response ?
self.assertEqual(response.status_code, 403)
except AssertionError:
# A redirection to the login view is fine too.
# Let's build the login url with the 'next' param on current
# page.
full_path = request.get_full_path()
querystring = QueryDict(mutable=True)
querystring['next'] = full_path
login_url = '/login?' + querystring.urlencode(safe='/')
# We don't focus on what the login view does.
# So don't fetch the redirect.
self.assertRedirects(
response, login_url,
fetch_redirect_response=False,
)
except AssertionError:
raise AssertionError(
"%(http_method)s request at %(path)s should be forbidden for "
"%(username)s user.\n"
"Response isn't 403, nor a redirect to login view. Instead, "
"response code is %(code)d." % {
'http_method': request.method,
'path': request.get_full_path(),
'username': (
"'{}'".format(request.user)
if request.user.is_authenticated()
else 'anonymous'
),
'code': response.status_code,
}
)
def assertForbiddenKfet(self, response, form_ctx='form'):
"""
Test that a response (retrieved with a Client) contains error due to
lack of kfet permissions.
It checks that 'Permission refusée' is present in the non-field errors
of the form of response context at key 'form_ctx', or present in
messages.
This should be used for pages which can be accessed by the kfet team
members, but require additionnal permission(s) to make an operation.
"""
try:
self.assertEqual(response.status_code, 200)
try:
form = response.context[form_ctx]
self.assertIn("Permission refusée", form.non_field_errors())
except (AssertionError, AttributeError, KeyError):
messages = [str(msg) for msg in response.context['messages']]
self.assertIn("Permission refusée", messages)
except AssertionError:
request = response.wsgi_request
raise AssertionError(
"%(http_method)s request at %(path)s should raise an error "
"for %(username)s user.\n"
"Cannot find any errors in non-field errors of form "
"'%(form_ctx)s', nor in messages." % {
'http_method': request.method,
'path': request.get_full_path(),
'username': (
"'%s'" % request.user
if request.user.is_authenticated()
else 'anonymous'
),
'form_ctx': form_ctx,
}
)
def assertInstanceExpected(self, instance, expected):
"""
Test that the values of the attributes and without-argument methods of
'instance' are equal to 'expected' pairs.
"""
for attr, expected_value in expected.items():
value = getattr(instance, attr)
if callable(value):
value = value()
self.assertEqual(value, expected_value)
def assertUrlsEqual(self, actual, expected):
"""
Test that the url 'actual' is as 'expected'.
Arguments:
actual (str): Url to verify.
expected: Two forms are accepted.
* (str): Expected url. Strings equality is checked.
* (dict): Its keys must be attributes of 'urlparse(actual)'.
Equality is checked for each present key, except for
'query' which must be a dict of the expected query string
parameters.
"""
if type(expected) == dict:
parsed = urlparse(actual)
for part, expected_part in expected.items():
if part == 'query':
self.assertDictEqual(
parse_qs(parsed.query),
expected.get('query', {}),
)
else:
self.assertEqual(getattr(parsed, part), expected_part)
else:
self.assertEqual(actual, expected)
class ViewTestCaseMixin(TestCaseMixin):
"""
TestCase extension to ease tests of kfet views.
Urls concerns
-------------
# Basic usage
Attributes:
url_name (str): Name of view under test, as given to 'reverse'
function.
url_args (list, optional): Will be given to 'reverse' call.
url_kwargs (dict, optional): Same.
url_expcted (str): What 'reverse' should return given previous
attributes.
View url can then be accessed at the 'url' attribute.
# Advanced usage
If multiple combinations of url name, args, kwargs can be used for a view,
it is possible to define 'urls_conf' attribute. It must be a list whose
each item is a dict defining arguments for 'reverse' call ('name', 'args',
'kwargs' keys) and its expected result ('expected' key).
The reversed urls can be accessed at the 't_urls' attribute.
Users concerns
--------------
During setup, three users are created with their kfet account:
- 'user': a basic user without any permission, account trigramme: 000,
- 'team': a user with kfet.is_team permission, account trigramme: 100,
- 'root': a superuser, account trigramme: 200.
Their password is their username.
One can create additionnal users with 'get_users_extra' method, or prevent
these 3 users to be created with 'get_users_base' method. See these two
methods for further informations.
By using 'register_user' method, these users can then be accessed at
'users' attribute by their label. Similarly, their kfet account is
registered on 'accounts' attribute.
A user label can be given to 'auth_user' attribute. The related user is
then authenticated on self.client during test setup. Its value defaults to
'None', meaning no user is authenticated.
Automated tests
---------------
# Url reverse
Based on url-related attributes/properties, the test 'test_urls' checks
that expected url is returned by 'reverse' (once with basic url usage and
each for advanced usage).
# Forbidden responses
The 'test_forbidden' test verifies that each user, from labels of
'auth_forbidden' attribute, can't access the url(s), i.e. response should
be a 403, or a redirect to login view.
Tested HTTP requests are given by 'http_methods' attribute. Additional data
can be given by defining an attribute '<method(lowercase)>_data'.
"""
url_name = None
url_expected = None
http_methods = ['GET']
auth_user = None
auth_forbidden = []
def setUp(self):
"""
Warning: Do not forget to call super().setUp() in subclasses.
"""
# Signals handlers on login/logout send messages.
# Due to the way the Django' test Client performs login, this raise an
# error. As workaround, we mock the Django' messages module.
patcher_messages = mock.patch('gestioncof.signals.messages')
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
# A test can mock 'django.utils.timezone.now' and give this as return
# value. E.g. it is useful if the test checks values of 'auto_now' or
# 'auto_now_add' fields.
self.now = timezone.now()
# These attributes register users and accounts instances.
self.users = {}
self.accounts = {}
for label, user in dict(self.users_base, **self.users_extra).items():
self.register_user(label, user)
if self.auth_user:
# The wrapper is a sanity check.
self.assertTrue(
self.client.login(
username=self.auth_user,
password=self.auth_user,
)
)
def tearDown(self):
del self.users_base
del self.users_extra
def get_users_base(self):
"""
Dict of <label: user instance>.
Note: Don't access yourself this property. Use 'users_base' attribute
which cache the returned value from here.
It allows to give functions calls, which creates users instances, as
values here.
"""
# Format desc: username, password, trigramme
return {
# user, user, 000
'user': create_user(),
# team, team, 100
'team': create_team(),
# root, root, 200
'root': create_root(),
}
@cached_property
def users_base(self):
return self.get_users_base()
def get_users_extra(self):
"""
Dict of <label: user instance>.
Note: Don't access yourself this property. Use 'users_base' attribute
which cache the returned value from here.
It allows to give functions calls, which create users instances, as
values here.
"""
return {}
@cached_property
def users_extra(self):
return self.get_users_extra()
def register_user(self, label, user):
self.users[label] = user
if hasattr(user.profile, 'account_kfet'):
self.accounts[label] = user.profile.account_kfet
def get_user(self, label):
if self.auth_user is not None:
return self.auth_user
return self.auth_user_mapping.get(label)
@property
def urls_conf(self):
return [{
'name': self.url_name,
'args': getattr(self, 'url_args', []),
'kwargs': getattr(self, 'url_kwargs', {}),
'expected': self.url_expected,
}]
@property
def t_urls(self):
return [
reverse(
url_conf['name'],
args=url_conf.get('args', []),
kwargs=url_conf.get('kwargs', {}),
)
for url_conf in self.urls_conf]
@property
def url(self):
return self.t_urls[0]
def test_urls(self):
for url, conf in zip(self.t_urls, self.urls_conf):
self.assertEqual(url, conf['expected'])
def test_forbidden(self):
for method in self.http_methods:
for user in self.auth_forbidden:
for url in self.t_urls:
self.check_forbidden(method, url, user)
def check_forbidden(self, method, url, user=None):
method = method.lower()
client = Client()
if user is not None:
client.login(username=user, password=user)
send_request = getattr(client, method)
data = getattr(self, '{}_data'.format(method), {})
r = send_request(url, data)
self.assertForbidden(r)

188
kfet/tests/utils.py Normal file
View file

@ -0,0 +1,188 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from ..models import Account
User = get_user_model()
def _create_user_and_account(user_attrs, account_attrs, perms=None):
"""
Create a user and its account, and assign permissions to this user.
Arguments
user_attrs (dict): User data (first name, last name, password...).
account_attrs (dict): Account data (department, kfet password...).
perms (list of str: 'app.perm'): These permissions will be assigned to
the created user. No permission are assigned by default.
If 'password' is not given in 'user_attrs', username is used as password.
If 'kfet.is_team' is in 'perms' and 'password' is not in 'account_attrs',
the account password is 'kfetpwd_<user pwd>'.
"""
user_pwd = user_attrs.pop('password', user_attrs['username'])
user = User.objects.create(**user_attrs)
user.set_password(user_pwd)
user.save()
account_attrs['cofprofile'] = user.profile
kfet_pwd = account_attrs.pop('password', 'kfetpwd_{}'.format(user_pwd))
account = Account.objects.create(**account_attrs)
if perms is not None:
user = user_add_perms(user, perms)
if 'kfet.is_team' in perms:
account.change_pwd(kfet_pwd)
account.save()
return user
def create_user(username='user', trigramme='000', **kwargs):
"""
Create a user without any permission and its kfet account.
username and trigramme are accepted as arguments (defaults to 'user' and
'000').
user_attrs, account_attrs and perms can be given as keyword arguments to
customize the user and its kfet account.
# Default values
User
* username: user
* password: user
* first_name: first
* last_name: last
* email: mail@user.net
Account
* trigramme: 000
"""
user_attrs = kwargs.setdefault('user_attrs', {})
user_attrs.setdefault('username', username)
user_attrs.setdefault('first_name', 'first')
user_attrs.setdefault('last_name', 'last')
user_attrs.setdefault('email', 'mail@user.net')
account_attrs = kwargs.setdefault('account_attrs', {})
account_attrs.setdefault('trigramme', trigramme)
return _create_user_and_account(**kwargs)
def create_team(username='team', trigramme='100', **kwargs):
"""
Create a user, member of the kfet team, and its kfet account.
username and trigramme are accepted as arguments (defaults to 'team' and
'100').
user_attrs, account_attrs and perms can be given as keyword arguments to
customize the user and its kfet account.
# Default values
User
* username: team
* password: team
* first_name: team
* last_name: member
* email: mail@team.net
Account
* trigramme: 100
* kfet password: kfetpwd_team
"""
user_attrs = kwargs.setdefault('user_attrs', {})
user_attrs.setdefault('username', username)
user_attrs.setdefault('first_name', 'team')
user_attrs.setdefault('last_name', 'member')
user_attrs.setdefault('email', 'mail@team.net')
account_attrs = kwargs.setdefault('account_attrs', {})
account_attrs.setdefault('trigramme', trigramme)
perms = kwargs.setdefault('perms', [])
perms.append('kfet.is_team')
return _create_user_and_account(**kwargs)
def create_root(username='root', trigramme='200', **kwargs):
"""
Create a superuser and its kfet account.
username and trigramme are accepted as arguments (defaults to 'root' and
'200').
user_attrs, account_attrs and perms can be given as keyword arguments to
customize the user and its kfet account.
# Default values
User
* username: root
* password: root
* first_name: super
* last_name: user
* email: mail@root.net
* is_staff, is_superuser: True
Account
* trigramme: 200
* kfet password: kfetpwd_root
"""
user_attrs = kwargs.setdefault('user_attrs', {})
user_attrs.setdefault('username', username)
user_attrs.setdefault('first_name', 'super')
user_attrs.setdefault('last_name', 'user')
user_attrs.setdefault('email', 'mail@root.net')
user_attrs['is_superuser'] = user_attrs['is_staff'] = True
account_attrs = kwargs.setdefault('account_attrs', {})
account_attrs.setdefault('trigramme', trigramme)
return _create_user_and_account(**kwargs)
def get_perms(*labels):
"""Return Permission instances from a list of '<app>.<perm_codename>'."""
perms = {}
for label in set(labels):
app_label, codename = label.split('.', 1)
perms[label] = Permission.objects.get(
content_type__app_label=app_label,
codename=codename,
)
return perms
def user_add_perms(user, perms_labels):
"""
Add perms to a user.
Args:
user (User instance)
perms (list of str 'app.perm_name')
Returns:
The same user (refetched from DB to avoid missing perms)
"""
perms = get_perms(*perms_labels)
user.user_permissions.add(*perms.values())
# If permissions have already been fetched for this user, we need to reload
# it to avoid using of the previous permissions cache.
# https://docs.djangoproject.com/en/1.11/topics/auth/default/#permission-caching
return User.objects.get(pk=user.pk)

View file

@ -174,13 +174,9 @@ urlpatterns = [
# Settings urls
# -----
url(r'^settings/$',
permission_required('kfet.change_settings')
(views.SettingsList.as_view()),
url(r'^settings/$', views.config_list,
name='kfet.settings'),
url(r'^settings/edit$',
permission_required('kfet.change_settings')
(views.SettingsUpdate.as_view()),
url(r'^settings/edit$', views.config_update,
name='kfet.settings.update'),

View file

@ -12,7 +12,7 @@ from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse, reverse_lazy
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User, Permission
from django.http import JsonResponse, Http404
from django.forms import formset_factory
@ -1399,6 +1399,9 @@ class SettingsList(TemplateView):
template_name = 'kfet/settings.html'
config_list = permission_required('kfet.see_config')(SettingsList.as_view())
class SettingsUpdate(SuccessMessageMixin, FormView):
form_class = KFetConfigForm
template_name = 'kfet/settings_update.html'
@ -1407,13 +1410,17 @@ class SettingsUpdate(SuccessMessageMixin, FormView):
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.change_settings'):
if not self.request.user.has_perm('kfet.change_config'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
form.save()
return super().form_valid(form)
config_update = (
permission_required('kfet.change_config')(SettingsUpdate.as_view())
)
# -----
# Transfer views

0
manage.py Normal file → Executable file
View file

View file

@ -26,6 +26,9 @@ python-dateutil
wagtail==1.10.*
wagtailmenus==2.2.*
# Remove this when we switch to Django 1.11
djangorestframework==3.6.4
# This fork enables restore of forms.
# Original project: https://bitbucket.org/tim_heap/django-formset-js
git+https://bitbucket.org/georgema1982/django-formset-js.git#egg=django-formset-js