diff --git a/com/migrations/0008_alter_news_options_alter_newsdate_options_and_more.py b/com/migrations/0008_alter_news_options_alter_newsdate_options_and_more.py new file mode 100644 index 00000000..98b09b65 --- /dev/null +++ b/com/migrations/0008_alter_news_options_alter_newsdate_options_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.17 on 2025-01-06 21:52 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("com", "0007_alter_news_club_alter_news_content_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="news", + options={ + "verbose_name": "news", + "permissions": [ + ("moderate_news", "Can moderate news"), + ("view_unmoderated_news", "Can view non-moderated news"), + ], + }, + ), + migrations.AlterModelOptions( + name="newsdate", + options={"verbose_name": "news date", "verbose_name_plural": "news dates"}, + ), + migrations.AlterModelOptions( + name="poster", + options={"permissions": [("moderate_poster", "Can moderate poster")]}, + ), + migrations.RemoveField(model_name="news", name="type"), + migrations.AlterField( + model_name="news", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="owned_news", + to=settings.AUTH_USER_MODEL, + verbose_name="author", + ), + ), + migrations.AlterField( + model_name="newsdate", + name="end_date", + field=models.DateTimeField(verbose_name="end_date"), + ), + migrations.AlterField( + model_name="newsdate", + name="start_date", + field=models.DateTimeField(verbose_name="start_date"), + ), + migrations.AddConstraint( + model_name="newsdate", + constraint=models.CheckConstraint( + check=models.Q(("end_date__gte", models.F("start_date"))), + name="news_date_end_date_after_start_date", + ), + ), + ] diff --git a/com/models.py b/com/models.py index 633c7671..dd5c5bea 100644 --- a/com/models.py +++ b/com/models.py @@ -27,7 +27,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives from django.db import models, transaction -from django.db.models import Q +from django.db.models import F, Q from django.shortcuts import render from django.templatetags.static import static from django.urls import reverse @@ -54,12 +54,9 @@ class Sith(models.Model): return user.is_com_admin -NEWS_TYPES = [ - ("NOTICE", _("Notice")), - ("EVENT", _("Event")), - ("WEEKLY", _("Weekly")), - ("CALL", _("Call")), -] +class NewsQuerySet(models.QuerySet): + def moderated(self): + return self.filter(is_moderated=True) class News(models.Model): @@ -79,9 +76,6 @@ class News(models.Model): default="", help_text=_("A more detailed and exhaustive description of the event."), ) - type = models.CharField( - _("type"), max_length=16, choices=NEWS_TYPES, default="EVENT" - ) club = models.ForeignKey( Club, related_name="news", @@ -93,7 +87,7 @@ class News(models.Model): User, related_name="owned_news", verbose_name=_("author"), - on_delete=models.CASCADE, + on_delete=models.PROTECT, ) is_moderated = models.BooleanField(_("is moderated"), default=False) moderator = models.ForeignKey( @@ -104,19 +98,27 @@ class News(models.Model): on_delete=models.SET_NULL, ) + objects = NewsQuerySet.as_manager() + + class Meta: + verbose_name = _("news") + permissions = [ + ("moderate_news", "Can moderate news"), + ("view_unmoderated_news", "Can view non-moderated news"), + ] + def __str__(self): - return "%s: %s" % (self.type, self.title) + return self.title def save(self, *args, **kwargs): super().save(*args, **kwargs) + if self.is_moderated: + return for user in User.objects.filter( groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID] ): Notification.objects.create( - user=user, - url=reverse("com:news_admin_list"), - type="NEWS_MODERATION", - param="1", + user=user, url=reverse("com:news_admin_list"), type="NEWS_MODERATION" ) def get_absolute_url(self): @@ -138,27 +140,21 @@ class News(models.Model): def news_notification_callback(notif): - count = ( - News.objects.filter( - Q(dates__start_date__gt=timezone.now(), is_moderated=False) - | Q(type="NOTICE", is_moderated=False) - ) - .distinct() - .count() - ) + count = News.objects.filter( + dates__start_date__gt=timezone.now(), is_moderated=False + ).count() if count: notif.viewed = False - notif.param = "%s" % count + notif.param = str(count) notif.date = timezone.now() else: notif.viewed = True class NewsDate(models.Model): - """A date class, useful for weekly events, or for events that just have no date. + """A date associated with news. - This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since - we don't have to make copies + A [News][] can have multiple dates, for example if it is a recurring event. """ news = models.ForeignKey( @@ -167,11 +163,21 @@ class NewsDate(models.Model): verbose_name=_("news_date"), on_delete=models.CASCADE, ) - start_date = models.DateTimeField(_("start_date"), null=True, blank=True) - end_date = models.DateTimeField(_("end_date"), null=True, blank=True) + start_date = models.DateTimeField(_("start_date")) + end_date = models.DateTimeField(_("end_date")) + + class Meta: + verbose_name = _("news date") + verbose_name_plural = _("news dates") + constraints = [ + models.CheckConstraint( + check=Q(end_date__gte=F("start_date")), + name="news_date_end_date_after_start_date", + ) + ] def __str__(self): - return "%s: %s - %s" % (self.news.title, self.start_date, self.end_date) + return f"{self.news.title}: {self.start_date} - {self.end_date}" class Weekmail(models.Model): @@ -330,6 +336,9 @@ class Poster(models.Model): on_delete=models.CASCADE, ) + class Meta: + permissions = [("moderate_poster", _("Can moderate poster"))] + def __str__(self): return self.name diff --git a/com/templates/com/news_admin_list.jinja b/com/templates/com/news_admin_list.jinja index 3214ffd5..3884cfc7 100644 --- a/com/templates/com/news_admin_list.jinja +++ b/com/templates/com/news_admin_list.jinja @@ -10,78 +10,13 @@

