1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-04-01 19:31:11 +00:00

Add survey feature

This commit is contained in:
Emmy D'Anello 2025-03-19 23:18:45 +01:00
parent ca0601fb24
commit 702c8d8c9e
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
24 changed files with 744 additions and 100 deletions

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TFJM\n" "Project-Id-Version: TFJM\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-09 11:04+0100\n" "POT-Creation-Date: 2025-03-19 23:07+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n" "Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -114,6 +114,7 @@ msgstr ""
#: registration/models.py:158 registration/models.py:754 #: registration/models.py:158 registration/models.py:754
#: registration/tables.py:39 #: registration/tables.py:39
#: registration/templates/registration/payment_form.html:52 #: registration/templates/registration/payment_form.html:52
#: survey/templates/survey/survey_detail.html:60
msgid "team" msgid "team"
msgstr "équipe" msgstr "équipe"
@ -217,7 +218,7 @@ msgstr ""
msgid "Toggle fullscreen mode" msgid "Toggle fullscreen mode"
msgstr "Inverse le mode plein écran" msgstr "Inverse le mode plein écran"
#: chat/templates/chat/content.html:76 tfjm/templates/navbar.html:126 #: chat/templates/chat/content.html:76 tfjm/templates/navbar.html:129
msgid "Log out" msgid "Log out"
msgstr "Déconnexion" msgstr "Déconnexion"
@ -251,7 +252,7 @@ msgstr "Chat"
#: chat/templates/chat/login.html:10 chat/templates/chat/login.html:36 #: chat/templates/chat/login.html:10 chat/templates/chat/login.html:36
#: registration/templates/registration/password_reset_complete.html:10 #: registration/templates/registration/password_reset_complete.html:10
#: tfjm/templates/base.html:89 tfjm/templates/base.html:90 #: tfjm/templates/base.html:89 tfjm/templates/base.html:90
#: tfjm/templates/navbar.html:107 #: tfjm/templates/navbar.html:110
#: tfjm/templates/registration/includes/login.html:22 #: tfjm/templates/registration/includes/login.html:22
#: tfjm/templates/registration/login.html:7 #: tfjm/templates/registration/login.html:7
#: tfjm/templates/registration/login.html:8 #: tfjm/templates/registration/login.html:8
@ -713,7 +714,7 @@ msgstr ""
#: draw/models.py:496 draw/models.py:519 participation/models.py:1258 #: draw/models.py:496 draw/models.py:519 participation/models.py:1258
#: participation/models.py:1695 participation/models.py:1933 #: participation/models.py:1695 participation/models.py:1933
#: participation/views.py:1495 participation/views.py:1760 #: participation/views.py:1501 participation/views.py:1766
#, python-brace-format #, python-brace-format
msgid "Problem #{problem}" msgid "Problem #{problem}"
msgstr "Problème n°{problem}" msgstr "Problème n°{problem}"
@ -911,8 +912,8 @@ msgstr "Êtes-vous sûr·e de vouloir annuler le tirage au sort ?"
msgid "Close" msgid "Close"
msgstr "Fermer" msgstr "Fermer"
#: draw/views.py:31 participation/views.py:163 participation/views.py:509 #: draw/views.py:31 participation/views.py:163 participation/views.py:512
#: participation/views.py:540 #: participation/views.py:543
msgid "You are not in a team." msgid "You are not in a team."
msgstr "Vous n'êtes pas dans une équipe." msgstr "Vous n'êtes pas dans une équipe."
@ -1029,7 +1030,7 @@ msgstr "Ce trigramme est déjà utilisé."
msgid "No team was found with this access code." msgid "No team was found with this access code."
msgstr "Aucune équipe n'a été trouvée avec ce code d'accès." msgstr "Aucune équipe n'a été trouvée avec ce code d'accès."
#: participation/forms.py:59 participation/views.py:511 #: participation/forms.py:59 participation/views.py:514
msgid "The team is already validated or the validation is pending." msgid "The team is already validated or the validation is pending."
msgstr "La validation de l'équipe est déjà faite ou en cours." msgstr "La validation de l'équipe est déjà faite ou en cours."
@ -1382,7 +1383,7 @@ msgstr "finale"
msgid "Google Sheet ID" msgid "Google Sheet ID"
msgstr "ID de la feuille Google Sheets" msgstr "ID de la feuille Google Sheets"
#: participation/models.py:467 participation/views.py:1814 #: participation/models.py:467 participation/views.py:1820
msgid "Notation sheet" msgid "Notation sheet"
msgstr "Feuille de notation" msgstr "Feuille de notation"
@ -1392,7 +1393,8 @@ msgid "Final ranking"
msgstr "Classement final" msgstr "Classement final"
#: participation/models.py:487 participation/models.py:559 #: participation/models.py:487 participation/models.py:559
#: participation/models.py:1333 participation/views.py:1734 #: participation/models.py:1333 participation/views.py:1740
#: survey/templates/survey/survey_detail.html:58
msgid "Team" msgid "Team"
msgstr "Équipe" msgstr "Équipe"
@ -1425,14 +1427,14 @@ msgid "Tweaks day 3"
msgstr "Ajustements 3" msgstr "Ajustements 3"
#: participation/models.py:491 participation/models.py:1333 #: participation/models.py:491 participation/models.py:1333
#: participation/views.py:1741 #: participation/views.py:1747
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
#: participation/models.py:491 participation/models.py:559 #: participation/models.py:491 participation/models.py:559
#: participation/models.py:1333 #: participation/models.py:1333
#: participation/templates/participation/tournament_harmonize.html:14 #: participation/templates/participation/tournament_harmonize.html:14
#: participation/views.py:1744 #: participation/views.py:1750
msgid "Rank" msgid "Rank"
msgstr "Rang" msgstr "Rang"
@ -1443,7 +1445,7 @@ msgstr "Rang"
#: participation/models.py:1360 participation/models.py:1367 #: participation/models.py:1360 participation/models.py:1367
#: participation/models.py:1371 participation/models.py:1616 #: participation/models.py:1371 participation/models.py:1616
#: participation/models.py:1638 participation/models.py:2102 #: participation/models.py:1638 participation/models.py:2102
#: participation/views.py:1815 #: participation/views.py:1821
msgid "Pool" msgid "Pool"
msgstr "Poule" msgstr "Poule"
@ -1707,59 +1709,59 @@ msgid "The president of the jury must be part of the jury."
msgstr "Læ président⋅e du jury doit faire partie du jury." msgstr "Læ président⋅e du jury doit faire partie du jury."
#: participation/models.py:1259 participation/models.py:1333 #: participation/models.py:1259 participation/models.py:1333
#: participation/views.py:1489 participation/views.py:1738 #: participation/views.py:1495 participation/views.py:1744
msgid "Problem" msgid "Problem"
msgstr "Problème" msgstr "Problème"
#: participation/models.py:1260 participation/views.py:1510 #: participation/models.py:1260 participation/views.py:1516
msgid "Reporter" msgid "Reporter"
msgstr "Défenseur⋅se" msgstr "Défenseur⋅se"
#: participation/models.py:1261 participation/views.py:1516 #: participation/models.py:1261 participation/views.py:1522
msgid "Opponent" msgid "Opponent"
msgstr "Opposant⋅e" msgstr "Opposant⋅e"
#: participation/models.py:1262 participation/views.py:1523 #: participation/models.py:1262 participation/views.py:1529
msgid "Reviewer" msgid "Reviewer"
msgstr "Rapporteur⋅rice" msgstr "Rapporteur⋅rice"
#: participation/models.py:1263 participation/views.py:1530 #: participation/models.py:1263 participation/views.py:1536
msgid "Observer" msgid "Observer"
msgstr "Observateur⋅rice" msgstr "Observateur⋅rice"
#: participation/models.py:1264 participation/views.py:1504 #: participation/models.py:1264 participation/views.py:1510
msgid "Role" msgid "Role"
msgstr "Rôle" msgstr "Rôle"
#: participation/models.py:1265 participation/models.py:1267 #: participation/models.py:1265 participation/models.py:1267
#: participation/models.py:1268 participation/views.py:1546 #: participation/models.py:1268 participation/views.py:1552
#: participation/views.py:1554 participation/views.py:1562 #: participation/views.py:1560 participation/views.py:1568
#: participation/views.py:1572 #: participation/views.py:1578
msgid "Writing" msgid "Writing"
msgstr "Écrit" msgstr "Écrit"
#: participation/models.py:1266 participation/models.py:1267 #: participation/models.py:1266 participation/models.py:1267
#: participation/models.py:1268 participation/views.py:1550 #: participation/models.py:1268 participation/views.py:1556
#: participation/views.py:1558 participation/views.py:1567 #: participation/views.py:1564 participation/views.py:1573
#: participation/views.py:1576 #: participation/views.py:1582
msgid "Oral" msgid "Oral"
msgstr "Oral" msgstr "Oral"
#: participation/models.py:1269 participation/views.py:1538 #: participation/models.py:1269 participation/views.py:1544
#: participation/views.py:1539 #: participation/views.py:1545
msgid "Juree" msgid "Juree"
msgstr "Juré⋅e" msgstr "Juré⋅e"
#: participation/models.py:1292 participation/models.py:1618 #: participation/models.py:1292 participation/models.py:1618
#: participation/models.py:1640 participation/views.py:1608 #: participation/models.py:1640 participation/views.py:1614
msgid "Average" msgid "Average"
msgstr "Moyenne" msgstr "Moyenne"
#: participation/models.py:1298 participation/views.py:1627 #: participation/models.py:1298 participation/views.py:1633
msgid "Coefficient" msgid "Coefficient"
msgstr "Coefficien" msgstr "Coefficien"
#: participation/models.py:1299 participation/views.py:1670 #: participation/models.py:1299 participation/views.py:1676
msgid "Subtotal" msgid "Subtotal"
msgstr "Sous-total" msgstr "Sous-total"
@ -1972,12 +1974,15 @@ msgstr "Pas d'équipe définie"
#: registration/templates/registration/payment_form.html:210 #: registration/templates/registration/payment_form.html:210
#: registration/templates/registration/update_user.html:16 #: registration/templates/registration/update_user.html:16
#: registration/templates/registration/user_detail.html:220 #: registration/templates/registration/user_detail.html:220
#: survey/templates/survey/survey_detail.html:40
#: survey/templates/survey/survey_detail.html:73
#: survey/templates/survey/survey_form.html:12
msgid "Update" msgid "Update"
msgstr "Modifier" msgstr "Modifier"
#: participation/templates/participation/create_team.html:11 #: participation/templates/participation/create_team.html:11
#: participation/templates/participation/tournament_form.html:14 #: participation/templates/participation/tournament_form.html:14
#: tfjm/templates/base.html:85 #: survey/templates/survey/survey_form.html:14 tfjm/templates/base.html:85
msgid "Create" msgid "Create"
msgstr "Créer" msgstr "Créer"
@ -2305,6 +2310,7 @@ msgid "Back to pool detail"
msgstr "Retour aux détails de la poule" msgstr "Retour aux détails de la poule"
#: participation/templates/participation/team_detail.html:13 #: participation/templates/participation/team_detail.html:13
#: survey/templates/survey/survey_detail.html:16
msgid "Name:" msgid "Name:"
msgstr "Nom :" msgstr "Nom :"
@ -2465,7 +2471,7 @@ msgid "Invalidate"
msgstr "Invalider" msgstr "Invalider"
#: participation/templates/participation/team_detail.html:244 #: participation/templates/participation/team_detail.html:244
#: participation/views.py:341 #: participation/views.py:344
msgid "Upload motivation letter" msgid "Upload motivation letter"
msgstr "Envoyer la lettre de motivation" msgstr "Envoyer la lettre de motivation"
@ -2474,7 +2480,7 @@ msgid "Update team"
msgstr "Modifier l'équipe" msgstr "Modifier l'équipe"
#: participation/templates/participation/team_detail.html:255 #: participation/templates/participation/team_detail.html:255
#: participation/views.py:503 #: participation/views.py:506
msgid "Leave team" msgid "Leave team"
msgstr "Quitter l'équipe" msgstr "Quitter l'équipe"
@ -2736,7 +2742,7 @@ msgstr "Vous êtes déjà dans une équipe."
msgid "Join team" msgid "Join team"
msgstr "Rejoindre une équipe" msgstr "Rejoindre une équipe"
#: participation/views.py:164 participation/views.py:541 #: participation/views.py:164 participation/views.py:544
msgid "You don't participate, so you don't have any team." msgid "You don't participate, so you don't have any team."
msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe." msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe."
@ -2764,189 +2770,189 @@ msgstr ""
"d'adresse e-mail, soit une autorisation, soit des personnes, soit la lettre " "d'adresse e-mail, soit une autorisation, soit des personnes, soit la lettre "
"de motivation, soit le tournoi n'a pas été choisi." "de motivation, soit le tournoi n'a pas été choisi."
#: participation/views.py:235 #: participation/views.py:236
msgid "Team validation" msgid "Team validation"
msgstr "Validation d'équipe" msgstr "Validation d'équipe"
#: participation/views.py:247 #: participation/views.py:248
msgid "You are not an organizer of the tournament." msgid "You are not an organizer of the tournament."
msgstr "Vous n'êtes pas un⋅e organisateur⋅rice du tournoi." msgstr "Vous n'êtes pas un⋅e organisateur⋅rice du tournoi."
#: participation/views.py:250 #: participation/views.py:251
msgid "This team has no pending validation." msgid "This team has no pending validation."
msgstr "L'équipe n'a pas de validation en attente." msgstr "L'équipe n'a pas de validation en attente."
#: participation/views.py:270 #: participation/views.py:272
msgid "Team validated" msgid "Team validated"
msgstr "Équipe validée" msgstr "Équipe validée"
#: participation/views.py:279 #: participation/views.py:282
msgid "Team not validated" msgid "Team not validated"
msgstr "Équipe non validée" msgstr "Équipe non validée"
#: participation/views.py:282 #: participation/views.py:285
msgid "You must specify if you validate the registration or not." msgid "You must specify if you validate the registration or not."
msgstr "Vous devez spécifier si vous validez l'inscription ou non." msgstr "Vous devez spécifier si vous validez l'inscription ou non."
#: participation/views.py:318 #: participation/views.py:321
#, python-brace-format #, python-brace-format
msgid "Update team {trigram}" msgid "Update team {trigram}"
msgstr "Mise à jour de l'équipe {trigram}" msgstr "Mise à jour de l'équipe {trigram}"
#: participation/views.py:380 participation/views.py:488 #: participation/views.py:383 participation/views.py:491
#, python-brace-format #, python-brace-format
msgid "Motivation letter of {team}.{ext}" msgid "Motivation letter of {team}.{ext}"
msgstr "Lettre de motivation de {team}.{ext}" msgstr "Lettre de motivation de {team}.{ext}"
#: participation/views.py:413 #: participation/views.py:416
#, python-brace-format #, python-brace-format
msgid "Authorizations of team {trigram}.zip" msgid "Authorizations of team {trigram}.zip"
msgstr "Autorisations de l'équipe {trigram}.zip" msgstr "Autorisations de l'équipe {trigram}.zip"
#: participation/views.py:417 #: participation/views.py:420
#, python-brace-format #, python-brace-format
msgid "Authorizations of {tournament}.zip" msgid "Authorizations of {tournament}.zip"
msgstr "Autorisations du tournoi {tournament}.zip" msgstr "Autorisations du tournoi {tournament}.zip"
#: participation/views.py:436 #: participation/views.py:439
#, python-brace-format #, python-brace-format
msgid "Photo authorization of {participant}.{ext}" msgid "Photo authorization of {participant}.{ext}"
msgstr "Autorisation de droit à l'image de {participant}.{ext}" msgstr "Autorisation de droit à l'image de {participant}.{ext}"
#: participation/views.py:445 #: participation/views.py:448
#, python-brace-format #, python-brace-format
msgid "Parental authorization of {participant}.{ext}" msgid "Parental authorization of {participant}.{ext}"
msgstr "Autorisation parentale de {participant}.{ext}" msgstr "Autorisation parentale de {participant}.{ext}"
#: participation/views.py:453 #: participation/views.py:456
#, python-brace-format #, python-brace-format
msgid "Health sheet of {participant}.{ext}" msgid "Health sheet of {participant}.{ext}"
msgstr "Fiche sanitaire de {participant}.{ext}" msgstr "Fiche sanitaire de {participant}.{ext}"
#: participation/views.py:461 #: participation/views.py:464
#, python-brace-format #, python-brace-format
msgid "Vaccine sheet of {participant}.{ext}" msgid "Vaccine sheet of {participant}.{ext}"
msgstr "Carnet de vaccination de {participant}.{ext}" msgstr "Carnet de vaccination de {participant}.{ext}"
#: participation/views.py:472 #: participation/views.py:475
#, python-brace-format #, python-brace-format
msgid "Photo authorization of {participant} (final).{ext}" msgid "Photo authorization of {participant} (final).{ext}"
msgstr "Autorisation de droit à l'image de {participant} (finale).{ext}" msgstr "Autorisation de droit à l'image de {participant} (finale).{ext}"
#: participation/views.py:481 #: participation/views.py:484
#, python-brace-format #, python-brace-format
msgid "Parental authorization of {participant} (final).{ext}" msgid "Parental authorization of {participant} (final).{ext}"
msgstr "Autorisation parentale de {participant} (finale).{ext}" msgstr "Autorisation parentale de {participant} (finale).{ext}"
#: participation/views.py:555 #: participation/views.py:558
msgid "The team is not validated yet." msgid "The team is not validated yet."
msgstr "L'équipe n'est pas encore validée." msgstr "L'équipe n'est pas encore validée."
#: participation/views.py:569 #: participation/views.py:572
#, python-brace-format #, python-brace-format
msgid "Participation of team {trigram}" msgid "Participation of team {trigram}"
msgstr "Participation de l'équipe {trigram}" msgstr "Participation de l'équipe {trigram}"
#: participation/views.py:658 #: participation/views.py:661
#, python-brace-format #, python-brace-format
msgid "Payments of {tournament}" msgid "Payments of {tournament}"
msgstr "Paiements de {tournament}" msgstr "Paiements de {tournament}"
#: participation/views.py:758 #: participation/views.py:761
msgid "Notes published!" msgid "Notes published!"
msgstr "Notes publiées !" msgstr "Notes publiées !"
#: participation/views.py:760 #: participation/views.py:763
msgid "Notes hidden!" msgid "Notes hidden!"
msgstr "Notes dissimulées !" msgstr "Notes dissimulées !"
#: participation/views.py:791 #: participation/views.py:794
#, python-brace-format #, python-brace-format
msgid "Harmonize notes of {tournament} - Day {round}" msgid "Harmonize notes of {tournament} - Day {round}"
msgstr "Harmoniser les notes de {tournament} - Jour {round}" msgstr "Harmoniser les notes de {tournament} - Jour {round}"
#: participation/views.py:905 #: participation/views.py:908
msgid "You can't upload a solution after the deadline." msgid "You can't upload a solution after the deadline."
msgstr "Vous ne pouvez pas envoyer de solution après la date limite." msgstr "Vous ne pouvez pas envoyer de solution après la date limite."
#: participation/views.py:1025 #: participation/views.py:1028
#, python-brace-format #, python-brace-format
msgid "Solutions of team {trigram}.zip" msgid "Solutions of team {trigram}.zip"
msgstr "Solutions de l'équipe {trigram}.zip" msgstr "Solutions de l'équipe {trigram}.zip"
#: participation/views.py:1026 #: participation/views.py:1029
#, python-brace-format #, python-brace-format
msgid "Written reviews of team {trigram}.zip" msgid "Written reviews of team {trigram}.zip"
msgstr "Notes de synthèse de l'équipe {trigram}.zip" msgstr "Notes de synthèse de l'équipe {trigram}.zip"
#: participation/views.py:1043 participation/views.py:1059 #: participation/views.py:1046 participation/views.py:1062
#, python-brace-format #, python-brace-format
msgid "Solutions of {tournament}.zip" msgid "Solutions of {tournament}.zip"
msgstr "Solutions de {tournament}.zip" msgstr "Solutions de {tournament}.zip"
#: participation/views.py:1044 participation/views.py:1060 #: participation/views.py:1047 participation/views.py:1063
#, python-brace-format #, python-brace-format
msgid "Written reviews of {tournament}.zip" msgid "Written reviews of {tournament}.zip"
msgstr "Notes de synthèse de {tournament}.zip" msgstr "Notes de synthèse de {tournament}.zip"
#: participation/views.py:1069 #: participation/views.py:1072
#, python-brace-format #, python-brace-format
msgid "Solutions for pool {pool} of tournament {tournament}.zip" msgid "Solutions for pool {pool} of tournament {tournament}.zip"
msgstr "Solutions pour la poule {pool} du tournoi {tournament}.zip" msgstr "Solutions pour la poule {pool} du tournoi {tournament}.zip"
#: participation/views.py:1070 #: participation/views.py:1073
#, python-brace-format #, python-brace-format
msgid "Written reviews for pool {pool} of tournament {tournament}.zip" msgid "Written reviews for pool {pool} of tournament {tournament}.zip"
msgstr "Notes de synthèses pour la poule {pool} du tournoi {tournament}.zip" msgstr "Notes de synthèses pour la poule {pool} du tournoi {tournament}.zip"
#: participation/views.py:1112 #: participation/views.py:1115
#, python-brace-format #, python-brace-format
msgid "Jury of pool {pool} for {tournament} with teams {teams}" msgid "Jury of pool {pool} for {tournament} with teams {teams}"
msgstr "Jury de la poule {pool} pour {tournament} avec les équipes {teams}" msgstr "Jury de la poule {pool} pour {tournament} avec les équipes {teams}"
#: participation/views.py:1128 #: participation/views.py:1131
#, python-brace-format #, python-brace-format
msgid "The jury {name} is already in the pool!" msgid "The jury {name} is already in the pool!"
msgstr "{name} est déjà dans la poule !" msgstr "{name} est déjà dans la poule !"
#: participation/views.py:1148 #: participation/views.py:1151
msgid "New jury account" msgid "New jury account"
msgstr "Nouveau compte de juré⋅e" msgstr "Nouveau compte de juré⋅e"
#: participation/views.py:1169 #: participation/views.py:1175
#, python-brace-format #, python-brace-format
msgid "The jury {name} has been successfully added!" msgid "The jury {name} has been successfully added!"
msgstr "{name} a été ajouté⋅e avec succès en tant que juré⋅e !" msgstr "{name} a été ajouté⋅e avec succès en tant que juré⋅e !"
#: participation/views.py:1205 #: participation/views.py:1211
#, python-brace-format #, python-brace-format
msgid "The jury {name} has been successfully removed!" msgid "The jury {name} has been successfully removed!"
msgstr "{name} a été retiré⋅e avec succès du jury !" msgstr "{name} a été retiré⋅e avec succès du jury !"
#: participation/views.py:1231 #: participation/views.py:1237
#, python-brace-format #, python-brace-format
msgid "The jury {name} has been successfully promoted president!" msgid "The jury {name} has been successfully promoted president!"
msgstr "{name} a été nommé⋅e président⋅e du jury !" msgstr "{name} a été nommé⋅e président⋅e du jury !"
#: participation/views.py:1259 #: participation/views.py:1265
msgid "The following user is not registered as a jury:" msgid "The following user is not registered as a jury:"
msgstr "L'utilisateur⋅rice suivant n'est pas inscrit⋅e en tant que juré⋅e :" msgstr "L'utilisateur⋅rice suivant n'est pas inscrit⋅e en tant que juré⋅e :"
#: participation/views.py:1275 #: participation/views.py:1281
msgid "Notes were successfully uploaded." msgid "Notes were successfully uploaded."
msgstr "Les notes ont bien été envoyées." msgstr "Les notes ont bien été envoyées."
#: participation/views.py:1907 #: participation/views.py:1912
#, python-brace-format #, python-brace-format
msgid "Notation sheets of pool {pool} of {tournament}.zip" msgid "Notation sheets of pool {pool} of {tournament}.zip"
msgstr "Feuilles de notations pour la poule {pool} du tournoi {tournament}.zip" msgstr "Feuilles de notations pour la poule {pool} du tournoi {tournament}.zip"
#: participation/views.py:1912 #: participation/views.py:1917
#, python-brace-format #, python-brace-format
msgid "Notation sheets of {tournament}.zip" msgid "Notation sheets of {tournament}.zip"
msgstr "Feuilles de notation de {tournament}.zip" msgstr "Feuilles de notation de {tournament}.zip"
#: participation/views.py:2079 #: participation/views.py:2091
msgid "You can't upload a written review after the deadline." msgid "You can't upload a written review after the deadline."
msgstr "Vous ne pouvez pas envoyer de note de synthèse après la date limite." msgstr "Vous ne pouvez pas envoyer de note de synthèse après la date limite."
@ -2981,7 +2987,7 @@ msgstr "Marquer comme invalide"
msgid "role" msgid "role"
msgstr "rôle" msgstr "rôle"
#: registration/forms.py:25 #: registration/forms.py:25 survey/templates/survey/survey_detail.html:50
msgid "participant" msgid "participant"
msgstr "participant⋅e" msgstr "participant⋅e"
@ -4200,22 +4206,22 @@ msgid "Impersonate"
msgstr "Impersonifier" msgstr "Impersonifier"
#: registration/templates/registration/user_detail.html:229 #: registration/templates/registration/user_detail.html:229
#: registration/views.py:333 #: registration/views.py:334
msgid "Upload photo authorization" msgid "Upload photo authorization"
msgstr "Téléverser l'autorisation de droit à l'image" msgstr "Téléverser l'autorisation de droit à l'image"
#: registration/templates/registration/user_detail.html:236 #: registration/templates/registration/user_detail.html:236
#: registration/views.py:367 #: registration/views.py:368
msgid "Upload health sheet" msgid "Upload health sheet"
msgstr "Téléverser la fiche sanitaire" msgstr "Téléverser la fiche sanitaire"
#: registration/templates/registration/user_detail.html:243 #: registration/templates/registration/user_detail.html:243
#: registration/views.py:388 #: registration/views.py:389
msgid "Upload vaccine sheet" msgid "Upload vaccine sheet"
msgstr "Téléverser le carnet de vaccination" msgstr "Téléverser le carnet de vaccination"
#: registration/templates/registration/user_detail.html:249 #: registration/templates/registration/user_detail.html:249
#: registration/views.py:408 #: registration/views.py:409
msgid "Upload parental authorization" msgid "Upload parental authorization"
msgstr "Téléverser l'autorisation parentale" msgstr "Téléverser l'autorisation parentale"
@ -4242,36 +4248,36 @@ msgstr ""
"Les inscriptions pour cette année sont closes depuis le {closing_date:%d/%m/" "Les inscriptions pour cette année sont closes depuis le {closing_date:%d/%m/"
"%Y %H:%M}." "%Y %H:%M}."
#: registration/views.py:140 #: registration/views.py:141
msgid "New organizer account" msgid "New organizer account"
msgstr "Nouveau compte organisateur⋅rice" msgstr "Nouveau compte organisateur⋅rice"
#: registration/views.py:166 #: registration/views.py:167
msgid "Email validation" msgid "Email validation"
msgstr "Validation de l'adresse mail" msgstr "Validation de l'adresse mail"
#: registration/views.py:168 #: registration/views.py:169
msgid "Validate email" msgid "Validate email"
msgstr "Valider l'adresse mail" msgstr "Valider l'adresse mail"
#: registration/views.py:207 #: registration/views.py:208
msgid "Email validation unsuccessful" msgid "Email validation unsuccessful"
msgstr "Échec de la validation de l'adresse mail" msgstr "Échec de la validation de l'adresse mail"
#: registration/views.py:218 #: registration/views.py:219
msgid "Email validation email sent" msgid "Email validation email sent"
msgstr "Mail de confirmation de l'adresse mail envoyé" msgstr "Mail de confirmation de l'adresse mail envoyé"
#: registration/views.py:226 #: registration/views.py:227
msgid "Resend email validation link" msgid "Resend email validation link"
msgstr "Renvoyé le lien de validation de l'adresse mail" msgstr "Renvoyé le lien de validation de l'adresse mail"
#: registration/views.py:269 #: registration/views.py:270
#, python-brace-format #, python-brace-format
msgid "Detail of user {user}" msgid "Detail of user {user}"
msgstr "Détails de l'utilisateur⋅rice {user}" msgstr "Détails de l'utilisateur⋅rice {user}"
#: registration/views.py:294 #: registration/views.py:295
#, python-brace-format #, python-brace-format
msgid "Update user {user}" msgid "Update user {user}"
msgstr "Mise à jour de l'utilisateur⋅rice {user}" msgstr "Mise à jour de l'utilisateur⋅rice {user}"
@ -4358,6 +4364,122 @@ msgstr "Autorisation parentale de {student}.{ext}"
msgid "Payment receipt of {registrations}.{ext}" msgid "Payment receipt of {registrations}.{ext}"
msgstr "Justificatif de paiement de {registrations}.{ext}" msgstr "Justificatif de paiement de {registrations}.{ext}"
#: survey/models.py:21 survey/tables.py:14
msgid "survey identifier"
msgstr "identifiant du sondage"
#: survey/models.py:22
msgid "The numeric identifier of the Limesurvey."
msgstr "L'identifiant numérique du Limesurvey."
#: survey/models.py:27
msgid "display name"
msgstr "nom affiché"
#: survey/models.py:32
msgid "invite whole team"
msgstr "inviter toute l'équipe"
#: survey/models.py:33
msgid ""
"When this field is checked, teams will get only one survey invitation "
"instead of one per person."
msgstr ""
"Lorsque ce champ est coché, les équipes n'auront qu'une seule invitation au "
"sondage au lieu de une par personne."
#: survey/models.py:38
msgid "invite coaches"
msgstr "inviter les encadrant⋅es"
#: survey/models.py:39
msgid ""
"When this field is checked, coaches will also be invited in the survey. No "
"effect when the whole team is invited."
msgstr ""
"Lorsque ce champ est coché, les encadrant⋅es seront aussi invité⋅es au "
"sondage. Aucun effet lorsque toute l'équipe est invité."
#: survey/models.py:48
msgid "tournament restriction"
msgstr "restriction de tournoi"
#: survey/models.py:49
msgid ""
"When this field is filled, the survey participants will be restricted to "
"this tournament members."
msgstr ""
"Lorsque ce champ est rempli, les participant⋅es au sondage seront "
"restreint⋅es aux membres de ce tournoi."
#: survey/models.py:55
msgid "participants that completed the survey"
msgstr "participant⋅es ayant complété le sondage"
#: survey/models.py:61
msgid "teams that completed the survey"
msgstr "équipes ayant complété le sondage"
#: survey/models.py:134 survey/templates/survey/survey_detail.html:10
msgid "survey"
msgstr "sondage"
#: survey/models.py:135 tfjm/templates/navbar.html:78
msgid "surveys"
msgstr "sondages"
#: survey/tables.py:18 survey/templates/survey/survey_detail.html:51
msgid "completed"
msgstr "complété"
#: survey/templates/survey/survey_detail.html:19
msgid "One answer per team:"
msgstr "Une réponse par équipe :"
#: survey/templates/survey/survey_detail.html:23
msgid "Coaches can answer the survey:"
msgstr "Les encadrant⋅es peuvent répondre au sondage :"
#: survey/templates/survey/survey_detail.html:28
msgid "Tournament restriction:"
msgstr "Restriction de tournoi :"
#: survey/templates/survey/survey_detail.html:32
msgid "Completion rate:"
msgstr "Taux de complétion :"
#: survey/templates/survey/survey_detail.html:41
msgid "Send invites"
msgstr "Envoyer les invitations"
#: survey/templates/survey/survey_detail.html:63
msgid "Yes"
msgstr "Oui"
#: survey/templates/survey/survey_detail.html:65
msgid "No"
msgstr "Non"
#: survey/templates/survey/survey_detail.html:72
msgid "Update survey"
msgstr "Modifier le sondage"
#: survey/templates/survey/survey_list.html:8
msgid "Add survey"
msgstr "Ajouter un sondage"
#: survey/views.py:38
msgid "Invites sent!"
msgstr "Invitations envoyées !"
#: survey/views.py:40
msgid "All invites were already sent."
msgstr "Toutes les invitations étaient déjà envoyés."
#: survey/views.py:50
msgid "Completion data refreshed!"
msgstr "Données de complétion actualisées !"
#: tfjm/permissions.py:9 #: tfjm/permissions.py:9
msgid "Everyone, including anonymous users" msgid "Everyone, including anonymous users"
msgstr "Tout le monde, incluant les utilisateur⋅rices anonymes" msgstr "Tout le monde, incluant les utilisateur⋅rices anonymes"
@ -4402,11 +4524,11 @@ msgstr "Privé, réservé aux utilisateur⋅rices explicitement autorisé⋅es"
msgid "Admin users" msgid "Admin users"
msgstr "Administrateur⋅rices" msgstr "Administrateur⋅rices"
#: tfjm/settings.py:174 #: tfjm/settings.py:175
msgid "English" msgid "English"
msgstr "Anglais" msgstr "Anglais"
#: tfjm/settings.py:175 #: tfjm/settings.py:176
msgid "French" msgid "French"
msgstr "Français" msgstr "Français"
@ -4674,23 +4796,23 @@ msgstr "Mon équipe"
msgid "My participation" msgid "My participation"
msgstr "Ma participation" msgstr "Ma participation"
#: tfjm/templates/navbar.html:78 #: tfjm/templates/navbar.html:81
msgid "Administration" msgid "Administration"
msgstr "Administration" msgstr "Administration"
#: tfjm/templates/navbar.html:86 #: tfjm/templates/navbar.html:89
msgid "Search…" msgid "Search…"
msgstr "Chercher…" msgstr "Chercher…"
#: tfjm/templates/navbar.html:95 #: tfjm/templates/navbar.html:98
msgid "Return to admin view" msgid "Return to admin view"
msgstr "Retourner à l'interface administrateur⋅rice" msgstr "Retourner à l'interface administrateur⋅rice"
#: tfjm/templates/navbar.html:102 #: tfjm/templates/navbar.html:105
msgid "Register" msgid "Register"
msgstr "S'inscrire" msgstr "S'inscrire"
#: tfjm/templates/navbar.html:119 #: tfjm/templates/navbar.html:122
msgid "My account" msgid "My account"
msgstr "Mon compte" msgstr "Mon compte"

