Merge pull request #821 from ae-utbm/taiste

Python upgrade and bugfixes
This commit is contained in:
thomas girod 2024-09-12 11:37:27 +02:00 committed by GitHub
commit ae16a1bd89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 561 additions and 516 deletions

View File

@ -16,16 +16,16 @@ runs:
shell: bash shell: bash
- name: Set up python - name: Set up python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.12"
- name: Load cached Poetry installation - name: Load cached Poetry installation
id: cached-poetry id: cached-poetry
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: ~/.local path: ~/.local
key: poetry-0 # increment to reset cache key: poetry-1 # increment to reset cache
- name: Install Poetry - name: Install Poetry
if: steps.cached-poetry.outputs.cache-hit != 'true' if: steps.cached-poetry.outputs.cache-hit != 'true'

View File

@ -36,7 +36,8 @@ jobs:
export PATH="/home/sith/.local/bin:$PATH" export PATH="/home/sith/.local/bin:$PATH"
pushd ${{secrets.SITH_PATH}} pushd ${{secrets.SITH_PATH}}
git pull git fetch
git reset --hard origin/master
poetry install --with prod --without docs,tests poetry install --with prod --without docs,tests
poetry run ./manage.py install_xapian poetry run ./manage.py install_xapian
poetry run ./manage.py migrate poetry run ./manage.py migrate

View File

@ -9,7 +9,7 @@ jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: ./.github/actions/setup_project - uses: ./.github/actions/setup_project
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v3 - uses: actions/cache@v3

View File

@ -35,7 +35,8 @@ jobs:
export PATH="$HOME/.poetry/bin:$PATH" export PATH="$HOME/.poetry/bin:$PATH"
pushd ${{secrets.SITH_PATH}} pushd ${{secrets.SITH_PATH}}
git pull git fetch
git reset --hard origin/taiste
poetry install --with prod --without docs,tests poetry install --with prod --without docs,tests
poetry run ./manage.py install_xapian poetry run ./manage.py install_xapian
poetry run ./manage.py migrate poetry run ./manage.py migrate

View File

@ -1,7 +1,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.5.5 rev: v0.5.7
hooks: hooks:
- id: ruff # just check the code, and print the errors - id: ruff # just check the code, and print the errors
- id: ruff # actually fix the fixable errors, but print nothing - id: ruff # actually fix the fixable errors, but print nothing

View File

@ -23,6 +23,8 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import Self
from django.conf import settings from django.conf import settings
from django.core import validators from django.core import validators
from django.core.cache import cache from django.core.cache import cache
@ -265,12 +267,11 @@ class Club(models.Model):
class MembershipQuerySet(models.QuerySet): class MembershipQuerySet(models.QuerySet):
def ongoing(self) -> "MembershipQuerySet": def ongoing(self) -> Self:
"""Filter all memberships which are not finished yet.""" """Filter all memberships which are not finished yet."""
# noinspection PyTypeChecker return self.filter(Q(end_date=None) | Q(end_date__gt=timezone.now().date()))
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
def board(self) -> "MembershipQuerySet": def board(self) -> Self:
"""Filter all memberships where the user is/was in the board. """Filter all memberships where the user is/was in the board.
Be aware that users who were in the board in the past Be aware that users who were in the board in the past
@ -279,7 +280,6 @@ class MembershipQuerySet(models.QuerySet):
If you want to get the users who are currently in the board, If you want to get the users who are currently in the board,
mind combining this with the :meth:`ongoing` queryset method mind combining this with the :meth:`ongoing` queryset method
""" """
# noinspection PyTypeChecker
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE) return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
def update(self, **kwargs): def update(self, **kwargs):

View File

