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
- name: Set up python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: Load cached Poetry installation
id: cached-poetry
uses: actions/cache@v3
with:
path: ~/.local
key: poetry-0 # increment to reset cache
key: poetry-1 # increment to reset cache
- name: Install Poetry
if: steps.cached-poetry.outputs.cache-hit != 'true'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@ from django.utils.translation import gettext as _
from club.forms import MailingForm
from club.models import Club, Mailing, Membership
from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, User
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)
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):
"""Test that the board queryset method returns the memberships
of user in the club board.
@ -422,11 +435,11 @@ class TestClubModel(TestClub):
of anyone.
"""
# make subscriber a board member
self.subscriber.memberships.all().delete()
Membership.objects.create(club=self.ae, user=self.subscriber, role=3)
subscriber = subscriber_user.make()
Membership.objects.create(club=self.ae, user=subscriber, role=3)
nb_memberships = self.club.members.count()
self.client.force_login(self.subscriber)
nb_memberships = self.club.members.ongoing().count()
self.client.force_login(subscriber)
response = self.client.post(
self.members_url,
{"users_old": self.comptable.id},
@ -437,7 +450,7 @@ class TestClubModel(TestClub):
def test_end_membership_as_root(self):
"""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)
response = self.client.post(
self.members_url,
@ -446,7 +459,6 @@ class TestClubModel(TestClub):
self.assertRedirects(response, self.members_url)
self.assert_membership_ended_today(self.comptable)
assert self.club.members.ongoing().count() == nb_memberships - 1
assert self.club.members.count() == nb_memberships
def test_end_membership_as_foreigner(self):
"""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
echo "Installing Xapian-core..."
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
echo "Installing Xapian-bindings..."
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
rm -rf "$VIRTUAL_ENV/packages"

View File

@ -944,40 +944,15 @@ class SithFile(models.Model):
param="1",
).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):
if user.is_anonymous:
return False
if hasattr(self, "profile_of") and user.is_board_member:
if user.is_root:
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:
return True
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 False
def delete(self):
def delete(self, *args, **kwargs):
for c in self.children.all():
c.delete()
self.file.delete()

View File

@ -190,13 +190,6 @@ class FileEditView(CanEditMixin, UpdateView):
template_name = "core/file_edit.jinja"
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):
fields = ["name", "is_moderated"]
if self.object.is_file:
@ -242,13 +235,6 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
context_object_name = "file"
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):
form = super().get_form(form_class)
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):
self.form = self.get_form()
if not self.object.can_be_managed_by(request.user):
raise PermissionDenied
if "clipboard" not in request.session.keys():
request.session["clipboard"] = []
return super().get(request, *args, **kwargs)
@ -372,13 +355,6 @@ class FileDeleteView(CanEditPropMixin, DeleteView):
template_name = "core/file_delete_confirm.jinja"
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):
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():
@ -416,6 +392,7 @@ class FileModerateView(CanEditPropMixin, SingleObjectMixin):
model = SithFile
pk_url_kwarg = "file_id"
# FIXME : wrong http method. This should be a POST or a DELETE request
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.is_moderated = True

View File

@ -561,6 +561,8 @@ class UserListView(ListView, CanEditPropMixin):
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):
"""Edit a user's profile."""

View File

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

View File

