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; border: #fc8181 1px solid;
} }
.alert-title {
margin-top: 0;
}
.alert-main { .alert-main {
flex: 2; flex: 2;
} }
.alert-aside {
display: flex;
flex-direction: column;
}
} }
.tool_bar { .tool_bar {

View File

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

View File

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

View File

@ -9,10 +9,17 @@ from ninja_extra.permissions import IsAuthenticated
from ninja_extra.schemas import PaginatedResponseSchema from ninja_extra.schemas import PaginatedResponseSchema
from pydantic import NonNegativeInt 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 core.models import Notification, User
from sas.models import PeoplePictureRelation, Picture 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") @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): def delete_picture(self, picture_id: int):
self.get_object_or_exception(Picture, pk=picture_id).delete() 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): 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 = self.get_object_or_exception(Picture, pk=picture_id)
picture.moderation_requests.all().delete()
picture.is_moderated = True picture.is_moderated = True
picture.moderator = self.context.request.user picture.moderator = self.context.request.user
picture.asked_for_removal = False picture.asked_for_removal = False
picture.save() 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") @api_controller("/sas/relation", tags="User identification on SAS pictures")
class UsersIdentifiedController(ControllerBase): class UsersIdentifiedController(ControllerBase):

View File

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

View File

@ -2,7 +2,7 @@
{%- block additional_css -%} {%- block additional_css -%}
<link rel="stylesheet" href="{{ static('sas/css/picture.scss') }}"> <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 -%} {%- endblock -%}
{%- block additional_js -%} {%- block additional_js -%}
@ -30,10 +30,10 @@
<br> <br>
<template x-if="!currentPicture.is_moderated"> <template x-if="!currentPicture.is_moderated">
<div class="alert alert-red"> <div class="alert alert-red" @click="console.log(currentPicture)">
<div class="alert-main"> <div class="alert-main">
<template x-if="currentPicture.askedForRemoval"> <template x-if="currentPicture.asked_for_removal">
<span class="important">{% trans %}Asked for removal{% endtrans %}</span> <h3 class="alert-title">{% trans %}Asked for removal{% endtrans %}</h3>
</template> </template>
<p> <p>
{% trans trimmed %} {% trans trimmed %}
@ -41,16 +41,33 @@
It will be hidden to other users until it has been moderated. It will be hidden to other users until it has been moderated.
{% endtrans %} {% endtrans %}
</p> </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> </div>
</template>
</div>
<div class="alert-aside">
<button class="btn btn-blue" @click="moderatePicture()"> <button class="btn btn-blue" @click="moderatePicture()">
{% trans %}Moderate{% endtrans %} {% trans %}Moderate{% endtrans %}
</button> </button>
<button class="btn btn-red" @click.prevent="deletePicture()"> <button class="btn btn-red" @click.prevent="deletePicture()">
{% trans %}Delete{% endtrans %} {% trans %}Delete{% endtrans %}
</button> </button>
</div>
<p x-show="!!moderationError" x-text="moderationError"></p> <p x-show="!!moderationError" x-text="moderationError"></p>
</div> </div>
</div> </div>
@ -58,7 +75,6 @@
<div class="container" id="pict"> <div class="container" id="pict">
<div class="main"> <div class="main">
<div class="photo" :aria-busy="currentPicture.imageLoading"> <div class="photo" :aria-busy="currentPicture.imageLoading">
<img <img
:src="currentPicture.compressed_url" :src="currentPicture.compressed_url"

View File

@ -3,35 +3,45 @@ from django.db import transaction
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from model_bakery.recipe import Recipe
from pytest_django.asserts import assertNumQueries from pytest_django.asserts import assertNumQueries
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, SithFile, User
from sas.baker_recipes import picture_recipe 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): class TestSas(TestCase):
@classmethod @classmethod
def setUpTestData(cls): 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") owner = User.objects.get(username="root")
cls.user_a = old_subscriber_user.make() cls.user_a = old_subscriber_user.make()
cls.user_b, cls.user_c = subscriber_user.make(_quantity=2) cls.user_b, cls.user_c = subscriber_user.make(_quantity=2)
picture = picture_recipe.extend(owner=owner) picture = picture_recipe.extend(owner=owner)
cls.album_a = 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) 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: for album in cls.album_a, cls.album_b:
pictures = picture.make(parent=album, _quantity=5, _bulk_create=True) pictures = picture.make(parent=album, _quantity=5, _bulk_create=True)
baker.make(PeoplePictureRelation, picture=pictures[1], user=cls.user_a) relations.extend(
baker.make(PeoplePictureRelation, picture=pictures[2], user=cls.user_a) [
baker.make(PeoplePictureRelation, picture=pictures[2], user=cls.user_b) relation_recipe.prepare(picture=pictures[1], user=cls.user_a),
baker.make(PeoplePictureRelation, picture=pictures[3], user=cls.user_b) relation_recipe.prepare(picture=pictures[2], user=cls.user_a),
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_a) relation_recipe.prepare(picture=pictures[2], user=cls.user_b),
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_b) relation_recipe.prepare(picture=pictures[3], user=cls.user_b),
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_c) 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): class TestPictureSearch(TestSas):
@ -170,3 +180,49 @@ class TestPictureRelation(TestSas):
res = self.client.delete(f"/api/sas/relation/{relation.id}") res = self.client.delete(f"/api/sas/relation/{relation.id}")
assert res.status_code == 404 assert res.status_code == 404
assert PeoplePictureRelation.objects.count() == relation_count 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.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 pytest_django.asserts import assertInHTML, 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
@ -70,7 +70,9 @@ def test_album_access_non_subscriber(client: Client):
class TestSasModeration(TestCase): class TestSasModeration(TestCase):
@classmethod @classmethod
def setUpTestData(cls): 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( cls.pictures = picture_recipe.make(
parent=album, _quantity=10, _bulk_create=True parent=album, _quantity=10, _bulk_create=True
) )
@ -82,6 +84,9 @@ class TestSasModeration(TestCase):
) )
cls.simple_user = subscriber_user.make() cls.simple_user = subscriber_user.make()
def setUp(self):
cache.clear()
def test_moderation_page_sas_admin(self): def test_moderation_page_sas_admin(self):
"""Test that a moderator can see the pictures needing moderation.""" """Test that a moderator can see the pictures needing moderation."""
self.client.force_login(self.moderator) self.client.force_login(self.moderator)
@ -132,3 +137,37 @@ class TestSasModeration(TestCase):
) )
assert res.status_code == 403 assert res.status_code == 403
assert Picture.objects.filter(pk=self.to_moderate.id).exists() 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 BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const config = require("./webpack.config.js"); const config = require("./webpack.config.js");