@ -24,6 +24,7 @@ from django.utils.translation import gettext as _
from club.forms import MailingForm from club.forms import MailingForm
from club.models import Club, Mailing, Membership from club.models import Club, Mailing, Membership
from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, User from core.models import AnonymousUser, User
from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID
@ -106,6 +107,18 @@ class TestMembershipQuerySet(TestClub):
expected.sort(key=lambda i: i.id) expected.sort(key=lambda i: i.id)
assert current_members == expected assert current_members == expected
def test_ongoing_with_membership_ending_today(self):
"""Test that a membership ending the present day is considered as ended."""
today = timezone.now().date()
self.richard.memberships.filter(club=self.club).update(end_date=today)
current_members = list(self.club.members.ongoing().order_by("id"))
expected = [
self.skia.memberships.get(club=self.club),
self.comptable.memberships.get(club=self.club),
]
expected.sort(key=lambda i: i.id)
assert current_members == expected
def test_board(self): def test_board(self):
"""Test that the board queryset method returns the memberships """Test that the board queryset method returns the memberships
of user in the club board. of user in the club board.
@ -422,11 +435,11 @@ class TestClubModel(TestClub):
of anyone. of anyone.
""" """
# make subscriber a board member # make subscriber a board member
self.subscriber.memberships.all().delete() subscriber = subscriber_user.make()
Membership.objects.create(club=self.ae, user=self.subscriber, role=3) Membership.objects.create(club=self.ae, user=subscriber, role=3)
nb_memberships = self.club.members.count() nb_memberships = self.club.members.ongoing().count()
self.client.force_login(self.subscriber) self.client.force_login(subscriber)
response = self.client.post( response = self.client.post(
self.members_url, self.members_url,
{"users_old": self.comptable.id}, {"users_old": self.comptable.id},
@ -437,7 +450,7 @@ class TestClubModel(TestClub):
def test_end_membership_as_root(self): def test_end_membership_as_root(self):
"""Test that root users can end the membership of anyone.""" """Test that root users can end the membership of anyone."""
nb_memberships = self.club.members.count() nb_memberships = self.club.members.ongoing().count()
self.client.force_login(self.root) self.client.force_login(self.root)
response = self.client.post( response = self.client.post(
self.members_url, self.members_url,
@ -446,7 +459,6 @@ class TestClubModel(TestClub):
self.assertRedirects(response, self.members_url) self.assertRedirects(response, self.members_url)
self.assert_membership_ended_today(self.comptable) self.assert_membership_ended_today(self.comptable)
assert self.club.members.ongoing().count() == nb_memberships - 1 assert self.club.members.ongoing().count() == nb_memberships - 1
assert self.club.members.count() == nb_memberships
def test_end_membership_as_foreigner(self): def test_end_membership_as_foreigner(self):
"""Test that users who are not in this club cannot end its memberships.""" """Test that users who are not in this club cannot end its memberships."""

View File

@ -32,13 +32,13 @@ tar xf "${BINDINGS}.tar.xz"
# install # install
echo "Installing Xapian-core..." echo "Installing Xapian-core..."
cd "$VIRTUAL_ENV/packages/${CORE}" || exit 1 cd "$VIRTUAL_ENV/packages/${CORE}" || exit 1
./configure --prefix="$VIRTUAL_ENV" && make && make install ./configure --prefix="$VIRTUAL_ENV" && make -j"$(nproc)" && make install
PYTHON_FLAG=--with-python3 PYTHON_FLAG=--with-python3
echo "Installing Xapian-bindings..." echo "Installing Xapian-bindings..."
cd "$VIRTUAL_ENV/packages/${BINDINGS}" || exit 1 cd "$VIRTUAL_ENV/packages/${BINDINGS}" || exit 1
./configure --prefix="$VIRTUAL_ENV" $PYTHON_FLAG XAPIAN_CONFIG="$VIRTUAL_ENV/bin/xapian-config" && make && make install ./configure --prefix="$VIRTUAL_ENV" $PYTHON_FLAG XAPIAN_CONFIG="$VIRTUAL_ENV/bin/xapian-config" && make -j"$(nproc)" && make install
# clean # clean
rm -rf "$VIRTUAL_ENV/packages" rm -rf "$VIRTUAL_ENV/packages"

View File

@ -944,40 +944,15 @@ class SithFile(models.Model):
param="1", param="1",
).save() ).save()
def can_be_managed_by(self, user: User) -> bool:
"""Tell if the user can manage the file (edit, delete, etc.) or not.
Apply the following rules:
- If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True
- If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root
- If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root.
Returns:
True if the file is managed by the SAS or within the profiles directory, False otherwise
"""
# If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone
profiles_dir = SithFile.objects.filter(name="profiles").first()
if not self.is_in_sas and not profiles_dir in self.get_parent_list():
return True
# If the file is in the SAS, only the SAS admins (or roots) can manage it
if self.is_in_sas and (
user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID) or user.is_root
):
return True
# If the file is in the profiles directory, only the roots can manage it
if profiles_dir in self.get_parent_list() and (
user.is_root or user.is_board_member
):
return True
return False
def is_owned_by(self, user): def is_owned_by(self, user):
if user.is_anonymous: if user.is_anonymous:
return False return False
if hasattr(self, "profile_of") and user.is_board_member: if user.is_root:
return True return True
if hasattr(self, "profile_of"):
# if the `profile_of` attribute is set, this file is a profile picture
# and profile pictures may only be edited by board members
return user.is_board_member
if user.is_com_admin: if user.is_com_admin:
return True return True
if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID): if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
@ -993,7 +968,7 @@ class SithFile(models.Model):
return user.can_view(self.scrub_of) return user.can_view(self.scrub_of)
return False return False
def delete(self): def delete(self, *args, **kwargs):
for c in self.children.all(): for c in self.children.all():
c.delete() c.delete()
self.file.delete() self.file.delete()

View File

