diff --git a/communication/migrations/0001_initial.py b/communication/migrations/0001_initial.py index 70a00de..bed5f49 100644 --- a/communication/migrations/0001_initial.py +++ b/communication/migrations/0001_initial.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-07-21 14:20 +# Generated by Django 1.11.3 on 2017-08-17 12:21 from __future__ import unicode_literals -from django.conf import settings from django.db import migrations, models import django.db.models.deletion @@ -13,8 +12,6 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), - ('auth', '0008_alter_user_username_max_length'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -23,8 +20,6 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('object_id', models.PositiveIntegerField()), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), ], options={ 'verbose_name': 'souscription en groupe', @@ -37,18 +32,9 @@ class Migration(migrations.Migration): ('object_id', models.PositiveIntegerField()), ('is_unsub', models.BooleanField(default=False, verbose_name='désinscription')), ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ 'verbose_name': 'souscription utilisateur', }, ), - migrations.AlterUniqueTogether( - name='usersubscription', - unique_together=set([('user', 'content_type', 'object_id')]), - ), - migrations.AlterUniqueTogether( - name='groupsubscription', - unique_together=set([('group', 'content_type', 'object_id')]), - ), ] diff --git a/communication/migrations/0002_auto_20170817_1221.py b/communication/migrations/0002_auto_20170817_1221.py new file mode 100644 index 0000000..e9dc990 --- /dev/null +++ b/communication/migrations/0002_auto_20170817_1221.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-08-17 12:21 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('communication', '0001_initial'), + ('auth', '0008_alter_user_username_max_length'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='usersubscription', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='groupsubscription', + name='content_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='groupsubscription', + name='group', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group'), + ), + migrations.AlterUniqueTogether( + name='usersubscription', + unique_together=set([('user', 'content_type', 'object_id')]), + ), + migrations.AlterUniqueTogether( + name='groupsubscription', + unique_together=set([('group', 'content_type', 'object_id')]), + ), + ] diff --git a/communication/tests.py b/communication/tests.py index 800eac6..0fce8fd 100644 --- a/communication/tests.py +++ b/communication/tests.py @@ -28,10 +28,8 @@ class SubscriptionTest(TestCase): created_by=cls.root, created_at=timezone.now(), description="Ceci est un test", - beginning_date=timezone.now() - + timedelta(days=30), - ending_date=timezone.now() - + timedelta(days=31), + beginning_date=timezone.now() + timedelta(days=30), + ending_date=timezone.now() + timedelta(days=31), ) cls.groupsub = GroupSubscription.objects.create( content_object=cls.event, @@ -55,4 +53,4 @@ class SubscriptionTest(TestCase): def test_all_subs(self): self.assertSetEqual(set(self.event.get_all_subscribers()), - {self.user_true, self.user_group_true}) + {self.user_true, self.user_group_true}) diff --git a/equipment/migrations/0001_initial.py b/equipment/migrations/0001_initial.py index 5e042ff..b0f6751 100644 --- a/equipment/migrations/0001_initial.py +++ b/equipment/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-08-12 12:47 +# Generated by Django 1.11.3 on 2017-08-17 12:21 from __future__ import unicode_literals from django.db import migrations, models @@ -11,7 +11,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('event', '0001_initial'), ] operations = [ @@ -34,8 +33,6 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('amount', models.PositiveSmallIntegerField(verbose_name='quantité attribuée')), ('remarks', models.TextField(verbose_name="remarques concernant l'attribution")), - ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Activity')), - ('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.Equipment')), ], options={ 'verbose_name': 'attribution de matériel', @@ -57,14 +54,4 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'remarques sur le matériel', }, ), - migrations.AddField( - model_name='equipment', - name='activities', - field=models.ManyToManyField(related_name='equipment', through='equipment.EquipmentAttribution', to='event.Activity'), - ), - migrations.AddField( - model_name='equipment', - name='event', - field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), ] diff --git a/equipment/migrations/0002_auto_20170817_1221.py b/equipment/migrations/0002_auto_20170817_1221.py new file mode 100644 index 0000000..06a612b --- /dev/null +++ b/equipment/migrations/0002_auto_20170817_1221.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-08-17 12:21 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('equipment', '0001_initial'), + ('event', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='equipmentattribution', + name='activity', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Activity'), + ), + migrations.AddField( + model_name='equipmentattribution', + name='equipment', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.Equipment'), + ), + migrations.AddField( + model_name='equipment', + name='activities', + field=models.ManyToManyField(related_name='equipment', through='equipment.EquipmentAttribution', to='event.Activity'), + ), + migrations.AddField( + model_name='equipment', + name='event', + field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + ] diff --git a/evenementiel/settings/common.py b/evenementiel/settings/common.py index e57d499..76c641d 100644 --- a/evenementiel/settings/common.py +++ b/evenementiel/settings/common.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ 'rest_framework', 'bootstrapform', 'widget_tweaks', + 'guardian', 'api', 'communication', @@ -136,6 +137,11 @@ CHANNEL_LAYERS = { } } +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'guardian.backends.ObjectPermissionBackend', +) + # Password validation # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ diff --git a/event/apps.py b/event/apps.py index 6889b59..65853b2 100644 --- a/event/apps.py +++ b/event/apps.py @@ -5,3 +5,6 @@ from django.utils.translation import ugettext_lazy as _ class EventConfig(AppConfig): name = 'event' verbose_name = _("Évènement") + + def ready(self): + from . import signals diff --git a/event/migrations/0001_initial.py b/event/migrations/0001_initial.py index 2c8ea07..010fe37 100644 --- a/event/migrations/0001_initial.py +++ b/event/migrations/0001_initial.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-08-12 12:47 +# Generated by Django 1.11.3 on 2017-08-17 12:21 from __future__ import unicode_literals -from django.conf import settings import django.core.validators from django.db import migrations, models import django.db.models.deletion @@ -13,7 +12,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('auth', '0008_alter_user_username_max_length'), ] operations = [ @@ -74,15 +73,25 @@ class Migration(migrations.Migration): ('slug', models.SlugField(help_text="Seulement des lettres, des chiffres ou les caractères '_' ou '-'.", unique=True, verbose_name='identificateur')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='date de création')), ('description', models.TextField(verbose_name='description')), - ('beginning_date', models.DateTimeField(verbose_name='date de début')), - ('ending_date', models.DateTimeField(verbose_name='date de fin')), - ('created_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='créé par')), + ('beginning_date', models.DateTimeField(help_text="date publique de l'évènement", verbose_name='date de début')), + ('ending_date', models.DateTimeField(help_text="date publique de l'évènement", verbose_name='date de fin')), ], options={ 'verbose_name': 'évènement', 'verbose_name_plural': 'évènements', }, ), + migrations.CreateModel( + name='EventGroup', + fields=[ + ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')), + ('event', models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement')), + ], + options={ + 'abstract': False, + }, + bases=('auth.group', models.Model), + ), migrations.CreateModel( name='Place', fields=[ @@ -96,49 +105,4 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'lieux', }, ), - migrations.AddField( - model_name='activitytemplate', - name='event', - field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), - migrations.AddField( - model_name='activitytemplate', - name='places', - field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), - ), - migrations.AddField( - model_name='activitytemplate', - name='tags', - field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), - ), - migrations.AddField( - model_name='activitytag', - name='event', - field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), - migrations.AddField( - model_name='activity', - name='event', - field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), - ), - migrations.AddField( - model_name='activity', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='event.ActivityTemplate', verbose_name='template'), - ), - migrations.AddField( - model_name='activity', - name='places', - field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), - ), - migrations.AddField( - model_name='activity', - name='staff', - field=models.ManyToManyField(blank=True, related_name='in_perm_activities', to=settings.AUTH_USER_MODEL, verbose_name='permanents'), - ), - migrations.AddField( - model_name='activity', - name='tags', - field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), - ), ] diff --git a/event/migrations/0002_auto_20170817_1221.py b/event/migrations/0002_auto_20170817_1221.py new file mode 100644 index 0000000..7c258f3 --- /dev/null +++ b/event/migrations/0002_auto_20170817_1221.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-08-17 12:21 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('event', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='created_by', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='créé par'), + ), + migrations.AddField( + model_name='activitytemplate', + name='event', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + migrations.AddField( + model_name='activitytemplate', + name='places', + field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), + ), + migrations.AddField( + model_name='activitytemplate', + name='tags', + field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), + ), + migrations.AddField( + model_name='activitytag', + name='event', + field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + migrations.AddField( + model_name='activity', + name='event', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), + ), + migrations.AddField( + model_name='activity', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='event.ActivityTemplate', verbose_name='template'), + ), + migrations.AddField( + model_name='activity', + name='places', + field=models.ManyToManyField(blank=True, to='event.Place', verbose_name='lieux'), + ), + migrations.AddField( + model_name='activity', + name='staff', + field=models.ManyToManyField(blank=True, related_name='in_perm_activities', to=settings.AUTH_USER_MODEL, verbose_name='permanents'), + ), + migrations.AddField( + model_name='activity', + name='tags', + field=models.ManyToManyField(blank=True, to='event.ActivityTag', verbose_name='tags'), + ), + ] diff --git a/event/models.py b/event/models.py index c58b17f..c328688 100644 --- a/event/models.py +++ b/event/models.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -65,11 +66,16 @@ class EventSpecificMixin(models.Model): on_delete=models.CASCADE, blank=True, null=True, ) + needs_event_permissions = True class Meta: abstract = True +class EventGroup(EventSpecificMixin, Group): + pass + + class Place(EventSpecificMixin, models.Model): name = models.CharField( _("nom du lieu"), @@ -161,12 +167,14 @@ class AbstractActivityTemplate(SubscriptionMixin, models.Model): verbose_name=_('lieux'), blank=True, ) + needs_event_permissions = True class Meta: abstract = True class ActivityTemplate(AbstractActivityTemplate): + class Meta: verbose_name = _("template activité") verbose_name_plural = _("templates activité") @@ -219,4 +227,4 @@ class Activity(AbstractActivityTemplate): verbose_name_plural = _("activités") def __str__(self): - return self.get_herited('title') \ No newline at end of file + return self.get_herited('title') diff --git a/event/signals.py b/event/signals.py new file mode 100644 index 0000000..edac92b --- /dev/null +++ b/event/signals.py @@ -0,0 +1,51 @@ +from django.dispatch import receiver +from django.db.models.signals import post_save, post_migrate +from django.apps import apps +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import Permission +from event.models import Event, EventGroup +from guardian.shortcuts import assign_perm + + +@receiver(post_save, sender=Event) +def create_groups_for_event(sender, **kwargs): + event, created = kwargs["instance"], kwargs["created"] + if created: + orgas = EventGroup.objects.create( + name="{}_orgas".format(event.slug), + event=event + ) + + for perm in Permission.objects.filter( + content_type=ContentType.objects.get_for_model(Event), + codename__contains="event_"): + assign_perm(perm.codename, orgas, event) + + EventGroup.objects.create( + name="{}_participants".format(event.slug), + event=event, + ) + + +@receiver(post_migrate) +def create_event_permissions(sender, **kwargs): + + def event_specific_permissions(): + opes = ['Add', 'Change', 'Delete'] + models = [model.__name__.lower() for model in apps.get_models() + if getattr(model, 'needs_event_permissions', False)] + + return [ + ('event_{}_{}'.format(op.lower(), model), + '{} {} for event'.format(op, model)) + for op in opes + for model in models + ] + + content_type = ContentType.objects.get_for_model(Event) + for (code, verbose) in event_specific_permissions(): + Permission.objects.get_or_create( + name=verbose, + content_type=content_type, + codename=code + ) diff --git a/event/tests.py b/event/tests.py index 29c840f..cac64c8 100644 --- a/event/tests.py +++ b/event/tests.py @@ -1,10 +1,13 @@ from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import Permission from django.core.exceptions import ValidationError from django.test import TestCase from datetime import timedelta from django.utils import timezone -from .models import Event, ActivityTemplate, Activity, Place, \ +from .models import Event, EventGroup, ActivityTemplate, Activity, Place, \ ActivityTag +from guardian.shortcuts import assign_perm User = get_user_model() @@ -24,10 +27,8 @@ class ActivityInheritanceTest(TestCase): created_by=cls.erkan, created_at=timezone.now(), description="La nuit c'est lol", - beginning_date=timezone.now() - + timedelta(days=30), - ending_date=timezone.now() - + timedelta(days=31), + beginning_date=timezone.now() + timedelta(days=30), + ending_date=timezone.now() + timedelta(days=31), ) cls.loge = Place.objects.create(name="Loge 45") cls.aqua = Place.objects.create(name="Aquarium") @@ -47,8 +48,7 @@ class ActivityInheritanceTest(TestCase): self.real_act = Activity.objects.create( parent=self.template_act, event=self.event, - beginning=timezone.now() - + timedelta(days=30), + beginning=timezone.now() + timedelta(days=30), end=timezone.now() + timedelta(days=30) + timedelta(hours=2), @@ -155,3 +155,50 @@ class ActivityTagColorTest(TestCase): ) with self.assertRaises(ValidationError): self.tag.full_clean() + + +class EventPermissionTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.user_perm = User.objects.create(username="userperm") + cls.user_noperm = User.objects.create(username="usernoperm") + cls.user_groupperm = User.objects.create(username="usergroupperm") + cls.user_groupnoperm = User.objects.create(username="usergroupnoperm") + cls.root = User.objects.create_superuser( + username="root", + email="toto@toto.io", + password="toto" + ) + cls.event = Event.objects.create( + title="Hackathon", + slug="django", + created_by=cls.root, + description="Le code c'est cool", + beginning_date=timezone.now(), + ending_date=timezone.now() + timedelta(days=1), + ) + + def test_event_groups(self): + groups = EventGroup.objects.filter( + event=self.event + ) + self.assertEqual(groups.count(), 2) + + def test_individual_perms(self): + assign_perm("event_add_place", self.user_perm, self.event) + self.assertTrue(self.user_perm.has_perm("event_add_place", self.event)) + self.assertFalse(self.user_noperm.has_perm("event_add_place", + self.event)) + + def test_group_perms(self): + orgas = EventGroup.objects.get( + name="{}_orgas".format(self.event.slug), + ) + self.user_groupperm.groups.add(orgas) + for perm in Permission.objects.filter( + content_type=ContentType.objects.get_for_model(Event), + codename__contains="event_"): + self.assertTrue(self.user_groupperm.has_perm(perm.codename, + self.event)) + self.assertFalse(self.user_groupnoperm.has_perm(perm.codename, + self.event)) diff --git a/requirements.txt b/requirements.txt index 04679a4..d6a6aba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ djangorestframework==3.6.3 drf-nested-routers==0.90.0 django-notifications django-contrib-comments +django-guardian # Production specific daphne diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index bd0ea70..f283dfb 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-07-23 14:14 +# Generated by Django 1.11.3 on 2017-08-17 12:21 from __future__ import unicode_literals import django.contrib.auth.models diff --git a/users/models.py b/users/models.py index 201983c..5e3448f 100644 --- a/users/models.py +++ b/users/models.py @@ -15,3 +15,6 @@ class User(AbstractUser): class Meta: verbose_name = _("utilisateur") verbose_name_plural = _("utilisateurs") + + +