@ -6,7 +6,7 @@
msgid ""
msgstr ""
"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"
"Last-Translator: Skia <skia@libskia.so>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@ -17,9 +17,9 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: 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
#: 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
#: stock/models.py:57 stock/models.py:97 stock/models.py:125
msgid "name"
@ -67,7 +67,7 @@ msgstr "numéro de compte"
#: 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
#: counter/models.py:372 trombi/models.py:210
#: counter/models.py:371 trombi/models.py:210
msgid "club"
msgstr "club"
@ -88,12 +88,12 @@ msgstr "Compte club"
msgid "%(club_account)s on %(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
msgid "start date"
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
msgid "end date"
msgstr "date de fin"
@ -107,7 +107,7 @@ msgid "club account"
msgstr "compte club"
#: accounting/models.py:200 accounting/models.py:260 counter/models.py:55
#: counter/models.py:576
#: counter/models.py:575
msgid "amount"
msgstr "montant"
@ -129,18 +129,18 @@ msgstr "classeur"
#: accounting/models.py:261 core/models.py:904 core/models.py:1431
#: 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
#: forum/models.py:412 stock/models.py:96
msgid "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
msgid "comment"
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
msgid "payment method"
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:503 core/models.py:1504 core/models.py:1530
#: counter/models.py:645
#: counter/models.py:644
msgid "label"
msgstr "étiquette"
@ -625,7 +625,7 @@ msgstr "No"
#: counter/templates/counter/last_ops.jinja:20
#: counter/templates/counter/last_ops.jinja:45
#: 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:54
#: trombi/templates/trombi/user_profile.jinja:40
@ -947,15 +947,15 @@ msgstr "Retirer"
msgid "Action"
msgstr "Action"
#: club/forms.py:108 club/tests.py:699
#: club/forms.py:108 club/tests.py:711
msgid "This field is required"
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"
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"
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"
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"
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"
msgstr "Vous n'avez pas la permission de faire cela"
#: club/models.py:57
#: club/models.py:59
msgid "unix name"
msgstr "nom unix"
#: club/models.py:64
#: club/models.py:66
msgid ""
"Enter a valid unix name. This value may contain only letters, numbers ./-/_ "
"characters."
@ -1025,41 +1025,41 @@ msgstr ""
"Entrez un nom UNIX valide. Cette valeur peut contenir uniquement des "
"lettres, des nombres, et les caractères ./-/_"
#: club/models.py:69
#: club/models.py:71
msgid "A club with that unix name already exists."
msgstr "Un club avec ce nom UNIX existe déjà."
#: club/models.py:72
#: club/models.py:74
msgid "logo"
msgstr "logo"
#: club/models.py:74
#: club/models.py:76
msgid "is active"
msgstr "actif"
#: club/models.py:76
#: club/models.py:78
msgid "short description"
msgstr "description courte"
#: club/models.py:78 core/models.py:366
#: club/models.py:80 core/models.py:366
msgid "address"
msgstr "Adresse"
#: club/models.py:95 core/models.py:277
#: club/models.py:97 core/models.py:277
msgid "home"
msgstr "home"
#: club/models.py:147
#: club/models.py:149
msgid "You can not make loops in 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"
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
#: 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
msgid "user"
msgstr "nom d'utilisateur"
@ -1104,7 +1104,7 @@ msgstr "Liste de diffusion"
msgid "At least user or email is required"
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"
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"
msgstr "avoir une notification pour chaque rechargement"
#: core/models.py:859
#: core/models.py:859 sas/views.py:356
msgid "file name"
msgstr "nom du fichier"
@ -2473,7 +2473,7 @@ msgstr "Forum"
msgid "Gallery"
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
#: eboutic/templates/eboutic/eboutic_main.jinja:4
#: 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"
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"
msgstr "Appliquer les droits récursivement"
@ -3716,8 +3716,8 @@ msgstr "Photos"
msgid "Galaxy"
msgstr "Galaxie"
#: counter/apps.py:30 counter/models.py:396 counter/models.py:850
#: counter/models.py:886 launderette/models.py:32 stock/models.py:39
#: counter/apps.py:30 counter/models.py:395 counter/models.py:849
#: counter/models.py:885 launderette/models.py:32 stock/models.py:39
msgid "counter"
msgstr "comptoir"
@ -3829,77 +3829,77 @@ msgstr "groupe d'achat"
msgid "archived"
msgstr "archivé"
#: counter/models.py:275 counter/models.py:986
#: counter/models.py:275 counter/models.py:985
msgid "product"
msgstr "produit"
#: counter/models.py:375
#: counter/models.py:374
msgid "products"
msgstr "produits"
#: counter/models.py:378
#: counter/models.py:377
msgid "counter type"
msgstr "type de comptoir"
#: counter/models.py:380
#: counter/models.py:379
msgid "Bar"
msgstr "Bar"
#: counter/models.py:380
#: counter/models.py:379
msgid "Office"
msgstr "Bureau"
#: counter/models.py:383
#: counter/models.py:382
msgid "sellers"
msgstr "vendeurs"
#: counter/models.py:391 launderette/models.py:192
#: counter/models.py:390 launderette/models.py:192
msgid "token"
msgstr "jeton"
#: counter/models.py:594
#: counter/models.py:593
msgid "bank"
msgstr "banque"
#: counter/models.py:596 counter/models.py:686
#: counter/models.py:595 counter/models.py:685
msgid "is validated"
msgstr "est validé"
#: counter/models.py:599
#: counter/models.py:598
msgid "refilling"
msgstr "rechargement"
#: counter/models.py:663 eboutic/models.py:227
#: counter/models.py:662 eboutic/models.py:227
msgid "unit price"
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"
msgstr "quantité"
#: counter/models.py:683
#: counter/models.py:682
msgid "Sith account"
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
msgid "Credit card"
msgstr "Carte bancaire"
#: counter/models.py:689
#: counter/models.py:688
msgid "selling"
msgstr "vente"
#: counter/models.py:793
#: counter/models.py:792
msgid "Unknown event"
msgstr "Événement inconnu"
#: counter/models.py:794
#: counter/models.py:793
#, python-format
msgid "Eticket bought for the event %(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
msgid ""
"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 "
"%(url)s."
#: counter/models.py:855
#: counter/models.py:854
msgid "last activity date"
msgstr "dernière activité"
#: counter/models.py:858
#: counter/models.py:857
msgid "permanency"
msgstr "permanence"
#: counter/models.py:891
#: counter/models.py:890
msgid "emptied"
msgstr "coffre vidée"
#: counter/models.py:894
#: counter/models.py:893
msgid "cash register summary"
msgstr "relevé de caisse"
#: counter/models.py:962
#: counter/models.py:961
msgid "cash summary"
msgstr "relevé"
#: counter/models.py:965
#: counter/models.py:964
msgid "value"
msgstr "valeur"
#: counter/models.py:968
#: counter/models.py:967
msgid "check"
msgstr "chèque"
#: counter/models.py:970
#: counter/models.py:969
msgid "True if this is a bank check, else False"
msgstr "Vrai si c'est un chèque, sinon Faux."
#: counter/models.py:974
#: counter/models.py:973
msgid "cash register summary item"
msgstr "élément de relevé de caisse"
#: counter/models.py:990
#: counter/models.py:989
msgid "banner"
msgstr "bannière"
#: counter/models.py:992
#: counter/models.py:991
msgid "event date"
msgstr "date de l'événement"
#: counter/models.py:994
#: counter/models.py:993
msgid "event title"
msgstr "titre de l'événement"
#: counter/models.py:996
#: counter/models.py:995
msgid "secret"
msgstr "secret"
#: counter/models.py:1035
#: counter/models.py:1034
msgid "uid"
msgstr "uid"
#: counter/models.py:1040
#: counter/models.py:1039
msgid "student cards"
msgstr "cartes étudiante"
@ -5323,7 +5323,7 @@ msgstr "Utilisateur qui sera supprimé"
msgid "User to be selected"
msgstr "Utilisateur à sélectionner"
#: sas/models.py:262
#: sas/models.py:277
msgid "picture"
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"
[tool.poetry.dependencies]
python = "^3.10"
python = "^3.12"
Django = "^4.2.14"
django-ninja = "^1.2.2"
django-ninja-extra = "^0.21.2"
django-ninja = "^1.3.0"
django-ninja-extra = "^0.21.4"
Pillow = "^10.4.0"
mistune = "^3.0.2"
django-jinja = "^2.11"
@ -63,7 +63,7 @@ optional = true
django-debug-toolbar = "^4.4.6"
ipython = "^8.26.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"
faker = "^26.1.0"
rjsmin = "^1.2.2"