@ -190,13 +190,6 @@ class FileEditView(CanEditMixin, UpdateView):
template_name = "core/file_edit.jinja" template_name = "core/file_edit.jinja"
context_object_name = "file" context_object_name = "file"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.can_be_managed_by(request.user):
raise PermissionDenied
return super().get(request, *args, **kwargs)
def get_form_class(self): def get_form_class(self):
fields = ["name", "is_moderated"] fields = ["name", "is_moderated"]
if self.object.is_file: if self.object.is_file:
@ -242,13 +235,6 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
context_object_name = "file" context_object_name = "file"
form_class = FileEditPropForm form_class = FileEditPropForm
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.can_be_managed_by(request.user):
raise PermissionDenied
return super().get(request, *args, **kwargs)
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
form.fields["parent"].queryset = SithFile.objects.filter(is_folder=True) form.fields["parent"].queryset = SithFile.objects.filter(is_folder=True)
@ -322,9 +308,6 @@ class FileView(CanViewMixin, DetailView, FormMixin):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.form = self.get_form() self.form = self.get_form()
if not self.object.can_be_managed_by(request.user):
raise PermissionDenied
if "clipboard" not in request.session.keys(): if "clipboard" not in request.session.keys():
request.session["clipboard"] = [] request.session["clipboard"] = []
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -372,13 +355,6 @@ class FileDeleteView(CanEditPropMixin, DeleteView):
template_name = "core/file_delete_confirm.jinja" template_name = "core/file_delete_confirm.jinja"
context_object_name = "file" context_object_name = "file"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.can_be_managed_by(request.user):
raise PermissionDenied
return super().get(request, *args, **kwargs)
def get_success_url(self): def get_success_url(self):
self.object.file.delete() # Doing it here or overloading delete() is the same, so let's do it here self.object.file.delete() # Doing it here or overloading delete() is the same, so let's do it here
if "next" in self.request.GET.keys(): if "next" in self.request.GET.keys():
@ -416,6 +392,7 @@ class FileModerateView(CanEditPropMixin, SingleObjectMixin):
model = SithFile model = SithFile
pk_url_kwarg = "file_id" pk_url_kwarg = "file_id"
# FIXME : wrong http method. This should be a POST or a DELETE request
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
self.object.is_moderated = True self.object.is_moderated = True

View File

@ -561,6 +561,8 @@ class UserListView(ListView, CanEditPropMixin):
template_name = "core/user_list.jinja" template_name = "core/user_list.jinja"
# FIXME: the edit_once fields aren't displayed to the user (as expected).
# However, if the user re-add them manually in the form, they are saved.
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
"""Edit a user's profile.""" """Edit a user's profile."""

View File

@ -362,7 +362,6 @@ class CounterQuerySet(models.QuerySet):
``` ```
""" """
subquery = user.counters.filter(pk=OuterRef("pk")) subquery = user.counters.filter(pk=OuterRef("pk"))
# noinspection PyTypeChecker
return self.annotate(has_annotated_barman=Exists(subquery)) return self.annotate(has_annotated_barman=Exists(subquery))

View File

