From 74385bde04cad61522791136908282c6dcb697d4 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 20 Jan 2025 17:03:25 +0100 Subject: [PATCH 1/7] API to moderate and delete news --- com/api.py | 27 +++++++++++++++++++- com/tests/test_api.py | 59 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/com/api.py b/com/api.py index e46daea9b..b39beac49 100644 --- a/com/api.py +++ b/com/api.py @@ -5,6 +5,8 @@ from ninja_extra import ControllerBase, api_controller, route from com.calendar import IcsCalendar +from com.models import News +from core.auth.api_permissions import HasPerm from core.views.files import send_raw_file @@ -17,7 +19,7 @@ def calendar_external(self): """Return the ICS file of the AE Google Calendar Because of Google's cors rules, we can't just do a request to google ics - from the frontend. Google is blocking CORS request in it's responses headers. + from the frontend. Google is blocking CORS request in its responses headers. The only way to do it from the frontend is to use Google Calendar API with an API key This is not especially desirable as your API key is going to be provided to the frontend. @@ -30,3 +32,26 @@ def calendar_external(self): @route.get("/internal.ics", url_name="calendar_internal") def calendar_internal(self): return send_raw_file(IcsCalendar.get_internal()) + + +@api_controller("/news") +class NewsController(ControllerBase): + @route.patch( + "/{news_id}/moderate", + permissions=[HasPerm("com.moderate_news")], + url_name="moderate_news", + ) + def moderate_news(self, news_id: int): + news = self.get_object_or_exception(News, id=news_id) + if not news.is_moderated: + news.is_moderated = True + news.save() + + @route.delete( + "/{news_id}", + permissions=[HasPerm("com.delete_news")], + url_name="delete_news", + ) + def delete_news(self, news_id: int): + news = self.get_object_or_exception(News, id=news_id) + news.delete() diff --git a/com/tests/test_api.py b/com/tests/test_api.py index f131052e1..1bcb4d135 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -6,12 +6,16 @@ import pytest from django.conf import settings +from django.contrib.auth.models import Permission from django.http import HttpResponse from django.test.client import Client from django.urls import reverse from django.utils import timezone +from model_bakery import baker from com.calendar import IcsCalendar +from com.models import News +from core.models import User @dataclass @@ -120,3 +124,58 @@ def test_fetch_success(self, client: Client): out_file = accel_redirect_to_file(response) assert out_file is not None assert out_file.exists() + + +@pytest.mark.django_db +class TestModerateNews: + @pytest.mark.parametrize("news_is_moderated", [True, False]) + def test_moderation_ok(self, client: Client, news_is_moderated: bool): # noqa FBT + user = baker.make( + User, user_permissions=[Permission.objects.get(codename="moderate_news")] + ) + # The API call should work even if the news is initially moderated. + # In the latter case, the result should be a noop, rather than an error. + news = baker.make(News, is_moderated=news_is_moderated) + client.force_login(user) + response = client.patch( + reverse("api:moderate_news", kwargs={"news_id": news.id}) + ) + assert response.status_code == 200 + news.refresh_from_db() + assert news.is_moderated + + def test_moderation_forbidden(self, client: Client): + user = baker.make(User) + news = baker.make(News, is_moderated=False) + client.force_login(user) + response = client.patch( + reverse("api:moderate_news", kwargs={"news_id": news.id}) + ) + assert response.status_code == 403 + news.refresh_from_db() + assert not news.is_moderated + + +@pytest.mark.django_db +class TestDeleteNews: + def test_delete_news_ok(self, client: Client): + user = baker.make( + User, user_permissions=[Permission.objects.get(codename="delete_news")] + ) + news = baker.make(News) + client.force_login(user) + response = client.delete( + reverse("api:delete_news", kwargs={"news_id": news.id}) + ) + assert response.status_code == 200 + assert not News.objects.filter(id=news.id).exists() + + def test_delete_news_forbidden(self, client: Client): + user = baker.make(User) + news = baker.make(News) + client.force_login(user) + response = client.delete( + reverse("api:delete_news", kwargs={"news_id": news.id}) + ) + assert response.status_code == 403 + assert News.objects.filter(id=news.id).exists() From df6fc489b640bcb8882c3acca404e36b9fa04fc4 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 20 Jan 2025 17:27:00 +0100 Subject: [PATCH 2/7] Improve news list display --- com/models.py | 18 +++++ com/static/com/css/news-list.scss | 120 +++--------------------------- com/templates/com/news_list.jinja | 107 +++++++++++++++----------- com/views.py | 37 +++++---- core/templates/core/macros.jinja | 8 -- 5 files changed, 115 insertions(+), 175 deletions(-) diff --git a/com/models.py b/com/models.py index 1219410aa..229d83c8f 100644 --- a/com/models.py +++ b/com/models.py @@ -172,6 +172,22 @@ def news_notification_callback(notif): notif.viewed = True +class NewsDateQuerySet(models.QuerySet): + def viewable_by(self, user: User) -> Self: + """Filter the event dates that the given user can view. + + - If the can view non moderated news, he can view all news dates + - else, he can view the dates of news that are either + authored by him or moderated. + """ + if user.has_perm("com.view_unmoderated_news"): + return self + q_filter = Q(news__is_moderated=True) + if user.is_authenticated: + q_filter |= Q(news__author_id=user.id) + return self.filter(q_filter) + + class NewsDate(models.Model): """A date associated with news. @@ -187,6 +203,8 @@ class NewsDate(models.Model): start_date = models.DateTimeField(_("start_date")) end_date = models.DateTimeField(_("end_date")) + objects = NewsDateQuerySet.as_manager() + class Meta: verbose_name = _("news date") verbose_name_plural = _("news dates") diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss index d073c4ac4..b068fb45b 100644 --- a/com/static/com/css/news-list.scss +++ b/com/static/com/css/news-list.scss @@ -172,53 +172,21 @@ .news_event { display: block; - padding: 0.4em; - - &:not(:last-child) { - border-bottom: 1px solid grey; - } - - div { - margin: 0.2em; - } - - h4 { - margin-top: 1em; - text-transform: uppercase; - } - - .club_logo { - float: left; - min-width: 7em; - max-width: 9em; - margin: 0; - margin-right: 1em; - margin-top: 0.8em; + padding: 1em; + header { img { - max-height: 6em; - max-width: 8em; - display: block; - margin: 0 auto; + height: 75px; } - } - - .news_date { - font-size: 100%; - } - - .news_content { - clear: left; - - .button_bar { - text-align: right; - - .fb { - color: $faceblue; - } - - .twitter { - color: $twitblue; + .header_content { + display: flex; + flex-direction: column; + justify-content: center; + gap: .2rem; + + h4 { + margin-top: 0; + text-transform: uppercase; } } } @@ -228,70 +196,6 @@ /* END EVENTS TODAY AND NEXT FEW DAYS */ - /* COMING SOON */ - .news_coming_soon { - display: list-item; - list-style-type: square; - list-style-position: inside; - margin-left: 1em; - padding-left: 0; - - a { - font-weight: bold; - text-transform: uppercase; - } - - .news_date { - font-size: 0.9em; - } - } - - /* END COMING SOON */ - - /* NOTICES */ - .news_notice { - margin: 0 0 1em 1em; - padding: 0.4em; - padding-left: 1em; - background: $secondary-neutral-light-color; - box-shadow: $shadow-color 0 0 2px; - border-radius: 18px 5px 18px 5px; - - h4 { - margin: 0; - } - - .news_content { - margin-left: 1em; - } - } - - /* END NOTICES */ - - /* CALLS */ - .news_call { - margin: 0 0 1em 1em; - padding: 0.4em; - padding-left: 1em; - background: $secondary-neutral-light-color; - border: 1px solid grey; - box-shadow: $shadow-color 1px 1px 1px; - - h4 { - margin: 0; - } - - .news_date { - font-size: 0.9em; - } - - .news_content { - margin-left: 1em; - } - } - - /* END CALLS */ - .news_empty { margin-left: 1em; } diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 0f1f43016..560eb5696 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -1,5 +1,4 @@ {% extends "core/base.jinja" %} -{% from 'core/macros.jinja' import tweet_quick, fb_quick %} {% block title %} {% trans %}News{% endtrans %} @@ -18,10 +17,8 @@ {% endblock %} {% block content %} -
- {% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__is_moderated=True).datetimes('start_date', 'day') %}

{% trans %}Events today and the next few days{% endtrans %} @@ -33,51 +30,58 @@ {% endif %} {% if user.is_com_admin %} - {% trans %}Administrate news{% endtrans %} + + {% trans %}Administrate news{% endtrans %} +
- {% endif %} - {% if events_dates %} - {% for d in events_dates %} -
-
-
-
{{ d|localtime|date('D') }}
-
{{ d|localtime|date('d') }}
-
{{ d|localtime|date('b') }}
-
+ {% endif %} + {% for day, dates_group in news_dates %} +
+
+
+
{{ day|date('D') }}
+
{{ day|date('d') }}
+
{{ day|date('b') }}
-
- {% for news in object_list.filter(dates__start_date__gte=d,dates__start_date__lte=d+timedelta(days=1)).exclude(dates__end_date__lt=timezone.now()).order_by('dates__start_date') %} -
- -

{{ news.title }}

- -
- {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} - - {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }} -
-
{{ news.summary|markdown }} -
- {{ fb_quick(news) }} - {{ tweet_quick(news) }} +
+
+ {% for date in dates_group %} +
+
+ {% if date.news.club.logo %} + {{ date.news.club }} + {% else %} + {{ date.news.club }} + {% endif %} +
+

+ + {{ date.news.title }} + +

+ {{ date.news.club }} +
+ - +
-
- {% endfor %} -
+ +
+ {{ date.news.summary|markdown }} +
+ + {% endfor %}
- {% endfor %} +
{% else %}
{% trans %}Nothing to come...{% endtrans %}
- {% endif %} + {% endfor %}

{% trans %}All coming events{% endtrans %} @@ -110,18 +114,26 @@

@@ -130,7 +142,7 @@

{% trans %}Birthdays{% endtrans %}

- {%- if user.was_subscribed -%} + {%- if user.has_perm("core.view_user") -%}
    {%- for year, users in birthdays -%}
  • @@ -143,8 +155,13 @@
  • {%- endfor -%}
- {%- else -%} + {%- elif not user.was_subscribed -%} + {# The user cannot view birthdays, because he never subscribed #}

{% trans %}You need to subscribe to access this content{% endtrans %}

+ {%- else -%} + {# There is another reason why user cannot view birthdays (maybe he is banned) + but we cannot know exactly what is this reason #} +

{% trans %}You cannot access this content{% endtrans %}

{%- endif -%}
diff --git a/com/views.py b/com/views.py index 0ab8fc1ce..3990fe9b0 100644 --- a/com/views.py +++ b/com/views.py @@ -37,9 +37,9 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy from django.utils import timezone -from django.utils.timezone import localdate +from django.utils.timezone import localdate, now from django.utils.translation import gettext_lazy as _ -from django.views.generic import DetailView, ListView, View +from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic.edit import CreateView, DeleteView, UpdateView from club.models import Club, Mailing @@ -236,28 +236,37 @@ class NewsAdminListView(PermissionRequiredMixin, ListView): permission_required = ["com.moderate_news", "com.delete_news"] -class NewsListView(ListView): - model = News +class NewsListView(TemplateView): template_name = "com/news_list.jinja" - queryset = News.objects.filter(is_moderated=True) - - def get_queryset(self): - return super().get_queryset().viewable_by(self.request.user) - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["NewsDate"] = NewsDate - kwargs["timedelta"] = timedelta - kwargs["birthdays"] = itertools.groupby( + def get_birthdays(self): + if not self.request.user.has_perm("core.view_user"): + return [] + return itertools.groupby( User.objects.filter( date_of_birth__month=localdate().month, date_of_birth__day=localdate().day, + is_subscriber_viewable=True, ) .filter(role__in=["STUDENT", "FORMER STUDENT"]) .order_by("-date_of_birth"), key=lambda u: u.date_of_birth.year, ) - return kwargs + + def get_news_dates(self): + return itertools.groupby( + NewsDate.objects.viewable_by(self.request.user) + .filter(end_date__gt=now(), start_date__lt=now() + timedelta(days=6)) + .order_by("start_date") + .select_related("news", "news__club"), + key=lambda d: d.start_date.date(), + ) + + def get_context_data(self, **kwargs): + return super().get_context_data(**kwargs) | { + "news_dates": self.get_news_dates(), + "birthdays": self.get_birthdays(), + } class NewsDetailView(CanViewMixin, DetailView): diff --git a/core/templates/core/macros.jinja b/core/templates/core/macros.jinja index 84c5b05a0..b43ba8445 100644 --- a/core/templates/core/macros.jinja +++ b/core/templates/core/macros.jinja @@ -39,14 +39,6 @@ {%- endmacro %} -{% macro fb_quick(news) -%} - -{%- endmacro %} - -{% macro tweet_quick(news) -%} - -{%- endmacro %} - {% macro user_mini_profile(user) %}
From 4a2fdb8ef7033c732b9af19beccaf89602e076db Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 20 Jan 2025 18:02:24 +0100 Subject: [PATCH 3/7] News moderation buttons directly on the home page --- .../com/components/moderation-alert-index.ts | 39 ++++++++++ com/static/com/css/news-list.scss | 4 +- com/templates/com/macros.jinja | 73 +++++++++++++++++++ com/templates/com/news_detail.jinja | 71 ++++++++++-------- com/templates/com/news_list.jinja | 57 +++++++++------ core/static/core/style.scss | 14 ++++ locale/fr/LC_MESSAGES/django.po | 64 ++++++++++------ 7 files changed, 244 insertions(+), 78 deletions(-) create mode 100644 com/static/bundled/com/components/moderation-alert-index.ts create mode 100644 com/templates/com/macros.jinja diff --git a/com/static/bundled/com/components/moderation-alert-index.ts b/com/static/bundled/com/components/moderation-alert-index.ts new file mode 100644 index 000000000..ba933b631 --- /dev/null +++ b/com/static/bundled/com/components/moderation-alert-index.ts @@ -0,0 +1,39 @@ +import { exportToHtml } from "#core:utils/globals"; +import Alpine from "alpinejs"; +import { newsDeleteNews, newsModerateNews } from "#openapi"; + +// This will be used in jinja templates, +// so we cannot use real enums as those are purely an abstraction of Typescript +const AlertState = { + // biome-ignore lint/style/useNamingConvention: this feels more like an enum + PENDING: 1, + // biome-ignore lint/style/useNamingConvention: this feels more like an enum + MODERATED: 2, + // biome-ignore lint/style/useNamingConvention: this feels more like an enum + DELETED: 3, +}; +exportToHtml("AlertState", AlertState); + +document.addEventListener("alpine:init", () => { + Alpine.data("moderationAlert", (newsId: number) => ({ + state: AlertState.PENDING, + newsId: newsId as number, + loading: false, + + async moderateNews() { + this.loading = true; + // biome-ignore lint/style/useNamingConvention: api is snake case + await newsModerateNews({ path: { news_id: this.newsId } }); + this.state = AlertState.MODERATED; + this.loading = false; + }, + + async deleteNews() { + this.loading = true; + // biome-ignore lint/style/useNamingConvention: api is snake case + await newsDeleteNews({ path: { news_id: this.newsId } }); + this.state = AlertState.DELETED; + this.loading = false; + }, + })); +}); diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss index b068fb45b..0df403b44 100644 --- a/com/static/com/css/news-list.scss +++ b/com/static/com/css/news-list.scss @@ -171,7 +171,9 @@ } .news_event { - display: block; + display: flex; + flex-direction: column; + gap: .5em; padding: 1em; header { diff --git a/com/templates/com/macros.jinja b/com/templates/com/macros.jinja new file mode 100644 index 000000000..388267d26 --- /dev/null +++ b/com/templates/com/macros.jinja @@ -0,0 +1,73 @@ +{% macro news_moderation_alert(news, user, alpineState = None) %} + {# An alert to display on top of non moderated news, + with actions to either moderate or delete them. + + The current state of the alert is accessible through + the given `alpineState` variable. + This state is a `AlertState`, as defined in `moderation-alert-index.ts` + + Example : + ```jinja +
+ {{ news_moderation_alert(news, user, "state") }} +
+ ``` + + Args: + news: The `News` object to which this alert is related + user: The request.user + alpineState: An alpine variable name + + Warning: + If you use this macro, you must also include `moderation-alert-index.ts` + in your template. + #} +
+ + + +
+{% endmacro %} diff --git a/com/templates/com/news_detail.jinja b/com/templates/com/news_detail.jinja index 454eb2ef2..d517ee72b 100644 --- a/com/templates/com/news_detail.jinja +++ b/com/templates/com/news_detail.jinja @@ -1,5 +1,6 @@ {% extends "core/base.jinja" %} {% from 'core/macros.jinja' import user_profile_link, facebook_share, tweet, link_news_logo, gen_news_metatags %} +{% from "com/macros.jinja" import news_moderation_alert %} {% block title %} {% trans %}News{% endtrans %} - @@ -16,39 +17,49 @@ {% endblock %} +{% block additional_js %} + +{% endblock %} + {% block content %}

{% trans %}Back to news{% endtrans %}

-
- -

{{ news.title }}

-

- {{ date.start_date|localtime|date(DATETIME_FORMAT) }} - {{ date.start_date|localtime|time(DATETIME_FORMAT) }} - - {{ date.end_date|localtime|date(DATETIME_FORMAT) }} - {{ date.end_date|localtime|time(DATETIME_FORMAT) }} -

-
-
{{ news.summary|markdown }}
-
-
{{ news.content|markdown }}
- {{ facebook_share(news) }} - {{ tweet(news) }} -
-

{% trans %}Author: {% endtrans %}{{ user_profile_link(news.author) }}

- {% if news.moderator %} -

{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}

- {% elif user.is_com_admin %} -

{% trans %}Moderate{% endtrans %}

- {% endif %} - {% if user.can_edit(news) %} -

{% trans %}Edit (will be moderated again){% endtrans %}

- {% endif %} +
+ + {% if not news.is_moderated %} + {{ news_moderation_alert(news, user, "newsState") }} + {% endif %} +
+ +

{{ news.title }}

+

+ {{ date.start_date|localtime|date(DATETIME_FORMAT) }} + {{ date.start_date|localtime|time(DATETIME_FORMAT) }} - + {{ date.end_date|localtime|date(DATETIME_FORMAT) }} + {{ date.end_date|localtime|time(DATETIME_FORMAT) }} +

+
+
{{ news.summary|markdown }}
+
+
{{ news.content|markdown }}
+ {{ facebook_share(news) }} + {{ tweet(news) }} +
+

{% trans %}Author: {% endtrans %}{{ user_profile_link(news.author) }}

+ {% if news.moderator %} +

{% trans %}Moderator: {% endtrans %}{{ user_profile_link(news.moderator) }}

+ {% elif user.is_com_admin %} +

{% trans %}Moderate{% endtrans %}

+ {% endif %} + {% if user.can_edit(news) %} +

{% trans %}Edit (will be moderated again){% endtrans %}

+ {% endif %} +
-
-
+ +
{% endblock %} diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 560eb5696..80b071769 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -1,4 +1,5 @@ {% extends "core/base.jinja" %} +{% from "com/macros.jinja" import news_moderation_alert %} {% block title %} {% trans %}News{% endtrans %} @@ -14,6 +15,7 @@ {% block additional_js %} + {% endblock %} {% block content %} @@ -46,32 +48,39 @@
{% for date in dates_group %} -
-
- {% if date.news.club.logo %} - {{ date.news.club }} - {% else %} - {{ date.news.club }} - {% endif %} -
-

- - {{ date.news.title }} - -

- {{ date.news.club }} -
- - - +
+ {% if not date.news.is_moderated %} + {# if a non moderated news is in the object list, + the logged user is either an admin or the news author #} + {{ news_moderation_alert(date.news, user, "newsState") }} + {% endif %} +
+
+ {% if date.news.club.logo %} + {{ date.news.club }} + {% else %} + {{ date.news.club }} + {% endif %} +
+

+ + {{ date.news.title }} + +

+ {{ date.news.club }} +
+ - + +
+
+
+ {{ date.news.summary|markdown }}
-
-
- {{ date.news.summary|markdown }}
{% endfor %} diff --git a/core/static/core/style.scss b/core/static/core/style.scss index a89d33cfe..062085793 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -244,6 +244,20 @@ body { } } + &.btn-green { + $bg-color: rgba(0, 210, 83, 0.4); + background-color: $bg-color; + color: $black-color; + + &:not(:disabled):hover { + background-color: darken($bg-color, 15%); + } + + &:disabled { + background-color: lighten($bg-color, 15%); + } + } + &.btn-red { background-color: #fc8181; color: black; diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index dc6b5ee67..134a903b0 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-19 18:12+0100\n" +"POT-Creation-Date: 2025-01-20 17:35+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -310,7 +310,7 @@ msgstr "Compte en banque : " #: accounting/templates/accounting/club_account_details.jinja #: accounting/templates/accounting/label_list.jinja #: club/templates/club/club_sellings.jinja club/templates/club/mailing.jinja -#: com/templates/com/mailing_admin.jinja +#: com/templates/com/macros.jinja com/templates/com/mailing_admin.jinja #: com/templates/com/news_admin_list.jinja com/templates/com/poster_edit.jinja #: com/templates/com/screen_edit.jinja com/templates/com/weekmail.jinja #: core/templates/core/file_detail.jinja @@ -935,10 +935,6 @@ msgstr "rôle" msgid "description" msgstr "description" -#: club/models.py -msgid "past member" -msgstr "ancien membre" - #: club/models.py msgid "Email address" msgstr "Adresse email" @@ -1408,12 +1404,25 @@ msgstr "temps d'affichage" msgid "Begin date should be before end date" msgstr "La date de début doit être avant celle de fin" -#: com/templates/com/mailing_admin.jinja com/views.py -#: core/templates/core/user_tools.jinja -msgid "Mailing lists administration" -msgstr "Administration des mailing listes" +#: com/templates/com/macros.jinja +msgid "Waiting moderation" +msgstr "En attente de modération" -#: com/templates/com/mailing_admin.jinja +#: com/templates/com/macros.jinja +msgid "" +"This news isn't moderated and is visible only by its author and the " +"communication admins." +msgstr "" +"Cette nouvelle n'est pas modérée et n'est visible que par son auteur et les " +"admins communication." + +#: com/templates/com/macros.jinja +msgid "It will stay hidden for other users until it has been moderated." +msgstr "" +"Elle sera cachée pour les autres utilisateurs tant qu'elle ne sera pas " +"modérée." + +#: com/templates/com/macros.jinja com/templates/com/mailing_admin.jinja #: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja #: core/templates/core/file_detail.jinja #: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja @@ -1421,6 +1430,19 @@ msgstr "Administration des mailing listes" msgid "Moderate" msgstr "Modérer" +#: com/templates/com/macros.jinja +msgid "News moderated" +msgstr "Nouvelle modérée" + +#: com/templates/com/macros.jinja +msgid "News deleted" +msgstr "Nouvelle supprimée" + +#: com/templates/com/mailing_admin.jinja com/views.py +#: core/templates/core/user_tools.jinja +msgid "Mailing lists administration" +msgstr "Administration des mailing listes" + #: com/templates/com/mailing_admin.jinja #, python-format msgid "Moderated by %(user)s" @@ -1578,14 +1600,6 @@ msgstr "Discord AE" msgid "Dev Team" msgstr "Pôle Informatique" -#: com/templates/com/news_list.jinja -msgid "Facebook" -msgstr "Facebook" - -#: com/templates/com/news_list.jinja -msgid "Instagram" -msgstr "Instagram" - #: com/templates/com/news_list.jinja msgid "Birthdays" msgstr "Anniversaires" @@ -1599,6 +1613,10 @@ msgstr "%(age)s ans" msgid "You need to subscribe to access this content" msgstr "Vous devez cotiser pour accéder à ce contenu" +#: com/templates/com/news_list.jinja +msgid "You cannot access this content" +msgstr "Vous n'avez pas accès à ce contenu" + #: com/templates/com/poster_edit.jinja com/templates/com/poster_list.jinja msgid "Poster" msgstr "Affiche" @@ -3299,8 +3317,8 @@ msgstr "Nom d'utilisateur, email, ou numéro de compte AE" #: core/views/forms.py msgid "" -"Profile: you need to be visible on the picture, in order to be recognized " -"(e.g. by the barmen)" +"Profile: you need to be visible on the picture, in order to be recognized (e." +"g. by the barmen)" msgstr "" "Photo de profil: vous devez être visible sur la photo afin d'être reconnu " "(par exemple par les barmen)" @@ -3906,8 +3924,8 @@ msgstr "" #: counter/templates/counter/mails/account_dump.jinja msgid "If you think this was a mistake, please mail us at ae@utbm.fr." msgstr "" -"Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à " -"ae@utbm.fr." +"Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à ae@utbm." +"fr." #: counter/templates/counter/mails/account_dump.jinja msgid "" From 96f587478680230a5f0b9392d817b9bb29de1f6a Mon Sep 17 00:00:00 2001 From: imperosol Date: Thu, 23 Jan 2025 14:32:10 +0100 Subject: [PATCH 4/7] Set the moderator when moderating news --- com/api.py | 1 + com/tests/test_api.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/com/api.py b/com/api.py index b39beac49..4a4d0e172 100644 --- a/com/api.py +++ b/com/api.py @@ -45,6 +45,7 @@ def moderate_news(self, news_id: int): news = self.get_object_or_exception(News, id=news_id) if not news.is_moderated: news.is_moderated = True + news.moderator = self.context.request.user news.save() @route.delete( diff --git a/com/tests/test_api.py b/com/tests/test_api.py index 1bcb4d135..8e4b00897 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -136,13 +136,21 @@ def test_moderation_ok(self, client: Client, news_is_moderated: bool): # noqa F # The API call should work even if the news is initially moderated. # In the latter case, the result should be a noop, rather than an error. news = baker.make(News, is_moderated=news_is_moderated) + initial_moderator = news.moderator client.force_login(user) response = client.patch( reverse("api:moderate_news", kwargs={"news_id": news.id}) ) + # if it wasn't moderated, it should now be moderated and the moderator should + # be the user that made the request. + # If it was already moderated, it should be a no-op, but not an error assert response.status_code == 200 news.refresh_from_db() assert news.is_moderated + if not news_is_moderated: + assert news.moderator == user + else: + assert news.moderator == initial_moderator def test_moderation_forbidden(self, client: Client): user = baker.make(User) From 8f1564185a62d8f7ae220c5a803a77445f15ab7f Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 25 Jan 2025 23:32:55 +0100 Subject: [PATCH 5/7] remove alpine instructions for moderated news --- com/templates/com/news_list.jinja | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 80b071769..ebb4f6e18 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -48,13 +48,22 @@
{% for date in dates_group %} -
+
{% if not date.news.is_moderated %} {# if a non moderated news is in the object list, the logged user is either an admin or the news author #} {{ news_moderation_alert(date.news, user, "newsState") }} {% endif %} -
+
{% if date.news.club.logo %} {{ date.news.club }} From 266cef31cb462214a42cb03b3c60f4a6821fd259 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 25 Jan 2025 23:34:59 +0100 Subject: [PATCH 6/7] remove Alpine import in moderation-alert-index.ts --- com/static/bundled/com/components/moderation-alert-index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/com/static/bundled/com/components/moderation-alert-index.ts b/com/static/bundled/com/components/moderation-alert-index.ts index ba933b631..e11290ada 100644 --- a/com/static/bundled/com/components/moderation-alert-index.ts +++ b/com/static/bundled/com/components/moderation-alert-index.ts @@ -1,5 +1,4 @@ import { exportToHtml } from "#core:utils/globals"; -import Alpine from "alpinejs"; import { newsDeleteNews, newsModerateNews } from "#openapi"; // This will be used in jinja templates, From b23c322caac20fa1caf8e0aa611cfe3db577274c Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 26 Jan 2025 00:10:01 +0100 Subject: [PATCH 7/7] Add a disclaimer when moderating weekly news --- com/templates/com/macros.jinja | 18 ++++++++++++++++++ locale/fr/LC_MESSAGES/django.po | 14 ++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/com/templates/com/macros.jinja b/com/templates/com/macros.jinja index 388267d26..46f4b7c4c 100644 --- a/com/templates/com/macros.jinja +++ b/com/templates/com/macros.jinja @@ -44,6 +44,24 @@ It will stay hidden for other users until it has been moderated. {% endtrans %}

+ {% if user.has_perm("com.moderate_news") %} + {# This is an additional query for each non-moderated news, + but it will be executed only for admin users, and only one time + (if they do their job and moderated news as soon as they see them), + so it's still reasonable #} + {% set nb_event=news.dates.count() %} + {% if nb_event > 1 %} +
+ {% trans %}Weekly event{% endtrans %} +

+ {% trans trimmed nb=nb_event %} + This event will take place every week for {{ nb }} weeks. + If you moderate or delete this event, + it will also be moderated (or deleted) for the following weeks. + {% endtrans %} +

+ {% endif %} + {% endif %}
{% if user.has_perm("com.moderate_news") %} diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 134a903b0..69fc684f3 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-20 17:35+0100\n" +"POT-Creation-Date: 2025-01-25 23:46+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -1263,7 +1263,7 @@ msgstr "Format : 16:9 | Résolution : 1920x1080" msgid "Start date" msgstr "Date de début" -#: com/forms.py +#: com/forms.py com/templates/com/macros.jinja msgid "Weekly event" msgstr "Événement Hebdomadaire" @@ -1422,6 +1422,16 @@ msgstr "" "Elle sera cachée pour les autres utilisateurs tant qu'elle ne sera pas " "modérée." +#: com/templates/com/macros.jinja +#, python-format +msgid "" +"This event will take place every week for %(nb)s weeks. If you moderate or delete " +"this event, it will also be moderated (or deleted) for the following weeks." +msgstr "" +"Cet événement se déroulera chaque semaine pendant %(nb)s semaines. Si vous " +"modérez ou supprimez cet événement, il sera également modéré (ou supprimé) " +"pour les semaines suivantes." + #: com/templates/com/macros.jinja com/templates/com/mailing_admin.jinja #: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja #: core/templates/core/file_detail.jinja