From a1bf86dabff55ca195e7cadef51499d9dde259b6 Mon Sep 17 00:00:00 2001 From: Sli Date: Thu, 20 Feb 2025 17:47:51 +0100 Subject: [PATCH 1/9] Add moderation through calendar widget --- com/api.py | 14 +- com/calendar.py | 37 +++-- .../com/components/ics-calendar-index.ts | 137 +++++++++++++++--- com/templates/com/news_list.jinja | 6 +- 4 files changed, 166 insertions(+), 28 deletions(-) diff --git a/com/api.py b/com/api.py index 5a3eef10..c624e093 100644 --- a/com/api.py +++ b/com/api.py @@ -2,10 +2,11 @@ from pathlib import Path from typing import Literal from django.conf import settings -from django.http import Http404 +from django.http import Http404, HttpResponse from ninja import Query from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra.pagination import PageNumberPaginationExtra +from ninja_extra.permissions import IsAuthenticated from ninja_extra.schemas import PaginatedResponseSchema from com.calendar import IcsCalendar @@ -38,6 +39,17 @@ class CalendarController(ControllerBase): def calendar_internal(self): return send_raw_file(IcsCalendar.get_internal()) + @route.get( + "/unmoderated.ics", + permissions=[IsAuthenticated], + url_name="calendar_unmoderated", + ) + def calendar_unmoderated(self): + return HttpResponse( + IcsCalendar.get_unmoderated(self.context.request.user), + content_type="text/calendar", + ) + @api_controller("/news") class NewsController(ControllerBase): diff --git a/com/calendar.py b/com/calendar.py index f3c612e1..dfe8fe6a 100644 --- a/com/calendar.py +++ b/com/calendar.py @@ -5,6 +5,7 @@ from typing import final import requests from dateutil.relativedelta import relativedelta from django.conf import settings +from django.db.models import QuerySet from django.urls import reverse from django.utils import timezone from ical.calendar import Calendar @@ -12,6 +13,7 @@ from ical.calendar_stream import IcsCalendarStream from ical.event import Event from com.models import NewsDate +from core.models import User @final @@ -55,11 +57,32 @@ class IcsCalendar: @classmethod def make_internal(cls) -> Path: # Updated through a post_save signal on News in com.signals + # Create a file so we can offload the download to the reverse proxy if available + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._INTERNAL_CALENDAR, "wb") as f: + _ = f.write( + cls.ics_from_queryset( + NewsDate.objects.filter( + news__is_moderated=True, + end_date__gte=timezone.now() - (relativedelta(months=6)), + ) + ) + ) + return cls._INTERNAL_CALENDAR + + @classmethod + def get_unmoderated(cls, user: User) -> bytes: + return cls.ics_from_queryset( + NewsDate.objects.viewable_by(user).filter( + news__is_moderated=False, + end_date__gte=timezone.now() - (relativedelta(months=6)), + ), + ) + + @classmethod + def ics_from_queryset(cls, queryset: QuerySet) -> bytes: calendar = Calendar() - for news_date in NewsDate.objects.filter( - news__is_moderated=True, - end_date__gte=timezone.now() - (relativedelta(months=6)), - ).prefetch_related("news"): + for news_date in queryset.prefetch_related("news"): event = Event( summary=news_date.news.title, start=news_date.start_date, @@ -68,8 +91,4 @@ class IcsCalendar: ) calendar.events.append(event) - # Create a file so we can offload the download to the reverse proxy if available - cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) - with open(cls._INTERNAL_CALENDAR, "wb") as f: - _ = f.write(IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8")) - return cls._INTERNAL_CALENDAR + return IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8") diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 3c78f98f..5e6818bf 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -7,20 +7,32 @@ import frLocale from "@fullcalendar/core/locales/fr"; import dayGridPlugin from "@fullcalendar/daygrid"; import iCalendarPlugin from "@fullcalendar/icalendar"; import listPlugin from "@fullcalendar/list"; -import { calendarCalendarExternal, calendarCalendarInternal } from "#openapi"; +import { + calendarCalendarExternal, + calendarCalendarInternal, + calendarCalendarUnmoderated, + newsDeleteNews, + newsModerateNews, +} from "#openapi"; @registerComponent("ics-calendar") export class IcsCalendar extends inheritHtmlElement("div") { - static observedAttributes = ["locale"]; + static observedAttributes = ["locale", "can_moderate", "can_delete"]; private calendar: Calendar; private locale = "en"; + private canModerate = false; + private canDelete = false; attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) { - if (name !== "locale") { - return; + if (name === "locale") { + this.locale = newValue; + } + if (name === "can_moderate") { + this.canModerate = newValue.toLowerCase() === "true"; + } + if (name === "can_delete") { + this.canDelete = newValue.toLowerCase() === "true"; } - - this.locale = newValue; } isMobile() { @@ -54,6 +66,68 @@ export class IcsCalendar extends inheritHtmlElement("div") { }).format(date); } + getNewsId(event: EventImpl) { + // return Number.parseInt(event.url.split("/").pop()); + return Number.parseInt( + event.url + .toString() + .split("/") + .filter((s) => s) // Remove blank characters + .pop(), + ); + } + + async refreshEvents() { + this.click(); // Remove focus from popup + this.calendar.removeAllEventSources(); + for (const source of await this.getEventSources()) { + this.calendar.addEventSource(source); + } + this.calendar.refetchEvents(); + } + + async moderate(id: number) { + await newsModerateNews({ + path: { + // biome-ignore lint/style/useNamingConvention: python API + news_id: id, + }, + }); + await this.refreshEvents(); + } + + async delete(id: number) { + await newsDeleteNews({ + path: { + // biome-ignore lint/style/useNamingConvention: python API + news_id: id, + }, + }); + await this.refreshEvents(); + } + + async getEventSources() { + const cacheInvalidate = `?invalidate=${Date.now()}`; + return [ + { + url: `${await makeUrl(calendarCalendarInternal)}${cacheInvalidate}`, + format: "ics", + className: "internal", + }, + { + url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`, + format: "ics", + className: "external", + }, + { + url: `${await makeUrl(calendarCalendarUnmoderated)}${cacheInvalidate}`, + format: "ics", + color: "red", + className: "unmoderated", + }, + ]; + } + createEventDetailPopup(event: EventClickArg) { // Delete previous popup const oldPopup = document.getElementById("event-details"); @@ -112,6 +186,40 @@ export class IcsCalendar extends inheritHtmlElement("div") { return makePopupInfo(url, "fa-solid fa-link"); }; + const makePopupTools = (event: EventImpl) => { + if (event.source.internalEventSource.ui.classNames.indexOf("external") >= 0) { + return null; + } + if (!(this.canDelete || this.canModerate)) { + return null; + } + const newsId = this.getNewsId(event); + const div = document.createElement("div"); + if ( + this.canModerate && + event.source.internalEventSource.ui.classNames.indexOf("unmoderated") >= 0 + ) { + const button = document.createElement("button"); + button.innerHTML = `${gettext("Moderate")}`; + button.setAttribute("class", "btn btn-green"); + button.onclick = () => { + this.moderate(newsId); + }; + div.appendChild(button); + } + if (this.canDelete) { + const button = document.createElement("button"); + button.innerHTML = `${gettext("Delete")}`; + button.setAttribute("class", "btn btn-red"); + button.onclick = () => { + this.delete(newsId); + }; + div.appendChild(button); + } + + return makePopupInfo(div, "fa-solid fa-toolbox"); + }; + // Create new popup const popup = document.createElement("div"); const popupContainer = document.createElement("div"); @@ -131,6 +239,11 @@ export class IcsCalendar extends inheritHtmlElement("div") { popupContainer.appendChild(url); } + const tools = makePopupTools(event.event); + if (tools !== null) { + popupContainer.appendChild(tools); + } + popup.appendChild(popupContainer); // We can't just add the element relative to the one we want to appear under @@ -152,7 +265,6 @@ export class IcsCalendar extends inheritHtmlElement("div") { async connectedCallback() { super.connectedCallback(); - const cacheInvalidate = `?invalidate=${Date.now()}`; this.calendar = new Calendar(this.node, { plugins: [dayGridPlugin, iCalendarPlugin, listPlugin], locales: [frLocale, enLocale], @@ -160,16 +272,7 @@ export class IcsCalendar extends inheritHtmlElement("div") { locale: this.locale, initialView: this.currentView(), headerToolbar: this.currentToolbar(), - eventSources: [ - { - url: `${await makeUrl(calendarCalendarInternal)}${cacheInvalidate}`, - format: "ics", - }, - { - url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`, - format: "ics", - }, - ], + eventSources: await this.getEventSources(), windowResize: () => { this.calendar.changeView(this.currentView()); this.calendar.setOption("headerToolbar", this.currentToolbar()); diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index a3d39f25..620e2916 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -179,7 +179,11 @@ {% trans %}All coming events{% endtrans %} - +
From 92d282f4bace91ac3a9924540dc214d5ce4df228 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 25 Feb 2025 11:35:55 +0100 Subject: [PATCH 2/9] Add possibility to de-moderate news through api and calendar widget --- com/api.py | 12 +++++ .../com/components/ics-calendar-index.ts | 48 +++++++++++++------ core/static/core/style.scss | 14 ++++++ locale/fr/LC_MESSAGES/djangojs.po | 19 ++++++-- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/com/api.py b/com/api.py index c624e093..a8cedc6f 100644 --- a/com/api.py +++ b/com/api.py @@ -65,6 +65,18 @@ class NewsController(ControllerBase): news.moderator = self.context.request.user news.save() + @route.patch( + "/{int:news_id}/remove", + permissions=[HasPerm("com.moderate_news")], + url_name="remove_news", + ) + def remove_news(self, news_id: int): + news = self.get_object_or_exception(News, id=news_id) + if news.is_moderated: + news.is_moderated = False + news.moderator = self.context.request.user + news.save() + @route.delete( "/{int:news_id}", permissions=[HasPerm("com.delete_news")], diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 5e6818bf..742a679d 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -13,6 +13,7 @@ import { calendarCalendarUnmoderated, newsDeleteNews, newsModerateNews, + newsRemoveNews, } from "#openapi"; @registerComponent("ics-calendar") @@ -86,7 +87,7 @@ export class IcsCalendar extends inheritHtmlElement("div") { this.calendar.refetchEvents(); } - async moderate(id: number) { + async moderateNews(id: number) { await newsModerateNews({ path: { // biome-ignore lint/style/useNamingConvention: python API @@ -96,7 +97,17 @@ export class IcsCalendar extends inheritHtmlElement("div") { await this.refreshEvents(); } - async delete(id: number) { + async removeNews(id: number) { + await newsRemoveNews({ + path: { + // biome-ignore lint/style/useNamingConvention: python API + news_id: id, + }, + }); + await this.refreshEvents(); + } + + async deleteNews(id: number) { await newsDeleteNews({ path: { // biome-ignore lint/style/useNamingConvention: python API @@ -195,24 +206,33 @@ export class IcsCalendar extends inheritHtmlElement("div") { } const newsId = this.getNewsId(event); const div = document.createElement("div"); - if ( - this.canModerate && - event.source.internalEventSource.ui.classNames.indexOf("unmoderated") >= 0 - ) { - const button = document.createElement("button"); - button.innerHTML = `${gettext("Moderate")}`; - button.setAttribute("class", "btn btn-green"); - button.onclick = () => { - this.moderate(newsId); - }; - div.appendChild(button); + if (this.canModerate) { + if ( + event.source.internalEventSource.ui.classNames.indexOf("unmoderated") >= 0 + ) { + const button = document.createElement("button"); + button.innerHTML = `${gettext("Moderate")}`; + button.setAttribute("class", "btn btn-green"); + button.onclick = () => { + this.moderateNews(newsId); + }; + div.appendChild(button); + } else { + const button = document.createElement("button"); + button.innerHTML = `${gettext("Remove")}`; + button.setAttribute("class", "btn btn-orange"); + button.onclick = () => { + this.removeNews(newsId); + }; + div.appendChild(button); + } } if (this.canDelete) { const button = document.createElement("button"); button.innerHTML = `${gettext("Delete")}`; button.setAttribute("class", "btn btn-red"); button.onclick = () => { - this.delete(newsId); + this.deleteNews(newsId); }; div.appendChild(button); } diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 22c8a583..6b037995 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -272,6 +272,20 @@ body { } } + &.btn-orange { + background-color: #fcbf81; + color: black; + + &:not(:disabled):hover { + background-color: darken(#fcbf81, 15%); + } + + &:disabled { + background-color: lighten(#fcbf81, 15%); + color: grey; + } + } + &:not(.btn-no-text) { i { margin-right: 4px; diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 6d6a483d..2f99ae24 100644 --- a/locale/fr/LC_MESSAGES/djangojs.po +++ b/locale/fr/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-25 11:05+0100\n" +"POT-Creation-Date: 2025-02-25 14:38+0100\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n" "Last-Translator: Sli \n" "Language-Team: AE info \n" @@ -21,6 +21,19 @@ msgstr "" msgid "More info" msgstr "Plus d'informations" +#: com/static/bundled/com/components/ics-calendar-index.ts +msgid "Moderate" +msgstr "Modérer" + +#: com/static/bundled/com/components/ics-calendar-index.ts +#: core/static/bundled/core/components/ajax-select-base.ts +msgid "Remove" +msgstr "Retirer" + +#: com/static/bundled/com/components/ics-calendar-index.ts +msgid "Delete" +msgstr "Supprimer" + #: com/static/bundled/com/components/moderation-alert-index.ts #, javascript-format msgid "" @@ -32,10 +45,6 @@ msgstr "" "modérez ou supprimez cet événement, il sera également modéré (ou supprimé) " "pour les semaines suivantes." -#: core/static/bundled/core/components/ajax-select-base.ts -msgid "Remove" -msgstr "Retirer" - #: core/static/bundled/core/components/ajax-select-base.ts msgid "You need to type %(number)s more characters" msgstr "Vous devez taper %(number)s caractères de plus" From f9c36c8f99de3d80d4a2ebd8c2fbdd2448ff0dc7 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 24 Feb 2025 19:05:18 +0100 Subject: [PATCH 3/9] Apply review comments --- com/calendar.py | 8 ++++---- .../bundled/com/components/ics-calendar-index.ts | 10 +++++----- tsconfig.json | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/com/calendar.py b/com/calendar.py index dfe8fe6a..cf35a522 100644 --- a/com/calendar.py +++ b/com/calendar.py @@ -5,7 +5,7 @@ from typing import final import requests from dateutil.relativedelta import relativedelta from django.conf import settings -from django.db.models import QuerySet +from django.db.models import F, QuerySet from django.urls import reverse from django.utils import timezone from ical.calendar import Calendar @@ -80,11 +80,11 @@ class IcsCalendar: ) @classmethod - def ics_from_queryset(cls, queryset: QuerySet) -> bytes: + def ics_from_queryset(cls, queryset: QuerySet[NewsDate]) -> bytes: calendar = Calendar() - for news_date in queryset.prefetch_related("news"): + for news_date in queryset.annotate(news_title=F("news__title")): event = Event( - summary=news_date.news.title, + summary=news_date.news_title, start=news_date.start_date, end=news_date.end_date, url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 742a679d..21019a4c 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -68,7 +68,6 @@ export class IcsCalendar extends inheritHtmlElement("div") { } getNewsId(event: EventImpl) { - // return Number.parseInt(event.url.split("/").pop()); return Number.parseInt( event.url .toString() @@ -80,6 +79,9 @@ export class IcsCalendar extends inheritHtmlElement("div") { async refreshEvents() { this.click(); // Remove focus from popup + // We can't just refresh events because some ics files are in + // local browser cache (especially internal.ics) + // To invalidate the cache, we need to remove the source and add it again this.calendar.removeAllEventSources(); for (const source of await this.getEventSources()) { this.calendar.addEventSource(source); @@ -198,7 +200,7 @@ export class IcsCalendar extends inheritHtmlElement("div") { }; const makePopupTools = (event: EventImpl) => { - if (event.source.internalEventSource.ui.classNames.indexOf("external") >= 0) { + if (event.source.internalEventSource.ui.classNames.includes("external")) { return null; } if (!(this.canDelete || this.canModerate)) { @@ -207,9 +209,7 @@ export class IcsCalendar extends inheritHtmlElement("div") { const newsId = this.getNewsId(event); const div = document.createElement("div"); if (this.canModerate) { - if ( - event.source.internalEventSource.ui.classNames.indexOf("unmoderated") >= 0 - ) { + if (event.source.internalEventSource.ui.classNames.includes("unmoderated")) { const button = document.createElement("button"); button.innerHTML = `${gettext("Moderate")}`; button.setAttribute("class", "btn btn-green"); diff --git a/tsconfig.json b/tsconfig.json index aaee9330..a93da92a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "esModuleInterop": true, "resolveJsonModule": true, "types": ["jquery", "alpinejs"], + "lib": ["es7"], "paths": { "#openapi": ["./staticfiles/generated/openapi/index.ts"], "#core:*": ["./core/static/bundled/*"], From 2e71275f5bd898ed991937e67e9f07b4b3118d86 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 25 Feb 2025 15:35:01 +0100 Subject: [PATCH 4/9] Connect calendar moderation with outside moderation --- .../com/components/ics-calendar-index.ts | 24 +++++++++++++++++++ com/templates/com/news_list.jinja | 12 ++++++++++ 2 files changed, 36 insertions(+) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 21019a4c..036d6b2d 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -96,6 +96,14 @@ export class IcsCalendar extends inheritHtmlElement("div") { news_id: id, }, }); + this.dispatchEvent( + new CustomEvent("calendar-moderate", { + bubbles: true, + detail: { + id: id, + }, + }), + ); await this.refreshEvents(); } @@ -106,6 +114,14 @@ export class IcsCalendar extends inheritHtmlElement("div") { news_id: id, }, }); + this.dispatchEvent( + new CustomEvent("calendar-remove", { + bubbles: true, + detail: { + id: id, + }, + }), + ); await this.refreshEvents(); } @@ -116,6 +132,14 @@ export class IcsCalendar extends inheritHtmlElement("div") { news_id: id, }, }); + this.dispatchEvent( + new CustomEvent("calendar-delete", { + bubbles: true, + detail: { + id: id, + }, + }), + ); await this.refreshEvents(); } diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 620e2916..b52324ed 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -180,6 +180,18 @@ Date: Tue, 25 Feb 2025 18:08:16 +0100 Subject: [PATCH 5/9] Rename news moderate to publish --- com/api.py | 26 ++++---- com/calendar.py | 6 +- com/forms.py | 10 ++-- ...ove_news_is_moderated_news_is_published.py | 16 +++++ com/models.py | 16 ++--- com/schemas.py | 4 +- .../com/components/ics-calendar-index.ts | 32 +++++----- .../com/components/moderation-alert-index.ts | 16 ++--- com/templates/com/macros.jinja | 22 +++---- com/templates/com/news_admin_list.jinja | 16 ++--- com/templates/com/news_detail.jinja | 4 +- com/templates/com/news_edit.jinja | 6 +- com/templates/com/news_list.jinja | 20 +++---- com/tests/test_api.py | 18 +++--- com/tests/test_models.py | 2 +- com/tests/test_views.py | 8 +-- com/views.py | 6 +- core/management/commands/populate.py | 13 ++-- core/migrations/0044_alter_userban_options.py | 16 +++++ locale/fr/LC_MESSAGES/django.po | 59 ++++++++++--------- locale/fr/LC_MESSAGES/djangojs.po | 23 ++++---- 21 files changed, 186 insertions(+), 153 deletions(-) create mode 100644 com/migrations/0009_remove_news_is_moderated_news_is_published.py create mode 100644 core/migrations/0044_alter_userban_options.py diff --git a/com/api.py b/com/api.py index a8cedc6f..99186f36 100644 --- a/com/api.py +++ b/com/api.py @@ -40,13 +40,13 @@ class CalendarController(ControllerBase): return send_raw_file(IcsCalendar.get_internal()) @route.get( - "/unmoderated.ics", + "/unpublished.ics", permissions=[IsAuthenticated], - url_name="calendar_unmoderated", + url_name="calendar_unpublished", ) - def calendar_unmoderated(self): + def calendar_unpublished(self): return HttpResponse( - IcsCalendar.get_unmoderated(self.context.request.user), + IcsCalendar.get_unpublished(self.context.request.user), content_type="text/calendar", ) @@ -54,26 +54,26 @@ class CalendarController(ControllerBase): @api_controller("/news") class NewsController(ControllerBase): @route.patch( - "/{int:news_id}/moderate", + "/{int:news_id}/publish", permissions=[HasPerm("com.moderate_news")], url_name="moderate_news", ) - def moderate_news(self, news_id: int): + def publish_news(self, news_id: int): news = self.get_object_or_exception(News, id=news_id) - if not news.is_moderated: - news.is_moderated = True + if not news.is_published: + news.is_published = True news.moderator = self.context.request.user news.save() @route.patch( - "/{int:news_id}/remove", + "/{int:news_id}/unpublish", permissions=[HasPerm("com.moderate_news")], - url_name="remove_news", + url_name="unpublish_news", ) - def remove_news(self, news_id: int): + def unpublish_news(self, news_id: int): news = self.get_object_or_exception(News, id=news_id) - if news.is_moderated: - news.is_moderated = False + if news.is_published: + news.is_published = False news.moderator = self.context.request.user news.save() diff --git a/com/calendar.py b/com/calendar.py index cf35a522..1c95a2b3 100644 --- a/com/calendar.py +++ b/com/calendar.py @@ -63,7 +63,7 @@ class IcsCalendar: _ = f.write( cls.ics_from_queryset( NewsDate.objects.filter( - news__is_moderated=True, + news__is_published=True, end_date__gte=timezone.now() - (relativedelta(months=6)), ) ) @@ -71,10 +71,10 @@ class IcsCalendar: return cls._INTERNAL_CALENDAR @classmethod - def get_unmoderated(cls, user: User) -> bytes: + def get_unpublished(cls, user: User) -> bytes: return cls.ics_from_queryset( NewsDate.objects.viewable_by(user).filter( - news__is_moderated=False, + news__is_published=False, end_date__gte=timezone.now() - (relativedelta(months=6)), ), ) diff --git a/com/forms.py b/com/forms.py index 471e6632..8b81a3f9 100644 --- a/com/forms.py +++ b/com/forms.py @@ -147,8 +147,8 @@ class NewsForm(forms.ModelForm): "content": MarkdownInput, } - auto_moderate = forms.BooleanField( - label=_("Automoderation"), + auto_publish = forms.BooleanField( + label=_("Auto publication"), widget=CheckboxInput(attrs={"class": "switch"}), required=False, ) @@ -182,12 +182,12 @@ class NewsForm(forms.ModelForm): def save(self, commit: bool = True): # noqa FBT001 self.instance.author = self.author if (self.author.is_com_admin or self.author.is_root) and ( - self.cleaned_data.get("auto_moderate") is True + self.cleaned_data.get("auto_publish") is True ): - self.instance.is_moderated = True + self.instance.is_published = True self.instance.moderator = self.author else: - self.instance.is_moderated = False + self.instance.is_published = False created_news = super().save(commit=commit) self.date_form.save(commit=commit, news=created_news) return created_news diff --git a/com/migrations/0009_remove_news_is_moderated_news_is_published.py b/com/migrations/0009_remove_news_is_moderated_news_is_published.py new file mode 100644 index 00000000..dd633820 --- /dev/null +++ b/com/migrations/0009_remove_news_is_moderated_news_is_published.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("com", "0008_alter_news_options_alter_newsdate_options_and_more")] + + operations = [ + migrations.RenameField( + model_name="news", old_name="is_moderated", new_name="is_published" + ), + migrations.AlterField( + model_name="news", + name="is_published", + field=models.BooleanField(default=False, verbose_name="is published"), + ), + ] diff --git a/com/models.py b/com/models.py index 229d83c8..2b3a76c4 100644 --- a/com/models.py +++ b/com/models.py @@ -56,7 +56,7 @@ class Sith(models.Model): class NewsQuerySet(models.QuerySet): def moderated(self) -> Self: - return self.filter(is_moderated=True) + return self.filter(is_published=True) def viewable_by(self, user: User) -> Self: """Filter news that the given user can view. @@ -68,7 +68,7 @@ class NewsQuerySet(models.QuerySet): """ if user.has_perm("com.view_unmoderated_news"): return self - q_filter = Q(is_moderated=True) + q_filter = Q(is_published=True) if user.is_authenticated: q_filter |= Q(author_id=user.id) return self.filter(q_filter) @@ -104,7 +104,7 @@ class News(models.Model): verbose_name=_("author"), on_delete=models.PROTECT, ) - is_moderated = models.BooleanField(_("is moderated"), default=False) + is_published = models.BooleanField(_("is published"), default=False) moderator = models.ForeignKey( User, related_name="moderated_news", @@ -127,7 +127,7 @@ class News(models.Model): def save(self, *args, **kwargs): super().save(*args, **kwargs) - if self.is_moderated: + if self.is_published: return for user in User.objects.filter( groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID] @@ -154,7 +154,7 @@ class News(models.Model): def can_be_viewed_by(self, user: User): return ( - self.is_moderated + self.is_published or user.has_perm("com.view_unmoderated_news") or (user.is_authenticated and self.author_id == user.id) ) @@ -162,7 +162,7 @@ class News(models.Model): def news_notification_callback(notif): count = News.objects.filter( - dates__start_date__gt=timezone.now(), is_moderated=False + dates__start_date__gt=timezone.now(), is_published=False ).count() if count: notif.viewed = False @@ -182,7 +182,7 @@ class NewsDateQuerySet(models.QuerySet): """ if user.has_perm("com.view_unmoderated_news"): return self - q_filter = Q(news__is_moderated=True) + q_filter = Q(news__is_published=True) if user.is_authenticated: q_filter |= Q(news__author_id=user.id) return self.filter(q_filter) @@ -337,7 +337,7 @@ class Screen(models.Model): def active_posters(self): now = timezone.now() - return self.posters.filter(is_moderated=True, date_begin__lte=now).filter( + return self.posters.filter(d=True, date_begin__lte=now).filter( Q(date_end__isnull=True) | Q(date_end__gte=now) ) diff --git a/com/schemas.py b/com/schemas.py index 967076ad..93ee5315 100644 --- a/com/schemas.py +++ b/com/schemas.py @@ -15,14 +15,14 @@ class NewsDateFilterSchema(FilterSchema): after: datetime | None = Field(None, q="start_date__gt") club_id: int | None = Field(None, q="news__club_id") news_id: int | None = None - is_moderated: bool | None = Field(None, q="news__is_moderated") + is_published: bool | None = Field(None, q="news__is_published") title: str | None = Field(None, q="news__title__icontains") class NewsSchema(ModelSchema): class Meta: model = News - fields = ["id", "title", "summary", "is_moderated"] + fields = ["id", "title", "summary", "is_published"] club: ClubProfileSchema url: str diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 036d6b2d..0b4976b0 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -10,10 +10,10 @@ import listPlugin from "@fullcalendar/list"; import { calendarCalendarExternal, calendarCalendarInternal, - calendarCalendarUnmoderated, + calendarCalendarUnpublished, newsDeleteNews, - newsModerateNews, - newsRemoveNews, + newsPublishNews, + newsUnpublishNews, } from "#openapi"; @registerComponent("ics-calendar") @@ -89,15 +89,15 @@ export class IcsCalendar extends inheritHtmlElement("div") { this.calendar.refetchEvents(); } - async moderateNews(id: number) { - await newsModerateNews({ + async publishNews(id: number) { + await newsPublishNews({ path: { // biome-ignore lint/style/useNamingConvention: python API news_id: id, }, }); this.dispatchEvent( - new CustomEvent("calendar-moderate", { + new CustomEvent("calendar-publish", { bubbles: true, detail: { id: id, @@ -107,15 +107,15 @@ export class IcsCalendar extends inheritHtmlElement("div") { await this.refreshEvents(); } - async removeNews(id: number) { - await newsRemoveNews({ + async unpublishNews(id: number) { + await newsUnpublishNews({ path: { // biome-ignore lint/style/useNamingConvention: python API news_id: id, }, }); this.dispatchEvent( - new CustomEvent("calendar-remove", { + new CustomEvent("calendar-unpublish", { bubbles: true, detail: { id: id, @@ -157,10 +157,10 @@ export class IcsCalendar extends inheritHtmlElement("div") { className: "external", }, { - url: `${await makeUrl(calendarCalendarUnmoderated)}${cacheInvalidate}`, + url: `${await makeUrl(calendarCalendarUnpublished)}${cacheInvalidate}`, format: "ics", color: "red", - className: "unmoderated", + className: "unpublished", }, ]; } @@ -233,20 +233,20 @@ export class IcsCalendar extends inheritHtmlElement("div") { const newsId = this.getNewsId(event); const div = document.createElement("div"); if (this.canModerate) { - if (event.source.internalEventSource.ui.classNames.includes("unmoderated")) { + if (event.source.internalEventSource.ui.classNames.includes("unpublished")) { const button = document.createElement("button"); - button.innerHTML = `${gettext("Moderate")}`; + button.innerHTML = `${gettext("Publish")}`; button.setAttribute("class", "btn btn-green"); button.onclick = () => { - this.moderateNews(newsId); + this.publishNews(newsId); }; div.appendChild(button); } else { const button = document.createElement("button"); - button.innerHTML = `${gettext("Remove")}`; + button.innerHTML = `${gettext("Unpublish")}`; button.setAttribute("class", "btn btn-orange"); button.onclick = () => { - this.removeNews(newsId); + this.unpublishNews(newsId); }; div.appendChild(button); } diff --git a/com/static/bundled/com/components/moderation-alert-index.ts b/com/static/bundled/com/components/moderation-alert-index.ts index f4ecc4d1..1c9274dd 100644 --- a/com/static/bundled/com/components/moderation-alert-index.ts +++ b/com/static/bundled/com/components/moderation-alert-index.ts @@ -1,5 +1,5 @@ import { exportToHtml } from "#core:utils/globals"; -import { newsDeleteNews, newsFetchNewsDates, newsModerateNews } from "#openapi"; +import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi"; // This will be used in jinja templates, // so we cannot use real enums as those are purely an abstraction of Typescript @@ -7,7 +7,7 @@ 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, + PUBLISHED: 2, // biome-ignore lint/style/useNamingConvention: this feels more like an enum DELETED: 3, }; @@ -19,11 +19,11 @@ document.addEventListener("alpine:init", () => { newsId: newsId as number, loading: false, - async moderateNews() { + async publishNews() { this.loading = true; // biome-ignore lint/style/useNamingConvention: api is snake case - await newsModerateNews({ path: { news_id: this.newsId } }); - this.state = AlertState.MODERATED; + await newsPublishNews({ path: { news_id: this.newsId } }); + this.state = AlertState.PUBLISHED; this.$dispatch("news-moderated", { newsId: this.newsId, state: this.state }); this.loading = false; }, @@ -54,7 +54,7 @@ document.addEventListener("alpine:init", () => { * Query the server to know the number of news dates that would be moderated * if this one is moderated. */ - async nbToModerate(): Promise { + async nbToPublish(): Promise { // What we want here is the count attribute of the response. // We don't care about the actual results, // so we ask for the minimum page size possible. @@ -69,8 +69,8 @@ document.addEventListener("alpine:init", () => { return interpolate( gettext( "This event will take place every week for %s weeks. " + - "If you moderate or delete this event, " + - "it will also be moderated (or deleted) for the following weeks.", + "If you publish or delete this event, " + + "it will also be published (or deleted) for the following weeks.", ), [nbEvents], ); diff --git a/com/templates/com/macros.jinja b/com/templates/com/macros.jinja index dbff9188..f3713717 100644 --- a/com/templates/com/macros.jinja +++ b/com/templates/com/macros.jinja @@ -1,6 +1,6 @@ {% 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. + {# An alert to display on top of unpublished news, + with actions to either publish or delete them. The current state of the alert is accessible through the given `alpineState` variable. @@ -8,7 +8,7 @@ This comes in three flavours : - You can pass the `News` object itself to the macro. - In this case, if `request.user` can moderate news, + In this case, if `request.user` can publish news, it will perform an additional db query to know if it is a recurring event. - You can also give only the news id. In this case, a server request will be issued to know @@ -64,16 +64,16 @@ -