View File

@ -211,7 +211,7 @@ class Team(models.Model):
""" """
:return: The mailing list to contact the team members. :return: The mailing list to contact the team members.
""" """
return f"equipe-{slugify(self.trigram)}@{os.getenv('SYMPA_HOST', 'localhost')}" return f"equipe-{slugify(self.trigram)}@{settings.SYMPA_HOST}"
def create_mailing_list(self): def create_mailing_list(self):
""" """
@ -392,21 +392,21 @@ class Tournament(models.Model):
""" """
:return: The mailing list to contact the team members. :return: The mailing list to contact the team members.
""" """
return f"equipes-{slugify(self.name)}@{os.getenv('SYMPA_HOST', 'localhost')}" return f"equipes-{slugify(self.name)}@{settings.SYMPA_HOST}"
@property @property
def organizers_email(self): def organizers_email(self):
""" """
:return: The mailing list to contact the team members. :return: The mailing list to contact the team members.
""" """
return f"organisateurs-{slugify(self.name)}@{os.getenv('SYMPA_HOST', 'localhost')}" return f"organisateurs-{slugify(self.name)}@{settings.SYMPA_HOST}"
@property @property
def jurys_email(self): def jurys_email(self):
""" """
:return: The mailing list to contact the team members. :return: The mailing list to contact the team members.
""" """
return f"jurys-{slugify(self.name)}@{os.getenv('SYMPA_HOST', 'localhost')}" return f"jurys-{slugify(self.name)}@{settings.SYMPA_HOST}"
def create_mailing_lists(self): def create_mailing_lists(self):
""" """

