diff --git a/.gitignore b/.gitignore index c067e72..7116ddc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ __pycache__ venv evenementiel/settings.py .*.swp + +*.pyc diff --git a/shared/migrations/__init__.py b/communication/__init__.py similarity index 100% rename from shared/migrations/__init__.py rename to communication/__init__.py diff --git a/communication/apps.py b/communication/apps.py new file mode 100644 index 0000000..e6c488d --- /dev/null +++ b/communication/apps.py @@ -0,0 +1,4 @@ +from django.apps import AppConfig + +class CommunicationConfig(AppConfig): + name = 'communication' diff --git a/communication/migrations/0001_initial.py b/communication/migrations/0001_initial.py new file mode 100644 index 0000000..f103630 --- /dev/null +++ b/communication/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-07-18 15:12 +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 = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0002_remove_content_type_name'), + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='GroupSubscription', + 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', + }, + ), + migrations.CreateModel( + name='UserSubscription', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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/__init__.py b/communication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/communication/models.py b/communication/models.py new file mode 100644 index 0000000..fd0ab1b --- /dev/null +++ b/communication/models.py @@ -0,0 +1,64 @@ +from django.db import models +from django.contrib.auth.models import Group +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.fields import ( + GenericForeignKey, GenericRelation +) +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext_lazy as _ + +User = get_user_model() + + +class Subscription(models.Model): + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + class Meta: + abstract = True + + +class UserSubscription(Subscription): + user = models.ForeignKey(User) + is_unsub = models.BooleanField( + _("désinscription"), + default=False + ) + + class Meta: + verbose_name = _("souscription utilisateur") + unique_together = ("user", "content_type", "object_id") + + +class GroupSubscription(Subscription): + group = models.ForeignKey(Group) + + class Meta: + verbose_name = _("souscription en groupe") + unique_together = ("group", "content_type", "object_id") + + +class SubscriptionMixin(models.Model): + user_subscriptions = GenericRelation(UserSubscription) + group_subscriptions = GenericRelation(GroupSubscription) + + def get_all_subscribers(self): + subscribed_users = User.objects.filter( + usersubscription__in=self.user_subscriptions.filter(is_unsub=False) + ) + subscribed_groups = Group.objects.filter( + groupsubscription__in=self.group_subscriptions.all() + ) + subscribers_from_groups = User.objects.filter( + groups__in=subscribed_groups, + ).exclude( + usersubscription__in=self.user_subscriptions.filter( + is_unsub=True + ) + ) + + return subscribed_users.union(subscribers_from_groups) + + class Meta: + abstract = True diff --git a/communication/tests.py b/communication/tests.py new file mode 100644 index 0000000..e9a1061 --- /dev/null +++ b/communication/tests.py @@ -0,0 +1,58 @@ +from django.contrib.auth.models import Group +from django.contrib.auth import get_user_model +from django.test import TestCase +from datetime import timedelta +from django.utils import timezone +from .models import (UserSubscription, GroupSubscription) +from event.models import Event + +User = get_user_model() + + +class SubscriptionTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.root = User.objects.create(username='root') + cls.user_true = User.objects.create(username='usertrue') + cls.user_false = User.objects.create(username='userfalse') + cls.user_group_true = User.objects.create(username='usergrouptrue') + cls.user_group_false = User.objects.create(username='usergroupfalse') + + cls.group = Group.objects.create(name="TestGroup") + cls.user_group_true.groups.add(cls.group) + cls.user_group_false.groups.add(cls.group) + + cls.event = Event.objects.create( + title='TestEvent', + slug='test', + created_by=cls.root, + creation_date=timezone.now(), + description="Ceci est un test", + beginning_date=timezone.now() + + timedelta(days=30), + ending_date=timezone.now() + + timedelta(days=31), + ) + cls.groupsub = GroupSubscription.objects.create( + content_object=cls.event, + group=cls.group + ) + cls.groupunsub = UserSubscription.objects.create( + content_object=cls.event, + user=cls.user_group_false, + is_unsub=True + ) + cls.userunsub = UserSubscription.objects.create( + content_object=cls.event, + user=cls.user_false, + is_unsub=True + ) + cls.usersub = UserSubscription.objects.create( + content_object=cls.event, + user=cls.user_true, + is_unsub=False + ) + + def test_all_subs(self): + self.assertSetEqual(set(self.event.get_all_subscribers()), + {self.user_true, self.user_group_true}) diff --git a/communication/views.py b/communication/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/communication/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/equipment/migrations/0001_initial.py b/equipment/migrations/0001_initial.py index ccc9e91..8ffae95 100644 --- a/equipment/migrations/0001_initial.py +++ b/equipment/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-02-21 20:14 +# Generated by Django 1.11.3 on 2017-07-18 15:12 from __future__ import unicode_literals from django.db import migrations, models @@ -19,9 +19,9 @@ class Migration(migrations.Migration): name='AbstractEquipment', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200, verbose_name='Nom du matériel')), - ('stock', models.PositiveSmallIntegerField(verbose_name='Quantité disponible')), - ('description', models.TextField(verbose_name='Description')), + ('name', models.CharField(max_length=200, verbose_name='nom du matériel')), + ('stock', models.PositiveSmallIntegerField(verbose_name='quantité disponible')), + ('description', models.TextField(verbose_name='description')), ], options={ 'verbose_name': 'matériel abstrait', @@ -32,8 +32,8 @@ class Migration(migrations.Migration): name='EquipmentAttribution', fields=[ ('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")), + ('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')), ], options={ @@ -45,7 +45,8 @@ class Migration(migrations.Migration): name='EquipmentRemark', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('remark', models.TextField(verbose_name='Remarque sur le matériel')), + ('remark', models.TextField(verbose_name='remarque sur le matériel')), + ('amount', models.PositiveSmallIntegerField(verbose_name='quantité concernée')), ('is_broken', models.BooleanField()), ('is_lost', models.BooleanField()), ], diff --git a/equipment/models.py b/equipment/models.py index 9233787..9fd3287 100644 --- a/equipment/models.py +++ b/equipment/models.py @@ -5,11 +5,11 @@ from event.models import Event, Activity class AbstractEquipment(models.Model): name = models.CharField( - _("Nom du matériel"), + _("nom du matériel"), max_length=200, ) - stock = models.PositiveSmallIntegerField(_("Quantité disponible")) - description = models.TextField(_("Description")) + stock = models.PositiveSmallIntegerField(_("quantité disponible")) + description = models.TextField(_("description")) activities = models.ManyToManyField( Activity, related_name="equipment", @@ -46,8 +46,8 @@ class TemporaryEquipment(AbstractEquipment): class EquipmentAttribution(models.Model): equipment = models.ForeignKey(AbstractEquipment) activity = models.ForeignKey(Activity) - amount = models.PositiveSmallIntegerField(_("Quantité attribuée")) - remarks = models.TextField(_("Remarques concernant l'attribution")) + amount = models.PositiveSmallIntegerField(_("quantité attribuée")) + remarks = models.TextField(_("remarques concernant l'attribution")) class Meta: verbose_name = _("attribution de matériel") @@ -60,12 +60,13 @@ class EquipmentAttribution(models.Model): class EquipmentRemark(models.Model): - remark = models.TextField(_("Remarque sur le matériel")) + remark = models.TextField(_("remarque sur le matériel")) equipment = models.ForeignKey( AbstractEquipment, related_name="remarks", help_text=_("Matériel concerné par la remarque"), ) + amount = models.PositiveSmallIntegerField(_("quantité concernée")) is_broken = models.BooleanField() is_lost = models.BooleanField() diff --git a/evenementiel/settings/common.py b/evenementiel/settings/common.py index 299894e..fea0ba5 100644 --- a/evenementiel/settings/common.py +++ b/evenementiel/settings/common.py @@ -44,6 +44,7 @@ BASE_DIR = os.path.dirname( INSTALLED_APPS = [ + 'communication.apps.CommunicationConfig', 'equipment.apps.EquipmentConfig', 'event.apps.EventConfig', 'users.apps.UsersConfig', diff --git a/evenementiel/urls.py b/evenementiel/urls.py index 3098985..ad68e7a 100644 --- a/evenementiel/urls.py +++ b/evenementiel/urls.py @@ -13,7 +13,7 @@ urlpatterns = [ url(r'^', include('shared.urls')), ] -if settings.DEBUG: +if 'debug_toolbar' in settings.INSTALLED_APPS: import debug_toolbar urlpatterns += [ url(r'^__debug__/', include(debug_toolbar.urls)), diff --git a/event/migrations/0001_initial.py b/event/migrations/0001_initial.py index 067d32d..ac25a16 100644 --- a/event/migrations/0001_initial.py +++ b/event/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-02-21 20:14 +# Generated by Django 1.11.3 on 2017-07-18 15:12 from __future__ import unicode_literals from django.conf import settings @@ -21,9 +21,9 @@ class Migration(migrations.Migration): name='ActivityTag', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200, verbose_name='Nom du tag')), + ('name', models.CharField(max_length=200, verbose_name='nom du tag')), ('is_public', models.BooleanField(help_text="Sert à faire une distinction dans l'affichage selon que cela soit destiné au public ou à l'équipe organisatrice")), - ('color', models.CharField(help_text='Rentrer une couleur en hexadécimal', max_length=7, validators=[django.core.validators.RegexValidator(message="La chaîne de caractère rentrée n'est pasune couleur en hexadécimal.", regex='^#(?:[0-9a-fA-F]{3}){1,2}$')], verbose_name='Couleur')), + ('color', models.CharField(help_text='Rentrer une couleur en hexadécimal', max_length=7, validators=[django.core.validators.RegexValidator(message="La chaîne de caractère rentrée n'est pas une couleur en hexadécimal.", regex='^#(?:[0-9a-fA-F]{3}){1,2}$')], verbose_name='couleur')), ], options={ 'verbose_name': 'tag', @@ -34,13 +34,13 @@ class Migration(migrations.Migration): name='ActivityTemplate', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(blank=True, max_length=200, null=True, verbose_name="Nom de l'activité")), + ('title', models.CharField(blank=True, max_length=200, null=True, verbose_name="nom de l'activité")), ('is_public', models.NullBooleanField()), ('has_perm', models.NullBooleanField()), - ('min_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Nombre minimum de permanents')), - ('max_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Nombre maximum de permanents')), - ('description', models.TextField(blank=True, help_text='Public, Visible par tout le monde.', null=True, verbose_name='Description')), - ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs', null=True, verbose_name='Remarques')), + ('min_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='nombre minimum de permanents')), + ('max_perm', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='nombre maximum de permanents')), + ('description', models.TextField(blank=True, help_text='Public, Visible par tout le monde.', null=True, verbose_name='description')), + ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs', null=True, verbose_name='remarques')), ], options={ 'verbose_name': 'template activité', @@ -50,12 +50,13 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Event', fields=[ - ('title', models.CharField(max_length=200, verbose_name="Nom de l'évènement")), - ('slug', models.SlugField(help_text="Seulement des lettres, des chiffres oules caractères '_' ou '-'.", primary_key=True, serialize=False, unique=True, verbose_name='Identificateur')), - ('creation_date', 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')), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, verbose_name="nom de l'évènement")), + ('slug', models.SlugField(help_text="Seulement des lettres, des chiffres oules caractères '_' ou '-'.", unique=True, verbose_name='identificateur')), + ('creation_date', 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(blank=True, null=True, verbose_name='date de fin')), ('created_by', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL)), ], options={ @@ -67,7 +68,7 @@ class Migration(migrations.Migration): name='Place', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200, verbose_name='Nom du lieu')), + ('name', models.CharField(max_length=200, verbose_name='nom du lieu')), ('description', models.TextField(blank=True)), ], options={ @@ -79,6 +80,8 @@ class Migration(migrations.Migration): name='Activity', fields=[ ('activitytemplate_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='event.ActivityTemplate')), + ('beginning', models.DateTimeField(verbose_name='heure de début')), + ('end', models.DateTimeField(verbose_name='heure de fin')), ], options={ 'verbose_name': 'activité', diff --git a/event/models.py b/event/models.py index 44f17bb..988b22d 100644 --- a/event/models.py +++ b/event/models.py @@ -3,19 +3,19 @@ from django.utils.translation import ugettext_lazy as _ from django.core.validators import RegexValidator from django.core.exceptions import FieldError from django.db import models +from communication.models import SubscriptionMixin User = get_user_model() -class Event(models.Model): +class Event(SubscriptionMixin, models.Model): title = models.CharField( - _("Nom de l'évènement"), + _("nom de l'évènement"), max_length=200, ) slug = models.SlugField( - _('Identificateur'), + _('identificateur'), unique=True, - primary_key=True, help_text=_("Seulement des lettres, des chiffres ou" "les caractères '_' ou '-'."), ) @@ -25,12 +25,12 @@ class Event(models.Model): editable=False, ) creation_date = models.DateTimeField( - _('Date de création'), + _('date de création'), auto_now_add=True, ) - description = models.TextField(_('Description')) - beginning_date = models.DateTimeField(_('Date de début')) - ending_date = models.DateTimeField(_('Date de fin')) + description = models.TextField(_('description')) + beginning_date = models.DateTimeField(_('date de début')) + ending_date = models.DateTimeField(_('date de fin')) class Meta: verbose_name = _("évènement") @@ -42,7 +42,7 @@ class Event(models.Model): class Place(models.Model): name = models.CharField( - _("Nom du lieu"), + _("nom du lieu"), max_length=200, ) description = models.TextField(blank=True) @@ -57,7 +57,7 @@ class Place(models.Model): class ActivityTag(models.Model): name = models.CharField( - _("Nom du tag"), + _("nom du tag"), max_length=200, ) is_public = models.BooleanField( @@ -72,7 +72,7 @@ class ActivityTag(models.Model): " une couleur en hexadécimal."), ) color = models.CharField( - _('Couleur'), + _('couleur'), max_length=7, validators=[color_regex], help_text=_("Rentrer une couleur en hexadécimal"), @@ -86,9 +86,9 @@ class ActivityTag(models.Model): return self.name -class ActivityTemplate(models.Model): +class ActivityTemplate(SubscriptionMixin, models.Model): title = models.CharField( - _("Nom de l'activité"), + _("nom de l'activité"), max_length=200, blank=True, null=True, @@ -107,23 +107,23 @@ class ActivityTemplate(models.Model): blank=True, ) min_perm = models.PositiveSmallIntegerField( - _('Nombre minimum de permanents'), + _('nombre minimum de permanents'), blank=True, null=True, ) max_perm = models.PositiveSmallIntegerField( - _('Nombre maximum de permanents'), + _('nombre maximum de permanents'), blank=True, null=True, ) description = models.TextField( - _('Description'), + _('description'), help_text=_("Public, Visible par tout le monde."), blank=True, null=True, ) remarks = models.TextField( - _('Remarques'), + _('remarques'), help_text=_("Visible uniquement par les organisateurs"), blank=True, null=True, @@ -158,6 +158,9 @@ class Activity(ActivityTemplate): blank=True, ) + beginning = models.DateTimeField(_("heure de début")) + end = models.DateTimeField(_("heure de fin")) + def get_herited(self, attrname): attr = super(Activity, self).__getattribute__(attrname) if attrname in {"parent", "staff", "equipment"}: diff --git a/event/tests.py b/event/tests.py index 38b2add..7185054 100644 --- a/event/tests.py +++ b/event/tests.py @@ -1,4 +1,4 @@ -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.test import TestCase from datetime import timedelta @@ -6,6 +6,8 @@ from django.utils import timezone from .models import Event, ActivityTemplate, Activity, Place, \ ActivityTag +User = get_user_model() + class ActivityInheritanceTest(TestCase): @classmethod @@ -44,51 +46,56 @@ class ActivityInheritanceTest(TestCase): self.template_act = ActivityTemplate.objects.create() self.real_act = Activity.objects.create( parent=self.template_act, + beginning=timezone.now() + + timedelta(days=30), + end=timezone.now() + + timedelta(days=30) + + timedelta(hours=2), ) - def test_inherites_title(self): + def test_inherits_title(self): self.template_act.title = "parent" self.assertEqual(self.real_act.get_herited('title'), "parent") self.real_act.title = "enfant" self.assertEqual(self.real_act.get_herited('title'), "enfant") - def test_inherites_description(self): + def test_inherits_description(self): self.template_act.description = "parent" self.assertEqual(self.real_act.get_herited('description'), "parent") self.real_act.description = "enfant" self.assertEqual(self.real_act.get_herited('description'), "enfant") - def test_inherites_remarks(self): + def test_inherits_remarks(self): self.template_act.remarks = "parent" self.assertEqual(self.real_act.get_herited('remarks'), "parent") self.real_act.remarks = "enfant" self.assertEqual(self.real_act.get_herited('remarks'), "enfant") - def test_inherites_is_public(self): + def test_inherits_is_public(self): self.template_act.is_public = True self.assertEqual(self.real_act.get_herited('is_public'), True) self.real_act.is_public = False self.assertEqual(self.real_act.get_herited('is_public'), False) - def test_inherites_has_perm(self): + def test_inherits_has_perm(self): self.template_act.has_perm = True self.assertEqual(self.real_act.get_herited('has_perm'), True) self.real_act.has_perm = False self.assertEqual(self.real_act.get_herited('has_perm'), False) - def test_inherites_min_perm(self): + def test_inherits_min_perm(self): self.template_act.min_perm = 42 self.assertEqual(self.real_act.get_herited('min_perm'), 42) self.real_act.min_perm = 1 self.assertEqual(self.real_act.get_herited('min_perm'), 1) - def test_inherites_max_perm(self): + def test_inherits_max_perm(self): self.template_act.max_perm = 42 self.assertEqual(self.real_act.get_herited('max_perm'), 42) self.real_act.max_perm = 1 self.assertEqual(self.real_act.get_herited('max_perm'), 1) - def test_inherites_place(self): + def test_inherits_place(self): self.template_act.place.add(self.loge) self.assertEqual( self.real_act.get_herited('place').get(), @@ -100,7 +107,7 @@ class ActivityInheritanceTest(TestCase): self.aqua ) - def test_inherites_tags(self): + def test_inherits_tags(self): self.template_act.tags.add(self.tag_foo) self.assertEqual( self.real_act.get_herited('tags').get(), diff --git a/requirements.txt b/requirements.txt index 9313a53..7357c33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,8 @@ channels django-bootstrap-form==3.2.1 django-widget-tweaks djangorestframework==3.6.3 +django-notifications +django-contrib-comments # Production specific daphne