@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-01 23:45+0200\n" "POT-Creation-Date: 2024-09-08 13:30+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Skia <skia@libskia.so>\n" "Last-Translator: Skia <skia@libskia.so>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@ -17,9 +17,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: accounting/models.py:50 accounting/models.py:91 accounting/models.py:124 #: accounting/models.py:50 accounting/models.py:91 accounting/models.py:124
#: accounting/models.py:191 club/models.py:52 com/models.py:274 #: accounting/models.py:191 club/models.py:54 com/models.py:274
#: com/models.py:293 counter/models.py:208 counter/models.py:239 #: com/models.py:293 counter/models.py:208 counter/models.py:239
#: counter/models.py:370 forum/models.py:59 launderette/models.py:29 #: counter/models.py:369 forum/models.py:59 launderette/models.py:29
#: launderette/models.py:84 launderette/models.py:122 stock/models.py:36 #: launderette/models.py:84 launderette/models.py:122 stock/models.py:36
#: stock/models.py:57 stock/models.py:97 stock/models.py:125 #: stock/models.py:57 stock/models.py:97 stock/models.py:125
msgid "name" msgid "name"
@ -67,7 +67,7 @@ msgstr "numéro de compte"
#: accounting/models.py:97 accounting/models.py:128 club/models.py:344 #: accounting/models.py:97 accounting/models.py:128 club/models.py:344
#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:257 #: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:257
#: counter/models.py:372 trombi/models.py:210 #: counter/models.py:371 trombi/models.py:210
msgid "club" msgid "club"
msgstr "club" msgstr "club"
@ -88,12 +88,12 @@ msgstr "Compte club"
msgid "%(club_account)s on %(bank_account)s" msgid "%(club_account)s on %(bank_account)s"
msgstr "%(club_account)s sur %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s"
#: accounting/models.py:189 club/models.py:350 counter/models.py:853 #: accounting/models.py:189 club/models.py:350 counter/models.py:852
#: election/models.py:16 launderette/models.py:179 #: election/models.py:16 launderette/models.py:179
msgid "start date" msgid "start date"
msgstr "date de début" msgstr "date de début"
#: accounting/models.py:190 club/models.py:351 counter/models.py:854 #: accounting/models.py:190 club/models.py:351 counter/models.py:853
#: election/models.py:17 #: election/models.py:17
msgid "end date" msgid "end date"
msgstr "date de fin" msgstr "date de fin"
@ -107,7 +107,7 @@ msgid "club account"
msgstr "compte club" msgstr "compte club"
#: accounting/models.py:200 accounting/models.py:260 counter/models.py:55 #: accounting/models.py:200 accounting/models.py:260 counter/models.py:55
#: counter/models.py:576 #: counter/models.py:575
msgid "amount" msgid "amount"
msgstr "montant" msgstr "montant"
@ -129,18 +129,18 @@ msgstr "classeur"
#: accounting/models.py:261 core/models.py:904 core/models.py:1431 #: accounting/models.py:261 core/models.py:904 core/models.py:1431
#: core/models.py:1476 core/models.py:1505 core/models.py:1529 #: core/models.py:1476 core/models.py:1505 core/models.py:1529
#: counter/models.py:586 counter/models.py:679 counter/models.py:889 #: counter/models.py:585 counter/models.py:678 counter/models.py:888
#: eboutic/models.py:57 eboutic/models.py:173 forum/models.py:311 #: eboutic/models.py:57 eboutic/models.py:173 forum/models.py:311
#: forum/models.py:412 stock/models.py:96 #: forum/models.py:412 stock/models.py:96
msgid "date" msgid "date"
msgstr "date" msgstr "date"
#: accounting/models.py:262 counter/models.py:210 counter/models.py:890 #: accounting/models.py:262 counter/models.py:210 counter/models.py:889
#: pedagogy/models.py:207 stock/models.py:99 #: pedagogy/models.py:207 stock/models.py:99
msgid "comment" msgid "comment"
msgstr "commentaire" msgstr "commentaire"
#: accounting/models.py:264 counter/models.py:588 counter/models.py:681 #: accounting/models.py:264 counter/models.py:587 counter/models.py:680
#: subscription/models.py:56 #: subscription/models.py:56
msgid "payment method" msgid "payment method"
msgstr "méthode de paiement" msgstr "méthode de paiement"
@ -167,7 +167,7 @@ msgstr "type comptable"
#: accounting/models.py:299 accounting/models.py:438 accounting/models.py:471 #: accounting/models.py:299 accounting/models.py:438 accounting/models.py:471
#: accounting/models.py:503 core/models.py:1504 core/models.py:1530 #: accounting/models.py:503 core/models.py:1504 core/models.py:1530
#: counter/models.py:645 #: counter/models.py:644
msgid "label" msgid "label"
msgstr "étiquette" msgstr "étiquette"
@ -625,7 +625,7 @@ msgstr "No"
#: counter/templates/counter/last_ops.jinja:20 #: counter/templates/counter/last_ops.jinja:20
#: counter/templates/counter/last_ops.jinja:45 #: counter/templates/counter/last_ops.jinja:45
#: counter/templates/counter/refilling_list.jinja:16 #: counter/templates/counter/refilling_list.jinja:16
#: rootplace/templates/rootplace/logs.jinja:12 sas/views.py:356 #: rootplace/templates/rootplace/logs.jinja:12 sas/views.py:357
#: stock/templates/stock/stock_shopping_list.jinja:25 #: stock/templates/stock/stock_shopping_list.jinja:25
#: stock/templates/stock/stock_shopping_list.jinja:54 #: stock/templates/stock/stock_shopping_list.jinja:54
#: trombi/templates/trombi/user_profile.jinja:40 #: trombi/templates/trombi/user_profile.jinja:40
@ -947,15 +947,15 @@ msgstr "Retirer"
msgid "Action" msgid "Action"
msgstr "Action" msgstr "Action"
#: club/forms.py:108 club/tests.py:699 #: club/forms.py:108 club/tests.py:711
msgid "This field is required" msgid "This field is required"
msgstr "Ce champ est obligatoire" msgstr "Ce champ est obligatoire"
#: club/forms.py:118 club/forms.py:245 club/tests.py:712 #: club/forms.py:118 club/forms.py:245 club/tests.py:724
msgid "One of the selected users doesn't exist" msgid "One of the selected users doesn't exist"
msgstr "Un des utilisateurs sélectionné n'existe pas" msgstr "Un des utilisateurs sélectionné n'existe pas"
#: club/forms.py:122 club/tests.py:729 #: club/forms.py:122 club/tests.py:741
msgid "One of the selected users doesn't have an email address" msgid "One of the selected users doesn't have an email address"
msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email" msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email"
@ -963,7 +963,7 @@ msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email"
msgid "An action is required" msgid "An action is required"
msgstr "Une action est requise" msgstr "Une action est requise"
#: club/forms.py:144 club/tests.py:686 #: club/forms.py:144 club/tests.py:698
msgid "You must specify at least an user or an email address" msgid "You must specify at least an user or an email address"
msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email"
@ -1013,11 +1013,11 @@ msgstr "Vous devez choisir un rôle"
msgid "You do not have the permission to do that" msgid "You do not have the permission to do that"
msgstr "Vous n'avez pas la permission de faire cela" msgstr "Vous n'avez pas la permission de faire cela"
#: club/models.py:57 #: club/models.py:59
msgid "unix name" msgid "unix name"
msgstr "nom unix" msgstr "nom unix"
#: club/models.py:64 #: club/models.py:66
msgid "" msgid ""
"Enter a valid unix name. This value may contain only letters, numbers ./-/_ " "Enter a valid unix name. This value may contain only letters, numbers ./-/_ "
"characters." "characters."
@ -1025,41 +1025,41 @@ msgstr ""
"Entrez un nom UNIX valide. Cette valeur peut contenir uniquement des " "Entrez un nom UNIX valide. Cette valeur peut contenir uniquement des "
"lettres, des nombres, et les caractères ./-/_" "lettres, des nombres, et les caractères ./-/_"
#: club/models.py:69 #: club/models.py:71
msgid "A club with that unix name already exists." msgid "A club with that unix name already exists."
msgstr "Un club avec ce nom UNIX existe déjà." msgstr "Un club avec ce nom UNIX existe déjà."
#: club/models.py:72 #: club/models.py:74
msgid "logo" msgid "logo"
msgstr "logo" msgstr "logo"
#: club/models.py:74 #: club/models.py:76
msgid "is active" msgid "is active"
msgstr "actif" msgstr "actif"
#: club/models.py:76 #: club/models.py:78
msgid "short description" msgid "short description"
msgstr "description courte" msgstr "description courte"
#: club/models.py:78 core/models.py:366 #: club/models.py:80 core/models.py:366
msgid "address" msgid "address"
msgstr "Adresse" msgstr "Adresse"
#: club/models.py:95 core/models.py:277 #: club/models.py:97 core/models.py:277
msgid "home" msgid "home"
msgstr "home" msgstr "home"
#: club/models.py:147 #: club/models.py:149
msgid "You can not make loops in clubs" msgid "You can not make loops in clubs"
msgstr "Vous ne pouvez pas faire de boucles dans les clubs" msgstr "Vous ne pouvez pas faire de boucles dans les clubs"
#: club/models.py:171 #: club/models.py:173
msgid "A club with that unix_name already exists" msgid "A club with that unix_name already exists"
msgstr "Un club avec ce nom UNIX existe déjà." msgstr "Un club avec ce nom UNIX existe déjà."
#: club/models.py:336 counter/models.py:844 counter/models.py:880 #: club/models.py:336 counter/models.py:843 counter/models.py:879
#: eboutic/models.py:53 eboutic/models.py:169 election/models.py:183 #: eboutic/models.py:53 eboutic/models.py:169 election/models.py:183
#: launderette/models.py:136 launderette/models.py:198 sas/models.py:254 #: launderette/models.py:136 launderette/models.py:198 sas/models.py:269
#: trombi/models.py:206 #: trombi/models.py:206
msgid "user" msgid "user"
msgstr "nom d'utilisateur" msgstr "nom d'utilisateur"
@ -1104,7 +1104,7 @@ msgstr "Liste de diffusion"
msgid "At least user or email is required" msgid "At least user or email is required"
msgstr "Au moins un utilisateur ou un email est nécessaire" msgstr "Au moins un utilisateur ou un email est nécessaire"
#: club/models.py:530 club/tests.py:757 #: club/models.py:530 club/tests.py:769
msgid "This email is already suscribed in this mailing" msgid "This email is already suscribed in this mailing"
msgstr "Cet email est déjà abonné à cette mailing" msgstr "Cet email est déjà abonné à cette mailing"
@ -2245,7 +2245,7 @@ msgstr "avoir une notification pour chaque click"
msgid "get a notification for every refilling" msgid "get a notification for every refilling"
msgstr "avoir une notification pour chaque rechargement" msgstr "avoir une notification pour chaque rechargement"
#: core/models.py:859 #: core/models.py:859 sas/views.py:356
msgid "file name" msgid "file name"
msgstr "nom du fichier" msgstr "nom du fichier"
@ -2473,7 +2473,7 @@ msgstr "Forum"
msgid "Gallery" msgid "Gallery"
msgstr "Photos" msgstr "Photos"
#: core/templates/core/base.jinja:221 counter/models.py:380 #: core/templates/core/base.jinja:221 counter/models.py:379
#: counter/templates/counter/counter_list.jinja:11 #: counter/templates/counter/counter_list.jinja:11
#: eboutic/templates/eboutic/eboutic_main.jinja:4 #: eboutic/templates/eboutic/eboutic_main.jinja:4
#: eboutic/templates/eboutic/eboutic_main.jinja:22 #: eboutic/templates/eboutic/eboutic_main.jinja:22
@ -3556,7 +3556,7 @@ msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s"
msgid "Error uploading file %(file_name)s: %(msg)s" msgid "Error uploading file %(file_name)s: %(msg)s"
msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s" msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s"
#: core/views/files.py:235 sas/views.py:359 #: core/views/files.py:235 sas/views.py:360
msgid "Apply rights recursively" msgid "Apply rights recursively"
msgstr "Appliquer les droits récursivement" msgstr "Appliquer les droits récursivement"
@ -3716,8 +3716,8 @@ msgstr "Photos"
msgid "Galaxy" msgid "Galaxy"
msgstr "Galaxie" msgstr "Galaxie"
#: counter/apps.py:30 counter/models.py:396 counter/models.py:850 #: counter/apps.py:30 counter/models.py:395 counter/models.py:849
#: counter/models.py:886 launderette/models.py:32 stock/models.py:39 #: counter/models.py:885 launderette/models.py:32 stock/models.py:39
msgid "counter" msgid "counter"
msgstr "comptoir" msgstr "comptoir"
@ -3829,77 +3829,77 @@ msgstr "groupe d'achat"
msgid "archived" msgid "archived"
msgstr "archivé" msgstr "archivé"
#: counter/models.py:275 counter/models.py:986 #: counter/models.py:275 counter/models.py:985
msgid "product" msgid "product"
msgstr "produit" msgstr "produit"
#: counter/models.py:375 #: counter/models.py:374
msgid "products" msgid "products"
msgstr "produits" msgstr "produits"
#: counter/models.py:378 #: counter/models.py:377
msgid "counter type" msgid "counter type"
msgstr "type de comptoir" msgstr "type de comptoir"
#: counter/models.py:380 #: counter/models.py:379
msgid "Bar" msgid "Bar"
msgstr "Bar" msgstr "Bar"
#: counter/models.py:380 #: counter/models.py:379
msgid "Office" msgid "Office"
msgstr "Bureau" msgstr "Bureau"
#: counter/models.py:383 #: counter/models.py:382
msgid "sellers" msgid "sellers"
msgstr "vendeurs" msgstr "vendeurs"
#: counter/models.py:391 launderette/models.py:192 #: counter/models.py:390 launderette/models.py:192
msgid "token" msgid "token"
msgstr "jeton" msgstr "jeton"
#: counter/models.py:594 #: counter/models.py:593
msgid "bank" msgid "bank"
msgstr "banque" msgstr "banque"
#: counter/models.py:596 counter/models.py:686 #: counter/models.py:595 counter/models.py:685
msgid "is validated" msgid "is validated"
msgstr "est validé" msgstr "est validé"
#: counter/models.py:599 #: counter/models.py:598
msgid "refilling" msgid "refilling"
msgstr "rechargement" msgstr "rechargement"
#: counter/models.py:663 eboutic/models.py:227 #: counter/models.py:662 eboutic/models.py:227
msgid "unit price" msgid "unit price"
msgstr "prix unitaire" msgstr "prix unitaire"
#: counter/models.py:664 counter/models.py:966 eboutic/models.py:228 #: counter/models.py:663 counter/models.py:965 eboutic/models.py:228
msgid "quantity" msgid "quantity"
msgstr "quantité" msgstr "quantité"
#: counter/models.py:683 #: counter/models.py:682
msgid "Sith account" msgid "Sith account"
msgstr "Compte utilisateur" msgstr "Compte utilisateur"
#: counter/models.py:683 sith/settings.py:405 sith/settings.py:410 #: counter/models.py:682 sith/settings.py:405 sith/settings.py:410
#: sith/settings.py:430 #: sith/settings.py:430
msgid "Credit card" msgid "Credit card"
msgstr "Carte bancaire" msgstr "Carte bancaire"
#: counter/models.py:689 #: counter/models.py:688
msgid "selling" msgid "selling"
msgstr "vente" msgstr "vente"
#: counter/models.py:793 #: counter/models.py:792
msgid "Unknown event" msgid "Unknown event"
msgstr "Événement inconnu" msgstr "Événement inconnu"
#: counter/models.py:794 #: counter/models.py:793
#, python-format #, python-format
msgid "Eticket bought for the event %(event)s" msgid "Eticket bought for the event %(event)s"
msgstr "Eticket acheté pour l'événement %(event)s" msgstr "Eticket acheté pour l'événement %(event)s"
#: counter/models.py:796 counter/models.py:819 #: counter/models.py:795 counter/models.py:818
#, python-format #, python-format
msgid "" msgid ""
"You bought an eticket for the event %(event)s.\n" "You bought an eticket for the event %(event)s.\n"
@ -3911,63 +3911,63 @@ msgstr ""
"Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte "
"%(url)s." "%(url)s."
#: counter/models.py:855 #: counter/models.py:854
msgid "last activity date" msgid "last activity date"
msgstr "dernière activité" msgstr "dernière activité"
#: counter/models.py:858 #: counter/models.py:857
msgid "permanency" msgid "permanency"
msgstr "permanence" msgstr "permanence"
#: counter/models.py:891 #: counter/models.py:890
msgid "emptied" msgid "emptied"
msgstr "coffre vidée" msgstr "coffre vidée"
#: counter/models.py:894 #: counter/models.py:893
msgid "cash register summary" msgid "cash register summary"
msgstr "relevé de caisse" msgstr "relevé de caisse"
#: counter/models.py:962 #: counter/models.py:961
msgid "cash summary" msgid "cash summary"
msgstr "relevé" msgstr "relevé"
#: counter/models.py:965 #: counter/models.py:964
msgid "value" msgid "value"
msgstr "valeur" msgstr "valeur"
#: counter/models.py:968 #: counter/models.py:967
msgid "check" msgid "check"
msgstr "chèque" msgstr "chèque"
#: counter/models.py:970 #: counter/models.py:969
msgid "True if this is a bank check, else False" msgid "True if this is a bank check, else False"
msgstr "Vrai si c'est un chèque, sinon Faux." msgstr "Vrai si c'est un chèque, sinon Faux."
#: counter/models.py:974 #: counter/models.py:973
msgid "cash register summary item" msgid "cash register summary item"
msgstr "élément de relevé de caisse" msgstr "élément de relevé de caisse"
#: counter/models.py:990 #: counter/models.py:989
msgid "banner" msgid "banner"
msgstr "bannière" msgstr "bannière"
#: counter/models.py:992 #: counter/models.py:991
msgid "event date" msgid "event date"
msgstr "date de l'événement" msgstr "date de l'événement"
#: counter/models.py:994 #: counter/models.py:993
msgid "event title" msgid "event title"
msgstr "titre de l'événement" msgstr "titre de l'événement"
#: counter/models.py:996 #: counter/models.py:995
msgid "secret" msgid "secret"
msgstr "secret" msgstr "secret"
#: counter/models.py:1035 #: counter/models.py:1034
msgid "uid" msgid "uid"
msgstr "uid" msgstr "uid"
#: counter/models.py:1040 #: counter/models.py:1039
msgid "student cards" msgid "student cards"
msgstr "cartes étudiante" msgstr "cartes étudiante"
@ -5323,7 +5323,7 @@ msgstr "Utilisateur qui sera supprimé"
msgid "User to be selected" msgid "User to be selected"
msgstr "Utilisateur à sélectionner" msgstr "Utilisateur à sélectionner"
#: sas/models.py:262 #: sas/models.py:277
msgid "picture" msgid "picture"
msgstr "photo" msgstr "photo"