View File

@ -1,5 +1,6 @@
channels[daphne]~=4.1.0 channels[daphne]~=4.1.0
channels-redis~=4.2.0 channels-redis~=4.2.0
citric~=1.4.0
crispy-bootstrap5~=2024.10 crispy-bootstrap5~=2024.10
Django>=5.1.2,<6.0 Django>=5.1.2,<6.0
django-crispy-forms~=2.3 django-crispy-forms~=2.3

0
survey/__init__.py Normal file
View File

13
survey/admin.py Normal file
View File

@ -0,0 +1,13 @@
# Copyright (C) 2025 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from .models import Survey
@admin.register(Survey)
class SurveyAdmin(admin.ModelAdmin):
list_display = ('survey_id', 'name', 'invite_team', 'invite_coaches', 'tournament',)
list_filter = ('invite_team', 'invite_coaches', 'tournament',)
search_fields = ('name',)

6
survey/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SurveyConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "survey"

28
survey/forms.py Normal file
View File

@ -0,0 +1,28 @@
from django import forms
from .models import Survey
class SurveyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'survey_id' in self.initial:
self.fields['survey_id'].disabled = True
class Meta:
model = Survey
exclude = ('completed_registrations', 'completed_teams',)
widgets = {
'completed_registrations': forms.SelectMultiple(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
'data-live-search-normalize': 'true',
'data-width': 'fit',
}),
'completed_teams': forms.SelectMultiple(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
'data-live-search-normalize': 'true',
'data-width': 'fit',
}),
}