{% trans %}Create news{% endtrans %}

-
-

{% trans %}Notices{% endtrans %}

- {% set notices = object_list.filter(type="NOTICE").distinct().order_by('id') %} -
{% trans %}Displayed notices{% endtrans %}
- - - - - - - - - - - - - - {% for news in notices.filter(is_moderated=True) %} - - - - - - - - - - {% endfor %} - -
{% trans %}Type{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}Summary{% endtrans %}{% trans %}Club{% endtrans %}{% trans %}Author{% endtrans %}{% trans %}Moderator{% endtrans %}{% trans %}Actions{% endtrans %}
{{ news.get_type_display() }}{{ news.title }}{{ news.summary|markdown }}{{ news.club }}{{ user_profile_link(news.author) }}{{ user_profile_link(news.moderator) }}{% trans %}View{% endtrans %} - {% trans %}Edit{% endtrans %} - {% trans %}Remove{% endtrans %} - {% trans %}Delete{% endtrans %} -
-
{% trans %}Notices to moderate{% endtrans %}
- - - - - - - - - - - - - {% for news in notices.filter(is_moderated=False) %} - - - - - - - - - {% endfor %} - -
{% trans %}Type{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}Summary{% endtrans %}{% trans %}Club{% endtrans %}{% trans %}Author{% endtrans %}{% trans %}Actions{% endtrans %}
{{ news.get_type_display() }}{{ news.title }}{{ news.summary|markdown }}{{ news.club }}{{ user_profile_link(news.author) }}{% trans %}View{% endtrans %} - {% trans %}Edit{% endtrans %} - {% trans %}Moderate{% endtrans %} - {% trans %}Delete{% endtrans %} -
-

{% trans %}Weeklies{% endtrans %}

