feat: make poster views available to club board members

This commit is contained in:
imperosol
2025-09-19 16:21:15 +02:00
parent c6baab068a
commit 08b16d6e74
10 changed files with 242 additions and 219 deletions

View File

@@ -42,6 +42,13 @@ from core.fields import ResizedImageField
from core.models import Group, Notification, Page, SithFile, User 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): class Club(models.Model):
"""The Club class, made as a tree to allow nice tidy organization.""" """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 Group, related_name="club_board", on_delete=models.PROTECT
) )
objects = ClubQuerySet.as_manager()
class Meta: class Meta:
ordering = ["name"] ordering = ["name"]

27
club/tests/test_club.py Normal file
View File

@@ -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}

View File

@@ -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

View File

@@ -51,13 +51,17 @@ from club.forms import (
SellingsForm, SellingsForm,
) )
from club.models import Club, Mailing, MailingSubscription, Membership from club.models import Club, Mailing, MailingSubscription, Membership
from com.models import Poster
from com.views import ( from com.views import (
PosterCreateBaseView, PosterCreateBaseView,
PosterDeleteBaseView, PosterDeleteBaseView,
PosterEditBaseView, PosterEditBaseView,
PosterListBaseView, PosterListBaseView,
) )
from core.auth.mixins import CanCreateMixin, CanEditMixin, CanViewMixin from core.auth.mixins import (
CanEditMixin,
CanViewMixin,
)
from core.models import PageRev from core.models import PageRev
from core.views import DetailFormView, PageEditViewBase from core.views import DetailFormView, PageEditViewBase
from core.views.mixins import TabedViewMixin from core.views.mixins import TabedViewMixin
@@ -66,9 +70,12 @@ from counter.models import Selling
class ClubTabsMixin(TabedViewMixin): class ClubTabsMixin(TabedViewMixin):
def get_tabs_title(self): def get_tabs_title(self):
obj = self.get_object() if not hasattr(self, "object") or not self.object:
if isinstance(obj, PageRev): self.object = self.get_object()
self.object = obj.page.club 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() return self.object.get_display_name()
def get_list_of_tabs(self): def get_list_of_tabs(self):
@@ -159,7 +166,7 @@ class ClubTabsMixin(TabedViewMixin):
"club:poster_list", kwargs={"club_id": self.object.id} "club:poster_list", kwargs={"club_id": self.object.id}
), ),
"slug": "posters", "slug": "posters",
"name": _("Posters list"), "name": _("Posters"),
}, },
] ]
) )
@@ -686,48 +693,45 @@ class MailingAutoGenerationView(View):
return redirect("club:mailing", club_id=club.id) return redirect("club:mailing", club_id=club.id)
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin): class PosterListView(ClubTabsMixin, PosterListBaseView):
"""List communication posters.""" """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): def get_object(self):
return self.club 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(ClubTabsMixin, PosterCreateBaseView):
class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
"""Create communication poster.""" """Create communication poster."""
pk_url_kwarg = "club_id" current_tab = "posters"
def get_object(self):
obj = super().get_object()
if not obj:
return self.club
return obj
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) 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.""" """Edit communication poster."""
current_tab = "posters"
extra_context = {"app": "club"}
def get_success_url(self): def get_success_url(self):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) 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(ClubTabsMixin, PosterDeleteBaseView):
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
"""Delete communication poster.""" """Delete communication poster."""
current_tab = "posters"
def get_success_url(self): def get_success_url(self):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})

View File

@@ -2,7 +2,6 @@ from datetime import date
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django import forms from django import forms
from django.db.models import Exists, OuterRef
from django.forms import CheckboxInput from django.forms import CheckboxInput
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -35,20 +34,18 @@ class PosterForm(forms.ModelForm):
label=_("Start date"), label=_("Start date"),
widget=SelectDateTime, widget=SelectDateTime,
required=True, required=True,
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"), initial=timezone.now(),
) )
date_end = forms.DateTimeField( date_end = forms.DateTimeField(
label=_("End date"), widget=SelectDateTime, required=False label=_("End date"), widget=SelectDateTime, required=False
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, user: User, **kwargs):
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.user and not self.user.is_com_admin: if user.is_root or user.is_com_admin:
self.fields["club"].queryset = Club.objects.filter( self.fields["club"].widget = AutoCompleteSelectClub()
id__in=self.user.clubs_with_rights else:
) self.fields["club"].queryset = Club.objects.having_board_member(user)
self.fields.pop("display_time")
class NewsDateForm(forms.ModelForm): class NewsDateForm(forms.ModelForm):
@@ -161,16 +158,9 @@ class NewsForm(forms.ModelForm):
# if the author is an admin, he/she can choose any club, # 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 # otherwise, only clubs for which he/she is a board member can be selected
if author.is_root or author.is_com_admin: if author.is_root or author.is_com_admin:
self.fields["club"] = forms.ModelChoiceField( self.fields["club"].widget = AutoCompleteSelectClub()
queryset=Club.objects.all(), widget=AutoCompleteSelectClub
)
else: else:
active_memberships = author.memberships.board().ongoing() self.fields["club"].queryset = Club.objects.having_board_member(author)
self.fields["club"] = forms.ModelChoiceField(
queryset=Club.objects.filter(
Exists(active_memberships.filter(club=OuterRef("pk")))
)
)
def is_valid(self): def is_valid(self):
return super().is_valid() and self.date_form.is_valid() return super().is_valid() and self.date_form.is_valid()

View File

@@ -412,17 +412,5 @@ class Poster(models.Model):
if self.date_end and self.date_begin > self.date_end: if self.date_end and self.date_begin > self.date_end:
raise ValidationError(_("Begin date should be before end date")) 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): def get_display_name(self):
return self.club.get_display_name() return self.club.get_display_name()
@property
def page(self):
return self.club.page

View File

@@ -18,17 +18,16 @@ from unittest.mock import patch
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import html 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 django.utils.translation import gettext as _
from model_bakery import baker from model_bakery import baker
from pytest_django.asserts import assertNumQueries, assertRedirects from pytest_django.asserts import assertNumQueries, assertRedirects
from club.models import Club, Membership 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.baker_recipes import subscriber_user
from core.models import AnonymousUser, Group, User from core.models import AnonymousUser, Group, User
@@ -207,31 +206,6 @@ class TestWeekmailArticle(TestCase):
assert not self.article.is_owned_by(self.sli) 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): class TestNewsCreation(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@@ -28,7 +28,9 @@ from typing import Any
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.conf import settings 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.contrib.syndication.views import Feed
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Max from django.db.models import Max
@@ -50,6 +52,7 @@ from core.auth.mixins import (
CanEditPropMixin, CanEditPropMixin,
CanViewMixin, CanViewMixin,
PermissionOrAuthorRequiredMixin, PermissionOrAuthorRequiredMixin,
PermissionOrClubBoardRequiredMixin,
) )
from core.models import User from core.models import User
from core.views.mixins import QuickNotifMixin, TabedViewMixin 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): class ComEditView(ComTabsMixin, CanEditPropMixin, UpdateView):
model = Sith model = Sith
template_name = "core/edit.jinja" template_name = "core/edit.jinja"
@@ -558,161 +554,109 @@ class MailingModerateView(View):
raise PermissionDenied raise PermissionDenied
class PosterAdminViewMixin(IsComAdminMixin, ComTabsMixin): class PosterListBaseView(PermissionOrClubBoardRequiredMixin, ListView):
current_tab = "posters"
class PosterListBaseView(PosterAdminViewMixin, ListView):
"""List communication posters.""" """List communication posters."""
current_tab = "posters"
model = Poster model = Poster
template_name = "com/poster_list.jinja" template_name = "com/poster_list.jinja"
permission_required = "com.view_poster"
def dispatch(self, request, *args, **kwargs): ordering = ["-date_begin"]
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)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) return super().get_context_data(**kwargs) | {"club": self.club}
if not self.request.user.is_com_admin:
kwargs["club"] = self.club
return kwargs
class PosterCreateBaseView(PosterAdminViewMixin, CreateView): class PosterCreateBaseView(PermissionOrClubBoardRequiredMixin, CreateView):
"""Create communication poster.""" """Create communication poster."""
current_tab = "posters"
form_class = PosterForm form_class = PosterForm
template_name = "core/create.jinja" template_name = "core/create.jinja"
permission_required = "com.add_poster"
def get_queryset(self): def get_queryset(self):
return Poster.objects.all() 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): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() return super().get_form_kwargs() | {"user": self.request.user}
kwargs.update({"user": self.request.user})
return kwargs def get_initial(self):
return {"club": self.club}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) return super().get_context_data(**kwargs) | {"club": self.club}
if not self.request.user.is_com_admin:
kwargs["club"] = self.club
return kwargs
def form_valid(self, form): 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 form.instance.is_moderated = True
return super().form_valid(form) return super().form_valid(form)
class PosterEditBaseView(PosterAdminViewMixin, UpdateView): class PosterEditBaseView(PermissionOrClubBoardRequiredMixin, UpdateView):
"""Edit communication poster.""" """Edit communication poster."""
pk_url_kwarg = "poster_id" pk_url_kwarg = "poster_id"
current_tab = "posters"
form_class = PosterForm form_class = PosterForm
template_name = "com/poster_edit.jinja" template_name = "com/poster_edit.jinja"
permission_required = "com.change_poster"
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)
def get_queryset(self): def get_queryset(self):
return Poster.objects.all() return Poster.objects.all()
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() return super().get_form_kwargs() | {"user": self.request.user}
kwargs.update({"user": self.request.user})
return kwargs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) return super().get_context_data(**kwargs) | {"club": self.club}
if hasattr(self, "club"):
kwargs["club"] = self.club
return kwargs
def form_valid(self, form): 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 form.instance.is_moderated = False
return super().form_valid(form) return super().form_valid(form)
class PosterDeleteBaseView(PosterAdminViewMixin, DeleteView): class PosterDeleteBaseView(
PermissionOrClubBoardRequiredMixin, ComTabsMixin, DeleteView
):
"""Edit communication poster.""" """Edit communication poster."""
pk_url_kwarg = "poster_id" pk_url_kwarg = "poster_id"
current_tab = "posters" current_tab = "posters"
model = Poster model = Poster
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
permission_required = "com.delete_poster"
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)
class PosterListView(PosterListBaseView): class PosterListView(ComTabsMixin, PosterListBaseView):
"""List communication posters.""" """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): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "com" kwargs["app"] = "com"
return kwargs return kwargs
class PosterCreateView(PosterCreateBaseView): class PosterCreateView(ComTabsMixin, PosterCreateBaseView):
"""Create communication poster.""" """Create communication poster."""
current_tab = "posters"
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
extra_context = {"app": "com"}
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "com"
return kwargs
class PosterEditView(PosterEditBaseView): class PosterEditView(ComTabsMixin, PosterEditBaseView):
"""Edit communication poster.""" """Edit communication poster."""
current_tab = "posters"
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
extra_context = {"app": "com"}
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "com"
return kwargs
class PosterDeleteView(PosterDeleteBaseView): class PosterDeleteView(PosterDeleteBaseView):
@@ -721,44 +665,39 @@ class PosterDeleteView(PosterDeleteBaseView):
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
class PosterModerateListView(PosterAdminViewMixin, ListView): class PosterModerateListView(PermissionRequiredMixin, ComTabsMixin, ListView):
"""Moderate list communication poster.""" """Moderate list communication poster."""
current_tab = "posters" current_tab = "posters"
model = Poster model = Poster
template_name = "com/poster_moderate.jinja" template_name = "com/poster_moderate.jinja"
queryset = Poster.objects.filter(is_moderated=False).all() queryset = Poster.objects.filter(is_moderated=False).all()
permission_required = "com.moderate_poster"
def get_context_data(self, **kwargs): extra_context = {"app": "com"}
kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "com"
return kwargs
class PosterModerateView(PosterAdminViewMixin, View): class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View):
"""Moderate communication poster.""" """Moderate communication poster."""
current_tab = "posters"
permission_required = "com.moderate_poster"
extra_context = {"app": "com"}
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
obj = get_object_or_404(Poster, pk=kwargs["object_id"]) obj = get_object_or_404(Poster, pk=kwargs["object_id"])
if obj.can_be_moderated_by(request.user): obj.is_moderated = True
obj.is_moderated = True obj.moderator = request.user
obj.moderator = request.user obj.save()
obj.save() return redirect("com:poster_moderate_list")
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
class ScreenListView(IsComAdminMixin, ComTabsMixin, ListView): class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView):
"""List communication screens.""" """List communication screens."""
current_tab = "screens" current_tab = "screens"
model = Screen model = Screen
template_name = "com/screen_list.jinja" template_name = "com/screen_list.jinja"
permission_required = "com.view_screen"
class ScreenSlideshowView(DetailView): class ScreenSlideshowView(DetailView):
@@ -769,12 +708,12 @@ class ScreenSlideshowView(DetailView):
template_name = "com/screen_slideshow.jinja" template_name = "com/screen_slideshow.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) return super().get_context_data(**kwargs) | {
kwargs["posters"] = self.object.active_posters() "posters": self.object.active_posters()
return kwargs }
class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView): class ScreenCreateView(PermissionRequiredMixin, ComTabsMixin, CreateView):
"""Create communication screen.""" """Create communication screen."""
current_tab = "screens" current_tab = "screens"
@@ -782,9 +721,10 @@ class ScreenCreateView(IsComAdminMixin, ComTabsMixin, CreateView):
fields = ["name"] fields = ["name"]
template_name = "core/create.jinja" template_name = "core/create.jinja"
success_url = reverse_lazy("com:screen_list") 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.""" """Edit communication screen."""
pk_url_kwarg = "screen_id" pk_url_kwarg = "screen_id"
@@ -793,9 +733,10 @@ class ScreenEditView(IsComAdminMixin, ComTabsMixin, UpdateView):
fields = ["name"] fields = ["name"]
template_name = "com/screen_edit.jinja" template_name = "com/screen_edit.jinja"
success_url = reverse_lazy("com:screen_list") 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.""" """Delete communication screen."""
pk_url_kwarg = "screen_id" pk_url_kwarg = "screen_id"
@@ -803,3 +744,4 @@ class ScreenDeleteView(IsComAdminMixin, ComTabsMixin, DeleteView):
model = Screen model = Screen
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy("com:screen_list") success_url = reverse_lazy("com:screen_list")
permission_required = "com.delete_screen"