View File

@ -0,0 +1,13 @@
# Copyright (C) 2025 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.core.management import BaseCommand
from ...models import Survey
class Command(BaseCommand):
def handle(self, *args, **kwargs):
for survey in Survey.objects.all():
survey.fetch_completion_data()

View File

@ -0,0 +1,83 @@
# Generated by Django 5.1.5 on 2025-03-19 21:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
(
"participation",
"0023_tournament_unified_registration",
),
("registration", "0014_participantregistration_country"),
]
operations = [
migrations.CreateModel(
name="Survey",
fields=[
(
"survey_id",
models.IntegerField(
help_text="The numeric identifier of the Limesurvey.",
primary_key=True,
serialize=False,
verbose_name="survey identifier",
),
),
("name", models.CharField(max_length=255, verbose_name="display name")),
(
"invite_team",
models.BooleanField(
default=False,
help_text="When this field is checked, teams will get only one survey invitation instead of one per person.",
verbose_name="invite whole team",
),
),
(
"invite_coaches",
models.BooleanField(
default=True,
help_text="When this field is checked, coaches will also be invited in the survey. No effect when the whole team is invited.",
verbose_name="invite coaches",
),
),
(
"completed_registrations",
models.ManyToManyField(
related_name="completed_surveys",
to="registration.participantregistration",
verbose_name="participants that completed the survey",
),
),
(
"completed_teams",
models.ManyToManyField(
related_name="completed_surveys",
to="participation.team",
verbose_name="teams that completed the survey",
),
),
(
"tournament",
models.ForeignKey(
blank=True,
default=None,
help_text="When this field is filled, the survey participants will be restricted to this tournament members.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="participation.tournament",
verbose_name="tournament restriction",
),
),
],
options={
"verbose_name": "survey",
"verbose_name_plural": "surveys",
},
),
]

