From 1d177412c3b035657deab093e645eee087a4f1d4 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 15 Feb 2025 18:44:20 +0100 Subject: [PATCH 1/8] change upcoming news selection on main page --- com/views.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/com/views.py b/com/views.py index 3990fe9b..7df0c1bd 100644 --- a/com/views.py +++ b/com/views.py @@ -22,7 +22,7 @@ # # import itertools -from datetime import timedelta +from datetime import date, timedelta from smtplib import SMTPRecipientsRefused from typing import Any @@ -254,9 +254,23 @@ class NewsListView(TemplateView): ) def get_news_dates(self): + """Return the event dates to display. + + The selected events are the ones that happens in the next 3 days + where something happen. + For example, if there are 6 events : A on 15/03, B and C on 17/03, + D on 20/03, E on 21/03 and F on 22/03 ; then the displayed dates + will be A, B, C and D. + """ + last_date: date = list( + NewsDate.objects.filter(end_date__gt=now()) + .order_by("start_date") + .values_list("start_date__date", flat=True) + .distinct()[:4] + )[-1] return itertools.groupby( NewsDate.objects.viewable_by(self.request.user) - .filter(end_date__gt=now(), start_date__lt=now() + timedelta(days=6)) + .filter(end_date__gt=now(), start_date__date__lte=last_date) .order_by("start_date") .select_related("news", "news__club"), key=lambda d: d.start_date.date(), From fc3b82c35cf38c83b7361972694e3cfd7c5ee5cd Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 16 Feb 2025 17:20:45 +0100 Subject: [PATCH 2/8] Make upcoming nws scrollable on y-overflow --- com/static/com/css/news-list.scss | 7 ++ com/templates/com/news_list.jinja | 114 +++++++++++++++--------------- 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss index 0df403b4..5b8cc8d5 100644 --- a/com/static/com/css/news-list.scss +++ b/com/static/com/css/news-list.scss @@ -51,6 +51,13 @@ } } + /* UPCOMING EVENTS */ + + #upcoming-events { + max-height: 600px; + overflow-y: scroll; + } + /* LINKS/BIRTHDAYS */ #links, #birthdays { diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index ebb4f6e1..beaae030 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -37,69 +37,71 @@
{% endif %} - {% for day, dates_group in news_dates %} -
-
-
-
{{ day|date('D') }}
-
{{ day|date('d') }}
-
{{ day|date('b') }}
+
+ {% for day, dates_group in news_dates %} +
+
+
+
{{ day|date('D') }}
+
{{ day|date('d') }}
+
{{ day|date('b') }}
+
-
-
- {% 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 %} -
+ {% 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 %} + + {% endfor %} +
-
- {% else %} -
- {% trans %}Nothing to come...{% endtrans %} -
- {% endfor %} + {% else %} +
+ {% trans %}Nothing to come...{% endtrans %} +
+ {% endfor %} +