- {% set weeklies = object_list.filter(type="WEEKLY", dates__end_date__gte=timezone.now()).distinct().order_by('id') %} + {% set weeklies = object_list.filter(dates__end_date__gte=timezone.now()).distinct().order_by('id') %}
{% trans %}Displayed weeklies{% endtrans %}
- @@ -94,7 +29,6 @@ {% for news in weeklies.filter(is_moderated=True) %} - @@ -124,7 +58,6 @@
{% trans %}Type{% endtrans %} {% trans %}Title{% endtrans %} {% trans %}Summary{% endtrans %} {% trans %}Club{% endtrans %}
{{ news.get_type_display() }} {{ news.title }} {{ news.summary|markdown }} {{ news.club }}
- @@ -136,7 +69,6 @@ {% for news in weeklies.filter(is_moderated=False) %} - @@ -161,91 +93,13 @@ {% endfor %}
{% trans %}Type{% endtrans %} {% trans %}Title{% endtrans %} {% trans %}Summary{% endtrans %} {% trans %}Club{% endtrans %}
{{ news.get_type_display() }} {{ news.title }} {{ news.summary|markdown }} {{ news.club }}
- -
-

{% trans %}Calls{% endtrans %}

- {% set calls = object_list.filter(type="CALL", dates__end_date__gte=timezone.now()).distinct().order_by('id') %} -
{% trans %}Displayed calls{% endtrans %}
- - - - - - - - - - - - - - - - {% for news in calls.filter(is_moderated=True) %} - - - - - - - - - - - - {% endfor %} - -
{% trans %}Type{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}Summary{% endtrans %}{% trans %}Club{% endtrans %}{% trans %}Author{% endtrans %}{% trans %}Moderator{% endtrans %}{% trans %}Start{% endtrans %}{% trans %}End{% endtrans %}{% trans %}Actions{% endtrans %}
{{ news.get_type_display() }}{{ news.title }}{{ news.summary|markdown }}{{ news.club }}{{ user_profile_link(news.author) }}{{ user_profile_link(news.moderator) }}{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}{% trans %}View{% endtrans %} - {% trans %}Edit{% endtrans %} - {% trans %}Remove{% endtrans %} - {% trans %}Delete{% endtrans %} -
-
{% trans %}Calls to moderate{% endtrans %}
- - - - - - - - - - - - - - - {% for news in calls.filter(is_moderated=False) %} - - - - - - - - - - - {% endfor %} - -
{% trans %}Type{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}Summary{% endtrans %}{% trans %}Club{% endtrans %}{% trans %}Author{% endtrans %}{% trans %}Start{% endtrans %}{% trans %}End{% endtrans %}{% trans %}Actions{% endtrans %}
{{ news.get_type_display() }}{{ news.title }}{{ news.summary|markdown }}{{ news.club }}{{ user_profile_link(news.author) }}{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}{% trans %}View{% endtrans %} - {% trans %}Edit{% endtrans %} - {% trans %}Moderate{% endtrans %} - {% trans %}Delete{% endtrans %} -
-

{% trans %}Events{% endtrans %}

