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 %}
-
- {% trans %}Notices to moderate{% 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 %}
- {% trans %}Type{% endtrans %} |
{% trans %}Title{% endtrans %} |
{% trans %}Summary{% endtrans %} |
{% trans %}Club{% endtrans %} |
@@ -94,7 +29,6 @@
{% for news in weeklies.filter(is_moderated=True) %}
- {{ news.get_type_display() }} |
{{ news.title }} |
{{ news.summary|markdown }} |
{{ news.club }} |
@@ -124,7 +58,6 @@
- {% trans %}Type{% endtrans %} |
{% trans %}Title{% endtrans %} |
{% trans %}Summary{% endtrans %} |
{% trans %}Club{% endtrans %} |
@@ -136,7 +69,6 @@
{% for news in weeklies.filter(is_moderated=False) %}
- {{ news.get_type_display() }} |
{{ news.title }} |
{{ news.summary|markdown }} |
{{ news.club }} |
@@ -161,91 +93,13 @@
{% endfor %}
-
-
- {% trans %}Calls{% endtrans %}
- {% set calls = object_list.filter(type="CALL", dates__end_date__gte=timezone.now()).distinct().order_by('id') %}
- {% trans %}Displayed calls{% endtrans %}
-
-
-
- {% 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 %} |
-
-
-
- {% for news in calls.filter(is_moderated=True) %}
-
- {{ 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 %}
- |
-
- {% endfor %}
-
-
- {% trans %}Calls to moderate{% endtrans %}
-
-
-
- {% trans %}Type{% endtrans %} |
- {% trans %}Title{% endtrans %} |
- {% trans %}Summary{% endtrans %} |
- {% trans %}Club{% endtrans %} |
- {% trans %}Author{% endtrans %} |
- {% trans %}Start{% endtrans %} |
- {% trans %}End{% endtrans %} |
- {% trans %}Actions{% endtrans %} |
-
-
-
- {% for news in calls.filter(is_moderated=False) %}
-
- {{ 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 %}
- |
-
- {% endfor %}
-
-
-
{% 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 %}
- {% trans %}Type{% endtrans %} |
{% trans %}Title{% endtrans %} |
{% trans %}Summary{% endtrans %} |
{% trans %}Club{% endtrans %} |
@@ -259,16 +113,15 @@
{% for news in events.filter(is_moderated=True) %}
- {{ 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 @@
- {% trans %}Type{% endtrans %} |
{% trans %}Title{% endtrans %} |
{% trans %}Summary{% endtrans %} |
{% trans %}Club{% endtrans %} |
@@ -295,15 +147,14 @@
{% for news in events.filter(is_moderated=False) %}
- {{ 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,
| |