{% trans %}All coming events{% endtrans %} From 86c2ea7fd960ec4844d5e5bd923123425a4ebf74 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 17 Feb 2025 13:29:12 +0100 Subject: [PATCH 3/8] API route to fetch news dates --- club/schemas.py | 14 +++++++++ com/api.py | 30 ++++++++++++++++--- com/schemas.py | 58 +++++++++++++++++++++++++++++++++++ com/tests/test_api.py | 70 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 com/schemas.py diff --git a/club/schemas.py b/club/schemas.py index cbd35988..7969f119 100644 --- a/club/schemas.py +++ b/club/schemas.py @@ -7,3 +7,17 @@ class ClubSchema(ModelSchema): class Meta: model = Club fields = ["id", "name"] + + +class ClubProfileSchema(ModelSchema): + """The infos needed to display a simple club profile.""" + + class Meta: + model = Club + fields = ["id", "name", "logo"] + + url: str + + @staticmethod + def resolve_url(obj: Club) -> str: + return obj.get_absolute_url() diff --git a/com/api.py b/com/api.py index 4a4d0e17..5a3eef10 100644 --- a/com/api.py +++ b/com/api.py @@ -1,11 +1,16 @@ from pathlib import Path +from typing import Literal from django.conf import settings from django.http import Http404 -from ninja_extra import ControllerBase, api_controller, route +from ninja import Query +from ninja_extra import ControllerBase, api_controller, paginate, route +from ninja_extra.pagination import PageNumberPaginationExtra +from ninja_extra.schemas import PaginatedResponseSchema from com.calendar import IcsCalendar -from com.models import News +from com.models import News, NewsDate +from com.schemas import NewsDateFilterSchema, NewsDateSchema from core.auth.api_permissions import HasPerm from core.views.files import send_raw_file @@ -37,7 +42,7 @@ class CalendarController(ControllerBase): @api_controller("/news") class NewsController(ControllerBase): @route.patch( - "/{news_id}/moderate", + "/{int:news_id}/moderate", permissions=[HasPerm("com.moderate_news")], url_name="moderate_news", ) @@ -49,10 +54,27 @@ class NewsController(ControllerBase): news.save() @route.delete( - "/{news_id}", + "/{int: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() + + @route.get( + "/date", + url_name="fetch_news_dates", + response=PaginatedResponseSchema[NewsDateSchema], + ) + @paginate(PageNumberPaginationExtra, page_size=50) + def fetch_news_dates( + self, + filters: Query[NewsDateFilterSchema], + text_format: Literal["md", "html"] = "md", + ): + return filters.filter( + NewsDate.objects.viewable_by(self.context.request.user) + .order_by("start_date") + .select_related("news", "news__club") + ) diff --git a/com/schemas.py b/com/schemas.py new file mode 100644 index 00000000..967076ad --- /dev/null +++ b/com/schemas.py @@ -0,0 +1,58 @@ +from datetime import datetime + +from ninja import FilterSchema, ModelSchema +from ninja_extra import service_resolver +from ninja_extra.controllers import RouteContext +from pydantic import Field + +from club.schemas import ClubProfileSchema +from com.models import News, NewsDate +from core.markdown import markdown + + +class NewsDateFilterSchema(FilterSchema): + before: datetime | None = Field(None, q="end_date__lt") + 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") + title: str | None = Field(None, q="news__title__icontains") + + +class NewsSchema(ModelSchema): + class Meta: + model = News + fields = ["id", "title", "summary", "is_moderated"] + + club: ClubProfileSchema + url: str + + @staticmethod + def resolve_summary(obj: News) -> str: + # if this is returned from a route that allows the + # user to choose the text format (md or html) + # and the user chose "html", convert the markdown to html + context: RouteContext = service_resolver(RouteContext) + if context.kwargs.get("text_format", "") == "html": + return markdown(obj.summary) + return obj.summary + + @staticmethod + def resolve_url(obj: News) -> str: + return obj.get_absolute_url() + + +class NewsDateSchema(ModelSchema): + """Basic infos about an event occurrence. + + Warning: + This uses [NewsSchema][], which itself + uses [ClubProfileSchema][club.schemas.ClubProfileSchema]. + Don't forget the appropriated `select_related`. + """ + + class Meta: + model = NewsDate + fields = ["id", "start_date", "end_date"] + + news: NewsSchema diff --git a/com/tests/test_api.py b/com/tests/test_api.py index da1419da..257ee056 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -3,18 +3,22 @@ from datetime import datetime, timedelta from pathlib import Path from typing import Callable from unittest.mock import MagicMock, patch +from urllib.parse import quote 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.test import Client, TestCase from django.urls import reverse from django.utils import timezone -from model_bakery import baker +from django.utils.timezone import now +from model_bakery import baker, seq +from pytest_django.asserts import assertNumQueries from com.calendar import IcsCalendar -from com.models import News +from com.models import News, NewsDate +from core.markdown import markdown from core.models import User @@ -184,3 +188,63 @@ class TestDeleteNews: ) assert response.status_code == 403 assert News.objects.filter(id=news.id).exists() + + +class TestFetchNewsDates(TestCase): + @classmethod + def setUpTestData(cls): + News.objects.all().delete() + cls.dates = baker.make( + NewsDate, + _quantity=5, + _bulk_create=True, + start_date=seq(value=now(), increment_by=timedelta(days=1)), + end_date=seq( + value=now() + timedelta(hours=2), increment_by=timedelta(days=1) + ), + news=iter( + baker.make(News, is_moderated=True, _quantity=5, _bulk_create=True) + ), + ) + cls.dates.append( + baker.make( + NewsDate, + start_date=now() + timedelta(days=2, hours=1), + end_date=now() + timedelta(days=2, hours=5), + news=baker.make(News, is_moderated=True), + ) + ) + cls.dates.sort(key=lambda d: d.start_date) + + def test_num_queries(self): + with assertNumQueries(2): + self.client.get(reverse("api:fetch_news_dates")) + + def test_html_format(self): + """Test that when the summary is asked in html, the summary is in html.""" + summary_1 = "# First event\nThere is something happening.\n" + self.dates[0].news.summary = summary_1 + self.dates[0].news.save() + summary_2 = ( + "# Second event\n" + "There is something happening **for real**.\n" + "Everything is [here](https://youtu.be/dQw4w9WgXcQ)\n" + ) + self.dates[1].news.summary = summary_2 + self.dates[1].news.save() + response = self.client.get( + reverse("api:fetch_news_dates") + "?page_size=2&text_format=html" + ) + assert response.status_code == 200 + dates = response.json()["results"] + assert dates[0]["news"]["summary"] == markdown(summary_1) + assert dates[1]["news"]["summary"] == markdown(summary_2) + + def test_fetch(self): + after = quote((now() + timedelta(days=1)).isoformat()) + response = self.client.get( + reverse("api:fetch_news_dates") + f"?page_size=3&after={after}" + ) + assert response.status_code == 200 + dates = response.json()["results"] + assert [d["id"] for d in dates] == [d.id for d in self.dates[1:4]] From 0e88260c31529a560429ff838672b15d35d21287 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 19 Feb 2025 16:51:55 +0100 Subject: [PATCH 4/8] fix news dates timestamp in populate.py --- core/management/commands/populate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 53638699..82dd2516 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -169,7 +169,7 @@ class Command(BaseCommand): Weekmail().save() # Here we add a lot of test datas, that are not necessary for the Sith, but that provide a basic development environment - self.now = timezone.now().replace(hour=12) + self.now = timezone.now().replace(hour=12, second=0) skia = User.objects.create_user( username="skia", @@ -681,7 +681,7 @@ Welcome to the wiki page! friday = self.now while friday.weekday() != 4: friday += timedelta(hours=6) - friday.replace(hour=20, minute=0, second=0) + friday.replace(hour=20, minute=0) # Event news_dates = [] n = News.objects.create( From 2def57d82cf99ffb07af02820b7847eec27a1a65 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 19 Feb 2025 19:28:50 +0100 Subject: [PATCH 5/8] Close alerts related to a moderated event --- .../com/components/moderation-alert-index.ts | 32 +++++++- com/templates/com/macros.jinja | 75 +++++++++++++++---- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/com/static/bundled/com/components/moderation-alert-index.ts b/com/static/bundled/com/components/moderation-alert-index.ts index e11290ad..02f3c564 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, newsModerateNews } from "#openapi"; +import { newsDeleteNews, newsFetchNewsDates, newsModerateNews } from "#openapi"; // This will be used in jinja templates, // so we cannot use real enums as those are purely an abstraction of Typescript @@ -24,6 +24,7 @@ document.addEventListener("alpine:init", () => { // biome-ignore lint/style/useNamingConvention: api is snake case await newsModerateNews({ path: { news_id: this.newsId } }); this.state = AlertState.MODERATED; + this.$dispatch("news-moderated", { newsId: this.newsId, state: this.state }); this.loading = false; }, @@ -32,7 +33,36 @@ document.addEventListener("alpine:init", () => { // biome-ignore lint/style/useNamingConvention: api is snake case await newsDeleteNews({ path: { news_id: this.newsId } }); this.state = AlertState.DELETED; + this.$dispatch("news-moderated", { newsId: this.newsId, state: this.state }); this.loading = false; }, + + /** + * Event receiver for when news dates are moderated. + * + * If the moderated date is linked to the same news + * as the one this moderation alert is attached to, + * then set the alert state to the same as the moderated one. + */ + dispatchModeration(event: CustomEvent) { + if (event.detail.newsId === this.newsId) { + this.state = event.detail.state; + } + }, + + /** + * Query the server to know the number of news dates that would be moderated + * if this one is moderated. + */ + async nbModerated() { + // 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. + const response = await newsFetchNewsDates({ + // biome-ignore lint/style/useNamingConvention: api is snake-case + query: { news_id: this.newsId, page: 1, page_size: 1 }, + }); + return response.data.count; + }, })); }); diff --git a/com/templates/com/macros.jinja b/com/templates/com/macros.jinja index 46f4b7c4..d851b52f 100644 --- a/com/templates/com/macros.jinja +++ b/com/templates/com/macros.jinja @@ -6,15 +6,41 @@ the given `alpineState` variable. This state is a `AlertState`, as defined in `moderation-alert-index.ts` - Example : + This comes in three flavours : + - You can pass the `News` object itself to the macro. + In this case, if `request.user` can moderate 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 + if it is a recurring event. + - Finally, you can pass the name of an alpine variable, which value is the id. + In this case, a server request will be issued to know + if it is a recurring event. + + Example with full `News` object : ```jinja
{{ news_moderation_alert(news, user, "state") }}
``` + With an id : + ```jinja +
+ {{ news_moderation_alert(news.id, user, "state") }} +
+ ``` + An with an alpine variable + ```jinja +
+ {{ news_moderation_alert("newsId", user, "state") }} +
+ ``` + Args: - news: The `News` object to which this alert is related + news: (News | int | string) + Either the `News` object to which this alert is related, + or its id, or the name of an Alpine which value is its id user: The request.user alpineState: An alpine variable name @@ -23,7 +49,13 @@ in your template. #}
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") %} From 71b358857735393968c9b7ea18fb2ea7485cd4b0 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 19 Feb 2025 19:29:18 +0100 Subject: [PATCH 6/8] Add a "see more" button on news dates list --- .../components/upcoming-news-loader-index.ts | 67 ++++++ com/static/com/css/news-list.scss | 7 + com/templates/com/news_list.jinja | 192 ++++++++++++------ com/views.py | 26 ++- locale/fr/LC_MESSAGES/django.po | 18 +- sith/settings.py | 1 + 6 files changed, 239 insertions(+), 72 deletions(-) create mode 100644 com/static/bundled/com/components/upcoming-news-loader-index.ts diff --git a/com/static/bundled/com/components/upcoming-news-loader-index.ts b/com/static/bundled/com/components/upcoming-news-loader-index.ts new file mode 100644 index 00000000..ccc1e714 --- /dev/null +++ b/com/static/bundled/com/components/upcoming-news-loader-index.ts @@ -0,0 +1,67 @@ +import { type NewsDateSchema, newsFetchNewsDates } from "#openapi"; + +interface ParsedNewsDateSchema extends Omit { + // biome-ignore lint/style/useNamingConvention: api is snake_case + start_date: Date; + // biome-ignore lint/style/useNamingConvention: api is snake_case + end_date: Date; +} + +document.addEventListener("alpine:init", () => { + Alpine.data("upcomingNewsLoader", (startDate: Date) => ({ + startDate: startDate, + currentPage: 1, + pageSize: 6, + hasNext: true, + loading: false, + newsDates: [] as NewsDateSchema[], + + async loadMore() { + this.loading = true; + const response = await newsFetchNewsDates({ + query: { + after: this.startDate.toISOString(), + // biome-ignore lint/style/useNamingConvention: api is snake_case + text_format: "html", + page: this.currentPage, + // biome-ignore lint/style/useNamingConvention: api is snake_case + page_size: this.pageSize, + }, + }); + if (response.response.status === 404) { + this.hasNext = false; + } else if (response.data.next === null) { + this.newsDates.push(...response.data.results); + this.hasNext = false; + } else { + this.newsDates.push(...response.data.results); + this.currentPage += 1; + } + this.loading = false; + }, + + groupedDates(): Record { + return this.newsDates + .map( + (date: NewsDateSchema): ParsedNewsDateSchema => ({ + ...date, + // biome-ignore lint/style/useNamingConvention: api is snake_case + start_date: new Date(date.start_date), + // biome-ignore lint/style/useNamingConvention: api is snake_case + end_date: new Date(date.end_date), + }), + ) + .reduce( + (acc: Record, date: ParsedNewsDateSchema) => { + const key = date.start_date.toDateString(); + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(date); + return acc; + }, + {}, + ); + }, + })); +}); diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss index 5b8cc8d5..40da2157 100644 --- a/com/static/com/css/news-list.scss +++ b/com/static/com/css/news-list.scss @@ -56,6 +56,13 @@ #upcoming-events { max-height: 600px; overflow-y: scroll; + + #load-more-news-button { + text-align: center; + button { + width: 150px; + } + } } /* LINKS/BIRTHDAYS */ diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index beaae030..a3d39f25 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -16,6 +16,7 @@ {% block additional_js %} + {% endblock %} {% block content %} @@ -38,69 +39,140 @@
{% endif %}
- {% for day, dates_group in news_dates %} -
-
-
-
{{ day|date('D') }}
-
{{ day|date('d') }}
-
{{ day|date('b') }}
-
-
-
- {% 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 }} - {% else %} - {{ date.news.club }} - {% endif %} -
-

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

- {{ date.news.club }} -
- - - -
-
-
-
- {{ date.news.summary|markdown }} -
-
-
- {% endfor %} -
-
- {% else %} + {% if not news_dates %}
{% trans %}Nothing to come...{% endtrans %}
- {% endfor %} + {% else %} + {% for day, dates_group in news_dates %} +
+
+
+
{{ day|date('D') }}
+
{{ day|date('d') }}
+
{{ day|date('b') }}
+
+
+
+ {% 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 }} + {% else %} + {{ date.news.club }} + {% endif %} +
+

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

+ {{ date.news.club }} +
+ - + +
+
+
+
+ {{ date.news.summary|markdown }} +
+
+
+ {% endfor %} +
+
+ {% endfor %} +
+ + +
+ +

+ + {% trans trimmed %} + It was too short. + You already reached the end of the upcoming events list. + {% endtrans %} + +

+
+
+ {% endif %}

diff --git a/com/views.py b/com/views.py index 7df0c1bd..ac29eb4a 100644 --- a/com/views.py +++ b/com/views.py @@ -253,33 +253,41 @@ class NewsListView(TemplateView): key=lambda u: u.date_of_birth.year, ) - def get_news_dates(self): - """Return the event dates to display. + def get_last_day(self) -> date: + """Get the last day when news will be displayed - The selected events are the ones that happens in the next 3 days - where something happen. + The returned day is the third one where something happen. For example, if there are 6 events : A on 15/03, B and C on 17/03, - D on 20/03, E on 21/03 and F on 22/03 ; then the displayed dates - will be A, B, C and D. + D on 20/03, E on 21/03 and F on 22/03 ; + then the result is 20/03. """ - last_date: date = list( + return list( NewsDate.objects.filter(end_date__gt=now()) .order_by("start_date") .values_list("start_date__date", flat=True) .distinct()[:4] )[-1] + + def get_news_dates(self, until: date): + """Return the event dates to display. + + The selected events are the ones that happens between + right now and the given day (included). + """ return itertools.groupby( NewsDate.objects.viewable_by(self.request.user) - .filter(end_date__gt=now(), start_date__date__lte=last_date) + .filter(end_date__gt=now(), start_date__date__lte=until) .order_by("start_date") .select_related("news", "news__club"), key=lambda d: d.start_date.date(), ) def get_context_data(self, **kwargs): + last_day = self.get_last_day() return super().get_context_data(**kwargs) | { - "news_dates": self.get_news_dates(), + "news_dates": self.get_news_dates(until=last_day), "birthdays": self.get_birthdays(), + "last_day": last_day, } diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index ea259ce0..bf44d1d5 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-02-18 15:03+0100\n" +"POT-Creation-Date: 2025-02-19 19:12+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -1429,11 +1429,11 @@ msgstr "" #: com/templates/com/macros.jinja #, python-format msgid "" -"This event will take place every week for %(nb)s weeks. If you moderate or " +"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." msgstr "" -"Cet événement se déroulera chaque semaine pendant %(nb)s semaines. Si vous " +"Cet événement se déroulera chaque semaine pendant %%s semaines. Si vous " "modérez ou supprimez cet événement, il sera également modéré (ou supprimé) " "pour les semaines suivantes." @@ -1578,6 +1578,18 @@ msgstr "Administrer les news" msgid "Nothing to come..." msgstr "Rien à venir..." +#: com/templates/com/news_list.jinja +msgid "See more" +msgstr "Voir plus" + +#: com/templates/com/news_list.jinja +msgid "" +"It was too short. You already reached the end of the upcoming events " +"list." +msgstr "" +"C'était trop court. Vous êtes déjà arrivés à la fin de la liste des " +"événements à venir." + #: com/templates/com/news_list.jinja msgid "All coming events" msgstr "Tous les événements à venir" diff --git a/sith/settings.py b/sith/settings.py index 9cd3ca89..8191251f 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -171,6 +171,7 @@ TEMPLATES = [ "timezone": "django.utils.timezone", "get_sith": "com.views.sith", "get_language": "django.utils.translation.get_language", + "timedelta": "datetime.timedelta", }, "bytecode_cache": { "name": "default", From 94d2c5660ad14fe4759e75c1009f0f6764caf6a6 Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 25 Feb 2025 11:10:05 +0100 Subject: [PATCH 7/8] move hybrid translation to full front translation --- .../com/components/moderation-alert-index.ts | 11 ++++++++ com/templates/com/macros.jinja | 12 +-------- locale/fr/LC_MESSAGES/django.po | 26 +++++++++---------- locale/fr/LC_MESSAGES/djangojs.po | 21 +++++++++++---- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/com/static/bundled/com/components/moderation-alert-index.ts b/com/static/bundled/com/components/moderation-alert-index.ts index 02f3c564..9987670c 100644 --- a/com/static/bundled/com/components/moderation-alert-index.ts +++ b/com/static/bundled/com/components/moderation-alert-index.ts @@ -64,5 +64,16 @@ document.addEventListener("alpine:init", () => { }); return response.data.count; }, + + weeklyEventWarningMessage(nbEvents: number): string { + 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.", + ), + [nbEvents], + ); + }, })); }); diff --git a/com/templates/com/macros.jinja b/com/templates/com/macros.jinja index d851b52f..5cda00b7 100644 --- a/com/templates/com/macros.jinja +++ b/com/templates/com/macros.jinja @@ -92,17 +92,7 @@

{% trans %}Weekly event{% endtrans %} - {# hybrid translation : the text is translated server-side, - but the interpolation of `nbEvents` is done client-side. #} -

+

diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index bf44d1d5..d27dccd6 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-02-19 19:12+0100\n" +"POT-Creation-Date: 2025-02-25 11:04+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -1426,17 +1426,6 @@ 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 %%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 %%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 @@ -1584,8 +1573,7 @@ msgstr "Voir plus" #: com/templates/com/news_list.jinja msgid "" -"It was too short. You already reached the end of the upcoming events " -"list." +"It was too short. You already reached the end of the upcoming events list." msgstr "" "C'était trop court. Vous êtes déjà arrivés à la fin de la liste des " "événements à venir." @@ -6043,3 +6031,13 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée." #, python-format msgid "Maximum characters: %(max_length)s" msgstr "Nombre de caractères max: %(max_length)s" + +#, python-format +#~ msgid "" +#~ "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." +#~ msgstr "" +#~ "Cet événement se déroulera chaque semaine pendant %%s semaines. Si vous " +#~ "modérez ou supprimez cet événement, il sera également modéré (ou " +#~ "supprimé) pour les semaines suivantes." diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 28177988..6d6a483d 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-01-08 12:23+0100\n" +"POT-Creation-Date: 2025-02-25 11:05+0100\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n" "Last-Translator: Sli \n" "Language-Team: AE info \n" @@ -21,6 +21,17 @@ msgstr "" msgid "More info" msgstr "Plus d'informations" +#: com/static/bundled/com/components/moderation-alert-index.ts +#, javascript-format +msgid "" +"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." +msgstr "" +"Cet événement se déroulera chaque semaine pendant %s semaines. Si vous " +"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" @@ -125,10 +136,6 @@ msgstr "Montrer plus" msgid "family_tree.%(extension)s" msgstr "arbre_genealogique.%(extension)s" -#: core/static/bundled/user/pictures-index.js -msgid "pictures.%(extension)s" -msgstr "photos.%(extension)s" - #: core/static/user/js/user_edit.js #, javascript-format msgid "captured.%s" @@ -187,6 +194,10 @@ msgstr "La réorganisation des types de produit a échoué avec le code : %d" msgid "Incorrect value" msgstr "Valeur incorrecte" +#: sas/static/bundled/sas/pictures-download-index.ts +msgid "pictures.%(extension)s" +msgstr "photos.%(extension)s" + #: sas/static/bundled/sas/viewer-index.ts msgid "Couldn't moderate picture" msgstr "Il n'a pas été possible de modérer l'image" From 01c92feb40761096aff05d48fb5356c78031c460 Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 25 Feb 2025 11:53:02 +0100 Subject: [PATCH 8/8] fix warning message display on subsequently loaded news --- com/static/bundled/com/components/moderation-alert-index.ts | 2 +- com/templates/com/macros.jinja | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/com/static/bundled/com/components/moderation-alert-index.ts b/com/static/bundled/com/components/moderation-alert-index.ts index 9987670c..f4ecc4d1 100644 --- a/com/static/bundled/com/components/moderation-alert-index.ts +++ b/com/static/bundled/com/components/moderation-alert-index.ts @@ -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 nbModerated() { + async nbToModerate(): 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. diff --git a/com/templates/com/macros.jinja b/com/templates/com/macros.jinja index 5cda00b7..dbff9188 100644 --- a/com/templates/com/macros.jinja +++ b/com/templates/com/macros.jinja @@ -83,7 +83,8 @@ so it's still reasonable #}