View File

134
survey/models.py Normal file
View File

@ -0,0 +1,134 @@
# Copyright (C) 2025 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from citric import Client
from django.conf import settings
from django.db import models
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from participation.models import Team, Tournament
from registration.models import ParticipantRegistration, StudentRegistration
class Survey(models.Model):
"""
Ce modèle représente un sondage LimeSurvey afin de faciliter l'import des
participantes au sondage et d'effectuer le suivi.
"""
survey_id = models.IntegerField(
primary_key=True,
verbose_name=_("survey identifier"),
help_text=_("The numeric identifier of the Limesurvey."),
)
name = models.CharField(
max_length=255,
verbose_name=_("display name"),
)
invite_team = models.BooleanField(
default=False,
verbose_name=_("invite whole team"),
help_text=_("When this field is checked, teams will get only one survey invitation instead of one per person."),
)
invite_coaches = models.BooleanField(
default=True,
verbose_name=_("invite coaches"),
help_text=_("When this field is checked, coaches will also be invited in the survey. No effect when the whole team is invited."),
)
tournament = models.ForeignKey(
Tournament,
null=True,
blank=True,
default=None,
on_delete=models.SET_NULL,
verbose_name=_("tournament restriction"),
help_text=_("When this field is filled, the survey participants will be restricted to this tournament members."),
)
completed_registrations = models.ManyToManyField(
ParticipantRegistration,
related_name="completed_surveys",
verbose_name=_("participants that completed the survey"),
)
completed_teams = models.ManyToManyField(
Team,
related_name="completed_surveys",
verbose_name=_("teams that completed the survey"),
)
@property
def participants(self):
if self.invite_team:
teams = Team.objects.filter(participation__valid=True)
if self.tournament:
teams = teams.filter(participation__tournament=self.tournament)
return teams.all()
else:
if self.invite_coaches:
registrations = ParticipantRegistration.objects.filter(team__participation__valid=True)
else:
registrations = StudentRegistration.objects.filter(team__participation__valid=True)
if self.tournament:
registrations = registrations.filter(team__participation__tournament=self.tournament)
return registrations.all()
@property
def completed(self):
if self.invite_team:
return self.completed_teams
else:
return self.completed_registrations
def get_absolute_url(self):
return reverse_lazy("survey:survey_detail", args=(self.survey_id,))
def generate_participants_data(self):
participants_data = []
if self.invite_team:
for team in self.participants:
participant_data = {"firstname": team.name, "lastname": f"(équipe {team.trigram})", "email": team.email}
participants_data.append(participant_data)
else:
for reg in self.participants:
participant_data = {"firstname": reg.user.first_name, "lastname": reg.user.last_name, "email": reg.user.email}
participants_data.append(participant_data)
return participants_data
def invite_all(self):
participants_data = self.generate_participants_data()
with Client(f"{settings.LIMESURVEY_URL}/index.php/admin/remotecontrol", settings.LIMESURVEY_USER, settings.LIMESURVEY_PASSWORD) as client:
try:
current_participants = client.list_participants(self.survey_id, limit=10000)
except:
current_participants = []
current_participants_email = set(participant['participant_info']['email'] for participant in current_participants)
participants_data = [participant_data for participant_data in participants_data if participant_data['email'] not in current_participants_email]
try:
client.activate_tokens(self.survey_id)
except:
pass
new_participants = client.add_participants(self.survey_id, participant_data=participants_data)
if new_participants:
client.invite_participants(self.survey_id, token_ids=[participant['tid'] for participant in new_participants])
return new_participants
def fetch_completion_data(self):
with Client(f"{settings.LIMESURVEY_URL}/index.php/admin/remotecontrol", settings.LIMESURVEY_USER, settings.LIMESURVEY_PASSWORD) as client:
participants = client.list_participants(self.survey_id, limit=10000, attributes=['completed'])
if self.invite_team:
team_names = [participant['participant_info']['firstname'] for participant in participants if participant['completed'] != 'N']
self.completed_teams.set(list(Team.objects.filter(name__in=team_names).values_list('id', flat=True)))
else:
mails = [participant['participant_info']['email'] for participant in participants if participant['completed'] != 'N']
self.completed_registrations.set(list(ParticipantRegistration.objects.filter(user__email__in=mails).values_list('id', flat=True)))
self.save()
class Meta:
verbose_name = _("survey")
verbose_name_plural = _("surveys")

