From 08b16d6e74e2cfdb514759f83fbbe63c61b01b9b Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 19 Sep 2025 16:21:15 +0200 Subject: [PATCH] feat: make poster views available to club board members --- club/models.py | 9 ++ club/tests/test_club.py | 27 +++++ club/tests/test_posters.py | 35 ++++++ club/views.py | 58 +++++----- com/forms.py | 26 ++--- com/models.py | 12 --- com/tests/test_views.py | 30 +----- com/views.py | 184 +++++++++++--------------------- core/auth/mixins.py | 53 +++++++++ locale/fr/LC_MESSAGES/django.po | 27 ++--- 10 files changed, 242 insertions(+), 219 deletions(-) create mode 100644 club/tests/test_club.py create mode 100644 club/tests/test_posters.py diff --git a/club/models.py b/club/models.py index fc5aa64b..800f67c2 100644 --- a/club/models.py +++ b/club/models.py @@ -42,6 +42,13 @@ from core.fields import ResizedImageField from core.models import Group, Notification, Page, SithFile, User +class ClubQuerySet(models.QuerySet): + def having_board_member(self, user: User) -> Self: + """Filter all club in which the given user is a board member.""" + active_memberships = user.memberships.board().ongoing() + return self.filter(Exists(active_memberships.filter(club=OuterRef("pk")))) + + class Club(models.Model): """The Club class, made as a tree to allow nice tidy organization.""" @@ -91,6 +98,8 @@ class Club(models.Model): Group, related_name="club_board", on_delete=models.PROTECT ) + objects = ClubQuerySet.as_manager() + class Meta: ordering = ["name"] diff --git a/club/tests/test_club.py b/club/tests/test_club.py new file mode 100644 index 00000000..2a232b19 --- /dev/null +++ b/club/tests/test_club.py @@ -0,0 +1,27 @@ +from datetime import timedelta + +import pytest +from django.utils.timezone import localdate +from model_bakery import baker +from model_bakery.recipe import Recipe + +from club.models import Club, Membership +from core.baker_recipes import subscriber_user + + +@pytest.mark.django_db +def test_club_queryset_having_board_member(): + clubs = baker.make(Club, _quantity=5) + user = subscriber_user.make() + membership_recipe = Recipe( + Membership, user=user, start_date=localdate() - timedelta(days=3) + ) + membership_recipe.make(club=clubs[0], role=1) + membership_recipe.make(club=clubs[1], role=3) + membership_recipe.make(club=clubs[2], role=7) + membership_recipe.make( + club=clubs[3], role=3, end_date=localdate() - timedelta(days=1) + ) + + club_ids = Club.objects.having_board_member(user).values_list("id", flat=True) + assert set(club_ids) == {clubs[1].id, clubs[2].id} diff --git a/club/tests/test_posters.py b/club/tests/test_posters.py new file mode 100644 index 00000000..8f9941e5 --- /dev/null +++ b/club/tests/test_posters.py @@ -0,0 +1,35 @@ +import pytest +from django.test import Client +from django.urls import reverse +from model_bakery import baker + +from club.models import Club +from com.models import Poster +from core.baker_recipes import subscriber_user + + +@pytest.mark.django_db +@pytest.mark.parametrize("route_url", ["club:poster_list", "club:poster_create"]) +def test_access(client: Client, route_url): + club = baker.make(Club) + user = subscriber_user.make() + url = reverse(route_url, kwargs={"club_id": club.id}) + + client.force_login(user) + assert client.get(url).status_code == 403 + club.board_group.users.add(user) + assert client.get(url).status_code == 200 + + +@pytest.mark.django_db +@pytest.mark.parametrize("route_url", ["club:poster_edit", "club:poster_delete"]) +def test_access_specific_poster(client: Client, route_url): + club = baker.make(Club) + user = subscriber_user.make() + poster = baker.make(Poster) + url = reverse(route_url, kwargs={"club_id": club.id, "poster_id": poster.id}) + + client.force_login(user) + assert client.get(url).status_code == 403 + club.board_group.users.add(user) + assert client.get(url).status_code == 200 diff --git a/club/views.py b/club/views.py index 735e8291..4ed40a9c 100644 --- a/club/views.py +++ b/club/views.py @@ -51,13 +51,17 @@ from club.forms import ( SellingsForm, ) from club.models import Club, Mailing, MailingSubscription, Membership +from com.models import Poster from com.views import ( PosterCreateBaseView, PosterDeleteBaseView, PosterEditBaseView, PosterListBaseView, ) -from core.auth.mixins import CanCreateMixin, CanEditMixin, CanViewMixin +from core.auth.mixins import ( + CanEditMixin, + CanViewMixin, +) from core.models import PageRev from core.views import DetailFormView, PageEditViewBase from core.views.mixins import TabedViewMixin @@ -66,9 +70,12 @@ from counter.models import Selling class ClubTabsMixin(TabedViewMixin): def get_tabs_title(self): - obj = self.get_object() - if isinstance(obj, PageRev): - self.object = obj.page.club + if not hasattr(self, "object") or not self.object: + self.object = self.get_object() + if isinstance(self.object, PageRev): + self.object = self.object.page.club + elif isinstance(self.object, Poster): + self.object = self.object.club return self.object.get_display_name() def get_list_of_tabs(self): @@ -159,7 +166,7 @@ class ClubTabsMixin(TabedViewMixin): "club:poster_list", kwargs={"club_id": self.object.id} ), "slug": "posters", - "name": _("Posters list"), + "name": _("Posters"), }, ] ) @@ -686,48 +693,45 @@ class MailingAutoGenerationView(View): return redirect("club:mailing", club_id=club.id) -class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin): +class PosterListView(ClubTabsMixin, PosterListBaseView): """List communication posters.""" + current_tab = "posters" + extra_context = {"app": "club"} + + def get_queryset(self): + return super().get_queryset().filter(club=self.club.id) + def get_object(self): return self.club - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["app"] = "club" - kwargs["club"] = self.club - return kwargs - -class PosterCreateView(PosterCreateBaseView, CanCreateMixin): +class PosterCreateView(ClubTabsMixin, PosterCreateBaseView): """Create communication poster.""" - pk_url_kwarg = "club_id" - - def get_object(self): - obj = super().get_object() - if not obj: - return self.club - return obj + current_tab = "posters" def get_success_url(self, **kwargs): return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) + def get_object(self, *args, **kwargs): + return self.club -class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin): + +class PosterEditView(ClubTabsMixin, PosterEditBaseView): """Edit communication poster.""" + current_tab = "posters" + extra_context = {"app": "club"} + def get_success_url(self): return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["app"] = "club" - return kwargs - -class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin): +class PosterDeleteView(ClubTabsMixin, PosterDeleteBaseView): """Delete communication poster.""" + current_tab = "posters" + def get_success_url(self): return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) diff --git a/com/forms.py b/com/forms.py index e94d697e..4d5d0e09 100644 --- a/com/forms.py +++ b/com/forms.py @@ -2,7 +2,6 @@ from datetime import date from dateutil.relativedelta import relativedelta from django import forms -from django.db.models import Exists, OuterRef from django.forms import CheckboxInput from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -35,20 +34,18 @@ class PosterForm(forms.ModelForm): label=_("Start date"), widget=SelectDateTime, required=True, - initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"), + initial=timezone.now(), ) date_end = forms.DateTimeField( label=_("End date"), widget=SelectDateTime, required=False ) - def __init__(self, *args, **kwargs): - self.user = kwargs.pop("user", None) + def __init__(self, *args, user: User, **kwargs): super().__init__(*args, **kwargs) - if self.user and not self.user.is_com_admin: - self.fields["club"].queryset = Club.objects.filter( - id__in=self.user.clubs_with_rights - ) - self.fields.pop("display_time") + if user.is_root or user.is_com_admin: + self.fields["club"].widget = AutoCompleteSelectClub() + else: + self.fields["club"].queryset = Club.objects.having_board_member(user) class NewsDateForm(forms.ModelForm): @@ -161,16 +158,9 @@ class NewsForm(forms.ModelForm): # if the author is an admin, he/she can choose any club, # otherwise, only clubs for which he/she is a board member can be selected if author.is_root or author.is_com_admin: - self.fields["club"] = forms.ModelChoiceField( - queryset=Club.objects.all(), widget=AutoCompleteSelectClub - ) + self.fields["club"].widget = AutoCompleteSelectClub() else: - active_memberships = author.memberships.board().ongoing() - self.fields["club"] = forms.ModelChoiceField( - queryset=Club.objects.filter( - Exists(active_memberships.filter(club=OuterRef("pk"))) - ) - ) + self.fields["club"].queryset = Club.objects.having_board_member(author) def is_valid(self): return super().is_valid() and self.date_form.is_valid() diff --git a/com/models.py b/com/models.py index 849c6b12..6485d73b 100644 --- a/com/models.py +++ b/com/models.py @@ -412,17 +412,5 @@ class Poster(models.Model): if self.date_end and self.date_begin > self.date_end: raise ValidationError(_("Begin date should be before end date")) - def is_owned_by(self, user): - if user.is_anonymous: - return False - return user.is_com_admin or len(user.clubs_with_rights) > 0 - - def can_be_moderated_by(self, user): - return user.is_com_admin - def get_display_name(self): return self.club.get_display_name() - - @property - def page(self): - return self.club.page diff --git a/com/tests/test_views.py b/com/tests/test_views.py index 607d4b3f..02081597 100644 --- a/com/tests/test_views.py +++ b/com/tests/test_views.py @@ -18,17 +18,16 @@ from unittest.mock import patch import pytest from django.conf import settings from django.contrib.sites.models import Site -from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client, TestCase from django.urls import reverse from django.utils import html -from django.utils.timezone import localtime, now +from django.utils.timezone import now from django.utils.translation import gettext as _ from model_bakery import baker from pytest_django.asserts import assertNumQueries, assertRedirects from club.models import Club, Membership -from com.models import News, NewsDate, Poster, Sith, Weekmail, WeekmailArticle +from com.models import News, NewsDate, Sith, Weekmail, WeekmailArticle from core.baker_recipes import subscriber_user from core.models import AnonymousUser, Group, User @@ -207,31 +206,6 @@ class TestWeekmailArticle(TestCase): assert not self.article.is_owned_by(self.sli) -class TestPoster(TestCase): - @classmethod - def setUpTestData(cls): - cls.com_admin = User.objects.get(username="comunity") - cls.poster = Poster.objects.create( - name="dummy", - file=SimpleUploadedFile("dummy.jpg", b"azertyuiop"), - club=Club.objects.first(), - date_begin=localtime(now()), - ) - cls.sli = User.objects.get(username="sli") - cls.sli.memberships.all().delete() - Membership(user=cls.sli, club=Club.objects.first(), role=5).save() - cls.susbcriber = User.objects.get(username="subscriber") - cls.anonymous = AnonymousUser() - - def test_poster_owner(self): - """Test that poster are owned by com admins and board members in clubs.""" - assert self.poster.is_owned_by(self.com_admin) - assert not self.poster.is_owned_by(self.anonymous) - - assert not self.poster.is_owned_by(self.susbcriber) - assert self.poster.is_owned_by(self.sli) - - class TestNewsCreation(TestCase): @classmethod def setUpTestData(cls): diff --git a/com/views.py b/com/views.py index 024cb781..1b58feac 100644 --- a/com/views.py +++ b/com/views.py @@ -28,7 +28,9 @@ from typing import Any from dateutil.relativedelta import relativedelta from django.conf import settings -from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin, +) from django.contrib.syndication.views import Feed from django.core.exceptions import PermissionDenied, ValidationError from django.db.models import Max @@ -50,6 +52,7 @@ from core.auth.mixins import ( CanEditPropMixin, CanViewMixin, PermissionOrAuthorRequiredMixin, + PermissionOrClubBoardRequiredMixin, ) from core.models import User from core.views.mixins import QuickNotifMixin, TabedViewMixin @@ -99,13 +102,6 @@ class ComTabsMixin(TabedViewMixin): ] -class IsComAdminMixin(AccessMixin): - def dispatch(self, request, *args, **kwargs): - if not request.user.is_com_admin: - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - class ComEditView(ComTabsMixin, CanEditPropMixin, UpdateView): model = Sith template_name = "core/edit.jinja" @@ -558,161 +554,109 @@ class MailingModerateView(View): raise PermissionDenied -class PosterAdminViewMixin(IsComAdminMixin, ComTabsMixin): - current_tab = "posters" - - -class PosterListBaseView(PosterAdminViewMixin, ListView): +class PosterListBaseView(PermissionOrClubBoardRequiredMixin, ListView): """List communication posters.""" - current_tab = "posters" model = Poster template_name = "com/poster_list.jinja" - - def dispatch(self, request, *args, **kwargs): - club_id = kwargs.pop("club_id", None) - self.club = None - if club_id: - self.club = get_object_or_404(Club, pk=club_id) - return super().dispatch(request, *args, **kwargs) - - def get_queryset(self): - if self.request.user.is_com_admin: - return Poster.objects.all().order_by("-date_begin") - else: - return Poster.objects.filter(club=self.club.id) + permission_required = "com.view_poster" + ordering = ["-date_begin"] def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - if not self.request.user.is_com_admin: - kwargs["club"] = self.club - return kwargs + return super().get_context_data(**kwargs) | {"club": self.club} -class PosterCreateBaseView(PosterAdminViewMixin, CreateView): +class PosterCreateBaseView(PermissionOrClubBoardRequiredMixin, CreateView): """Create communication poster.""" - current_tab = "posters" form_class = PosterForm template_name = "core/create.jinja" + permission_required = "com.add_poster" def get_queryset(self): return Poster.objects.all() - def dispatch(self, request, *args, **kwargs): - if "club_id" in kwargs: - self.club = get_object_or_404(Club, pk=kwargs["club_id"]) - return super().dispatch(request, *args, **kwargs) - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs.update({"user": self.request.user}) - return kwargs + return super().get_form_kwargs() | {"user": self.request.user} + + def get_initial(self): + return {"club": self.club} def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - if not self.request.user.is_com_admin: - kwargs["club"] = self.club - return kwargs + return super().get_context_data(**kwargs) | {"club": self.club} def form_valid(self, form): - if self.request.user.is_com_admin: + if self.request.user.has_perm("com.moderate_poster"): form.instance.is_moderated = True return super().form_valid(form) -class PosterEditBaseView(PosterAdminViewMixin, UpdateView): +class PosterEditBaseView(PermissionOrClubBoardRequiredMixin, UpdateView): """Edit communication poster.""" pk_url_kwarg = "poster_id" - current_tab = "posters" form_class = PosterForm template_name = "com/poster_edit.jinja" - - def get_initial(self): - return { - "date_begin": self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S") - if self.object.date_begin - else None, - "date_end": self.object.date_end.strftime("%Y-%m-%d %H:%M:%S") - if self.object.date_end - else None, - } - - def dispatch(self, request, *args, **kwargs): - if kwargs.get("club_id"): - try: - self.club = Club.objects.get(pk=kwargs["club_id"]) - except Club.DoesNotExist as e: - raise PermissionDenied from e - return super().dispatch(request, *args, **kwargs) + permission_required = "com.change_poster" def get_queryset(self): return Poster.objects.all() def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs.update({"user": self.request.user}) - return kwargs + return super().get_form_kwargs() | {"user": self.request.user} def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - if hasattr(self, "club"): - kwargs["club"] = self.club - return kwargs + return super().get_context_data(**kwargs) | {"club": self.club} def form_valid(self, form): - if self.request.user.is_com_admin: + if not self.request.user.has_perm("com.moderate_poster"): form.instance.is_moderated = False return super().form_valid(form) -class PosterDeleteBaseView(PosterAdminViewMixin, DeleteView): +class PosterDeleteBaseView( + PermissionOrClubBoardRequiredMixin, ComTabsMixin, DeleteView +): """Edit communication poster.""" pk_url_kwarg = "poster_id" current_tab = "posters" model = Poster template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - if kwargs.get("club_id"): - try: - self.club = Club.objects.get(pk=kwargs["club_id"]) - except Club.DoesNotExist as e: - raise PermissionDenied from e - return super().dispatch(request, *args, **kwargs) + permission_required = "com.delete_poster" -class PosterListView(PosterListBaseView): +class PosterListView(ComTabsMixin, PosterListBaseView): """List communication posters.""" + current_tab = "posters" + + def get_queryset(self): + qs = super().get_queryset() + if self.request.user.has_perm("com.view_poster"): + return qs + return qs.filter(club=self.club.id) + def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["app"] = "com" return kwargs -class PosterCreateView(PosterCreateBaseView): +class PosterCreateView(ComTabsMixin, PosterCreateBaseView): """Create communication poster.""" + current_tab = "posters" success_url = reverse_lazy("com:poster_list") - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["app"] = "com" - return kwargs + extra_context = {"app": "com"} -class PosterEditView(PosterEditBaseView): +class PosterEditView(ComTabsMixin, PosterEditBaseView): """Edit communication poster.""" + current_tab = "posters" success_url = reverse_lazy("com:poster_list") - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["app"] = "com" - return kwargs + extra_context = {"app": "com"} class PosterDeleteView(PosterDeleteBaseView): @@ -721,44 +665,39 @@ class PosterDeleteView(PosterDeleteBaseView): success_url = reverse_lazy("com:poster_list") -class PosterModerateListView(PosterAdminViewMixin, ListView): +class PosterModerateListView(PermissionRequiredMixin, ComTabsMixin, ListView): """Moderate list communication poster.""" current_tab = "posters" model = Poster template_name = "com/poster_moderate.jinja" queryset = Poster.objects.filter(is_moderated=False).all() - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["app"] = "com" - return kwargs + permission_required = "com.moderate_poster" + extra_context = {"app": "com"} -class PosterModerateView(PosterAdminViewMixin, View): +class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View): """Moderate communication poster.""" + current_tab = "posters" + permission_required = "com.moderate_poster" + extra_context = {"app": "com"} + def get(self, request, *args, **kwargs): obj = get_object_or_404(Poster, pk=kwargs["object_id"]) - if obj.can_be_moderated_by(request.user): - obj.is_moderated = True - obj.moderator = request.user - obj.save() - return redirect("com:poster_moderate_list") - raise PermissionDenied - - def get_context_data(self, **kwargs): - kwargs = super(PosterModerateListView, self).get_context_data(**kwargs) - kwargs["app"] = "com" - return kwargs + obj.is_moderated = True + obj.moderator = request.user + obj.save() + return redirect("com:poster_moderate_list") -class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView): +class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView): """List communication screens.""" current_tab = "screens" model = Screen template_name = "com/screen_list.jinja" + permission_required = "com.view_screen" class ScreenSlideshowView(DetailView): @@ -769,12 +708,12 @@ class ScreenSlideshowView(DetailView): template_name = "com/screen_slideshow.jinja" def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["posters"] = self.object.active_posters() - return kwargs + return super().get_context_data(**kwargs) | { + "posters": self.object.active_posters() + } -class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView): +class ScreenCreateView(PermissionRequiredMixin, ComTabsMixin, CreateView): """Create communication screen.""" current_tab = "screens" @@ -782,9 +721,10 @@ class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView): fields = ["name"] template_name = "core/create.jinja" success_url = reverse_lazy("com:screen_list") + permission_required = "com.add_screen" -class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView): +class ScreenEditView(PermissionRequiredMixin, ComTabsMixin, UpdateView): """Edit communication screen.""" pk_url_kwarg = "screen_id" @@ -793,9 +733,10 @@ class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView): fields = ["name"] template_name = "com/screen_edit.jinja" success_url = reverse_lazy("com:screen_list") + permission_required = "com.change_screen" -class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView): +class ScreenDeleteView(PermissionRequiredMixin, ComTabsMixin, DeleteView): """Delete communication screen.""" pk_url_kwarg = "screen_id" @@ -803,3 +744,4 @@ class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView): model = Screen template_name = "core/delete_confirm.jinja" success_url = reverse_lazy("com:screen_list") + permission_required = "com.delete_screen" diff --git a/core/auth/mixins.py b/core/auth/mixins.py index dfba3f11..c4e603a9 100644 --- a/core/auth/mixins.py +++ b/core/auth/mixins.py @@ -29,8 +29,14 @@ from typing import TYPE_CHECKING, Any, LiteralString from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.utils.functional import cached_property +from django.utils.translation import gettext as _ from django.views.generic.base import View +from club.models import Club + if TYPE_CHECKING: from django.db.models import Model @@ -297,3 +303,50 @@ class PermissionOrAuthorRequiredMixin(PermissionRequiredMixin): self.author_field += "_id" author_id = getattr(obj, self.author_field, None) return author_id == self.request.user.id + + +class PermissionOrClubBoardRequiredMixin(PermissionRequiredMixin): + """Require that the user has the required perm or is the board of the club. + + This mixin can be used in any view that is called from a url + having a `club_id` kwarg. + + Example: + + In `urls.py` : + ```python + urlpatterns = [ + path("foo//bar/", FooView.as_view()) + ] + ``` + + In `views.py` : + + ```python + # this view is available to users that either have the + # "foo.view_foo" permission or are in the board of the club + # which id was given in the url + class FooView(PermissionOrClubBoardRequiredMixin, View): + permission_required = "foo.view_foo" + ``` + """ + + club_pk_url_kwarg = "club_id" + + @cached_property + def club(self): + club_id: str | int = self.kwargs.pop(self.club_pk_url_kwarg, None) + if club_id is None: + return None + if isinstance(club_id, int) or club_id.isdigit(): + return get_object_or_404(Club, pk=club_id) + raise Http404(_("No club found with id %(id)s") % {"id": club_id}) + + def has_permission(self): + if self.request.user.is_anonymous: + return False + if super().has_permission(): + return True + return self.club is not None and any( + g.id == self.club.board_group_id for g in self.request.user.cached_groups + ) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index dcf19d5e..9b43dadf 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-09-02 15:56+0200\n" +"POT-Creation-Date: 2025-09-19 17:22+0200\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -514,8 +514,8 @@ msgstr "Éditer le Trombi" msgid "New Trombi" msgstr "Nouveau Trombi" -#: club/templates/club/club_tools.jinja com/templates/com/poster_list.jinja -#: core/templates/core/user_tools.jinja +#: club/templates/club/club_tools.jinja club/views.py +#: com/templates/com/poster_list.jinja core/templates/core/user_tools.jinja msgid "Posters" msgstr "Affiches" @@ -675,10 +675,6 @@ msgstr "Vente" msgid "Mailing list" msgstr "Listes de diffusion" -#: club/views.py com/views.py -msgid "Posters list" -msgstr "Liste d'affiches" - #: com/forms.py msgid "Format: 16:9 | Resolution: 1920x1080" msgstr "Format : 16:9 | Résolution : 1920x1080" @@ -1249,6 +1245,10 @@ msgstr "Message d'info" msgid "Alert message" msgstr "Message d'alerte" +#: com/views.py +msgid "Posters list" +msgstr "Liste d'affiches" + #: com/views.py msgid "Screens list" msgstr "Liste d'écrans" @@ -1272,6 +1272,11 @@ msgstr "" "Vous devez êtres un membre du bureau du club sélectionné pour poster dans le " "Weekmail." +#: core/auth/mixins.py +#, python-format +msgid "No club found with id %(id)s" +msgstr "Pas de club avec l'id %(id)s trouvé" + #: core/models.py msgid "Is manually manageable" msgstr "Est gérable manuellement" @@ -1713,8 +1718,8 @@ msgid "" "AE UTBM is a voluntary organisation run by UTBM students. It organises " "student life at UTBM and manages its student facilities." msgstr "" -"L'AE UTBM est une association bénévole gérée par les étudiants de " -"l'UTBM. Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie." +"L'AE UTBM est une association bénévole gérée par les étudiants de l'UTBM. " +"Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie." #: core/templates/core/base/footer.jinja core/templates/core/base/navbar.jinja msgid "Contacts" @@ -2157,10 +2162,6 @@ msgstr "" msgid "Page history" msgstr "Historique de la page" -#: core/templates/core/page_list.jinja -msgid "There is no page in this website." -msgstr "Il n'y a pas de page sur ce site web." - #: core/templates/core/page_prop.jinja msgid "Page properties" msgstr "Propriétés de la page"