View File

@@ -29,8 +29,14 @@ from typing import TYPE_CHECKING, Any, LiteralString
from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin
from django.core.exceptions import ImproperlyConfigured, PermissionDenied 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 django.views.generic.base import View
from club.models import Club
if TYPE_CHECKING: if TYPE_CHECKING:
from django.db.models import Model from django.db.models import Model
@@ -297,3 +303,50 @@ class PermissionOrAuthorRequiredMixin(PermissionRequiredMixin):
self.author_field += "_id" self.author_field += "_id"
author_id = getattr(obj, self.author_field, None) author_id = getattr(obj, self.author_field, None)
return author_id == self.request.user.id 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/<int:club_id>/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
)

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "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" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -514,8 +514,8 @@ msgstr "Éditer le Trombi"
msgid "New Trombi" msgid "New Trombi"
msgstr "Nouveau Trombi" msgstr "Nouveau Trombi"
#: club/templates/club/club_tools.jinja com/templates/com/poster_list.jinja #: club/templates/club/club_tools.jinja club/views.py
#: core/templates/core/user_tools.jinja #: com/templates/com/poster_list.jinja core/templates/core/user_tools.jinja
msgid "Posters" msgid "Posters"
msgstr "Affiches" msgstr "Affiches"
@@ -675,10 +675,6 @@ msgstr "Vente"
msgid "Mailing list" msgid "Mailing list"
msgstr "Listes de diffusion" msgstr "Listes de diffusion"
#: club/views.py com/views.py
msgid "Posters list"
msgstr "Liste d'affiches"
#: com/forms.py #: com/forms.py
msgid "Format: 16:9 | Resolution: 1920x1080" msgid "Format: 16:9 | Resolution: 1920x1080"
msgstr "Format : 16:9 | Résolution : 1920x1080" msgstr "Format : 16:9 | Résolution : 1920x1080"
@@ -1249,6 +1245,10 @@ msgstr "Message d'info"
msgid "Alert message" msgid "Alert message"
msgstr "Message d'alerte" msgstr "Message d'alerte"
#: com/views.py
msgid "Posters list"
msgstr "Liste d'affiches"
#: com/views.py #: com/views.py
msgid "Screens list" msgid "Screens list"
msgstr "Liste d'écrans" msgstr "Liste d'écrans"
@@ -1272,6 +1272,11 @@ msgstr ""
"Vous devez êtres un membre du bureau du club sélectionné pour poster dans le " "Vous devez êtres un membre du bureau du club sélectionné pour poster dans le "
"Weekmail." "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 #: core/models.py
msgid "Is manually manageable" msgid "Is manually manageable"
msgstr "Est gérable manuellement" msgstr "Est gérable manuellement"
@@ -1713,8 +1718,8 @@ msgid ""
"AE UTBM is a voluntary organisation run by UTBM students. It organises " "AE UTBM is a voluntary organisation run by UTBM students. It organises "
"student life at UTBM and manages its student facilities." "student life at UTBM and manages its student facilities."
msgstr "" msgstr ""
"L'AE UTBM est une association bénévole gérée par les étudiants de " "L'AE UTBM est une association bénévole gérée par les étudiants de l'UTBM. "
"l'UTBM. Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie." "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 #: core/templates/core/base/footer.jinja core/templates/core/base/navbar.jinja
msgid "Contacts" msgid "Contacts"
@@ -2157,10 +2162,6 @@ msgstr ""
msgid "Page history" msgid "Page history"
msgstr "Historique de la page" 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 #: core/templates/core/page_prop.jinja
msgid "Page properties" msgid "Page properties"
msgstr "Propriétés de la page" msgstr "Propriétés de la page"