713
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,10 +20,10 @@ homepage = "https://ae.utbm.fr/"
license = "GPL-3.0-only" license = "GPL-3.0-only"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.12"
Django = "^4.2.14" Django = "^4.2.14"
django-ninja = "^1.2.2" django-ninja = "^1.3.0"
django-ninja-extra = "^0.21.2" django-ninja-extra = "^0.21.4"
Pillow = "^10.4.0" Pillow = "^10.4.0"
mistune = "^3.0.2" mistune = "^3.0.2"
django-jinja = "^2.11" django-jinja = "^2.11"
@ -63,7 +63,7 @@ optional = true
django-debug-toolbar = "^4.4.6" django-debug-toolbar = "^4.4.6"
ipython = "^8.26.0" ipython = "^8.26.0"
pre-commit = "^3.8.0" pre-commit = "^3.8.0"
ruff = "^0.5.5" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml ruff = "^0.5.7" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml
djhtml = "^3.0.6" djhtml = "^3.0.6"
faker = "^26.1.0" faker = "^26.1.0"
rjsmin = "^1.2.2" rjsmin = "^1.2.2"

View File

@ -16,6 +16,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from typing import ClassVar, Self
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
@ -61,7 +62,7 @@ class SasFile(SithFile):
class PictureQuerySet(models.QuerySet): class PictureQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> PictureQuerySet: def viewable_by(self, user: User) -> Self:
"""Filter the pictures that this user can view. """Filter the pictures that this user can view.
Warnings: Warnings:
@ -173,7 +174,7 @@ class Picture(SasFile):
class AlbumQuerySet(models.QuerySet): class AlbumQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> PictureQuerySet: def viewable_by(self, user: User) -> Self:
"""Filter the albums that this user can view. """Filter the albums that this user can view.
Warnings: Warnings:
@ -202,6 +203,20 @@ class SASAlbumManager(models.Manager):
class Album(SasFile): class Album(SasFile):
NAME_MAX_LENGTH: ClassVar[int] = 50
"""Maximum length of an album's name.
[SithFile][core.models.SithFile] have a maximum length
of 256 characters.
However, this limit is too high for albums.
Names longer than 50 characters are harder to read
and harder to display on the SAS page.
It is to be noted, though, that this does not
add or modify any db behaviour.
It's just a constant to be used in views and forms.
"""
class Meta: class Meta:
proxy = True proxy = True

