mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-21 21:53:30 +00:00
Merge pull request #868 from ae-utbm/delete-picture-confirm-button
Delete picture confirm button
This commit is contained in:
commit
496ad7ce9b
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
@ -616,7 +616,7 @@ msgstr "No"
|
|||||||
#: counter/templates/counter/last_ops.jinja:20
|
#: counter/templates/counter/last_ops.jinja:20
|
||||||
#: counter/templates/counter/last_ops.jinja:45
|
#: counter/templates/counter/last_ops.jinja:45
|
||||||
#: counter/templates/counter/refilling_list.jinja:16
|
#: counter/templates/counter/refilling_list.jinja:16
|
||||||
#: rootplace/templates/rootplace/logs.jinja:12 sas/views.py:310
|
#: rootplace/templates/rootplace/logs.jinja:12 sas/forms.py:90
|
||||||
#: trombi/templates/trombi/user_profile.jinja:40
|
#: trombi/templates/trombi/user_profile.jinja:40
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr "Date"
|
msgstr "Date"
|
||||||
@ -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"
|
||||||
|
|
||||||
@ -997,7 +997,7 @@ msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur"
|
|||||||
msgid "You should specify a role"
|
msgid "You should specify a role"
|
||||||
msgstr "Vous devez choisir un rôle"
|
msgstr "Vous devez choisir un rôle"
|
||||||
|
|
||||||
#: club/forms.py:283 sas/views.py:117 sas/views.py:241
|
#: club/forms.py:283 sas/views.py:58 sas/views.py:177
|
||||||
msgid "You do not have the permission to do that"
|
msgid "You do not have the permission to do that"
|
||||||
msgstr "Vous n'avez pas la permission de faire cela"
|
msgstr "Vous n'avez pas la permission de faire cela"
|
||||||
|
|
||||||
@ -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"
|
||||||
|
|
||||||
@ -1613,7 +1613,7 @@ msgstr "Résumé"
|
|||||||
#: com/templates/com/news_admin_list.jinja:252
|
#: com/templates/com/news_admin_list.jinja:252
|
||||||
#: com/templates/com/news_admin_list.jinja:289
|
#: com/templates/com/news_admin_list.jinja:289
|
||||||
#: com/templates/com/weekmail.jinja:17 com/templates/com/weekmail.jinja:46
|
#: com/templates/com/weekmail.jinja:17 com/templates/com/weekmail.jinja:46
|
||||||
#: forum/templates/forum/forum.jinja:55
|
#: forum/templates/forum/forum.jinja:55 sas/models.py:298
|
||||||
msgid "Author"
|
msgid "Author"
|
||||||
msgstr "Auteur"
|
msgstr "Auteur"
|
||||||
|
|
||||||
@ -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"
|
||||||
|
|
||||||
@ -2246,7 +2246,7 @@ msgstr "avoir une notification pour chaque click"
|
|||||||
msgid "get a notification for every refilling"
|
msgid "get a notification for every refilling"
|
||||||
msgstr "avoir une notification pour chaque rechargement"
|
msgstr "avoir une notification pour chaque rechargement"
|
||||||
|
|
||||||
#: core/models.py:914 sas/views.py:309
|
#: core/models.py:914 sas/forms.py:89
|
||||||
msgid "file name"
|
msgid "file name"
|
||||||
msgstr "nom du fichier"
|
msgstr "nom du fichier"
|
||||||
|
|
||||||
@ -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"
|
||||||
|
|
||||||
@ -2582,6 +2582,7 @@ msgstr "Confirmation"
|
|||||||
#: core/templates/core/delete_confirm.jinja:20
|
#: core/templates/core/delete_confirm.jinja:20
|
||||||
#: core/templates/core/file_delete_confirm.jinja:14
|
#: core/templates/core/file_delete_confirm.jinja:14
|
||||||
#: counter/templates/counter/counter_click.jinja:121
|
#: counter/templates/counter/counter_click.jinja:121
|
||||||
|
#: sas/templates/sas/ask_picture_removal.jinja:20
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Annuler"
|
msgstr "Annuler"
|
||||||
|
|
||||||
@ -2614,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 : "
|
||||||
@ -2642,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 : "
|
||||||
|
|
||||||
@ -2998,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"
|
||||||
|
|
||||||
@ -3039,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"
|
||||||
|
|
||||||
@ -3265,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"
|
||||||
|
|
||||||
@ -3346,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"
|
||||||
|
|
||||||
@ -3375,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"
|
||||||
|
|
||||||
@ -3392,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
|
||||||
@ -3496,12 +3497,12 @@ msgid "Error creating folder %(folder_name)s: %(msg)s"
|
|||||||
msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s"
|
msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s"
|
||||||
|
|
||||||
#: core/views/files.py:153 core/views/forms.py:277 core/views/forms.py:284
|
#: core/views/files.py:153 core/views/forms.py:277 core/views/forms.py:284
|
||||||
#: sas/views.py:81
|
#: sas/forms.py:60
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Error uploading file %(file_name)s: %(msg)s"
|
msgid "Error uploading file %(file_name)s: %(msg)s"
|
||||||
msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s"
|
msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s"
|
||||||
|
|
||||||
#: core/views/files.py:228 sas/views.py:313
|
#: core/views/files.py:228 sas/forms.py:93
|
||||||
msgid "Apply rights recursively"
|
msgid "Apply rights recursively"
|
||||||
msgstr "Appliquer les droits récursivement"
|
msgstr "Appliquer les droits récursivement"
|
||||||
|
|
||||||
@ -3589,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"
|
||||||
|
|
||||||
@ -3652,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"
|
||||||
|
|
||||||
@ -3780,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é"
|
||||||
|
|
||||||
@ -3970,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é"
|
||||||
|
|
||||||
@ -3996,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:141
|
#: 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"
|
||||||
@ -4127,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"
|
||||||
|
|
||||||
@ -4135,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."
|
||||||
|
|
||||||
@ -4196,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)"
|
||||||
|
|
||||||
@ -4307,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"
|
||||||
|
|
||||||
@ -5099,7 +5100,7 @@ msgstr "non noté"
|
|||||||
msgid "UV comment moderation"
|
msgid "UV comment moderation"
|
||||||
msgstr "Modération des commentaires d'UV"
|
msgstr "Modération des commentaires d'UV"
|
||||||
|
|
||||||
#: pedagogy/templates/pedagogy/moderation.jinja:14
|
#: pedagogy/templates/pedagogy/moderation.jinja:14 sas/models.py:309
|
||||||
msgid "Reason"
|
msgid "Reason"
|
||||||
msgstr "Raison"
|
msgstr "Raison"
|
||||||
|
|
||||||
@ -5267,27 +5268,73 @@ msgstr "Utilisateur qui sera supprimé"
|
|||||||
msgid "User to be selected"
|
msgid "User to be selected"
|
||||||
msgstr "Utilisateur à sélectionner"
|
msgstr "Utilisateur à sélectionner"
|
||||||
|
|
||||||
#: sas/models.py:282
|
#: sas/forms.py:16
|
||||||
|
msgid "Add a new album"
|
||||||
|
msgstr "Ajouter un nouvel album"
|
||||||
|
|
||||||
|
#: sas/forms.py:19
|
||||||
|
msgid "Upload images"
|
||||||
|
msgstr "Envoyer les images"
|
||||||
|
|
||||||
|
#: sas/forms.py:37
|
||||||
|
#, python-format
|
||||||
|
msgid "Error creating album %(album)s: %(msg)s"
|
||||||
|
msgstr "Erreur de création de l'album %(album)s : %(msg)s"
|
||||||
|
|
||||||
|
#: sas/forms.py:72 trombi/templates/trombi/detail.jinja:15
|
||||||
|
msgid "Add user"
|
||||||
|
msgstr "Ajouter une personne"
|
||||||
|
|
||||||
|
#: sas/forms.py:117
|
||||||
|
msgid "You already requested moderation for this picture."
|
||||||
|
msgstr "Vous avez déjà déposé une demande de retrait pour cette photo."
|
||||||
|
|
||||||
|
#: sas/models.py:280
|
||||||
msgid "picture"
|
msgid "picture"
|
||||||
msgstr "photo"
|
msgstr "photo"
|
||||||
|
|
||||||
#: sas/templates/sas/album.jinja:9 sas/templates/sas/main.jinja:8
|
#: sas/models.py:304
|
||||||
#: sas/templates/sas/main.jinja:17 sas/templates/sas/picture.jinja:12
|
msgid "Picture"
|
||||||
|
msgstr "Photo"
|
||||||
|
|
||||||
|
#: sas/models.py:311
|
||||||
|
msgid "Why do you want this image to be removed ?"
|
||||||
|
msgstr "Pourquoi voulez-vous retirer cette image ?"
|
||||||
|
|
||||||
|
#: sas/models.py:315
|
||||||
|
msgid "Picture moderation request"
|
||||||
|
msgstr "Demande de modération de photo"
|
||||||
|
|
||||||
|
#: sas/models.py:316
|
||||||
|
msgid "Picture moderation requests"
|
||||||
|
msgstr "Demandes de modération de photo"
|
||||||
|
|
||||||
|
#: 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: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 : "
|
||||||
|
|
||||||
|
#: sas/templates/sas/ask_picture_removal.jinja:9
|
||||||
|
msgid "Image removal request"
|
||||||
|
msgstr "Demande de retrait d'image"
|
||||||
|
|
||||||
|
#: sas/templates/sas/ask_picture_removal.jinja:25
|
||||||
|
msgid "Request removal"
|
||||||
|
msgstr "Demander le retrait"
|
||||||
|
|
||||||
#: sas/templates/sas/main.jinja:20
|
#: sas/templates/sas/main.jinja:20
|
||||||
msgid "You must be logged in to see the SAS."
|
msgid "You must be logged in to see the SAS."
|
||||||
msgstr "Vous devez être connecté pour voir les photos."
|
msgstr "Vous devez être connecté pour voir les photos."
|
||||||
@ -5304,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."
|
||||||
@ -5317,39 +5364,26 @@ 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:98
|
#: 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:137
|
#: sas/templates/sas/picture.jinja:156
|
||||||
msgid "People"
|
msgid "People"
|
||||||
msgstr "Personne(s)"
|
msgstr "Personne(s)"
|
||||||
|
|
||||||
#: sas/views.py:37
|
|
||||||
msgid "Add a new album"
|
|
||||||
msgstr "Ajouter un nouvel album"
|
|
||||||
|
|
||||||
#: sas/views.py:40
|
|
||||||
msgid "Upload images"
|
|
||||||
msgstr "Envoyer les images"
|
|
||||||
|
|
||||||
#: sas/views.py:58
|
|
||||||
#, python-format
|
|
||||||
msgid "Error creating album %(album)s: %(msg)s"
|
|
||||||
msgstr "Erreur de création de l'album %(album)s : %(msg)s"
|
|
||||||
|
|
||||||
#: sas/views.py:93 trombi/templates/trombi/detail.jinja:15
|
|
||||||
msgid "Add user"
|
|
||||||
msgstr "Ajouter une personne"
|
|
||||||
|
|
||||||
#: sith/settings.py:255 sith/settings.py:474
|
#: sith/settings.py:255 sith/settings.py:474
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "Anglais"
|
msgstr "Anglais"
|
||||||
|
15
sas/admin.py
15
sas/admin.py
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from sas.models import Album, PeoplePictureRelation, Picture
|
from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Picture)
|
@admin.register(Picture)
|
||||||
@ -31,4 +31,15 @@ class PeoplePictureRelationAdmin(admin.ModelAdmin):
|
|||||||
autocomplete_fields = ("picture", "user")
|
autocomplete_fields = ("picture", "user")
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Album)
|
@admin.register(Album)
|
||||||
|
class AlbumAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "parent", "date", "owner", "is_moderated")
|
||||||
|
search_fields = ("name",)
|
||||||
|
autocomplete_fields = ("owner", "parent", "edit_groups", "view_groups")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PictureModerationRequest)
|
||||||
|
class PictureModerationRequestAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("author", "picture", "created_at")
|
||||||
|
search_fields = ("author", "picture")
|
||||||
|
autocomplete_fields = ("author", "picture")
|
||||||
|
32
sas/api.py
32
sas/api.py
@ -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):
|
||||||
|
124
sas/forms.py
Normal file
124
sas/forms.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ajax_select import make_ajax_field
|
||||||
|
from ajax_select.fields import AutoCompleteSelectMultipleField
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
from core.views import MultipleImageField
|
||||||
|
from core.views.forms import SelectDate
|
||||||
|
from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest
|
||||||
|
|
||||||
|
|
||||||
|
class SASForm(forms.Form):
|
||||||
|
album_name = forms.CharField(
|
||||||
|
label=_("Add a new album"), max_length=Album.NAME_MAX_LENGTH, required=False
|
||||||
|
)
|
||||||
|
images = MultipleImageField(
|
||||||
|
label=_("Upload images"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def process(self, parent, owner, files, *, automodere=False):
|
||||||
|
try:
|
||||||
|
if self.cleaned_data["album_name"] != "":
|
||||||
|
album = Album(
|
||||||
|
parent=parent,
|
||||||
|
name=self.cleaned_data["album_name"],
|
||||||
|
owner=owner,
|
||||||
|
is_moderated=automodere,
|
||||||
|
)
|
||||||
|
album.clean()
|
||||||
|
album.save()
|
||||||
|
except Exception as e:
|
||||||
|
self.add_error(
|
||||||
|
None,
|
||||||
|
_("Error creating album %(album)s: %(msg)s")
|
||||||
|
% {"album": self.cleaned_data["album_name"], "msg": repr(e)},
|
||||||
|
)
|
||||||
|
for f in files:
|
||||||
|
new_file = Picture(
|
||||||
|
parent=parent,
|
||||||
|
name=f.name,
|
||||||
|
file=f,
|
||||||
|
owner=owner,
|
||||||
|
mime_type=f.content_type,
|
||||||
|
size=f.size,
|
||||||
|
is_folder=False,
|
||||||
|
is_moderated=automodere,
|
||||||
|
)
|
||||||
|
if automodere:
|
||||||
|
new_file.moderator = owner
|
||||||
|
try:
|
||||||
|
new_file.clean()
|
||||||
|
new_file.generate_thumbnails()
|
||||||
|
new_file.save()
|
||||||
|
except Exception as e:
|
||||||
|
self.add_error(
|
||||||
|
None,
|
||||||
|
_("Error uploading file %(file_name)s: %(msg)s")
|
||||||
|
% {"file_name": f, "msg": repr(e)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RelationForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = PeoplePictureRelation
|
||||||
|
fields = ["picture"]
|
||||||
|
widgets = {"picture": forms.HiddenInput}
|
||||||
|
|
||||||
|
users = AutoCompleteSelectMultipleField(
|
||||||
|
"users", show_help_text=False, help_text="", label=_("Add user"), required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PictureEditForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Picture
|
||||||
|
fields = ["name", "parent"]
|
||||||
|
|
||||||
|
parent = make_ajax_field(Picture, "parent", "files", help_text="")
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumEditForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Album
|
||||||
|
fields = ["name", "date", "file", "parent", "edit_groups"]
|
||||||
|
|
||||||
|
name = forms.CharField(max_length=Album.NAME_MAX_LENGTH, label=_("file name"))
|
||||||
|
date = forms.DateField(label=_("Date"), widget=SelectDate, required=True)
|
||||||
|
parent = make_ajax_field(Album, "parent", "files", help_text="")
|
||||||
|
edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="")
|
||||||
|
recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class PictureModerationRequestForm(forms.ModelForm):
|
||||||
|
"""Form to create a PictureModerationRequest.
|
||||||
|
|
||||||
|
The form only manages the reason field,
|
||||||
|
because the author and the picture are set in the view.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PictureModerationRequest
|
||||||
|
fields = ["reason"]
|
||||||
|
|
||||||
|
def __init__(self, *args, user: User, picture: Picture, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.user = user
|
||||||
|
self.picture = picture
|
||||||
|
|
||||||
|
def clean(self) -> dict[str, Any]:
|
||||||
|
if PictureModerationRequest.objects.filter(
|
||||||
|
author=self.user, picture=self.picture
|
||||||
|
).exists():
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("You already requested moderation for this picture.")
|
||||||
|
)
|
||||||
|
return super().clean()
|
||||||
|
|
||||||
|
def save(self, *, commit=True) -> PictureModerationRequest:
|
||||||
|
self.instance.author = self.user
|
||||||
|
self.instance.picture = self.picture
|
||||||
|
return super().save(commit)
|
68
sas/migrations/0004_picturemoderationrequest_and_more.py
Normal file
68
sas/migrations/0004_picturemoderationrequest_and_more.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-10-10 20:44
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("sas", "0003_sasfile"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PictureModerationRequest",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"reason",
|
||||||
|
models.TextField(
|
||||||
|
default="",
|
||||||
|
help_text="Why do you want this image to be removed ?",
|
||||||
|
verbose_name="Reason",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Picture moderation request",
|
||||||
|
"verbose_name_plural": "Picture moderation requests",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="picturemoderationrequest",
|
||||||
|
name="author",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="moderation_requests",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Author",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="picturemoderationrequest",
|
||||||
|
name="picture",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="moderation_requests",
|
||||||
|
to="sas.picture",
|
||||||
|
verbose_name="Picture",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="picturemoderationrequest",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("author", "picture"), name="one_request_per_user_per_picture"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -273,16 +273,12 @@ class PeoplePictureRelation(models.Model):
|
|||||||
User,
|
User,
|
||||||
verbose_name=_("user"),
|
verbose_name=_("user"),
|
||||||
related_name="pictures",
|
related_name="pictures",
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
picture = models.ForeignKey(
|
picture = models.ForeignKey(
|
||||||
Picture,
|
Picture,
|
||||||
verbose_name=_("picture"),
|
verbose_name=_("picture"),
|
||||||
related_name="people",
|
related_name="people",
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -290,4 +286,39 @@ class PeoplePictureRelation(models.Model):
|
|||||||
unique_together = ["user", "picture"]
|
unique_together = ["user", "picture"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.user.get_display_name() + " - " + str(self.picture)
|
return f"Moderation request by {self.user.get_short_name()} - {self.picture}"
|
||||||
|
|
||||||
|
|
||||||
|
class PictureModerationRequest(models.Model):
|
||||||
|
"""A request to remove a Picture from the SAS."""
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
author = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
verbose_name=_("Author"),
|
||||||
|
related_name="moderation_requests",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
picture = models.ForeignKey(
|
||||||
|
Picture,
|
||||||
|
verbose_name=_("Picture"),
|
||||||
|
related_name="moderation_requests",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
reason = models.TextField(
|
||||||
|
verbose_name=_("Reason"),
|
||||||
|
default="",
|
||||||
|
help_text=_("Why do you want this image to be removed ?"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Picture moderation request")
|
||||||
|
verbose_name_plural = _("Picture moderation requests")
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["author", "picture"], name="one_request_per_user_per_picture"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Moderation request by {self.author.get_short_name()}"
|
||||||
|
@ -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"]
|
||||||
|
@ -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 },
|
||||||
|
28
sas/templates/sas/ask_picture_removal.jinja
Normal file
28
sas/templates/sas/ask_picture_removal.jinja
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% trans %}SAS{% endtrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans %}Image removal request{% endtrans %}</h1>
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.non_field_errors() }}
|
||||||
|
<div class="helptext">
|
||||||
|
{{ form.reason.help_text }}
|
||||||
|
</div>
|
||||||
|
{{ form.reason }}
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<a class="clickable btn btn-grey" href="{{ object.get_absolute_url() }}">
|
||||||
|
{% trans %}Cancel{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<input
|
||||||
|
class="btn btn-blue"
|
||||||
|
type="submit"
|
||||||
|
value="{% trans %}Request removal{% endtrans %}"
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
@ -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>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="alert-aside">
|
||||||
<div>
|
<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"
|
||||||
@ -96,7 +112,9 @@
|
|||||||
{% trans %}HD version{% endtrans %}
|
{% trans %}HD version{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<a class="text danger" href="?ask_removal">{% trans %}Ask for removal{% endtrans %}</a>
|
<a class="text danger" :href="`/sas/picture/${currentPicture.id}/report`">
|
||||||
|
{% trans %}Ask for removal{% endtrans %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<a class="button" :href="`/sas/picture/${currentPicture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a>
|
<a class="button" :href="`/sas/picture/${currentPicture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a>
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
|
)
|
||||||
|
@ -34,6 +34,11 @@ urlpatterns = [
|
|||||||
PictureEditView.as_view(),
|
PictureEditView.as_view(),
|
||||||
name="picture_edit",
|
name="picture_edit",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"picture/<int:picture_id>/report",
|
||||||
|
PictureAskRemovalView.as_view(),
|
||||||
|
name="picture_ask_removal",
|
||||||
|
),
|
||||||
path("picture/<int:picture_id>/download/", send_pict, name="download"),
|
path("picture/<int:picture_id>/download/", send_pict, name="download"),
|
||||||
path(
|
path(
|
||||||
"picture/<int:picture_id>/download/compressed/",
|
"picture/<int:picture_id>/download/compressed/",
|
||||||
|
141
sas/views.py
141
sas/views.py
@ -12,14 +12,12 @@
|
|||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from ajax_select import make_ajax_field
|
|
||||||
from ajax_select.fields import AutoCompleteSelectMultipleField
|
|
||||||
from django import forms
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, TemplateView
|
from django.views.generic import DetailView, TemplateView
|
||||||
@ -27,71 +25,14 @@ from django.views.generic.edit import FormMixin, FormView, UpdateView
|
|||||||
|
|
||||||
from core.models import SithFile, User
|
from core.models import SithFile, User
|
||||||
from core.views import CanEditMixin, CanViewMixin
|
from core.views import CanEditMixin, CanViewMixin
|
||||||
from core.views.files import FileView, MultipleImageField, send_file
|
from core.views.files import FileView, send_file
|
||||||
from core.views.forms import SelectDate
|
from sas.forms import (
|
||||||
from sas.models import Album, PeoplePictureRelation, Picture
|
AlbumEditForm,
|
||||||
|
PictureEditForm,
|
||||||
|
PictureModerationRequestForm,
|
||||||
class SASForm(forms.Form):
|
SASForm,
|
||||||
album_name = forms.CharField(
|
)
|
||||||
label=_("Add a new album"), max_length=Album.NAME_MAX_LENGTH, required=False
|
from sas.models import Album, Picture
|
||||||
)
|
|
||||||
images = MultipleImageField(
|
|
||||||
label=_("Upload images"),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def process(self, parent, owner, files, *, automodere=False):
|
|
||||||
try:
|
|
||||||
if self.cleaned_data["album_name"] != "":
|
|
||||||
album = Album(
|
|
||||||
parent=parent,
|
|
||||||
name=self.cleaned_data["album_name"],
|
|
||||||
owner=owner,
|
|
||||||
is_moderated=automodere,
|
|
||||||
)
|
|
||||||
album.clean()
|
|
||||||
album.save()
|
|
||||||
except Exception as e:
|
|
||||||
self.add_error(
|
|
||||||
None,
|
|
||||||
_("Error creating album %(album)s: %(msg)s")
|
|
||||||
% {"album": self.cleaned_data["album_name"], "msg": repr(e)},
|
|
||||||
)
|
|
||||||
for f in files:
|
|
||||||
new_file = Picture(
|
|
||||||
parent=parent,
|
|
||||||
name=f.name,
|
|
||||||
file=f,
|
|
||||||
owner=owner,
|
|
||||||
mime_type=f.content_type,
|
|
||||||
size=f.size,
|
|
||||||
is_folder=False,
|
|
||||||
is_moderated=automodere,
|
|
||||||
)
|
|
||||||
if automodere:
|
|
||||||
new_file.moderator = owner
|
|
||||||
try:
|
|
||||||
new_file.clean()
|
|
||||||
new_file.generate_thumbnails()
|
|
||||||
new_file.save()
|
|
||||||
except Exception as e:
|
|
||||||
self.add_error(
|
|
||||||
None,
|
|
||||||
_("Error uploading file %(file_name)s: %(msg)s")
|
|
||||||
% {"file_name": f, "msg": repr(e)},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RelationForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = PeoplePictureRelation
|
|
||||||
fields = ["picture"]
|
|
||||||
widgets = {"picture": forms.HiddenInput}
|
|
||||||
|
|
||||||
users = AutoCompleteSelectMultipleField(
|
|
||||||
"users", show_help_text=False, help_text="", label=_("Add user"), required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SASMainView(FormView):
|
class SASMainView(FormView):
|
||||||
@ -138,11 +79,6 @@ class PictureView(CanViewMixin, DetailView):
|
|||||||
self.object.rotate(270)
|
self.object.rotate(270)
|
||||||
if "rotate_left" in request.GET:
|
if "rotate_left" in request.GET:
|
||||||
self.object.rotate(90)
|
self.object.rotate(90)
|
||||||
if "ask_removal" in request.GET.keys():
|
|
||||||
self.object.is_moderated = False
|
|
||||||
self.object.asked_for_removal = True
|
|
||||||
self.object.save()
|
|
||||||
return redirect("sas:album", album_id=self.object.parent.id)
|
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -293,26 +229,6 @@ class ModerationView(TemplateView):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class PictureEditForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Picture
|
|
||||||
fields = ["name", "parent"]
|
|
||||||
|
|
||||||
parent = make_ajax_field(Picture, "parent", "files", help_text="")
|
|
||||||
|
|
||||||
|
|
||||||
class AlbumEditForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Album
|
|
||||||
fields = ["name", "date", "file", "parent", "edit_groups"]
|
|
||||||
|
|
||||||
name = forms.CharField(max_length=Album.NAME_MAX_LENGTH, label=_("file name"))
|
|
||||||
date = forms.DateField(label=_("Date"), widget=SelectDate, required=True)
|
|
||||||
parent = make_ajax_field(Album, "parent", "files", help_text="")
|
|
||||||
edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="")
|
|
||||||
recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False)
|
|
||||||
|
|
||||||
|
|
||||||
class PictureEditView(CanEditMixin, UpdateView):
|
class PictureEditView(CanEditMixin, UpdateView):
|
||||||
model = Picture
|
model = Picture
|
||||||
form_class = PictureEditForm
|
form_class = PictureEditForm
|
||||||
@ -320,6 +236,41 @@ class PictureEditView(CanEditMixin, UpdateView):
|
|||||||
pk_url_kwarg = "picture_id"
|
pk_url_kwarg = "picture_id"
|
||||||
|
|
||||||
|
|
||||||
|
class PictureAskRemovalView(CanViewMixin, DetailView, FormView):
|
||||||
|
"""View to allow users to ask pictures to be removed."""
|
||||||
|
|
||||||
|
model = Picture
|
||||||
|
template_name = "sas/ask_picture_removal.jinja"
|
||||||
|
pk_url_kwarg = "picture_id"
|
||||||
|
form_class = PictureModerationRequestForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self) -> dict[str, Any]:
|
||||||
|
"""Add the user and picture to the form kwargs.
|
||||||
|
|
||||||
|
Those are required to create the PictureModerationRequest,
|
||||||
|
and aren't part of the form itself
|
||||||
|
(picture is a path parameter, and user is the request user).
|
||||||
|
"""
|
||||||
|
return super().get_form_kwargs() | {
|
||||||
|
"user": self.request.user,
|
||||||
|
"picture": self.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
"""Return the URL to the album containing the picture."""
|
||||||
|
album = Album.objects.filter(pk=self.object.parent_id).first()
|
||||||
|
if not album:
|
||||||
|
return reverse("sas:main")
|
||||||
|
return album.get_absolute_url()
|
||||||
|
|
||||||
|
def form_valid(self, form: PictureModerationRequestForm) -> HttpResponseRedirect:
|
||||||
|
form.save()
|
||||||
|
self.object.is_moderated = False
|
||||||
|
self.object.asked_for_removal = True
|
||||||
|
self.object.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class AlbumEditView(CanEditMixin, UpdateView):
|
class AlbumEditView(CanEditMixin, UpdateView):
|
||||||
model = Album
|
model = Album
|
||||||
form_class = AlbumEditForm
|
form_class = AlbumEditForm
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user