31
survey/tables.py Normal file
View File

@ -0,0 +1,31 @@
# Copyright (C) 2025 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from .models import Survey
class SurveyTable(tables.Table):
survey_id = tables.LinkColumn(
'survey:survey_detail',
args=[tables.A('survey_id')],
verbose_name=lambda: _("survey identifier").capitalize(),
)
nb_completed = tables.Column(
verbose_name=_("completed").capitalize,
accessor='survey_id'
)
def render_nb_completed(self, record):
return f"{record.completed.count()}/{record.participants.count()}"
class Meta:
attrs = {
'class': 'table table-condensed table-striped',
}
model = Survey
fields = ('survey_id', 'name', 'invite_team', 'invite_coaches', 'tournament', 'nb_completed',)
order_by = ('survey_id',)

View File

@ -0,0 +1,84 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block content %}
<div class="card bg-body shadow">
<div class="card-header text-center">
<h4>
{% trans "survey"|capfirst %} {{ survey.survey_id }}
<a href="{{ TFJM.LIMESURVEY_URL }}/index.php/{{ survey.survey_id }}" target="_blank"><i class="fas fa-arrow-up-right-from-square"></i></a>
</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6 text-sm-end">{% trans "Name:" %}</dt>
<dd class="col-sm-6">{{ survey.name }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans "One answer per team:" %}</dt>
<dd class="col-sm-6">{{ survey.invite_team|yesno }}</dd>
{% if not survey.invite_team %}
<dt class="col-sm-6 text-sm-end">{% trans "Coaches can answer the survey:" %}</dt>
<dd class="col-sm-6">{{ survey.invite_coaches|yesno }}</dd>
{% endif %}
{% if survey.tournament %}
<dt class="col-sm-6 text-sm-end">{% trans "Tournament restriction:" %}</dt>
<dd class="col-sm-6">{{ survey.tournament }}</dd>
{% endif %}
<dt class="col-sm-6 text-sm-end">{% trans "Completion rate:" %}</dt>
<dd class="col-sm-6">
{{ survey.completed.count }}/{{ survey.participants.count }}
<a href="{% url "survey:survey_refresh_completed" pk=survey.pk %}"><i class="fas fa-arrow-rotate-right" alt="refresh"></i></a>
</dd>
</dl>
</div>
<div class="card-footer text-center">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateSurveyModal">{% trans "Update" %}</button>
<a class="btn btn-secondary" href="{% url "survey:survey_invite" pk=survey.pk %}">{% trans "Send invites" %}</a>
</div>
</div>
<hr>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>{% trans "participant"|capfirst %}</th>
<th>{% trans "completed"|capfirst %}</th>
</tr>
</thead>
<tbody>
{% for participant in survey.participants %}
<tr class="{% if participant in survey.completed.all %}table-success{% else %}table-danger{% endif %}">
{% if survey.invite_team %}
<td>{% trans "Team" %} {{ participant.name }} ({{ participant.trigram }})</td>
{% else %}
<td>{{ participant.user.first_name }} {{ participant.user.last_name }} ({% trans "team" %} {{ participant.team.trigram }})</td>
{% endif %}
{% if participant in survey.completed.all %}
<td>{% trans "Yes" %}</td>
{% else %}
<td>{% trans "No" %}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% trans "Update survey" as modal_title %}
{% trans "Update" as modal_button %}
{% url "survey:survey_update" pk=survey.pk as modal_action %}
{% include "base_modal.html" with modal_id="updateSurvey" %}
{% endblock %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
initModal("updateSurvey", "{% url "survey:survey_update" pk=survey.pk %}")
})
</script>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
{% if object.pk %}
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
{% else %}
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
{% endif %}
</form>
{% endblock content %}

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% load django_tables2 i18n %}
{% block content %}
<div class="d-grid">
<a href="{% url "survey:survey_create" %}" class="btn gap-0 btn-success">
<i class="fas fa-square-poll-horizontal"></i> {% trans "Add survey" %}
</a>
</div>
<hr>
{% render_table table %}
{% endblock %}