View File

@ -17,14 +17,15 @@ from typing import Callable
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.test import Client from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from pytest_django.asserts import assertRedirects
from core.baker_recipes import old_subscriber_user, subscriber_user from core.baker_recipes import old_subscriber_user, subscriber_user
from core.models import RealGroup, User from core.models import RealGroup, User
from sas.baker_recipes import picture_recipe from sas.baker_recipes import picture_recipe
from sas.models import Album from sas.models import Album, Picture
# Create your tests here. # Create your tests here.
@ -64,3 +65,70 @@ def test_album_access_non_subscriber(client: Client):
cache.clear() cache.clear()
res = client.get(reverse("sas:album", kwargs={"album_id": album.id})) res = client.get(reverse("sas:album", kwargs={"album_id": album.id}))
assert res.status_code == 200 assert res.status_code == 200
class TestSasModeration(TestCase):
@classmethod
def setUpTestData(cls):
album = baker.make(Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID)
cls.pictures = picture_recipe.make(
parent=album, _quantity=10, _bulk_create=True
)
cls.to_moderate = cls.pictures[0]
cls.to_moderate.is_moderated = False
cls.to_moderate.save()
cls.moderator = baker.make(
User, groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
)
cls.simple_user = subscriber_user.make()
def test_moderation_page_sas_admin(self):
"""Test that a moderator can see the pictures needing moderation."""
self.client.force_login(self.moderator)
res = self.client.get(reverse("sas:moderation"))
assert res.status_code == 200
assert len(res.context_data["pictures"]) == 1
assert res.context_data["pictures"][0] == self.to_moderate
res = self.client.post(
reverse("sas:moderation"),
data={"album_id": self.to_moderate.id, "picture_id": self.to_moderate.id},
)
def test_moderation_page_forbidden(self):
self.client.force_login(self.simple_user)
res = self.client.get(reverse("sas:moderation"))
assert res.status_code == 403
def test_moderate_picture(self):
self.client.force_login(self.moderator)
res = self.client.get(
reverse("core:file_moderate", kwargs={"file_id": self.to_moderate.id}),
data={"next": self.pictures[1].get_absolute_url()},
)
assertRedirects(res, self.pictures[1].get_absolute_url())
self.to_moderate.refresh_from_db()
assert self.to_moderate.is_moderated
def test_delete_picture(self):
self.client.force_login(self.moderator)
res = self.client.post(
reverse("core:file_delete", kwargs={"file_id": self.to_moderate.id})
)
assert res.status_code == 302
assert not Picture.objects.filter(pk=self.to_moderate.id).exists()
def test_moderation_action_non_authorized_user(self):
"""Test that a non-authorized user cannot moderate a picture."""
self.client.force_login(self.simple_user)
res = self.client.post(
reverse("core:file_moderate", kwargs={"file_id": self.to_moderate.id}),
)
assert res.status_code == 403
self.to_moderate.refresh_from_db()
assert not self.to_moderate.is_moderated
res = self.client.post(
reverse("core:file_delete", kwargs={"file_id": self.to_moderate.id}),
)
assert res.status_code == 403
assert Picture.objects.filter(pk=self.to_moderate.id).exists()

