diff --git a/club/templates/club/club_news_list.jinja b/club/templates/club/club_news_list.jinja new file mode 100644 index 00000000..2087bcc3 --- /dev/null +++ b/club/templates/club/club_news_list.jinja @@ -0,0 +1,79 @@ +{% extends "core/base.jinja" %} + +{% block content %} + {% if club.has_rights_in_club(user) %} + + {% trans %}Create news{% endtrans %} + + {% endif %} + + {% for semester_code, semester_events in events %} +

{{ semester_code }}

+ {% for event in semester_events %} +
+ {{ event.title }} +
+ + {# Events that occur weekly from the start to the end of the semester + would take a lot of space if shown by default. + Thus, if the event has more than one date, those are + hidden by a details element. + If there is only one date, it is directly displayed. #} + {% set dates = event.dates.all()|list %} + {% if dates|length == 1 %} + + - + + + {% else %} +
+ + {% trans trimmed nb_occurrences=dates|length %} + Dates ({{ nb_occurrences }} occurrences) + {% endtrans %} + +
    + {% for d in dates %} +
  • + - + +
  • + {% endfor %} +
+
+ {% endif %} + +

{% trans %}Permalink{% endtrans %}

+

{{ request.build_absolute_uri(event.get_absolute_url())|urlize }}

+ +

{% trans %}Summary{% endtrans %}

+ {% if event.summary %} + {{ event.summary|markdown }} + {% else %} +

{% trans %}No summary{% endtrans %}

+ {% endif %} + +

{% trans %}Description{% endtrans %}

+ {% if event.content %} + {{ event.content|markdown }} + {% else %} +

{% trans %}No detailed description{% endtrans %}

+ {% endif %} +
+
+ {% endfor %} +
+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/club/tests/test_club_news.py b/club/tests/test_club_news.py new file mode 100644 index 00000000..75d58cb8 --- /dev/null +++ b/club/tests/test_club_news.py @@ -0,0 +1,60 @@ +from datetime import datetime + +from django.test import RequestFactory, TestCase +from django.urls import reverse +from django.utils.timezone import get_current_timezone +from model_bakery import baker + +from club.models import Club +from club.views import ClubNewsView +from com.models import News, NewsDate +from core.baker_recipes import subscriber_user +from core.models import User + + +class TestClubNews(TestCase): + @classmethod + def setUpTestData(cls): + cls.club = baker.make(Club) + cls.news = baker.make( + News, club=cls.club, is_published=True, author=baker.make(User), _quantity=5 + ) + d = datetime(year=2025, month=10, day=1, tzinfo=get_current_timezone()) + dates = [ + baker.prepare(NewsDate, news=cls.news[0], start_date=d), + baker.prepare(NewsDate, news=cls.news[0], start_date=d.replace(day=8)), + baker.prepare(NewsDate, news=cls.news[0], start_date=d.replace(day=15)), + baker.prepare(NewsDate, news=cls.news[1], start_date=d.replace(month=5)), + baker.prepare(NewsDate, news=cls.news[2], start_date=d.replace(month=4)), + baker.prepare(NewsDate, news=cls.news[3], start_date=d.replace(year=2024)), + baker.prepare(NewsDate, news=cls.news[4], start_date=d.replace(year=2020)), + ] + NewsDate.objects.bulk_create(dates) + cls.url = reverse("club:news_list", kwargs={"club_id": cls.club.id}) + + def test_load_page(self): + """Just test that the page loads.""" + self.client.force_login(subscriber_user.make()) + res = self.client.get(self.url) + assert res.status_code == 200 + + def test_page_data(self): + """Test that we display the good events.""" + # For this test, the object we want to test + # is built with `itertools.groupby`. + # However, it is consumed when rendering the template, + # making it unusable for later testing purposes, + # so we must use lower level django APIs in this test. + request = RequestFactory().get(self.url) + request.user = subscriber_user.make() + response = ClubNewsView.as_view()(request, club_id=self.club.id) + events = [ + (semester_code, list(dates)) + for semester_code, dates in response.context_data["events"] + ] + assert events == [ + ("A25", [self.news[0]]), + ("P25", [self.news[1], self.news[2]]), + ("A24", [self.news[3]]), + ("A20", [self.news[4]]), + ] diff --git a/club/urls.py b/club/urls.py index 6a08bbe5..d5b9f1a5 100644 --- a/club/urls.py +++ b/club/urls.py @@ -31,6 +31,7 @@ from club.views import ( ClubListView, ClubMailingView, ClubMembersView, + ClubNewsView, ClubOldMembersView, ClubPageEditView, ClubPageHistView, @@ -136,4 +137,5 @@ urlpatterns = [ PosterDeleteView.as_view(), name="poster_delete", ), + path("/news/", ClubNewsView.as_view(), name="news_list"), ] diff --git a/club/views.py b/club/views.py index 77e59721..fcf63f6a 100644 --- a/club/views.py +++ b/club/views.py @@ -37,6 +37,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError from django.core.paginator import InvalidPage, Paginator from django.db.models import F, Prefetch, Q, Sum +from django.db.models.expressions import OuterRef, Subquery from django.db.models.functions import Length from django.http import Http404, StreamingHttpResponse from django.shortcuts import get_object_or_404, redirect @@ -71,7 +72,7 @@ from club.models import ( MailingSubscription, Membership, ) -from com.models import Poster +from com.models import NewsDate, Poster from com.views import ( PosterCreateBaseView, PosterDeleteBaseView, @@ -80,6 +81,7 @@ from com.views import ( ) from core.auth.mixins import CanEditMixin, PermissionOrClubBoardRequiredMixin from core.models import Page, PageRev +from core.utils import get_semester_code from core.views import BasePageEditView, DetailFormView, UseFragmentsMixin from core.views.mixins import ( AllowFragment, @@ -134,6 +136,11 @@ class ClubTabsMixin(TabedViewMixin): "slug": "history", "name": _("History"), }, + { + "url": reverse("club:news_list", kwargs={"club_id": self.object.id}), + "slug": "events", + "name": _("Events"), + }, ] if self.request.user.can_edit(self.object): tab_list.extend( @@ -935,3 +942,30 @@ class PosterDeleteView(ClubTabsMixin, PosterDeleteBaseView): def get_success_url(self): return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) + + +class ClubNewsView(PermissionRequiredMixin, ClubTabsMixin, DetailView): + pk_url_kwarg = "club_id" + model = Club + permission_required = "club.view_club" + template_name = "club/club_news_list.jinja" + current_tab = "events" + + def get_context_data(self, **kwargs: Any): + events = list( + self.object.news.viewable_by(self.request.user) + .annotate( + first_date=Subquery( + NewsDate.objects.filter(news_id=OuterRef("pk")) + .order_by("start_date") + .values("start_date")[:1] + ), + ) + .prefetch_related("dates") + .order_by("-first_date") + ) + return super().get_context_data(**kwargs) | { + "events": itertools.groupby( + events, key=lambda event: get_semester_code(event.first_date.date()) + ), + } diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 978c8cba..133b4895 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: 2026-10-19 19:45+0200\n" +"POT-Creation-Date: 2026-10-25 12:04+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -394,7 +394,8 @@ msgstr "" "de la présidence." #: club/models.py core/models.py counter/models.py eboutic/models.py -#: election/models.py pedagogy/models.py sas/models.py trombi/models.py +#: election/models.py pedagogy/models.py sas/models.py timetable/models.py +#: trombi/models.py msgid "user" msgstr "utilisateur" @@ -580,6 +581,7 @@ msgid "Role" msgstr "Rôle" #: club/templates/club/club_members.jinja +#: club/templates/club/club_news_list.jinja #: club/templates/club/club_old_members.jinja #: core/templates/core/group_list.jinja core/templates/core/user_clubs.jinja msgid "Description" @@ -594,6 +596,34 @@ msgstr "Depuis" msgid "There are no members in this club." msgstr "Il n'y a pas de membres dans ce club." +#: club/templates/club/club_news_list.jinja +#: com/templates/com/news_admin_list.jinja com/templates/com/news_edit.jinja +#: com/templates/com/news_list.jinja core/templates/core/user_tools.jinja +msgid "Create news" +msgstr "Créer une nouvelle" + +#: club/templates/club/club_news_list.jinja +#, python-format +msgid "Dates (%(nb_occurrences)s occurrences)" +msgstr "Dates (%(nb_occurrences)s occurrences)" + +#: club/templates/club/club_news_list.jinja +msgid "Permalink" +msgstr "Permalien" + +#: club/templates/club/club_news_list.jinja +#: com/templates/com/news_admin_list.jinja +msgid "Summary" +msgstr "Résumé" + +#: club/templates/club/club_news_list.jinja +msgid "No summary" +msgstr "Pas de Résumé" + +#: club/templates/club/club_news_list.jinja +msgid "No detailed description" +msgstr "Pas de description détaillée" + #: club/templates/club/club_old_members.jinja msgid "Club old members" msgstr "Anciens membres du club" @@ -1030,6 +1060,11 @@ msgstr "Anciens membres" msgid "History" msgstr "Historique" +#: club/views.py com/templates/com/news_admin_list.jinja +#: core/templates/core/base/navbar.jinja +msgid "Events" +msgstr "Événements" + #: club/views.py core/templates/core/base/header.jinja core/views/user.py #: sas/templates/sas/picture.jinja trombi/views.py msgid "Tools" @@ -1317,11 +1352,6 @@ msgstr "Administration des nouvelles" msgid "News" msgstr "Nouvelles" -#: com/templates/com/news_admin_list.jinja com/templates/com/news_edit.jinja -#: com/templates/com/news_list.jinja core/templates/core/user_tools.jinja -msgid "Create news" -msgstr "Créer une nouvelle" - #: com/templates/com/news_admin_list.jinja msgid "Weeklies" msgstr "Événements hebdomadaires" @@ -1336,10 +1366,6 @@ msgstr "Événements hebdomadaires affichées" msgid "Title" msgstr "Titre" -#: com/templates/com/news_admin_list.jinja -msgid "Summary" -msgstr "Résumé" - #: com/templates/com/news_admin_list.jinja com/templates/com/weekmail.jinja #: forum/templates/forum/forum.jinja sas/models.py msgid "Author" @@ -1367,11 +1393,6 @@ msgstr "Dépublier" msgid "Weeklies to moderate" msgstr "Nouvelles hebdomadaires à modérer" -#: com/templates/com/news_admin_list.jinja -#: core/templates/core/base/navbar.jinja -msgid "Events" -msgstr "Événements" - #: com/templates/com/news_admin_list.jinja msgid "Displayed events" msgstr "Événements affichés" @@ -1869,7 +1890,7 @@ msgstr "département" msgid "dpt option" msgstr "Filière" -#: core/models.py pedagogy/models.py +#: core/models.py pedagogy/models.py timetable/models.py msgid "semester" msgstr "semestre" @@ -1994,6 +2015,18 @@ msgstr "" "Autorise les cotisants (ou les personnes whitelistées, si votre profil est " "caché) à accéder aux statistiques de votre compte AE" +#: core/models.py +msgid "show your timetable to others" +msgstr "montrez votre emploi du temps aux autres" + +#: core/models.py +msgid "" +"Allow subscribers to see your timetable and to compare it to theirs (if you " +"have filled it)" +msgstr "" +"Permet aux cotisants de voir votre emploi du temps et de le comparer au leur " +"(si vous l'avez enregistré)" + #: core/models.py msgid "get a notification for every click" msgstr "avoir une notification pour chaque click" @@ -5201,7 +5234,7 @@ msgstr "heures THE" msgid "hours TE" msgstr "heures TE" -#: pedagogy/models.py +#: pedagogy/models.py timetable/models.py msgid "ue" msgstr "UE" @@ -5999,6 +6032,46 @@ msgstr "Membre existant" msgid "the groups that can create subscriptions" msgstr "les groupes pouvant créer des cotisations" +#: timetable/models.py +msgid "timetable" +msgstr "emploi du temps" + +#: timetable/models.py +msgid "timetables" +msgstr "emplois du temps" + +#: timetable/models.py +msgid "weekday" +msgstr "jour de la semaine" + +#: timetable/models.py +msgid "start at" +msgstr "début" + +#: timetable/models.py +msgid "end at" +msgstr "fin" + +#: timetable/models.py +msgid "course type" +msgstr "type de cours" + +#: timetable/models.py +msgid "room" +msgstr "salle" + +#: timetable/models.py +msgid "week group" +msgstr "groupe de semaine" + +#: timetable/models.py +msgid "timetable slot" +msgstr "créneau d'emploi du temps" + +#: timetable/models.py +msgid "timetable slots" +msgstr "créneaux d'emploi du temps" + #: timetable/templates/timetable/generator.jinja msgid "Timetable generator" msgstr "Générateur d'emploi du temps" @@ -6029,8 +6102,44 @@ msgid "Generate" msgstr "Générer" #: timetable/templates/timetable/generator.jinja -msgid "Save to PNG" -msgstr "Sauver en PNG" +msgid "Save on your profile" +msgstr "Enregistrer sur votre profil" + +#: timetable/templates/timetable/generator.jinja +msgid "Make this timetable public" +msgstr "Rendre cet emploi du temps publique" + +#: timetable/templates/timetable/generator.jinja +#, python-format +msgid "" +"You can change this at any time in your preferences" +msgstr "" +"Vous pouvez changer ceci n'importe quand dans vos préférences" + +#: timetable/templates/timetable/generator.jinja +msgid "Save timetable" +msgstr "Enregistrer l'emploi du temps" + +#: timetable/templates/timetable/generator.jinja +#: trombi/templates/trombi/detail.jinja +msgid "Export" +msgstr "Export" + +#: timetable/templates/timetable/generator.jinja +msgid "Export to PNG" +msgstr "Exporter en PNG" + +#: timetable/views.py +msgid "Timetables" +msgstr "Emplois du temps" + +#: timetable/views.py +msgid "Generator" +msgstr "Générateur" + +#: timetable/views.py +msgid "My timetables" +msgstr "Mes emplois du temps" #: trombi/models.py msgid "subscription deadline" @@ -6145,10 +6254,6 @@ msgstr "Fin des inscriptions : " msgid "Comment deadline: " msgstr "Fin des commentaires : " -#: trombi/templates/trombi/detail.jinja -msgid "Export" -msgstr "Exporter" - #: trombi/templates/trombi/detail.jinja msgid "Add user" msgstr "Ajouter une personne"