View File

@ -16,6 +16,7 @@
from __future__ import annotations
from io import BytesIO
from typing import ClassVar, Self
from django.conf import settings
from django.core.cache import cache
@ -61,7 +62,7 @@ class SasFile(SithFile):
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.
Warnings:
@ -173,7 +174,7 @@ class Picture(SasFile):
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.
Warnings:
@ -202,6 +203,20 @@ class SASAlbumManager(models.Manager):
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:
proxy = True

View File

@ -17,14 +17,15 @@ from typing import Callable
import pytest
from django.conf import settings
from django.core.cache import cache
from django.test import Client
from django.test import Client, TestCase
from django.urls import reverse
from model_bakery import baker
from pytest_django.asserts import assertRedirects
from core.baker_recipes import old_subscriber_user, subscriber_user
from core.models import RealGroup, User
from sas.baker_recipes import picture_recipe
from sas.models import Album
from sas.models import Album, Picture
# Create your tests here.
@ -64,3 +65,70 @@ def test_album_access_non_subscriber(client: Client):
cache.clear()
res = client.get(reverse("sas:album", kwargs={"album_id": album.id}))
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):
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(
label=_("Upload images"),
@ -333,10 +333,9 @@ class ModerationView(TemplateView):
kwargs["albums_to_moderate"] = Album.objects.filter(
is_moderated=False, is_in_sas=True, is_folder=True
).order_by("id")
kwargs["pictures"] = Picture.objects.filter(is_moderated=False)
kwargs["albums"] = Album.objects.filter(
id__in=kwargs["pictures"].values("parent").distinct("parent")
)
pictures = Picture.objects.filter(is_moderated=False).select_related("parent")
kwargs["pictures"] = pictures
kwargs["albums"] = [p.parent for p in pictures]
return kwargs
@ -353,6 +352,7 @@ class AlbumEditForm(forms.ModelForm):
model = Album
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)
parent = make_ajax_field(Album, "parent", "files", help_text="")
edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="")