Merge branch 'Aufinal/permissions' into 'master'

Permissions par évènement

See merge request cof-geek/GestionEvenementiel!15
This commit is contained in:
Martin Pepin 2018-05-03 14:34:36 +02:00
commit 3aab76613a
15 changed files with 302 additions and 94 deletions

View file

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*- # -*- 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 __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -13,8 +12,6 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
('auth', '0008_alter_user_username_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
@ -23,8 +20,6 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()), ('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={ options={
'verbose_name': 'souscription en groupe', 'verbose_name': 'souscription en groupe',
@ -37,18 +32,9 @@ class Migration(migrations.Migration):
('object_id', models.PositiveIntegerField()), ('object_id', models.PositiveIntegerField()),
('is_unsub', models.BooleanField(default=False, verbose_name='désinscription')), ('is_unsub', models.BooleanField(default=False, verbose_name='désinscription')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), ('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={ options={
'verbose_name': 'souscription utilisateur', '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')]),
),
] ]

View file

@ -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')]),
),
]

View file

@ -28,10 +28,8 @@ class SubscriptionTest(TestCase):
created_by=cls.root, created_by=cls.root,
created_at=timezone.now(), created_at=timezone.now(),
description="Ceci est un test", description="Ceci est un test",
beginning_date=timezone.now() beginning_date=timezone.now() + timedelta(days=30),
+ timedelta(days=30), ending_date=timezone.now() + timedelta(days=31),
ending_date=timezone.now()
+ timedelta(days=31),
) )
cls.groupsub = GroupSubscription.objects.create( cls.groupsub = GroupSubscription.objects.create(
content_object=cls.event, content_object=cls.event,

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- 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 __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -11,7 +11,6 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('event', '0001_initial'),
] ]
operations = [ operations = [
@ -34,8 +33,6 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.PositiveSmallIntegerField(verbose_name='quantité attribuée')), ('amount', models.PositiveSmallIntegerField(verbose_name='quantité attribuée')),
('remarks', models.TextField(verbose_name="remarques concernant l'attribution")), ('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={ options={
'verbose_name': 'attribution de matériel', 'verbose_name': 'attribution de matériel',
@ -57,14 +54,4 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'remarques sur le matériel', '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'),
),
] ]

View file

@ -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'),
),
]

View file

@ -55,6 +55,7 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'bootstrapform', 'bootstrapform',
'widget_tweaks', 'widget_tweaks',
'guardian',
'api', 'api',
'communication', 'communication',
@ -136,6 +137,11 @@ CHANNEL_LAYERS = {
} }
} }
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'guardian.backends.ObjectPermissionBackend',
)
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [

View file

@ -5,3 +5,6 @@ from django.utils.translation import ugettext_lazy as _
class EventConfig(AppConfig): class EventConfig(AppConfig):
name = 'event' name = 'event'
verbose_name = _("Évènement") verbose_name = _("Évènement")
def ready(self):
from . import signals

View file

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*- # -*- 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 __future__ import unicode_literals
from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -13,7 +12,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('auth', '0008_alter_user_username_max_length'),
] ]
operations = [ 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')), ('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')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='date de création')),
('description', models.TextField(verbose_name='description')), ('description', models.TextField(verbose_name='description')),
('beginning_date', models.DateTimeField(verbose_name='date de début')), ('beginning_date', models.DateTimeField(help_text="date publique de l'évènement", verbose_name='date de début')),
('ending_date', models.DateTimeField(verbose_name='date de fin')), ('ending_date', models.DateTimeField(help_text="date publique de l'évènement", 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')),
], ],
options={ options={
'verbose_name': 'évènement', 'verbose_name': 'évènement',
'verbose_name_plural': 'évènements', '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( migrations.CreateModel(
name='Place', name='Place',
fields=[ fields=[
@ -96,49 +105,4 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'lieux', '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'),
),
] ]

View file

@ -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'),
),
]

View file

@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.exceptions import FieldDoesNotExist, FieldError from django.core.exceptions import FieldDoesNotExist, FieldError
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -65,11 +66,16 @@ class EventSpecificMixin(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=True, null=True, blank=True, null=True,
) )
needs_event_permissions = True
class Meta: class Meta:
abstract = True abstract = True
class EventGroup(EventSpecificMixin, Group):
pass
class Place(EventSpecificMixin, models.Model): class Place(EventSpecificMixin, models.Model):
name = models.CharField( name = models.CharField(
_("nom du lieu"), _("nom du lieu"),
@ -161,12 +167,14 @@ class AbstractActivityTemplate(SubscriptionMixin, models.Model):
verbose_name=_('lieux'), verbose_name=_('lieux'),
blank=True, blank=True,
) )
needs_event_permissions = True
class Meta: class Meta:
abstract = True abstract = True
class ActivityTemplate(AbstractActivityTemplate): class ActivityTemplate(AbstractActivityTemplate):
class Meta: class Meta:
verbose_name = _("template activité") verbose_name = _("template activité")
verbose_name_plural = _("templates activité") verbose_name_plural = _("templates activité")

51
event/signals.py Normal file
View file

@ -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
)

View file

@ -1,10 +1,13 @@
from django.contrib.auth import get_user_model 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.core.exceptions import ValidationError
from django.test import TestCase from django.test import TestCase
from datetime import timedelta from datetime import timedelta
from django.utils import timezone from django.utils import timezone
from .models import Event, ActivityTemplate, Activity, Place, \ from .models import Event, EventGroup, ActivityTemplate, Activity, Place, \
ActivityTag ActivityTag
from guardian.shortcuts import assign_perm
User = get_user_model() User = get_user_model()
@ -24,10 +27,8 @@ class ActivityInheritanceTest(TestCase):
created_by=cls.erkan, created_by=cls.erkan,
created_at=timezone.now(), created_at=timezone.now(),
description="La nuit c'est lol", description="La nuit c'est lol",
beginning_date=timezone.now() beginning_date=timezone.now() + timedelta(days=30),
+ timedelta(days=30), ending_date=timezone.now() + timedelta(days=31),
ending_date=timezone.now()
+ timedelta(days=31),
) )
cls.loge = Place.objects.create(name="Loge 45") cls.loge = Place.objects.create(name="Loge 45")
cls.aqua = Place.objects.create(name="Aquarium") cls.aqua = Place.objects.create(name="Aquarium")
@ -47,8 +48,7 @@ class ActivityInheritanceTest(TestCase):
self.real_act = Activity.objects.create( self.real_act = Activity.objects.create(
parent=self.template_act, parent=self.template_act,
event=self.event, event=self.event,
beginning=timezone.now() beginning=timezone.now() + timedelta(days=30),
+ timedelta(days=30),
end=timezone.now() end=timezone.now()
+ timedelta(days=30) + timedelta(days=30)
+ timedelta(hours=2), + timedelta(hours=2),
@ -155,3 +155,50 @@ class ActivityTagColorTest(TestCase):
) )
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.tag.full_clean() 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))

View file

@ -9,6 +9,7 @@ djangorestframework==3.6.3
drf-nested-routers==0.90.0 drf-nested-routers==0.90.0
django-notifications django-notifications
django-contrib-comments django-contrib-comments
django-guardian
# Production specific # Production specific
daphne daphne

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- 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 from __future__ import unicode_literals
import django.contrib.auth.models import django.contrib.auth.models

View file

@ -15,3 +15,6 @@ class User(AbstractUser):
class Meta: class Meta:
verbose_name = _("utilisateur") verbose_name = _("utilisateur")
verbose_name_plural = _("utilisateurs") verbose_name_plural = _("utilisateurs")