View File

@ -34,7 +34,7 @@ from sas.models import Album, PeoplePictureRelation, Picture
class SASForm(forms.Form): class SASForm(forms.Form):
album_name = forms.CharField( album_name = forms.CharField(
label=_("Add a new album"), max_length=30, required=False label=_("Add a new album"), max_length=Album.NAME_MAX_LENGTH, required=False
) )
images = MultipleImageField( images = MultipleImageField(
label=_("Upload images"), label=_("Upload images"),
@ -333,10 +333,9 @@ class ModerationView(TemplateView):
kwargs["albums_to_moderate"] = Album.objects.filter( kwargs["albums_to_moderate"] = Album.objects.filter(
is_moderated=False, is_in_sas=True, is_folder=True is_moderated=False, is_in_sas=True, is_folder=True
).order_by("id") ).order_by("id")
kwargs["pictures"] = Picture.objects.filter(is_moderated=False) pictures = Picture.objects.filter(is_moderated=False).select_related("parent")
kwargs["albums"] = Album.objects.filter( kwargs["pictures"] = pictures
id__in=kwargs["pictures"].values("parent").distinct("parent") kwargs["albums"] = [p.parent for p in pictures]
)
return kwargs return kwargs
@ -353,6 +352,7 @@ class AlbumEditForm(forms.ModelForm):
model = Album model = Album
fields = ["name", "date", "file", "parent", "edit_groups"] fields = ["name", "date", "file", "parent", "edit_groups"]
name = forms.CharField(max_length=Album.NAME_MAX_LENGTH, label=_("file name"))
date = forms.DateField(label=_("Date"), widget=SelectDate, required=True) date = forms.DateField(label=_("Date"), widget=SelectDate, required=True)
parent = make_ajax_field(Album, "parent", "files", help_text="") parent = make_ajax_field(Album, "parent", "files", help_text="")
edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="") edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="")