From 35e8585f4c80d82740a4c5b3e53ed72209f1e016 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 17:44:15 +0200 Subject: [PATCH 01/15] Verbose names in lowercase --- equipment/models.py | 12 ++++++------ event/models.py | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/equipment/models.py b/equipment/models.py index 9233787..6e22edd 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,7 +60,7 @@ 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", diff --git a/event/models.py b/event/models.py index aaad757..6210c81 100644 --- a/event/models.py +++ b/event/models.py @@ -7,11 +7,11 @@ from django.db import models class Event(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" @@ -23,12 +23,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") @@ -40,7 +40,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) @@ -55,7 +55,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( @@ -70,7 +70,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,7 +86,7 @@ class ActivityTag(models.Model): class ActivityTemplate(models.Model): title = models.CharField( - _("Nom de l'activité"), + _("nom de l'activité"), max_length=200, blank=True, null=True, @@ -105,23 +105,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, From 350b0c58be1e5c445a1e96a7ed0626e1defad0db Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 17:46:13 +0200 Subject: [PATCH 02/15] Add begin/end date to activities --- event/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/event/models.py b/event/models.py index 6210c81..a40ba6d 100644 --- a/event/models.py +++ b/event/models.py @@ -156,6 +156,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"}: From 1d07c5e8fd9cb67e446b2bdb116b69dcaaf39bb5 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 17:50:15 +0200 Subject: [PATCH 03/15] Add amount field to EquipmentRemark --- equipment/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/equipment/models.py b/equipment/models.py index 6e22edd..9fd3287 100644 --- a/equipment/models.py +++ b/equipment/models.py @@ -66,6 +66,7 @@ class EquipmentRemark(models.Model): 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() From f1afadda76ea8d83cbd0e2ef382e8f11736ae156 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 17:50:58 +0200 Subject: [PATCH 04/15] Add possibility for endless events --- event/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/event/models.py b/event/models.py index a40ba6d..5b8910f 100644 --- a/event/models.py +++ b/event/models.py @@ -28,7 +28,10 @@ class Event(models.Model): ) description = models.TextField(_('description')) beginning_date = models.DateTimeField(_('date de début')) - ending_date = models.DateTimeField(_('date de fin')) + ending_date = models.DateTimeField( + _('date de fin'), + blank=True + ) class Meta: verbose_name = _("évènement") From a22ab92e87164d3ffcc4333c1ffe3ada04716304 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 18:13:45 +0200 Subject: [PATCH 05/15] Add comment and notification models --- communication/__init__.py | 0 communication/migrations/__init__.py | 0 communication/models.py | 53 ++++++++++++++++++++++++++++ communication/views.py | 3 ++ 4 files changed, 56 insertions(+) create mode 100644 communication/__init__.py create mode 100644 communication/migrations/__init__.py create mode 100644 communication/models.py create mode 100644 communication/views.py diff --git a/communication/__init__.py b/communication/__init__.py new file mode 100644 index 0000000..e69de29 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..25584c9 --- /dev/null +++ b/communication/models.py @@ -0,0 +1,53 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Commentable(models.Model): + pass + + +class Thread(models.Model): + topic = models.OneToOne(Commentable) + + +class Comment(models.Model): + thread = models.ForeignKey(Thread) + parent = models.ForeignKey( + 'Comment', + related_name="replies", + ) + author = models.ForeignKey( + User, + verbose_name=_("auteur"), + editable=False + ) + at = models.DateTimeField( + _("date d'écriture"), + auto_now_add=True + ) + text = models.TextField(_("corps du message")) + + +class Notifying(models.Model): + subscribed = models.ManyToManyField( + User, + verbose_name=_("abonnés"), + related_name="subscribed_to" + ) + + def get_url(self, *args, **kwargs): + pass + + +class Notification(models.Model): + sent_by = models.ForeignKey(Notifying) + about = models.ForeignKey(Notifying) + to = models.ForeignKey( + User, + verbose_name=_("envoyée à") + ) + viewed_at = models.DateTimeField( + _("Vue à"), + blank=True + ) + text = models.TextField(_("corps de la notification")) 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. From 3bbea85b3af39ee439a53196ade6ed6370daf6a2 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 18:14:15 +0200 Subject: [PATCH 06/15] Add commentable property to activities --- event/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/event/models.py b/event/models.py index 5b8910f..186a3db 100644 --- a/event/models.py +++ b/event/models.py @@ -3,9 +3,10 @@ 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 Commentable, Notifying -class Event(models.Model): +class Event(Commentable): title = models.CharField( _("nom de l'évènement"), max_length=200, @@ -87,7 +88,7 @@ class ActivityTag(models.Model): return self.name -class ActivityTemplate(models.Model): +class ActivityTemplate(Commentable): title = models.CharField( _("nom de l'activité"), max_length=200, From 80be26c5b3c9c1c7f9e04d0fd412831bdd8203b4 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 18:14:26 +0200 Subject: [PATCH 07/15] Minor changes --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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 From 26c02df53d52976c31d6fdae22a9bdf5e2fb9105 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 18:37:47 +0200 Subject: [PATCH 08/15] All commentables are notifying --- communication/models.py | 52 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/communication/models.py b/communication/models.py index 25584c9..5ac52a3 100644 --- a/communication/models.py +++ b/communication/models.py @@ -2,32 +2,6 @@ from django.db import models from django.contrib.auth.models import User -class Commentable(models.Model): - pass - - -class Thread(models.Model): - topic = models.OneToOne(Commentable) - - -class Comment(models.Model): - thread = models.ForeignKey(Thread) - parent = models.ForeignKey( - 'Comment', - related_name="replies", - ) - author = models.ForeignKey( - User, - verbose_name=_("auteur"), - editable=False - ) - at = models.DateTimeField( - _("date d'écriture"), - auto_now_add=True - ) - text = models.TextField(_("corps du message")) - - class Notifying(models.Model): subscribed = models.ManyToManyField( User, @@ -51,3 +25,29 @@ class Notification(models.Model): blank=True ) text = models.TextField(_("corps de la notification")) + + +class Commentable(Notifying): + pass + + +class Thread(models.Model): + topic = models.OneToOne(Commentable) + + +class Comment(models.Model): + thread = models.ForeignKey(Thread) + parent = models.ForeignKey( + 'Comment', + related_name="replies", + ) + author = models.ForeignKey( + User, + verbose_name=_("auteur"), + editable=False + ) + at = models.DateTimeField( + _("date d'écriture"), + auto_now_add=True + ) + text = models.TextField(_("corps du message")) From 6f158638bfd0c8d2b7c84d8ed3f4e9cfd71f0fb3 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 15 Jul 2017 21:22:15 +0200 Subject: [PATCH 09/15] Various bugfixes --- communication/apps.py | 4 ++++ communication/models.py | 13 ++++++++++--- evenementiel/settings/common.py | 1 + event/models.py | 5 +++++ 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 communication/apps.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/models.py b/communication/models.py index 5ac52a3..c0d19a0 100644 --- a/communication/models.py +++ b/communication/models.py @@ -1,5 +1,6 @@ from django.db import models from django.contrib.auth.models import User +from django.utils.translation import ugettext_lazy as _ class Notifying(models.Model): @@ -14,8 +15,14 @@ class Notifying(models.Model): class Notification(models.Model): - sent_by = models.ForeignKey(Notifying) - about = models.ForeignKey(Notifying) + sent_by = models.ForeignKey( + Notifying, + related_name="notifs_sent" + ) + about = models.ForeignKey( + Notifying, + related_name="notifs", + ) to = models.ForeignKey( User, verbose_name=_("envoyée à") @@ -32,7 +39,7 @@ class Commentable(Notifying): class Thread(models.Model): - topic = models.OneToOne(Commentable) + topic = models.OneToOneField(Commentable) class Comment(models.Model): diff --git a/evenementiel/settings/common.py b/evenementiel/settings/common.py index 61bf2d6..5d291cb 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/event/models.py b/event/models.py index 186a3db..c1c89ce 100644 --- a/event/models.py +++ b/event/models.py @@ -7,6 +7,11 @@ from communication.models import Commentable, Notifying class Event(Commentable): + commentable_model = models.OneToOneField( + Commentable, + related_name="event_model", + parent_link=True + ) title = models.CharField( _("nom de l'évènement"), max_length=200, From af75c90b84072ff0ebc2971d10bb734529d33bdb Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 17 Jul 2017 17:47:34 +0200 Subject: [PATCH 10/15] Refactor communication models We now use `django-notifications` and `django-contrib-comments` to manage notifications and comments. Subscriptions are managed through the `SubscriptionMixin` model, and can correspond to unique users and to group-like subscriptions. --- communication/models.py | 83 +++++++++++++++++------------------------ event/models.py | 15 +++----- requirements.txt | 2 + 3 files changed, 42 insertions(+), 58 deletions(-) diff --git a/communication/models.py b/communication/models.py index c0d19a0..220d29d 100644 --- a/communication/models.py +++ b/communication/models.py @@ -1,60 +1,47 @@ from django.db import models -from django.contrib.auth.models import User +from django.conf import settings +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 Notifying(models.Model): - subscribed = models.ManyToManyField( - User, - verbose_name=_("abonnés"), - related_name="subscribed_to" + +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 UserSubscription(Subscription): + user = models.ForeignKey(settings.AUTH_USER_MODEL) + is_unsub = models.BooleanField( + _("désinscription"), + default=False ) - def get_url(self, *args, **kwargs): - pass + +class GroupSubscription(Subscription): + group = models.ForeignKey(Group) -class Notification(models.Model): - sent_by = models.ForeignKey( - Notifying, - related_name="notifs_sent" - ) - about = models.ForeignKey( - Notifying, - related_name="notifs", - ) - to = models.ForeignKey( - User, - verbose_name=_("envoyée à") - ) - viewed_at = models.DateTimeField( - _("Vue à"), - blank=True - ) - text = models.TextField(_("corps de la notification")) +class SubscriptionMixin(models.Model): + subscribed_users = GenericRelation(UserSubscription) + subscribed_groups = GenericRelation(GroupSubscription) + def get_unique_users(self): + return self.subscribed_users.filter(is_unsub=False) -class Commentable(Notifying): - pass + def get_group_users(self): + return User.objects.filter(group__in=self.subscribed_groups.all()) + def get_all_subscribers(self): + return (self.get_unique_users().union(self.get_group_users()) + .exclude(is_unsub=True)) -class Thread(models.Model): - topic = models.OneToOneField(Commentable) - - -class Comment(models.Model): - thread = models.ForeignKey(Thread) - parent = models.ForeignKey( - 'Comment', - related_name="replies", - ) - author = models.ForeignKey( - User, - verbose_name=_("auteur"), - editable=False - ) - at = models.DateTimeField( - _("date d'écriture"), - auto_now_add=True - ) - text = models.TextField(_("corps du message")) + class Meta: + abstract = True diff --git a/event/models.py b/event/models.py index c1c89ce..d0e4f94 100644 --- a/event/models.py +++ b/event/models.py @@ -3,15 +3,10 @@ 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 Commentable, Notifying +from communication.models import SubscriptionMixin -class Event(Commentable): - commentable_model = models.OneToOneField( - Commentable, - related_name="event_model", - parent_link=True - ) +class Event(SubscriptionMixin, models.Model): title = models.CharField( _("nom de l'évènement"), max_length=200, @@ -19,7 +14,6 @@ class Event(Commentable): slug = models.SlugField( _('identificateur'), unique=True, - primary_key=True, help_text=_("Seulement des lettres, des chiffres ou" "les caractères '_' ou '-'."), ) @@ -36,7 +30,8 @@ class Event(Commentable): beginning_date = models.DateTimeField(_('date de début')) ending_date = models.DateTimeField( _('date de fin'), - blank=True + blank=True, + null=True, ) class Meta: @@ -93,7 +88,7 @@ class ActivityTag(models.Model): return self.name -class ActivityTemplate(Commentable): +class ActivityTemplate(SubscriptionMixin, models.Model): title = models.CharField( _("nom de l'activité"), max_length=200, diff --git a/requirements.txt b/requirements.txt index 54d9b30..41fb065 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,8 @@ Pillow channels django-bootstrap-form==3.2.1 django-widget-tweaks +django-notifications +django-contrib-comments # Production specific daphne From 19093d01f34277943280100b185f902fa7c47a04 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 18 Jul 2017 13:54:11 +0200 Subject: [PATCH 11/15] Small fixes --- communication/models.py | 19 ++++++++++---- vim | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 vim diff --git a/communication/models.py b/communication/models.py index 220d29d..8a89504 100644 --- a/communication/models.py +++ b/communication/models.py @@ -1,5 +1,4 @@ from django.db import models -from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import ( @@ -16,31 +15,41 @@ class Subscription(models.Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') + class Meta: + abstract = True + class UserSubscription(Subscription): - user = models.ForeignKey(settings.AUTH_USER_MODEL) + user = models.ForeignKey(User) is_unsub = models.BooleanField( _("désinscription"), default=False ) + class Meta: + verbose_name = _("souscription utilisateur") + class GroupSubscription(Subscription): group = models.ForeignKey(Group) + class Meta: + verbose_name = _("souscription en groupe") + class SubscriptionMixin(models.Model): subscribed_users = GenericRelation(UserSubscription) subscribed_groups = GenericRelation(GroupSubscription) - def get_unique_users(self): + def get_manual_subscribers(self): return self.subscribed_users.filter(is_unsub=False) - def get_group_users(self): + def get_subscribers_from_groups(self): return User.objects.filter(group__in=self.subscribed_groups.all()) def get_all_subscribers(self): - return (self.get_unique_users().union(self.get_group_users()) + return (self.get_manual_subscribers() + .union(self.get_subscribers_from_groups()) .exclude(is_unsub=True)) class Meta: diff --git a/vim b/vim new file mode 100644 index 0000000..930386b --- /dev/null +++ b/vim @@ -0,0 +1,56 @@ +diff --git a/communication/models.py b/communication/models.py +index 220d29d..8a89504 100644 +--- a/communication/models.py ++++ b/communication/models.py +@@ -1,5 +1,4 @@ + from django.db import models +-from django.conf import settings + from django.contrib.auth.models import Group + from django.contrib.auth import get_user_model + from django.contrib.contenttypes.fields import ( +@@ -16,31 +15,41 @@ class Subscription(models.Model): + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + ++ class Meta: ++ abstract = True ++ + + class UserSubscription(Subscription): +- user = models.ForeignKey(settings.AUTH_USER_MODEL) ++ user = models.ForeignKey(User) + is_unsub = models.BooleanField( + _("désinscription"), + default=False + ) + ++ class Meta: ++ verbose_name = _("souscription utilisateur") ++ + + class GroupSubscription(Subscription): + group = models.ForeignKey(Group) + ++ class Meta: ++ verbose_name = _("souscription en groupe") ++ + + class SubscriptionMixin(models.Model): + subscribed_users = GenericRelation(UserSubscription) + subscribed_groups = GenericRelation(GroupSubscription) + +- def get_unique_users(self): ++ def get_manual_subscribers(self): + return self.subscribed_users.filter(is_unsub=False) + +- def get_group_users(self): ++ def get_subscribers_from_groups(self): + return User.objects.filter(group__in=self.subscribed_groups.all()) + + def get_all_subscribers(self): +- return (self.get_unique_users().union(self.get_group_users()) ++ return (self.get_manual_subscribers() ++ .union(self.get_subscribers_from_groups()) + .exclude(is_unsub=True)) + + class Meta: From 4e8873226fb9b0ce371b94fd07532c58daef3e3e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 18 Jul 2017 13:55:09 +0200 Subject: [PATCH 12/15] Oops --- vim | 56 -------------------------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 vim diff --git a/vim b/vim deleted file mode 100644 index 930386b..0000000 --- a/vim +++ /dev/null @@ -1,56 +0,0 @@ -diff --git a/communication/models.py b/communication/models.py -index 220d29d..8a89504 100644 ---- a/communication/models.py -+++ b/communication/models.py -@@ -1,5 +1,4 @@ - from django.db import models --from django.conf import settings - from django.contrib.auth.models import Group - from django.contrib.auth import get_user_model - from django.contrib.contenttypes.fields import ( -@@ -16,31 +15,41 @@ class Subscription(models.Model): - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') - -+ class Meta: -+ abstract = True -+ - - class UserSubscription(Subscription): -- user = models.ForeignKey(settings.AUTH_USER_MODEL) -+ user = models.ForeignKey(User) - is_unsub = models.BooleanField( - _("désinscription"), - default=False - ) - -+ class Meta: -+ verbose_name = _("souscription utilisateur") -+ - - class GroupSubscription(Subscription): - group = models.ForeignKey(Group) - -+ class Meta: -+ verbose_name = _("souscription en groupe") -+ - - class SubscriptionMixin(models.Model): - subscribed_users = GenericRelation(UserSubscription) - subscribed_groups = GenericRelation(GroupSubscription) - -- def get_unique_users(self): -+ def get_manual_subscribers(self): - return self.subscribed_users.filter(is_unsub=False) - -- def get_group_users(self): -+ def get_subscribers_from_groups(self): - return User.objects.filter(group__in=self.subscribed_groups.all()) - - def get_all_subscribers(self): -- return (self.get_unique_users().union(self.get_group_users()) -+ return (self.get_manual_subscribers() -+ .union(self.get_subscribers_from_groups()) - .exclude(is_unsub=True)) - - class Meta: From 270e31f12be4a21dbc68a5e5c974105d7d95c0a0 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 18 Jul 2017 15:37:13 +0200 Subject: [PATCH 13/15] adapt migrations and tests --- communication/migrations/0001_initial.py | 46 ++++++++++++++++++++++++ equipment/migrations/0001_initial.py | 15 ++++---- evenementiel/urls.py | 2 +- event/migrations/0001_initial.py | 43 +++++++++++----------- event/tests.py | 23 +++++++----- shared/migrations/__init__.py | 0 6 files changed, 92 insertions(+), 37 deletions(-) create mode 100644 communication/migrations/0001_initial.py delete mode 100644 shared/migrations/__init__.py diff --git a/communication/migrations/0001_initial.py b/communication/migrations/0001_initial.py new file mode 100644 index 0000000..c4e91f1 --- /dev/null +++ b/communication/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-07-18 13:17 +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', + }, + ), + ] diff --git a/equipment/migrations/0001_initial.py b/equipment/migrations/0001_initial.py index ccc9e91..eea7be7 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 13:17 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/evenementiel/urls.py b/evenementiel/urls.py index d83241d..e002332 100644 --- a/evenementiel/urls.py +++ b/evenementiel/urls.py @@ -12,7 +12,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..d5343fb 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 13:17 from __future__ import unicode_literals from django.conf import settings @@ -21,68 +21,71 @@ 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', 'verbose_name_plural': 'tags', + 'verbose_name': 'tag', }, ), migrations.CreateModel( 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é', 'verbose_name_plural': 'templates activité', + 'verbose_name': 'template activité', }, ), 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={ - 'verbose_name': 'évènement', 'verbose_name_plural': 'évènements', + 'verbose_name': 'évènement', }, ), migrations.CreateModel( 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={ - 'verbose_name': 'lieu', 'verbose_name_plural': 'lieux', + 'verbose_name': 'lieu', }, ), migrations.CreateModel( 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é', 'verbose_name_plural': 'activités', + 'verbose_name': 'activité', }, bases=('event.activitytemplate',), ), diff --git a/event/tests.py b/event/tests.py index 38b2add..fbffaba 100644 --- a/event/tests.py +++ b/event/tests.py @@ -44,51 +44,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 +105,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/shared/migrations/__init__.py b/shared/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 From da75dc7d9cd465b4dc94fd8dc82a29980bf46e67 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 18 Jul 2017 17:18:54 +0200 Subject: [PATCH 14/15] Test + small changes --- communication/migrations/0001_initial.py | 10 +++- communication/models.py | 30 +++++++----- communication/tests.py | 58 ++++++++++++++++++++++++ equipment/migrations/0001_initial.py | 2 +- event/migrations/0001_initial.py | 12 ++--- event/tests.py | 4 +- 6 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 communication/tests.py diff --git a/communication/migrations/0001_initial.py b/communication/migrations/0001_initial.py index c4e91f1..f103630 100644 --- a/communication/migrations/0001_initial.py +++ b/communication/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-07-18 13:17 +# Generated by Django 1.11.3 on 2017-07-18 15:12 from __future__ import unicode_literals from django.conf import settings @@ -43,4 +43,12 @@ class Migration(migrations.Migration): '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/models.py b/communication/models.py index 8a89504..fd0ab1b 100644 --- a/communication/models.py +++ b/communication/models.py @@ -28,6 +28,7 @@ class UserSubscription(Subscription): class Meta: verbose_name = _("souscription utilisateur") + unique_together = ("user", "content_type", "object_id") class GroupSubscription(Subscription): @@ -35,22 +36,29 @@ class GroupSubscription(Subscription): class Meta: verbose_name = _("souscription en groupe") + unique_together = ("group", "content_type", "object_id") class SubscriptionMixin(models.Model): - subscribed_users = GenericRelation(UserSubscription) - subscribed_groups = GenericRelation(GroupSubscription) - - def get_manual_subscribers(self): - return self.subscribed_users.filter(is_unsub=False) - - def get_subscribers_from_groups(self): - return User.objects.filter(group__in=self.subscribed_groups.all()) + user_subscriptions = GenericRelation(UserSubscription) + group_subscriptions = GenericRelation(GroupSubscription) def get_all_subscribers(self): - return (self.get_manual_subscribers() - .union(self.get_subscribers_from_groups()) - .exclude(is_unsub=True)) + 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/equipment/migrations/0001_initial.py b/equipment/migrations/0001_initial.py index eea7be7..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.11.3 on 2017-07-18 13:17 +# Generated by Django 1.11.3 on 2017-07-18 15:12 from __future__ import unicode_literals from django.db import migrations, models diff --git a/event/migrations/0001_initial.py b/event/migrations/0001_initial.py index d5343fb..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.11.3 on 2017-07-18 13:17 +# Generated by Django 1.11.3 on 2017-07-18 15:12 from __future__ import unicode_literals from django.conf import settings @@ -26,8 +26,8 @@ class Migration(migrations.Migration): ('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_plural': 'tags', 'verbose_name': 'tag', + 'verbose_name_plural': 'tags', }, ), migrations.CreateModel( @@ -43,8 +43,8 @@ class Migration(migrations.Migration): ('remarks', models.TextField(blank=True, help_text='Visible uniquement par les organisateurs', null=True, verbose_name='remarques')), ], options={ - 'verbose_name_plural': 'templates activité', 'verbose_name': 'template activité', + 'verbose_name_plural': 'templates activité', }, ), migrations.CreateModel( @@ -60,8 +60,8 @@ class Migration(migrations.Migration): ('created_by', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='created_events', to=settings.AUTH_USER_MODEL)), ], options={ - 'verbose_name_plural': 'évènements', 'verbose_name': 'évènement', + 'verbose_name_plural': 'évènements', }, ), migrations.CreateModel( @@ -72,8 +72,8 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True)), ], options={ - 'verbose_name_plural': 'lieux', 'verbose_name': 'lieu', + 'verbose_name_plural': 'lieux', }, ), migrations.CreateModel( @@ -84,8 +84,8 @@ class Migration(migrations.Migration): ('end', models.DateTimeField(verbose_name='heure de fin')), ], options={ - 'verbose_name_plural': 'activités', 'verbose_name': 'activité', + 'verbose_name_plural': 'activités', }, bases=('event.activitytemplate',), ), diff --git a/event/tests.py b/event/tests.py index fbffaba..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 From 5b005571d5ff68228d65678d23a7d1c4edadea6f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 18 Jul 2017 17:31:01 +0200 Subject: [PATCH 15/15] Event ending_date no longer optional --- event/models.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/event/models.py b/event/models.py index d0e4f94..9cee1ee 100644 --- a/event/models.py +++ b/event/models.py @@ -28,11 +28,7 @@ class Event(SubscriptionMixin, models.Model): ) description = models.TextField(_('description')) beginning_date = models.DateTimeField(_('date de début')) - ending_date = models.DateTimeField( - _('date de fin'), - blank=True, - null=True, - ) + ending_date = models.DateTimeField(_('date de fin')) class Meta: verbose_name = _("évènement")