3
survey/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

18
survey/urls.py Normal file
View File

@ -0,0 +1,18 @@
# Copyright (C) 2025 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from .views import SurveyCreateView, SurveyDetailView, SurveyInviteView, \
SurveyListView, SurveyRefreshCompletedView, SurveyUpdateView
app_name = "survey"
urlpatterns = [
path("", SurveyListView.as_view(), name="survey_list"),
path("create/", SurveyCreateView.as_view(), name="survey_create"),
path("<int:pk>/", SurveyDetailView.as_view(), name="survey_detail"),
path("<int:pk>/invite/", SurveyInviteView.as_view(), name="survey_invite"),
path("<int:pk>/refresh/", SurveyRefreshCompletedView.as_view(), name="survey_refresh_completed"),
path("<int:pk>/update/", SurveyUpdateView.as_view(), name="survey_update"),
]

56
survey/views.py Normal file
View File

@ -0,0 +1,56 @@
# Copyright (C) 2025 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import messages
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView
from django_tables2 import SingleTableView
from tfjm.views import AdminMixin
from .forms import SurveyForm
from .models import Survey
from .tables import SurveyTable
class SurveyListView(AdminMixin, SingleTableView):
model = Survey
table_class = SurveyTable
template_name = "survey/survey_list.html"
class SurveyCreateView(AdminMixin, CreateView):
model = Survey
form_class = SurveyForm
class SurveyDetailView(AdminMixin, DetailView):
model = Survey
class SurveyInviteView(AdminMixin, DetailView):
model = Survey
def get(self, request, *args, **kwargs):
survey = self.get_object()
new_participants = survey.invite_all()
if new_participants:
messages.success(request, _("Invites sent!"))
else:
messages.warning(request, _("All invites were already sent."))
return redirect("survey:survey_detail", survey.pk)
class SurveyRefreshCompletedView(AdminMixin, DetailView):
model = Survey
def get(self, request, *args, **kwargs):
survey = self.get_object()
survey.fetch_completion_data()
messages.success(request, _("Completion data refreshed!"))
return redirect("survey:survey_detail", survey.pk)
class SurveyUpdateView(AdminMixin, UpdateView):
model = Survey
form_class = SurveyForm

