feat: display moderation requests to moderators

This commit is contained in:
imperosol 2024-10-14 00:45:31 +02:00
parent 5348a451e9
commit 19cd51043a
10 changed files with 362 additions and 187 deletions

View File

@ -333,9 +333,18 @@ a:not(.button) {
border: #fc8181 1px solid;
}
.alert-title {
margin-top: 0;
}
.alert-main {
flex: 2;
}
.alert-aside {
display: flex;
flex-direction: column;
}
}
.tool_bar {

View File

@ -148,7 +148,6 @@ import type {
GroupedDataFormat,
LoadingData,
Options,
PlainObject,
} from "select2";
import "select2/dist/css/select2.css";
@ -181,7 +180,7 @@ interface Select2Options {
* Create a new select2 with sith presets
*/
export function sithSelect2(options: Select2Options) {
const elem: PlainObject = $(options.element);
const elem = $(options.element as HTMLInputElement);
return elem.select2({
theme: elem[0].multiple ? "classic" : "default",
minimumInputLength: 2,

View File

@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-11 09:58+0200\n"
"POT-Creation-Date: 2024-10-14 00:46+0200\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@ -129,7 +129,7 @@ msgstr "classeur"
#: accounting/models.py:273 core/models.py:959 core/models.py:1479
#: core/models.py:1524 core/models.py:1553 core/models.py:1577
#: counter/models.py:664 counter/models.py:768 counter/models.py:980
#: eboutic/models.py:57 eboutic/models.py:189 forum/models.py:311
#: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:311
#: forum/models.py:412
msgid "date"
msgstr "date"
@ -148,7 +148,7 @@ msgstr "méthode de paiement"
msgid "cheque number"
msgstr "numéro de chèque"
#: accounting/models.py:286 eboutic/models.py:287
#: accounting/models.py:286 eboutic/models.py:291
msgid "invoice"
msgstr "facture"
@ -210,7 +210,7 @@ msgstr "Utilisateur"
msgid "Club"
msgstr "Club"
#: accounting/models.py:322 core/views/user.py:281
#: accounting/models.py:322 core/views/user.py:283
msgid "Account"
msgstr "Compte"
@ -378,14 +378,14 @@ msgstr "Compte en banque : "
#: launderette/views.py:217 pedagogy/templates/pedagogy/guide.jinja:99
#: pedagogy/templates/pedagogy/guide.jinja:114
#: pedagogy/templates/pedagogy/uv_detail.jinja:189
#: sas/templates/sas/album.jinja:32 sas/templates/sas/moderation.jinja:18
#: sas/templates/sas/picture.jinja:50 trombi/templates/trombi/detail.jinja:35
#: sas/templates/sas/album.jinja:36 sas/templates/sas/moderation.jinja:18
#: sas/templates/sas/picture.jinja:69 trombi/templates/trombi/detail.jinja:35
#: trombi/templates/trombi/edit_profile.jinja:35
msgid "Delete"
msgstr "Supprimer"
#: accounting/templates/accounting/bank_account_details.jinja:18
#: club/views.py:79 core/views/user.py:200 sas/templates/sas/picture.jinja:72
#: club/views.py:79 core/views/user.py:202 sas/templates/sas/picture.jinja:89
msgid "Infos"
msgstr "Infos"
@ -419,7 +419,7 @@ msgstr "Nouveau compte club"
#: com/templates/com/weekmail.jinja:61 core/templates/core/file.jinja:38
#: core/templates/core/group_list.jinja:24 core/templates/core/page.jinja:35
#: core/templates/core/poster_list.jinja:40
#: core/templates/core/user_tools.jinja:71 core/views/user.py:230
#: core/templates/core/user_tools.jinja:71 core/views/user.py:232
#: counter/templates/counter/cash_summary_list.jinja:53
#: counter/templates/counter/counter_list.jinja:17
#: counter/templates/counter/counter_list.jinja:33
@ -430,7 +430,7 @@ msgstr "Nouveau compte club"
#: pedagogy/templates/pedagogy/guide.jinja:98
#: pedagogy/templates/pedagogy/guide.jinja:113
#: pedagogy/templates/pedagogy/uv_detail.jinja:188
#: sas/templates/sas/album.jinja:31 trombi/templates/trombi/detail.jinja:9
#: sas/templates/sas/album.jinja:35 trombi/templates/trombi/detail.jinja:9
#: trombi/templates/trombi/edit_profile.jinja:34
msgid "Edit"
msgstr "Éditer"
@ -650,7 +650,7 @@ msgid "Done"
msgstr "Effectuées"
#: accounting/templates/accounting/journal_details.jinja:41
#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:944
#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:962
#: pedagogy/templates/pedagogy/moderation.jinja:13
#: pedagogy/templates/pedagogy/uv_detail.jinja:142
#: trombi/templates/trombi/comment.jinja:4
@ -967,15 +967,15 @@ msgstr "Date de fin"
#: club/forms.py:160 club/templates/club/club_sellings.jinja:49
#: core/templates/core/user_account_detail.jinja:17
#: core/templates/core/user_account_detail.jinja:56
#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:141
#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:143
msgid "Counter"
msgstr "Comptoir"
#: club/forms.py:167 counter/views.py:688
#: club/forms.py:167 counter/views.py:690
msgid "Products"
msgstr "Produits"
#: club/forms.py:172 counter/views.py:693
#: club/forms.py:172 counter/views.py:695
msgid "Archived products"
msgstr "Produits archivés"
@ -1046,7 +1046,7 @@ msgid "A club with that unix_name already exists"
msgstr "Un club avec ce nom UNIX existe déjà."
#: club/models.py:337 counter/models.py:935 counter/models.py:971
#: eboutic/models.py:53 eboutic/models.py:185 election/models.py:183
#: eboutic/models.py:53 eboutic/models.py:189 election/models.py:183
#: launderette/models.py:136 launderette/models.py:198 sas/models.py:274
#: trombi/models.py:206
msgid "user"
@ -1373,8 +1373,8 @@ msgstr "Anciens membres"
msgid "History"
msgstr "Historique"
#: club/views.py:116 core/templates/core/base.jinja:107 core/views/user.py:223
#: sas/templates/sas/picture.jinja:91 trombi/views.py:61
#: club/views.py:116 core/templates/core/base.jinja:104 core/views/user.py:225
#: sas/templates/sas/picture.jinja:108 trombi/views.py:61
msgid "Tools"
msgstr "Outils"
@ -1517,7 +1517,7 @@ msgstr "Administration des mailing listes"
#: com/templates/com/news_detail.jinja:39
#: core/templates/core/file_detail.jinja:65
#: core/templates/core/file_moderation.jinja:23
#: sas/templates/sas/moderation.jinja:17 sas/templates/sas/picture.jinja:47
#: sas/templates/sas/moderation.jinja:17 sas/templates/sas/picture.jinja:66
msgid "Moderate"
msgstr "Modérer"
@ -1659,7 +1659,7 @@ msgid "Calls to moderate"
msgstr "Appels à modérer"
#: com/templates/com/news_admin_list.jinja:242
#: core/templates/core/base.jinja:222
#: core/templates/core/base.jinja:219
msgid "Events"
msgstr "Événements"
@ -2388,7 +2388,7 @@ msgstr "403, Non autorisé"
msgid "404, Not Found"
msgstr "404. Non trouvé"
#: core/templates/core/500.jinja:11
#: core/templates/core/500.jinja:9
msgid "500, Server Error"
msgstr "500, Erreur Serveur"
@ -2396,18 +2396,18 @@ msgstr "500, Erreur Serveur"
msgid "Welcome!"
msgstr "Bienvenue !"
#: core/templates/core/base.jinja:59 core/templates/core/login.jinja:8
#: core/templates/core/base.jinja:56 core/templates/core/login.jinja:8
#: core/templates/core/login.jinja:18 core/templates/core/login.jinja:51
#: core/templates/core/password_reset_complete.jinja:5
msgid "Login"
msgstr "Connexion"
#: core/templates/core/base.jinja:60 core/templates/core/register.jinja:7
#: core/templates/core/base.jinja:57 core/templates/core/register.jinja:7
#: core/templates/core/register.jinja:16 core/templates/core/register.jinja:22
msgid "Register"
msgstr "Inscription"
#: core/templates/core/base.jinja:66 core/templates/core/base.jinja:67
#: core/templates/core/base.jinja:63 core/templates/core/base.jinja:64
#: forum/templates/forum/macros.jinja:179
#: forum/templates/forum/macros.jinja:183
#: matmat/templates/matmat/search_form.jinja:39
@ -2416,52 +2416,52 @@ msgstr "Inscription"
msgid "Search"
msgstr "Recherche"
#: core/templates/core/base.jinja:108
#: core/templates/core/base.jinja:105
msgid "Logout"
msgstr "Déconnexion"
#: core/templates/core/base.jinja:156
#: core/templates/core/base.jinja:153
msgid "You do not have any unread notification"
msgstr "Vous n'avez aucune notification non lue"
#: core/templates/core/base.jinja:161
#: core/templates/core/base.jinja:158
msgid "View more"
msgstr "Voir plus"
#: core/templates/core/base.jinja:164
#: core/templates/core/base.jinja:161
#: forum/templates/forum/last_unread.jinja:21
msgid "Mark all as read"
msgstr "Marquer tout comme lu"
#: core/templates/core/base.jinja:212
#: core/templates/core/base.jinja:209
msgid "Main"
msgstr "Accueil"
#: core/templates/core/base.jinja:214
#: core/templates/core/base.jinja:211
msgid "Associations & Clubs"
msgstr "Associations & Clubs"
#: core/templates/core/base.jinja:216
#: core/templates/core/base.jinja:213
msgid "AE"
msgstr "L'AE"
#: core/templates/core/base.jinja:217
#: core/templates/core/base.jinja:214
msgid "AE's clubs"
msgstr "Les clubs de L'AE"
#: core/templates/core/base.jinja:218
#: core/templates/core/base.jinja:215
msgid "Others UTBM's Associations"
msgstr "Les autres associations de l'UTBM"
#: core/templates/core/base.jinja:224 core/templates/core/user_tools.jinja:172
#: core/templates/core/base.jinja:221 core/templates/core/user_tools.jinja:172
msgid "Elections"
msgstr "Élections"
#: core/templates/core/base.jinja:225
#: core/templates/core/base.jinja:222
msgid "Big event"
msgstr "Grandes Activités"
#: core/templates/core/base.jinja:228
#: core/templates/core/base.jinja:225
#: forum/templates/forum/favorite_topics.jinja:18
#: forum/templates/forum/last_unread.jinja:18
#: forum/templates/forum/macros.jinja:90 forum/templates/forum/main.jinja:6
@ -2470,11 +2470,11 @@ msgstr "Grandes Activités"
msgid "Forum"
msgstr "Forum"
#: core/templates/core/base.jinja:229
#: core/templates/core/base.jinja:226
msgid "Gallery"
msgstr "Photos"
#: core/templates/core/base.jinja:230 counter/models.py:466
#: core/templates/core/base.jinja:227 counter/models.py:466
#: counter/templates/counter/counter_list.jinja:11
#: eboutic/templates/eboutic/eboutic_main.jinja:4
#: eboutic/templates/eboutic/eboutic_main.jinja:22
@ -2484,75 +2484,75 @@ msgstr "Photos"
msgid "Eboutic"
msgstr "Eboutic"
#: core/templates/core/base.jinja:232
#: core/templates/core/base.jinja:229
msgid "Services"
msgstr "Services"
#: core/templates/core/base.jinja:234
#: core/templates/core/base.jinja:231
msgid "Matmatronch"
msgstr "Matmatronch"
#: core/templates/core/base.jinja:235 launderette/models.py:38
#: core/templates/core/base.jinja:232 launderette/models.py:38
#: launderette/templates/launderette/launderette_book.jinja:5
#: launderette/templates/launderette/launderette_book_choose.jinja:4
#: launderette/templates/launderette/launderette_main.jinja:4
msgid "Launderette"
msgstr "Laverie"
#: core/templates/core/base.jinja:236 core/templates/core/file.jinja:20
#: core/templates/core/base.jinja:233 core/templates/core/file.jinja:20
#: core/views/files.py:116
msgid "Files"
msgstr "Fichiers"
#: core/templates/core/base.jinja:237 core/templates/core/user_tools.jinja:163
#: core/templates/core/base.jinja:234 core/templates/core/user_tools.jinja:163
msgid "Pedagogy"
msgstr "Pédagogie"
#: core/templates/core/base.jinja:241
#: core/templates/core/base.jinja:238
msgid "My Benefits"
msgstr "Mes Avantages"
#: core/templates/core/base.jinja:243
#: core/templates/core/base.jinja:240
msgid "Sponsors"
msgstr "Partenaires"
#: core/templates/core/base.jinja:244
#: core/templates/core/base.jinja:241
msgid "Subscriber benefits"
msgstr "Les avantages cotisants"
#: core/templates/core/base.jinja:248
#: core/templates/core/base.jinja:245
msgid "Help"
msgstr "Aide"
#: core/templates/core/base.jinja:250
#: core/templates/core/base.jinja:247
msgid "FAQ"
msgstr "FAQ"
#: core/templates/core/base.jinja:251 core/templates/core/base.jinja:291
#: core/templates/core/base.jinja:248 core/templates/core/base.jinja:288
msgid "Contacts"
msgstr "Contacts"
#: core/templates/core/base.jinja:252
#: core/templates/core/base.jinja:249
msgid "Wiki"
msgstr "Wiki"
#: core/templates/core/base.jinja:292
#: core/templates/core/base.jinja:289
msgid "Legal notices"
msgstr "Mentions légales"
#: core/templates/core/base.jinja:293
#: core/templates/core/base.jinja:290
msgid "Intellectual property"
msgstr "Propriété intellectuelle"
#: core/templates/core/base.jinja:294
#: core/templates/core/base.jinja:291
msgid "Help & Documentation"
msgstr "Aide & Documentation"
#: core/templates/core/base.jinja:295
#: core/templates/core/base.jinja:292
msgid "R&D"
msgstr "R&D"
#: core/templates/core/base.jinja:298
#: core/templates/core/base.jinja:295
msgid "Site created by the IT Department of the AE"
msgstr "Site réalisé par le Pôle Informatique de l'AE"
@ -2615,24 +2615,24 @@ msgstr "Propriétés"
#: core/templates/core/file_detail.jinja:13
#: core/templates/core/file_moderation.jinja:20
#: sas/templates/sas/picture.jinja:84
#: sas/templates/sas/picture.jinja:101
msgid "Owner: "
msgstr "Propriétaire : "
#: core/templates/core/file_detail.jinja:26 sas/templates/sas/album.jinja:46
#: core/templates/core/file_detail.jinja:26 sas/templates/sas/album.jinja:50
#: sas/templates/sas/main.jinja:49
msgid "Clear clipboard"
msgstr "Vider le presse-papier"
#: core/templates/core/file_detail.jinja:27 sas/templates/sas/album.jinja:33
#: core/templates/core/file_detail.jinja:27 sas/templates/sas/album.jinja:37
msgid "Cut"
msgstr "Couper"
#: core/templates/core/file_detail.jinja:28 sas/templates/sas/album.jinja:34
#: core/templates/core/file_detail.jinja:28 sas/templates/sas/album.jinja:38
msgid "Paste"
msgstr "Coller"
#: core/templates/core/file_detail.jinja:31 sas/templates/sas/album.jinja:40
#: core/templates/core/file_detail.jinja:31 sas/templates/sas/album.jinja:44
#: sas/templates/sas/main.jinja:43
msgid "Clipboard: "
msgstr "Presse-papier : "
@ -2643,7 +2643,7 @@ msgstr "Nom réel : "
#: core/templates/core/file_detail.jinja:54
#: core/templates/core/file_moderation.jinja:21
#: sas/templates/sas/picture.jinja:75
#: sas/templates/sas/picture.jinja:92
msgid "Date: "
msgstr "Date : "
@ -2999,7 +2999,7 @@ msgstr "Résultat de la recherche"
msgid "Users"
msgstr "Utilisateurs"
#: core/templates/core/search.jinja:20 core/views/user.py:245
#: core/templates/core/search.jinja:20 core/views/user.py:247
msgid "Clubs"
msgstr "Clubs"
@ -3040,11 +3040,11 @@ msgid "Eboutic invoices"
msgstr "Facture eboutic"
#: core/templates/core/user_account.jinja:54
#: core/templates/core/user_tools.jinja:58 counter/views.py:713
#: core/templates/core/user_tools.jinja:58 counter/views.py:715
msgid "Etickets"
msgstr "Etickets"
#: core/templates/core/user_account.jinja:69 core/views/user.py:638
#: core/templates/core/user_account.jinja:69 core/views/user.py:640
msgid "User has no account"
msgstr "L'utilisateur n'a pas de compte"
@ -3266,13 +3266,13 @@ msgstr "Photos de %(user_name)s"
msgid "Download all my pictures"
msgstr "Télécharger toutes mes photos"
#: core/templates/core/user_pictures.jinja:45 sas/templates/sas/album.jinja:74
#: core/templates/core/user_pictures.jinja:45 sas/templates/sas/album.jinja:78
#: sas/templates/sas/macros.jinja:16
msgid "To be moderated"
msgstr "A modérer"
#: core/templates/core/user_preferences.jinja:8
#: core/templates/core/user_preferences.jinja:13 core/views/user.py:237
#: core/templates/core/user_preferences.jinja:13 core/views/user.py:239
msgid "Preferences"
msgstr "Préférences"
@ -3347,7 +3347,7 @@ msgstr "Outils utilisateurs"
msgid "Sith management"
msgstr "Gestion de Sith"
#: core/templates/core/user_tools.jinja:21 core/views/user.py:253
#: core/templates/core/user_tools.jinja:21 core/views/user.py:255
msgid "Groups"
msgstr "Groupes"
@ -3376,7 +3376,7 @@ msgid "Subscription stats"
msgstr "Statistiques de cotisation"
#: core/templates/core/user_tools.jinja:48 counter/forms.py:164
#: counter/views.py:683
#: counter/views.py:685
msgid "Counters"
msgstr "Comptoirs"
@ -3393,16 +3393,16 @@ msgid "Product types management"
msgstr "Gestion des types de produit"
#: core/templates/core/user_tools.jinja:56
#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:703
#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:705
msgid "Cash register summaries"
msgstr "Relevés de caisse"
#: core/templates/core/user_tools.jinja:57
#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:708
#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:710
msgid "Invoices call"
msgstr "Appels à facture"
#: core/templates/core/user_tools.jinja:72 core/views/user.py:272
#: core/templates/core/user_tools.jinja:72 core/views/user.py:274
#: counter/templates/counter/counter_list.jinja:18
#: counter/templates/counter/counter_list.jinja:34
#: counter/templates/counter/counter_list.jinja:50
@ -3590,21 +3590,21 @@ msgstr "Utilisateurs à retirer du groupe"
msgid "Users to add to group"
msgstr "Utilisateurs à ajouter au groupe"
#: core/views/user.py:182
#: core/views/user.py:184
msgid "We couldn't verify that this email actually exists"
msgstr "Nous n'avons pas réussi à vérifier que cette adresse mail existe."
#: core/views/user.py:205
#: core/views/user.py:207
msgid "Family"
msgstr "Famille"
#: core/views/user.py:210 sas/templates/sas/album.jinja:63
#: core/views/user.py:212 sas/templates/sas/album.jinja:67
#: trombi/templates/trombi/export.jinja:25
#: trombi/templates/trombi/user_profile.jinja:11
msgid "Pictures"
msgstr "Photos"
#: core/views/user.py:218
#: core/views/user.py:220
msgid "Galaxy"
msgstr "Galaxie"
@ -3653,7 +3653,7 @@ msgstr "client"
msgid "customers"
msgstr "clients"
#: counter/models.py:74 counter/views.py:265
#: counter/models.py:74 counter/views.py:267
msgid "Not enough money"
msgstr "Solde insuffisant"
@ -3781,11 +3781,11 @@ msgstr "est validé"
msgid "refilling"
msgstr "rechargement"
#: counter/models.py:752 eboutic/models.py:245
#: counter/models.py:752 eboutic/models.py:249
msgid "unit price"
msgstr "prix unitaire"
#: counter/models.py:753 counter/models.py:1057 eboutic/models.py:246
#: counter/models.py:753 counter/models.py:1057 eboutic/models.py:250
msgid "quantity"
msgstr "quantité"
@ -3971,7 +3971,7 @@ msgstr "Liste des relevés de caisse"
msgid "Theoric sums"
msgstr "Sommes théoriques"
#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:945
#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:963
msgid "Emptied"
msgstr "Coffre vidé"
@ -3997,7 +3997,7 @@ msgstr "Ce n'est pas un UID de carte étudiante valide"
#: counter/templates/counter/invoices_call.jinja:16
#: launderette/templates/launderette/launderette_admin.jinja:35
#: launderette/templates/launderette/launderette_click.jinja:13
#: sas/templates/sas/picture.jinja:143
#: sas/templates/sas/picture.jinja:160
#: subscription/templates/subscription/stats.jinja:19
msgid "Go"
msgstr "Valider"
@ -4128,7 +4128,7 @@ msgid "%(counter_name)s last operations"
msgstr "Dernières opérations sur %(counter_name)s"
#: counter/templates/counter/product_list.jinja:4
#: counter/templates/counter/product_list.jinja:12
#: counter/templates/counter/product_list.jinja:11
msgid "Product list"
msgstr "Liste des produits"
@ -4136,11 +4136,11 @@ msgstr "Liste des produits"
msgid "New product"
msgstr "Nouveau produit"
#: counter/templates/counter/product_list.jinja:21
#: counter/templates/counter/product_list.jinja:13
msgid "Uncategorized"
msgstr "Sans catégorie"
#: counter/templates/counter/product_list.jinja:28
#: counter/templates/counter/product_list.jinja:20
msgid "There is no products in this website."
msgstr "Il n'y a pas de produits dans ce site web."
@ -4197,101 +4197,101 @@ msgstr "Temps"
msgid "Top 100 barman %(counter_name)s (all semesters)"
msgstr "Top 100 barman %(counter_name)s (tous les semestres)"
#: counter/views.py:151
#: counter/views.py:153
msgid "Cash summary"
msgstr "Relevé de caisse"
#: counter/views.py:160
#: counter/views.py:162
msgid "Last operations"
msgstr "Dernières opérations"
#: counter/views.py:207
#: counter/views.py:209
msgid "Bad credentials"
msgstr "Mauvais identifiants"
#: counter/views.py:209
#: counter/views.py:211
msgid "User is not barman"
msgstr "L'utilisateur n'est pas barman."
#: counter/views.py:214
#: counter/views.py:216
msgid "Bad location, someone is already logged in somewhere else"
msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs"
#: counter/views.py:256
#: counter/views.py:258
msgid "Too young for that product"
msgstr "Trop jeune pour ce produit"
#: counter/views.py:259
#: counter/views.py:261
msgid "Not allowed for that product"
msgstr "Non autorisé pour ce produit"
#: counter/views.py:262
#: counter/views.py:264
msgid "No date of birth provided"
msgstr "Pas de date de naissance renseignée"
#: counter/views.py:551
#: counter/views.py:553
msgid "You have not enough money to buy all the basket"
msgstr "Vous n'avez pas assez d'argent pour acheter le panier"
#: counter/views.py:678
#: counter/views.py:680
msgid "Counter administration"
msgstr "Administration des comptoirs"
#: counter/views.py:698
#: counter/views.py:700
msgid "Product types"
msgstr "Types de produit"
#: counter/views.py:902
#: counter/views.py:920
msgid "10 cents"
msgstr "10 centimes"
#: counter/views.py:903
#: counter/views.py:921
msgid "20 cents"
msgstr "20 centimes"
#: counter/views.py:904
#: counter/views.py:922
msgid "50 cents"
msgstr "50 centimes"
#: counter/views.py:905
#: counter/views.py:923
msgid "1 euro"
msgstr "1 €"
#: counter/views.py:906
#: counter/views.py:924
msgid "2 euros"
msgstr "2 €"
#: counter/views.py:907
#: counter/views.py:925
msgid "5 euros"
msgstr "5 €"
#: counter/views.py:908
#: counter/views.py:926
msgid "10 euros"
msgstr "10 €"
#: counter/views.py:909
#: counter/views.py:927
msgid "20 euros"
msgstr "20 €"
#: counter/views.py:910
#: counter/views.py:928
msgid "50 euros"
msgstr "50 €"
#: counter/views.py:912
#: counter/views.py:930
msgid "100 euros"
msgstr "100 €"
#: counter/views.py:915 counter/views.py:921 counter/views.py:927
#: counter/views.py:933 counter/views.py:939
#: counter/views.py:933 counter/views.py:939 counter/views.py:945
#: counter/views.py:951 counter/views.py:957
msgid "Check amount"
msgstr "Montant du chèque"
#: counter/views.py:918 counter/views.py:924 counter/views.py:930
#: counter/views.py:936 counter/views.py:942
#: counter/views.py:936 counter/views.py:942 counter/views.py:948
#: counter/views.py:954 counter/views.py:960
msgid "Check quantity"
msgstr "Nombre de chèque"
#: counter/views.py:1462
#: counter/views.py:1480
msgid "people(s)"
msgstr "personne(s)"
@ -4308,27 +4308,27 @@ msgstr "Votre panier est vide"
msgid "%(name)s : this product does not exist or may no longer be available."
msgstr "%(name)s : ce produit n'existe pas ou n'est peut-être plus disponible."
#: eboutic/models.py:190
#: eboutic/models.py:194
msgid "validated"
msgstr "validé"
#: eboutic/models.py:206
#: eboutic/models.py:210
msgid "Invoice already validated"
msgstr "Facture déjà validée"
#: eboutic/models.py:242
#: eboutic/models.py:246
msgid "product id"
msgstr "ID du produit"
#: eboutic/models.py:243
#: eboutic/models.py:247
msgid "product name"
msgstr "nom du produit"
#: eboutic/models.py:244
#: eboutic/models.py:248
msgid "product type id"
msgstr "id du type du produit"
#: eboutic/models.py:261
#: eboutic/models.py:265
msgid "basket"
msgstr "panier"
@ -5309,21 +5309,21 @@ msgstr "Demande de modération de photo"
msgid "Picture moderation requests"
msgstr "Demandes de modération de photo"
#: sas/templates/sas/album.jinja:9
#: sas/templates/sas/album.jinja:13
#: sas/templates/sas/ask_picture_removal.jinja:4 sas/templates/sas/main.jinja:8
#: sas/templates/sas/main.jinja:17 sas/templates/sas/picture.jinja:12
#: sas/templates/sas/main.jinja:17 sas/templates/sas/picture.jinja:13
msgid "SAS"
msgstr "SAS"
#: sas/templates/sas/album.jinja:52 sas/templates/sas/moderation.jinja:10
#: sas/templates/sas/album.jinja:56 sas/templates/sas/moderation.jinja:10
msgid "Albums"
msgstr "Albums"
#: sas/templates/sas/album.jinja:96
#: sas/templates/sas/album.jinja:100
msgid "Upload"
msgstr "Envoyer"
#: sas/templates/sas/album.jinja:103
#: sas/templates/sas/album.jinja:107
msgid "Template generation time: "
msgstr "Temps de génération du template : "
@ -5351,11 +5351,11 @@ msgstr "Toutes les catégories"
msgid "SAS moderation"
msgstr "Modération du SAS"
#: sas/templates/sas/picture.jinja:35
#: sas/templates/sas/picture.jinja:36
msgid "Asked for removal"
msgstr "Retrait demandé"
#: sas/templates/sas/picture.jinja:38
#: sas/templates/sas/picture.jinja:39
msgid ""
"This picture can be viewed only by root users and by SAS admins. It will be "
"hidden to other users until it has been moderated."
@ -5364,19 +5364,23 @@ msgstr ""
"SAS. Elle sera cachée pour les autres utilisateurs tant qu'elle ne sera pas "
"modérée."
#: sas/templates/sas/picture.jinja:95
#: sas/templates/sas/picture.jinja:47
msgid "The following issues have been raised:"
msgstr "Les problèmes suivants ont été remontés :"
#: sas/templates/sas/picture.jinja:112
msgid "HD version"
msgstr "Version HD"
#: sas/templates/sas/picture.jinja:99
#: sas/templates/sas/picture.jinja:116
msgid "Ask for removal"
msgstr "Demander le retrait"
#: sas/templates/sas/picture.jinja:118 sas/templates/sas/picture.jinja:129
#: sas/templates/sas/picture.jinja:137 sas/templates/sas/picture.jinja:148
msgid "Previous picture"
msgstr "Image précédente"
#: sas/templates/sas/picture.jinja:139
#: sas/templates/sas/picture.jinja:156
msgid "People"
msgstr "Personne(s)"

View File

@ -9,10 +9,17 @@ from ninja_extra.permissions import IsAuthenticated
from ninja_extra.schemas import PaginatedResponseSchema
from pydantic import NonNegativeInt
from core.api_permissions import CanView, IsOwner
from core.api_permissions import CanView, IsInGroup, IsRoot
from core.models import Notification, User
from sas.models import PeoplePictureRelation, Picture
from sas.schemas import IdentifiedUserSchema, PictureFilterSchema, PictureSchema
from sas.schemas import (
IdentifiedUserSchema,
ModerationRequestSchema,
PictureFilterSchema,
PictureSchema,
)
IsSasAdmin = IsRoot | IsInGroup(settings.SITH_GROUP_SAS_ADMIN_ID)
@api_controller("/sas/picture")
@ -85,18 +92,35 @@ class PicturesController(ControllerBase):
},
)
@route.delete("/{picture_id}", permissions=[IsOwner])
@route.delete("/{picture_id}", permissions=[IsSasAdmin])
def delete_picture(self, picture_id: int):
self.get_object_or_exception(Picture, pk=picture_id).delete()
@route.patch("/{picture_id}/moderate", permissions=[IsOwner])
@route.patch(
"/{picture_id}/moderation",
permissions=[IsSasAdmin],
url_name="picture_moderate",
)
def moderate_picture(self, picture_id: int):
"""Mark a picture as moderated and remove its pending moderation requests."""
picture = self.get_object_or_exception(Picture, pk=picture_id)
picture.moderation_requests.all().delete()
picture.is_moderated = True
picture.moderator = self.context.request.user
picture.asked_for_removal = False
picture.save()
@route.get(
"/{picture_id}/moderation",
permissions=[IsSasAdmin],
response=list[ModerationRequestSchema],
url_name="picture_moderation_requests",
)
def fetch_moderation_requests(self, picture_id: int):
"""Fetch the moderation requests issued on this picture."""
picture = self.get_object_or_exception(Picture, pk=picture_id)
return picture.moderation_requests.select_related("author")
@api_controller("/sas/relation", tags="User identification on SAS pictures")
class UsersIdentifiedController(ControllerBase):

View File

@ -4,8 +4,8 @@ from django.urls import reverse
from ninja import FilterSchema, ModelSchema, Schema
from pydantic import Field, NonNegativeInt
from core.schemas import UserProfileSchema
from sas.models import Picture
from core.schemas import SimpleUserSchema, UserProfileSchema
from sas.models import Picture, PictureModerationRequest
class PictureFilterSchema(FilterSchema):
@ -52,3 +52,11 @@ class PictureRelationCreationSchema(Schema):
class IdentifiedUserSchema(Schema):
id: int
user: UserProfileSchema
class ModerationRequestSchema(ModelSchema):
author: SimpleUserSchema
class Meta:
model = PictureModerationRequest
fields = ["id", "created_at", "reason"]

View File

@ -11,10 +11,12 @@ import {
type IdentifiedUserSchema,
type PictureSchema,
type PicturesFetchIdentificationsResponse,
type PicturesFetchModerationRequestsResponse,
type PicturesFetchPicturesData,
type UserProfileSchema,
picturesDeletePicture,
picturesFetchIdentifications,
picturesFetchModerationRequests,
picturesFetchPictures,
picturesIdentifyUsers,
picturesModeratePicture,
@ -27,18 +29,20 @@ import {
* able to prefetch its data.
*/
class PictureWithIdentifications {
identifications: PicturesFetchIdentificationsResponse | null = null;
identifications: PicturesFetchIdentificationsResponse = null;
imageLoading = false;
identificationsLoading = false;
moderationLoading = false;
id: number;
// biome-ignore lint/style/useNamingConvention: api is in snake_case
compressed_url: string;
moderationRequests: PicturesFetchModerationRequestsResponse = null;
constructor(picture: PictureSchema) {
Object.assign(this, picture);
}
static fromPicture(picture: PictureSchema) {
static fromPicture(picture: PictureSchema): PictureWithIdentifications {
return new PictureWithIdentifications(picture);
}
@ -46,7 +50,7 @@ class PictureWithIdentifications {
* If not already done, fetch the users identified on this picture and
* populate the identifications field
*/
async loadIdentifications(options?: { forceReload: boolean }) {
async loadIdentifications(options?: { forceReload: boolean }): Promise<void> {
if (this.identificationsLoading) {
return; // The users are already being fetched.
}
@ -65,11 +69,29 @@ class PictureWithIdentifications {
this.identificationsLoading = false;
}
async loadModeration(options?: { forceReload: boolean }): Promise<void> {
if (this.moderationLoading) {
return; // The moderation requests are already being fetched.
}
if (!!this.moderationRequests && !options?.forceReload) {
// The moderation requests are already fetched
// and the user does not want to force the reload
return;
}
this.moderationLoading = true;
this.moderationRequests = (
await picturesFetchModerationRequests({
// biome-ignore lint/style/useNamingConvention: api is in snake_case
path: { picture_id: this.id },
})
).data;
this.moderationLoading = false;
}
/**
* Preload the photo and the identifications
* @return {Promise<void>}
*/
async preload() {
async preload(): Promise<void> {
const img = new Image();
img.src = this.compressed_url;
if (!img.complete) {
@ -87,12 +109,12 @@ interface ViewerConfig {
userId: number;
/** Url of the current album */
albumUrl: string;
/** Id of the album to displlay */
/** Id of the album to display */
albumId: number;
/** id of the first picture to load on the page */
firstPictureId: number;
/** if the user is sas admin */
userIsSasAdmin: number;
userIsSasAdmin: boolean;
}
/**
@ -103,9 +125,8 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
Alpine.data("picture_viewer", () => ({
/**
* All the pictures that can be displayed on this picture viewer
* @type PictureWithIdentifications[]
**/
pictures: [],
pictures: [] as PictureWithIdentifications[],
/**
* The currently displayed picture
* Default dummy data are pre-loaded to avoid javascript error
@ -131,14 +152,12 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
},
/**
* The picture which will be displayed next if the user press the "next" button
* @type ?PictureWithIdentifications
**/
nextPicture: null,
nextPicture: null as PictureWithIdentifications,
/**
* The picture which will be displayed next if the user press the "previous" button
* @type ?PictureWithIdentifications
**/
previousPicture: null,
previousPicture: null as PictureWithIdentifications,
/**
* The select2 component used to identify users
**/
@ -148,13 +167,11 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
**/
/**
* Error message when a moderation operation fails
* @type string
**/
moderationError: "",
/**
* Method of pushing new url to the browser history
* Used by popstate event and always reset to it's default value when used
* @type History
**/
pushstate: History.Push,
@ -166,7 +183,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
} as PicturesFetchPicturesData)
).map(PictureWithIdentifications.fromPicture);
this.selector = sithSelect2({
element: $(this.$refs.search) as unknown as HTMLElement,
element: this.$refs.search,
dataSource: remoteDataSource(await makeUrl(userSearchUsers), {
excluded: () => [
...(this.currentPicture.identifications || []).map(
@ -213,7 +230,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
* and the previous picture, the next picture and
* the list of identified users are updated.
*/
async updatePicture() {
async updatePicture(): Promise<void> {
const updateArgs = {
data: { sasPictureId: this.currentPicture.id },
unused: "",
@ -231,16 +248,23 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
}
this.moderationError = "";
const index = this.pictures.indexOf(this.currentPicture);
const index: number = this.pictures.indexOf(this.currentPicture);
this.previousPicture = this.pictures[index - 1] || null;
this.nextPicture = this.pictures[index + 1] || null;
await this.currentPicture.loadIdentifications();
this.$refs.mainPicture?.addEventListener("load", () => {
// once the current picture is loaded,
// start preloading the next and previous pictures
this.nextPicture?.preload();
this.previousPicture?.preload();
});
if (this.currentPicture.asked_for_removal && config.userIsSasAdmin) {
await Promise.all([
this.currentPicture.loadIdentifications(),
this.currentPicture.loadModeration(),
]);
} else {
await this.currentPicture.loadIdentifications();
}
},
async moderatePicture() {
@ -253,7 +277,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
return;
}
this.currentPicture.is_moderated = true;
this.currentPicture.askedForRemoval = false;
this.currentPicture.asked_for_removal = false;
},
async deletePicture() {
@ -277,7 +301,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
/**
* Send the identification request and update the list of identified users.
*/
async submitIdentification() {
async submitIdentification(): Promise<void> {
await picturesIdentifyUsers({
path: {
// biome-ignore lint/style/useNamingConvention: api is in snake_case
@ -292,18 +316,15 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
/**
* Check if an identification can be removed by the currently logged user
* @param {PictureIdentification} identification
* @return {boolean}
*/
canBeRemoved(identification: IdentifiedUserSchema) {
canBeRemoved(identification: IdentifiedUserSchema): boolean {
return config.userIsSasAdmin || identification.user.id === config.userId;
},
/**
* Untag a user from the current picture
* @param {PictureIdentification} identification
*/
async removeIdentification(identification: IdentifiedUserSchema) {
async removeIdentification(identification: IdentifiedUserSchema): Promise<void> {
const res = await usersidentifiedDeleteRelation({
// biome-ignore lint/style/useNamingConvention: api is in snake_case
path: { relation_id: identification.id },

View File

@ -2,7 +2,7 @@
{%- block additional_css -%}
<link rel="stylesheet" href="{{ static('sas/css/picture.scss') }}">
<link rel="stylesheet" href="{{ static('webpack/sas/viewer-index.css') }}" defer>
<link rel="stylesheet" href="{{ static('webpack/sas/viewer-index.css') }}">
{%- endblock -%}
{%- block additional_js -%}
@ -30,10 +30,10 @@
<br>
<template x-if="!currentPicture.is_moderated">
<div class="alert alert-red">
<div class="alert alert-red" @click="console.log(currentPicture)">
<div class="alert-main">
<template x-if="currentPicture.askedForRemoval">
<span class="important">{% trans %}Asked for removal{% endtrans %}</span>
<template x-if="currentPicture.asked_for_removal">
<h3 class="alert-title">{% trans %}Asked for removal{% endtrans %}</h3>
</template>
<p>
{% trans trimmed %}
@ -41,16 +41,33 @@
It will be hidden to other users until it has been moderated.
{% endtrans %}
</p>
<template x-if="currentPicture.asked_for_removal">
<div>
<h5 @click="console.log(currentPicture.moderationRequests)">
{% trans %}The following issues have been raised:{% endtrans %}
</h5>
<template x-for="req in (currentPicture.moderationRequests ?? [])" :key="req.id">
<div>
<h6
x-text="`${req.author.first_name} ${req.author.last_name}`"
></h6>
<i x-text="Intl.DateTimeFormat(
'{{ LANGUAGE_CODE }}',
{dateStyle: 'long', timeStyle: 'short'}
).format(new Date(req.created_at))"></i>
<blockquote x-text="`> ${req.reason}`"></blockquote>
</div>
<div>
<div>
</template>
</div>
</template>
</div>
<div class="alert-aside">
<button class="btn btn-blue" @click="moderatePicture()">
{% trans %}Moderate{% endtrans %}
</button>
<button class="btn btn-red" @click.prevent="deletePicture()">
{% trans %}Delete{% endtrans %}
</button>
</div>
<p x-show="!!moderationError" x-text="moderationError"></p>
</div>
</div>
@ -58,7 +75,6 @@
<div class="container" id="pict">
<div class="main">
<div class="photo" :aria-busy="currentPicture.imageLoading">
<img
:src="currentPicture.compressed_url"

View File

@ -3,35 +3,45 @@ from django.db import transaction
from django.test import TestCase
from django.urls import reverse
from model_bakery import baker
from model_bakery.recipe import Recipe
from pytest_django.asserts import assertNumQueries
from core.baker_recipes import old_subscriber_user, subscriber_user
from core.models import RealGroup, User
from core.models import RealGroup, SithFile, User
from sas.baker_recipes import picture_recipe
from sas.models import Album, PeoplePictureRelation, Picture
from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest
class TestSas(TestCase):
@classmethod
def setUpTestData(cls):
Picture.objects.all().delete()
sas = SithFile.objects.get(pk=settings.SITH_SAS_ROOT_DIR_ID)
Picture.objects.exclude(id=sas.id).delete()
owner = User.objects.get(username="root")
cls.user_a = old_subscriber_user.make()
cls.user_b, cls.user_c = subscriber_user.make(_quantity=2)
picture = picture_recipe.extend(owner=owner)
cls.album_a = baker.make(Album, is_in_sas=True)
cls.album_b = baker.make(Album, is_in_sas=True)
cls.album_a = baker.make(Album, is_in_sas=True, parent=sas)
cls.album_b = baker.make(Album, is_in_sas=True, parent=sas)
relation_recipe = Recipe(PeoplePictureRelation)
relations = []
for album in cls.album_a, cls.album_b:
pictures = picture.make(parent=album, _quantity=5, _bulk_create=True)
baker.make(PeoplePictureRelation, picture=pictures[1], user=cls.user_a)
baker.make(PeoplePictureRelation, picture=pictures[2], user=cls.user_a)
baker.make(PeoplePictureRelation, picture=pictures[2], user=cls.user_b)
baker.make(PeoplePictureRelation, picture=pictures[3], user=cls.user_b)
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_a)
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_b)
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_c)
relations.extend(
[
relation_recipe.prepare(picture=pictures[1], user=cls.user_a),
relation_recipe.prepare(picture=pictures[2], user=cls.user_a),
relation_recipe.prepare(picture=pictures[2], user=cls.user_b),
relation_recipe.prepare(picture=pictures[3], user=cls.user_b),
relation_recipe.prepare(picture=pictures[4], user=cls.user_a),
relation_recipe.prepare(picture=pictures[4], user=cls.user_b),
relation_recipe.prepare(picture=pictures[4], user=cls.user_c),
]
)
PeoplePictureRelation.objects.bulk_create(relations)
class TestPictureSearch(TestSas):
@ -170,3 +180,49 @@ class TestPictureRelation(TestSas):
res = self.client.delete(f"/api/sas/relation/{relation.id}")
assert res.status_code == 404
assert PeoplePictureRelation.objects.count() == relation_count
class TestPictureModeration(TestSas):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
cls.sas_admin = baker.make(
User, groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
)
cls.picture = Picture.objects.filter(parent=cls.album_a)[0]
cls.picture.is_moderated = False
cls.picture.asked_for_removal = True
cls.picture.save()
cls.url = reverse("api:picture_moderate", kwargs={"picture_id": cls.picture.id})
baker.make(PictureModerationRequest, picture=cls.picture, author=cls.user_a)
def test_moderation_route_forbidden(self):
"""Test that basic users (even if owner) cannot moderate a picture."""
self.picture.owner = self.user_b
for user in baker.make(User), subscriber_user.make(), self.user_b:
self.client.force_login(user)
res = self.client.patch(self.url)
assert res.status_code == 403
def test_moderation_route_authorized(self):
"""Test that sas admins can moderate a picture."""
self.client.force_login(self.sas_admin)
res = self.client.patch(self.url)
assert res.status_code == 200
self.picture.refresh_from_db()
assert self.picture.is_moderated
assert not self.picture.asked_for_removal
assert not self.picture.moderation_requests.exists()
def test_get_moderation_requests(self):
"""Test that fetching moderation requests work."""
url = reverse(
"api:picture_moderation_requests", kwargs={"picture_id": self.picture.id}
)
self.client.force_login(self.sas_admin)
res = self.client.get(url)
assert res.status_code == 200
assert len(res.json()) == 1
assert res.json()[0]["author"]["id"] == self.user_a.id

View File

@ -20,7 +20,7 @@ from django.core.cache import cache
from django.test import Client, TestCase
from django.urls import reverse
from model_bakery import baker
from pytest_django.asserts import assertRedirects
from pytest_django.asserts import assertInHTML, assertRedirects
from core.baker_recipes import old_subscriber_user, subscriber_user
from core.models import RealGroup, User
@ -70,7 +70,9 @@ def test_album_access_non_subscriber(client: Client):
class TestSasModeration(TestCase):
@classmethod
def setUpTestData(cls):
album = baker.make(Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID)
album = baker.make(
Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID, is_moderated=True
)
cls.pictures = picture_recipe.make(
parent=album, _quantity=10, _bulk_create=True
)
@ -82,6 +84,9 @@ class TestSasModeration(TestCase):
)
cls.simple_user = subscriber_user.make()
def setUp(self):
cache.clear()
def test_moderation_page_sas_admin(self):
"""Test that a moderator can see the pictures needing moderation."""
self.client.force_login(self.moderator)
@ -132,3 +137,37 @@ class TestSasModeration(TestCase):
)
assert res.status_code == 403
assert Picture.objects.filter(pk=self.to_moderate.id).exists()
def test_request_moderation_form_access(self):
"""Test that regular can access the form to ask for moderation."""
self.client.force_login(self.simple_user)
res = self.client.get(
reverse(
"sas:picture_ask_removal", kwargs={"picture_id": self.pictures[1].id}
),
)
assert res.status_code == 200
def test_request_moderation_form_submit(self):
"""Test that moderation requests are created."""
self.client.force_login(self.simple_user)
message = "J'aime pas cette photo (ni la Cocarde)."
url = reverse(
"sas:picture_ask_removal", kwargs={"picture_id": self.pictures[1].id}
)
res = self.client.post(url, data={"reason": message})
assertRedirects(
res, reverse("sas:album", kwargs={"album_id": self.pictures[1].parent_id})
)
assert self.pictures[1].moderation_requests.count() == 1
assert self.pictures[1].moderation_requests.first().reason == message
# test that the user cannot ask for moderation twice
res = self.client.post(url, data={"reason": message})
assert res.status_code == 200
assert self.pictures[1].moderation_requests.count() == 1
assertInHTML(
'<ul class="errorlist nonfield"><li>'
"Vous avez déjà déposé une demande de retrait pour cette photo.</li></ul>",
res.content.decode(),
)

View File

@ -1,4 +1,3 @@
// biome-ignore lint/correctness/noUndeclaredDependencies: webpack works with commonjs
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const config = require("./webpack.config.js");