- {% set events = object_list.filter(type="EVENT", dates__end_date__gte=timezone.now()).distinct().order_by('id') %} + {% set events = object_list.filter(dates__end_date__gte=timezone.now()).order_by('id') %}
{% trans %}Displayed events{% endtrans %}
- @@ -259,16 +113,15 @@ {% for news in events.filter(is_moderated=True) %} - - - + +
{% trans %}Type{% endtrans %} {% trans %}Title{% endtrans %} {% trans %}Summary{% endtrans %} {% trans %}Club{% endtrans %}
{{ news.get_type_display() }} {{ news.title }} {{ news.summary|markdown }} {{ news.club }} {{ user_profile_link(news.author) }} {{ user_profile_link(news.moderator) }}{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}{{ news.dates.all()[0].start_date|localtime|date(DATETIME_FORMAT) }} + {{ news.dates.all()[0].start_date|localtime|time(DATETIME_FORMAT) }}{{ news.dates.all()[0].end_date|localtime|date(DATETIME_FORMAT) }} + {{ news.dates.all()[0].end_date|localtime|time(DATETIME_FORMAT) }} {% trans %}View{% endtrans %} {% trans %}Edit{% endtrans %} {% trans %}Remove{% endtrans %} @@ -282,7 +135,6 @@ - @@ -295,15 +147,14 @@ {% for news in events.filter(is_moderated=False) %} - - - + +
{% trans %}Type{% endtrans %} {% trans %}Title{% endtrans %} {% trans %}Summary{% endtrans %} {% trans %}Club{% endtrans %}
{{ news.get_type_display() }} {{ news.title }} {{ news.summary|markdown }} {{ news.club }} {{ user_profile_link(news.author) }}{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}{{ news.dates.all()[0].start_date|localtime|date(DATETIME_FORMAT) }} + {{ news.dates.all()[0].start_date|localtime|time(DATETIME_FORMAT) }}{{ news.dates.all()[0].end_date|localtime|date(DATETIME_FORMAT) }} + {{ news.dates.all()[0].end_date|localtime|time(DATETIME_FORMAT) }} {% trans %}View{% endtrans %} {% trans %}Edit{% endtrans %} {% trans %}Moderate{% endtrans %} diff --git a/com/views.py b/com/views.py index 66186643..4396e6f2 100644 --- a/com/views.py +++ b/com/views.py @@ -174,11 +174,10 @@ class NewsEditView(CanEditMixin, UpdateView): self.object.is_moderated = False self.object.save() unread_notif_subquery = Notification.objects.filter( - user=OuterRef("pk"), type="NEWS_MODERATION", viewed=False + user=OuterRef("pk"), viewed=False ) - for user in User.objects.filter( - ~Exists(unread_notif_subquery), - groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID], + for user in User.objects.with_perm("com.moderate_news").filter( + ~Exists(unread_notif_subquery) ): Notification.objects.create( user=user, @@ -216,11 +215,10 @@ class NewsCreateView(CanCreateMixin, CreateView): self.object.save() else: unread_notif_subquery = Notification.objects.filter( - user=OuterRef("pk"), type="NEWS_MODERATION", viewed=False + user=OuterRef("pk"), viewed=False ) - for user in User.objects.filter( - ~Exists(unread_notif_subquery), - groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID], + for user in User.objects.with_perm("com.moderate_news").filter( + ~Exists(unread_notif_subquery) ): Notification.objects.create( user=user, diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 918c7dd7..1f8fcb7e 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -678,7 +678,6 @@ Welcome to the wiki page! title="Apero barman", summary="Viens boire un coup avec les barmans", content="Glou glou glou glou glou glou glou", - type="EVENT", club=bar_club, author=subscriber, is_moderated=True, @@ -698,7 +697,6 @@ Welcome to the wiki page! "Viens donc t'enjailler avec les autres barmans aux " "frais du BdF! \\o/" ), - type="EVENT", club=bar_club, author=subscriber, is_moderated=True, @@ -715,7 +713,6 @@ Welcome to the wiki page! title="Repas fromager", summary="Wien manger du l'bon fromeug'", content="Fô viendre mangey d'la bonne fondue!", - type="EVENT", club=bar_club, author=subscriber, is_moderated=True, @@ -732,7 +729,6 @@ Welcome to the wiki page! title="SdF", summary="Enjoy la fin des finaux!", content="Viens faire la fête avec tout plein de gens!", - type="EVENT", club=bar_club, author=subscriber, is_moderated=True, @@ -751,7 +747,6 @@ Welcome to the wiki page! summary="Viens jouer!", content="Rejoins la fine équipe du Troll Penché et viens " "t'amuser le Vendredi soir!", - type="WEEKLY", club=troll, author=subscriber, is_moderated=True, diff --git a/core/tests/test_user.py b/core/tests/test_user.py index a87827d5..9dc65ca8 100644 --- a/core/tests/test_user.py +++ b/core/tests/test_user.py @@ -9,6 +9,7 @@ from django.utils.timezone import now from model_bakery import baker, seq from model_bakery.recipe import Recipe, foreign_key +from com.models import News from core.baker_recipes import ( old_subscriber_user, subscriber_user, @@ -22,6 +23,8 @@ from eboutic.models import Invoice, InvoiceItem class TestSearchUsers(TestCase): @classmethod def setUpTestData(cls): + # News.author has on_delete=PROTECT, so news must be deleted beforehand + News.objects.all().delete() User.objects.all().delete() user_recipe = Recipe( User,