View File

@ -19,5 +19,8 @@
# Update Google Drive notifications daily # Update Google Drive notifications daily
0 0 * * * cd /code && python manage.py renew_gdrive_notifications -v 0 0 0 * * * cd /code && python manage.py renew_gdrive_notifications -v 0
# Fetch LimeSurvey completion data
*/15 * * 03-06 * cd /code && python manage.py fetch_survey_completion_data -v 0
# Clean temporary files # Clean temporary files
30 * * * * rm -rf /tmp/* 30 * * * * rm -rf /tmp/*

View File

@ -13,6 +13,7 @@ def tfjm_context(request):
'HAS_OBSERVER': settings.HAS_OBSERVER, 'HAS_OBSERVER': settings.HAS_OBSERVER,
'HAS_FINAL': settings.HAS_FINAL, 'HAS_FINAL': settings.HAS_FINAL,
'HOME_PAGE_LINK': settings.HOME_PAGE_LINK, 'HOME_PAGE_LINK': settings.HOME_PAGE_LINK,
'LIMESURVEY_URL': settings.LIMESURVEY_URL,
'LOGO_PATH': "tfjm/img/" + settings.LOGO_FILE, 'LOGO_PATH': "tfjm/img/" + settings.LOGO_FILE,
'NB_ROUNDS': settings.NB_ROUNDS, 'NB_ROUNDS': settings.NB_ROUNDS,
'ML_MANAGEMENT': settings.ML_MANAGEMENT, 'ML_MANAGEMENT': settings.ML_MANAGEMENT,

View File

@ -3,16 +3,18 @@
import os import os
from django.conf import settings
_client = None _client = None
def get_sympa_client(): def get_sympa_client():
global _client global _client
if _client is None: if _client is None:
if os.getenv("SYMPA_PASSWORD", None): # pragma: no cover if settings.SYMPA_PASSWORD is not None: # pragma: no cover
from sympasoap import Client from sympasoap import Client
_client = Client("https://" + os.getenv("SYMPA_URL")) _client = Client("https://" + settings.SYMPA_URL)
_client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD")) _client.login(settings.SYMPA_EMAIL, settings.SYMPA_PASSWORD)
else: else:
_client = FakeSympaSoapClient() _client = FakeSympaSoapClient()
return _client return _client

View File

@ -74,6 +74,7 @@ INSTALLED_APPS = [
'draw', 'draw',
'registration', 'registration',
'participation', 'participation',
'survey',
] ]
if "test" not in sys.argv: # pragma: no cover if "test" not in sys.argv: # pragma: no cover
@ -300,6 +301,12 @@ CHANNEL_LAYERS = {
PHONENUMBER_DB_FORMAT = 'NATIONAL' PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = 'FR' PHONENUMBER_DEFAULT_REGION = 'FR'
# Sympa configuration
SYMPA_HOST = os.getenv("SYMPA_HOST", "localhost")
SYMPA_URL = os.getenv("SYMPA_URL", "localhost")
SYMPA_EMAIL = os.getenv("SYMPA_EMAIL", "contact@localhost")
SYMPA_PASSWORD = os.getenv("SYMPA_PASSWORD", None)
# Hello Asso API creds # Hello Asso API creds
HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTINGS') HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTINGS')
HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS') HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS')
@ -322,6 +329,10 @@ GOOGLE_SERVICE_CLIENT = {
# The ID of the Google Drive folder where to store the notation sheets # The ID of the Google Drive folder where to store the notation sheets
NOTES_DRIVE_FOLDER_ID = os.getenv("NOTES_DRIVE_FOLDER_ID", "CHANGE_ME_IN_ENV_SETTINGS") NOTES_DRIVE_FOLDER_ID = os.getenv("NOTES_DRIVE_FOLDER_ID", "CHANGE_ME_IN_ENV_SETTINGS")
LIMESURVEY_URL = os.getenv("LIMESURVEY_URL", "https://survey.example.com")
LIMESURVEY_USER = os.getenv("LIMESURVEY_USER", "CHANGE_ME_IN_ENV_SETTINGS")
LIMESURVEY_PASSWORD = os.getenv("LIMESURVEY_PASSWORD", "CHANGE_ME_IN_ENV_SETTINGS")
# Custom parameters # Custom parameters
FORBIDDEN_TRIGRAMS = [ FORBIDDEN_TRIGRAMS = [
"BIT", "BIT",

View File

@ -74,6 +74,9 @@
</li> </li>
{% endif %} {% endif %}
{% if user.registration.is_admin %} {% if user.registration.is_admin %}
<li class="nav-item active">
<a class="nav-link" href="{% url "survey:survey_list" %}"><i class="fas fa-square-poll-horizontal"></i> {% trans "surveys"|capfirst %}</a>
</li>
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url "admin:index" %}"><i class="fas fa-cog"></i> {% trans "Administration" %}</a> <a class="nav-link" href="{% url "admin:index" %}"><i class="fas fa-cog"></i> {% trans "Administration" %}</a>
</li> </li>

View File

@ -44,6 +44,7 @@ urlpatterns = [
path('draw/', include('draw.urls')), path('draw/', include('draw.urls')),
path('participation/', include('participation.urls')), path('participation/', include('participation.urls')),
path('registration/', include('registration.urls')), path('registration/', include('registration.urls')),
path('survey/', include('survey.urls')),
path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(), path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(),
name='photo_authorization'), name='photo_authorization'),