mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-10-31 15:00:00 +01:00 
			
		
		
		
	Add survey feature
This commit is contained in:
		| @@ -7,7 +7,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: TFJM\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" | ||||
| "Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @@ -114,6 +114,7 @@ msgstr "" | ||||
| #: registration/models.py:158 registration/models.py:754 | ||||
| #: registration/tables.py:39 | ||||
| #: registration/templates/registration/payment_form.html:52 | ||||
| #: survey/templates/survey/survey_detail.html:60 | ||||
| msgid "team" | ||||
| msgstr "équipe" | ||||
|  | ||||
| @@ -217,7 +218,7 @@ msgstr "" | ||||
| msgid "Toggle fullscreen mode" | ||||
| 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" | ||||
| msgstr "Déconnexion" | ||||
|  | ||||
| @@ -251,7 +252,7 @@ msgstr "Chat" | ||||
| #: chat/templates/chat/login.html:10 chat/templates/chat/login.html:36 | ||||
| #: registration/templates/registration/password_reset_complete.html:10 | ||||
| #: 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/login.html:7 | ||||
| #: tfjm/templates/registration/login.html:8 | ||||
| @@ -713,7 +714,7 @@ msgstr "" | ||||
|  | ||||
| #: draw/models.py:496 draw/models.py:519 participation/models.py:1258 | ||||
| #: 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 | ||||
| msgid "Problem #{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" | ||||
| msgstr "Fermer" | ||||
|  | ||||
| #: draw/views.py:31 participation/views.py:163 participation/views.py:509 | ||||
| #: participation/views.py:540 | ||||
| #: draw/views.py:31 participation/views.py:163 participation/views.py:512 | ||||
| #: participation/views.py:543 | ||||
| msgid "You are not in a team." | ||||
| 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." | ||||
| 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." | ||||
| msgstr "La validation de l'équipe est déjà faite ou en cours." | ||||
|  | ||||
| @@ -1382,7 +1383,7 @@ msgstr "finale" | ||||
| msgid "Google Sheet ID" | ||||
| 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" | ||||
| msgstr "Feuille de notation" | ||||
|  | ||||
| @@ -1392,7 +1393,8 @@ msgid "Final ranking" | ||||
| msgstr "Classement final" | ||||
|  | ||||
| #: 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" | ||||
| msgstr "Équipe" | ||||
|  | ||||
| @@ -1425,14 +1427,14 @@ msgid "Tweaks day 3" | ||||
| msgstr "Ajustements 3" | ||||
|  | ||||
| #: participation/models.py:491 participation/models.py:1333 | ||||
| #: participation/views.py:1741 | ||||
| #: participation/views.py:1747 | ||||
| msgid "Total" | ||||
| msgstr "Total" | ||||
|  | ||||
| #: participation/models.py:491 participation/models.py:559 | ||||
| #: participation/models.py:1333 | ||||
| #: participation/templates/participation/tournament_harmonize.html:14 | ||||
| #: participation/views.py:1744 | ||||
| #: participation/views.py:1750 | ||||
| msgid "Rank" | ||||
| msgstr "Rang" | ||||
|  | ||||
| @@ -1443,7 +1445,7 @@ msgstr "Rang" | ||||
| #: participation/models.py:1360 participation/models.py:1367 | ||||
| #: participation/models.py:1371 participation/models.py:1616 | ||||
| #: participation/models.py:1638 participation/models.py:2102 | ||||
| #: participation/views.py:1815 | ||||
| #: participation/views.py:1821 | ||||
| msgid "Pool" | ||||
| 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." | ||||
|  | ||||
| #: 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" | ||||
| msgstr "Problème" | ||||
|  | ||||
| #: participation/models.py:1260 participation/views.py:1510 | ||||
| #: participation/models.py:1260 participation/views.py:1516 | ||||
| msgid "Reporter" | ||||
| msgstr "Défenseur⋅se" | ||||
|  | ||||
| #: participation/models.py:1261 participation/views.py:1516 | ||||
| #: participation/models.py:1261 participation/views.py:1522 | ||||
| msgid "Opponent" | ||||
| msgstr "Opposant⋅e" | ||||
|  | ||||
| #: participation/models.py:1262 participation/views.py:1523 | ||||
| #: participation/models.py:1262 participation/views.py:1529 | ||||
| msgid "Reviewer" | ||||
| msgstr "Rapporteur⋅rice" | ||||
|  | ||||
| #: participation/models.py:1263 participation/views.py:1530 | ||||
| #: participation/models.py:1263 participation/views.py:1536 | ||||
| msgid "Observer" | ||||
| msgstr "Observateur⋅rice" | ||||
|  | ||||
| #: participation/models.py:1264 participation/views.py:1504 | ||||
| #: participation/models.py:1264 participation/views.py:1510 | ||||
| msgid "Role" | ||||
| msgstr "Rôle" | ||||
|  | ||||
| #: participation/models.py:1265 participation/models.py:1267 | ||||
| #: participation/models.py:1268 participation/views.py:1546 | ||||
| #: participation/views.py:1554 participation/views.py:1562 | ||||
| #: participation/views.py:1572 | ||||
| #: participation/models.py:1268 participation/views.py:1552 | ||||
| #: participation/views.py:1560 participation/views.py:1568 | ||||
| #: participation/views.py:1578 | ||||
| msgid "Writing" | ||||
| msgstr "Écrit" | ||||
|  | ||||
| #: participation/models.py:1266 participation/models.py:1267 | ||||
| #: participation/models.py:1268 participation/views.py:1550 | ||||
| #: participation/views.py:1558 participation/views.py:1567 | ||||
| #: participation/views.py:1576 | ||||
| #: participation/models.py:1268 participation/views.py:1556 | ||||
| #: participation/views.py:1564 participation/views.py:1573 | ||||
| #: participation/views.py:1582 | ||||
| msgid "Oral" | ||||
| msgstr "Oral" | ||||
|  | ||||
| #: participation/models.py:1269 participation/views.py:1538 | ||||
| #: participation/views.py:1539 | ||||
| #: participation/models.py:1269 participation/views.py:1544 | ||||
| #: participation/views.py:1545 | ||||
| msgid "Juree" | ||||
| msgstr "Juré⋅e" | ||||
|  | ||||
| #: 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" | ||||
| msgstr "Moyenne" | ||||
|  | ||||
| #: participation/models.py:1298 participation/views.py:1627 | ||||
| #: participation/models.py:1298 participation/views.py:1633 | ||||
| msgid "Coefficient" | ||||
| msgstr "Coefficien" | ||||
|  | ||||
| #: participation/models.py:1299 participation/views.py:1670 | ||||
| #: participation/models.py:1299 participation/views.py:1676 | ||||
| msgid "Subtotal" | ||||
| msgstr "Sous-total" | ||||
|  | ||||
| @@ -1972,12 +1974,15 @@ msgstr "Pas d'équipe définie" | ||||
| #: registration/templates/registration/payment_form.html:210 | ||||
| #: registration/templates/registration/update_user.html:16 | ||||
| #: 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" | ||||
| msgstr "Modifier" | ||||
|  | ||||
| #: participation/templates/participation/create_team.html:11 | ||||
| #: 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" | ||||
| msgstr "Créer" | ||||
|  | ||||
| @@ -2305,6 +2310,7 @@ msgid "Back to pool detail" | ||||
| msgstr "Retour aux détails de la poule" | ||||
|  | ||||
| #: participation/templates/participation/team_detail.html:13 | ||||
| #: survey/templates/survey/survey_detail.html:16 | ||||
| msgid "Name:" | ||||
| msgstr "Nom :" | ||||
|  | ||||
| @@ -2465,7 +2471,7 @@ msgid "Invalidate" | ||||
| msgstr "Invalider" | ||||
|  | ||||
| #: participation/templates/participation/team_detail.html:244 | ||||
| #: participation/views.py:341 | ||||
| #: participation/views.py:344 | ||||
| msgid "Upload motivation letter" | ||||
| msgstr "Envoyer la lettre de motivation" | ||||
|  | ||||
| @@ -2474,7 +2480,7 @@ msgid "Update team" | ||||
| msgstr "Modifier l'équipe" | ||||
|  | ||||
| #: participation/templates/participation/team_detail.html:255 | ||||
| #: participation/views.py:503 | ||||
| #: participation/views.py:506 | ||||
| msgid "Leave team" | ||||
| msgstr "Quitter l'équipe" | ||||
|  | ||||
| @@ -2736,7 +2742,7 @@ msgstr "Vous êtes déjà dans une équipe." | ||||
| msgid "Join team" | ||||
| 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." | ||||
| 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 " | ||||
| "de motivation, soit le tournoi n'a pas été choisi." | ||||
|  | ||||
| #: participation/views.py:235 | ||||
| #: participation/views.py:236 | ||||
| msgid "Team validation" | ||||
| msgstr "Validation d'équipe" | ||||
|  | ||||
| #: participation/views.py:247 | ||||
| #: participation/views.py:248 | ||||
| msgid "You are not an organizer of the tournament." | ||||
| 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." | ||||
| msgstr "L'équipe n'a pas de validation en attente." | ||||
|  | ||||
| #: participation/views.py:270 | ||||
| #: participation/views.py:272 | ||||
| msgid "Team validated" | ||||
| msgstr "Équipe validée" | ||||
|  | ||||
| #: participation/views.py:279 | ||||
| #: participation/views.py:282 | ||||
| msgid "Team not validated" | ||||
| msgstr "Équipe non validée" | ||||
|  | ||||
| #: participation/views.py:282 | ||||
| #: participation/views.py:285 | ||||
| msgid "You must specify if you validate the registration or not." | ||||
| msgstr "Vous devez spécifier si vous validez l'inscription ou non." | ||||
|  | ||||
| #: participation/views.py:318 | ||||
| #: participation/views.py:321 | ||||
| #, python-brace-format | ||||
| msgid "Update team {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 | ||||
| msgid "Motivation letter of {team}.{ext}" | ||||
| msgstr "Lettre de motivation de {team}.{ext}" | ||||
|  | ||||
| #: participation/views.py:413 | ||||
| #: participation/views.py:416 | ||||
| #, python-brace-format | ||||
| msgid "Authorizations of team {trigram}.zip" | ||||
| msgstr "Autorisations de l'équipe {trigram}.zip" | ||||
|  | ||||
| #: participation/views.py:417 | ||||
| #: participation/views.py:420 | ||||
| #, python-brace-format | ||||
| msgid "Authorizations of {tournament}.zip" | ||||
| msgstr "Autorisations du tournoi {tournament}.zip" | ||||
|  | ||||
| #: participation/views.py:436 | ||||
| #: participation/views.py:439 | ||||
| #, python-brace-format | ||||
| msgid "Photo authorization of {participant}.{ext}" | ||||
| msgstr "Autorisation de droit à l'image de {participant}.{ext}" | ||||
|  | ||||
| #: participation/views.py:445 | ||||
| #: participation/views.py:448 | ||||
| #, python-brace-format | ||||
| msgid "Parental authorization of {participant}.{ext}" | ||||
| msgstr "Autorisation parentale de {participant}.{ext}" | ||||
|  | ||||
| #: participation/views.py:453 | ||||
| #: participation/views.py:456 | ||||
| #, python-brace-format | ||||
| msgid "Health sheet of {participant}.{ext}" | ||||
| msgstr "Fiche sanitaire de {participant}.{ext}" | ||||
|  | ||||
| #: participation/views.py:461 | ||||
| #: participation/views.py:464 | ||||
| #, python-brace-format | ||||
| msgid "Vaccine sheet of {participant}.{ext}" | ||||
| msgstr "Carnet de vaccination de {participant}.{ext}" | ||||
|  | ||||
| #: participation/views.py:472 | ||||
| #: participation/views.py:475 | ||||
| #, python-brace-format | ||||
| msgid "Photo authorization of {participant} (final).{ext}" | ||||
| msgstr "Autorisation de droit à l'image de {participant} (finale).{ext}" | ||||
|  | ||||
| #: participation/views.py:481 | ||||
| #: participation/views.py:484 | ||||
| #, python-brace-format | ||||
| msgid "Parental authorization of {participant} (final).{ext}" | ||||
| msgstr "Autorisation parentale de {participant} (finale).{ext}" | ||||
|  | ||||
| #: participation/views.py:555 | ||||
| #: participation/views.py:558 | ||||
| msgid "The team is not validated yet." | ||||
| msgstr "L'équipe n'est pas encore validée." | ||||
|  | ||||
| #: participation/views.py:569 | ||||
| #: participation/views.py:572 | ||||
| #, python-brace-format | ||||
| msgid "Participation of team {trigram}" | ||||
| msgstr "Participation de l'équipe {trigram}" | ||||
|  | ||||
| #: participation/views.py:658 | ||||
| #: participation/views.py:661 | ||||
| #, python-brace-format | ||||
| msgid "Payments of {tournament}" | ||||
| msgstr "Paiements de {tournament}" | ||||
|  | ||||
| #: participation/views.py:758 | ||||
| #: participation/views.py:761 | ||||
| msgid "Notes published!" | ||||
| msgstr "Notes publiées !" | ||||
|  | ||||
| #: participation/views.py:760 | ||||
| #: participation/views.py:763 | ||||
| msgid "Notes hidden!" | ||||
| msgstr "Notes dissimulées !" | ||||
|  | ||||
| #: participation/views.py:791 | ||||
| #: participation/views.py:794 | ||||
| #, python-brace-format | ||||
| msgid "Harmonize notes of {tournament} - Day {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." | ||||
| msgstr "Vous ne pouvez pas envoyer de solution après la date limite." | ||||
|  | ||||
| #: participation/views.py:1025 | ||||
| #: participation/views.py:1028 | ||||
| #, python-brace-format | ||||
| msgid "Solutions of team {trigram}.zip" | ||||
| msgstr "Solutions de l'équipe {trigram}.zip" | ||||
|  | ||||
| #: participation/views.py:1026 | ||||
| #: participation/views.py:1029 | ||||
| #, python-brace-format | ||||
| msgid "Written reviews of team {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 | ||||
| msgid "Solutions of {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 | ||||
| msgid "Written reviews of {tournament}.zip" | ||||
| msgstr "Notes de synthèse de {tournament}.zip" | ||||
|  | ||||
| #: participation/views.py:1069 | ||||
| #: participation/views.py:1072 | ||||
| #, python-brace-format | ||||
| msgid "Solutions for pool {pool} of tournament {tournament}.zip" | ||||
| msgstr "Solutions pour la poule {pool} du tournoi {tournament}.zip" | ||||
|  | ||||
| #: participation/views.py:1070 | ||||
| #: participation/views.py:1073 | ||||
| #, python-brace-format | ||||
| msgid "Written reviews for pool {pool} of tournament {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 | ||||
| msgid "Jury of pool {pool} for {tournament} with teams {teams}" | ||||
| msgstr "Jury de la poule {pool} pour {tournament} avec les équipes {teams}" | ||||
|  | ||||
| #: participation/views.py:1128 | ||||
| #: participation/views.py:1131 | ||||
| #, python-brace-format | ||||
| msgid "The jury {name} is already in the pool!" | ||||
| msgstr "{name} est déjà dans la poule !" | ||||
|  | ||||
| #: participation/views.py:1148 | ||||
| #: participation/views.py:1151 | ||||
| msgid "New jury account" | ||||
| msgstr "Nouveau compte de juré⋅e" | ||||
|  | ||||
| #: participation/views.py:1169 | ||||
| #: participation/views.py:1175 | ||||
| #, python-brace-format | ||||
| msgid "The jury {name} has been successfully added!" | ||||
| 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 | ||||
| msgid "The jury {name} has been successfully removed!" | ||||
| msgstr "{name} a été retiré⋅e avec succès du jury !" | ||||
|  | ||||
| #: participation/views.py:1231 | ||||
| #: participation/views.py:1237 | ||||
| #, python-brace-format | ||||
| msgid "The jury {name} has been successfully promoted president!" | ||||
| 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:" | ||||
| 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." | ||||
| msgstr "Les notes ont bien été envoyées." | ||||
|  | ||||
| #: participation/views.py:1907 | ||||
| #: participation/views.py:1912 | ||||
| #, python-brace-format | ||||
| msgid "Notation sheets of pool {pool} of {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 | ||||
| msgid "Notation sheets of {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." | ||||
| 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" | ||||
| msgstr "rôle" | ||||
|  | ||||
| #: registration/forms.py:25 | ||||
| #: registration/forms.py:25 survey/templates/survey/survey_detail.html:50 | ||||
| msgid "participant" | ||||
| msgstr "participant⋅e" | ||||
|  | ||||
| @@ -4200,22 +4206,22 @@ msgid "Impersonate" | ||||
| msgstr "Impersonifier" | ||||
|  | ||||
| #: registration/templates/registration/user_detail.html:229 | ||||
| #: registration/views.py:333 | ||||
| #: registration/views.py:334 | ||||
| msgid "Upload photo authorization" | ||||
| msgstr "Téléverser l'autorisation de droit à l'image" | ||||
|  | ||||
| #: registration/templates/registration/user_detail.html:236 | ||||
| #: registration/views.py:367 | ||||
| #: registration/views.py:368 | ||||
| msgid "Upload health sheet" | ||||
| msgstr "Téléverser la fiche sanitaire" | ||||
|  | ||||
| #: registration/templates/registration/user_detail.html:243 | ||||
| #: registration/views.py:388 | ||||
| #: registration/views.py:389 | ||||
| msgid "Upload vaccine sheet" | ||||
| msgstr "Téléverser le carnet de vaccination" | ||||
|  | ||||
| #: registration/templates/registration/user_detail.html:249 | ||||
| #: registration/views.py:408 | ||||
| #: registration/views.py:409 | ||||
| msgid "Upload parental authorization" | ||||
| 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/" | ||||
| "%Y %H:%M}." | ||||
|  | ||||
| #: registration/views.py:140 | ||||
| #: registration/views.py:141 | ||||
| msgid "New organizer account" | ||||
| msgstr "Nouveau compte organisateur⋅rice" | ||||
|  | ||||
| #: registration/views.py:166 | ||||
| #: registration/views.py:167 | ||||
| msgid "Email validation" | ||||
| msgstr "Validation de l'adresse mail" | ||||
|  | ||||
| #: registration/views.py:168 | ||||
| #: registration/views.py:169 | ||||
| msgid "Validate email" | ||||
| msgstr "Valider l'adresse mail" | ||||
|  | ||||
| #: registration/views.py:207 | ||||
| #: registration/views.py:208 | ||||
| msgid "Email validation unsuccessful" | ||||
| msgstr "Échec de la validation de l'adresse mail" | ||||
|  | ||||
| #: registration/views.py:218 | ||||
| #: registration/views.py:219 | ||||
| msgid "Email validation email sent" | ||||
| msgstr "Mail de confirmation de l'adresse mail envoyé" | ||||
|  | ||||
| #: registration/views.py:226 | ||||
| #: registration/views.py:227 | ||||
| msgid "Resend email validation link" | ||||
| msgstr "Renvoyé le lien de validation de l'adresse mail" | ||||
|  | ||||
| #: registration/views.py:269 | ||||
| #: registration/views.py:270 | ||||
| #, python-brace-format | ||||
| msgid "Detail of user {user}" | ||||
| msgstr "Détails de l'utilisateur⋅rice {user}" | ||||
|  | ||||
| #: registration/views.py:294 | ||||
| #: registration/views.py:295 | ||||
| #, python-brace-format | ||||
| msgid "Update user {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}" | ||||
| 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 | ||||
| msgid "Everyone, including anonymous users" | ||||
| 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" | ||||
| msgstr "Administrateur⋅rices" | ||||
|  | ||||
| #: tfjm/settings.py:174 | ||||
| #: tfjm/settings.py:175 | ||||
| msgid "English" | ||||
| msgstr "Anglais" | ||||
|  | ||||
| #: tfjm/settings.py:175 | ||||
| #: tfjm/settings.py:176 | ||||
| msgid "French" | ||||
| msgstr "Français" | ||||
|  | ||||
| @@ -4674,23 +4796,23 @@ msgstr "Mon équipe" | ||||
| msgid "My participation" | ||||
| msgstr "Ma participation" | ||||
|  | ||||
| #: tfjm/templates/navbar.html:78 | ||||
| #: tfjm/templates/navbar.html:81 | ||||
| msgid "Administration" | ||||
| msgstr "Administration" | ||||
|  | ||||
| #: tfjm/templates/navbar.html:86 | ||||
| #: tfjm/templates/navbar.html:89 | ||||
| msgid "Search…" | ||||
| msgstr "Chercher…" | ||||
|  | ||||
| #: tfjm/templates/navbar.html:95 | ||||
| #: tfjm/templates/navbar.html:98 | ||||
| msgid "Return to admin view" | ||||
| msgstr "Retourner à l'interface administrateur⋅rice" | ||||
|  | ||||
| #: tfjm/templates/navbar.html:102 | ||||
| #: tfjm/templates/navbar.html:105 | ||||
| msgid "Register" | ||||
| msgstr "S'inscrire" | ||||
|  | ||||
| #: tfjm/templates/navbar.html:119 | ||||
| #: tfjm/templates/navbar.html:122 | ||||
| msgid "My account" | ||||
| msgstr "Mon compte" | ||||
|  | ||||
|   | ||||
| @@ -211,7 +211,7 @@ class Team(models.Model): | ||||
|         """ | ||||
|         :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): | ||||
|         """ | ||||
| @@ -392,21 +392,21 @@ class Tournament(models.Model): | ||||
|         """ | ||||
|         :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 | ||||
|     def organizers_email(self): | ||||
|         """ | ||||
|         :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 | ||||
|     def jurys_email(self): | ||||
|         """ | ||||
|         :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): | ||||
|         """ | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| channels[daphne]~=4.1.0 | ||||
| channels-redis~=4.2.0 | ||||
| citric~=1.4.0 | ||||
| crispy-bootstrap5~=2024.10 | ||||
| Django>=5.1.2,<6.0 | ||||
| django-crispy-forms~=2.3 | ||||
|   | ||||
							
								
								
									
										0
									
								
								survey/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								survey/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										13
									
								
								survey/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								survey/admin.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										6
									
								
								survey/apps.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										28
									
								
								survey/forms.py
									
									
									
									
									
										Normal 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', | ||||
|             }), | ||||
|         } | ||||
							
								
								
									
										13
									
								
								survey/management/commands/fetch_survey_completion_data.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								survey/management/commands/fetch_survey_completion_data.py
									
									
									
									
									
										Normal 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() | ||||
							
								
								
									
										83
									
								
								survey/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								survey/migrations/0001_initial.py
									
									
									
									
									
										Normal 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", | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								survey/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								survey/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										134
									
								
								survey/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								survey/models.py
									
									
									
									
									
										Normal 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 | ||||
|     participant⋅es 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
									
								
							
							
						
						
									
										31
									
								
								survey/tables.py
									
									
									
									
									
										Normal 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',) | ||||
							
								
								
									
										84
									
								
								survey/templates/survey/survey_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								survey/templates/survey/survey_detail.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										17
									
								
								survey/templates/survey/survey_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								survey/templates/survey/survey_form.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										14
									
								
								survey/templates/survey/survey_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								survey/templates/survey/survey_list.html
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										3
									
								
								survey/tests.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| from django.test import TestCase | ||||
|  | ||||
| # Create your tests here. | ||||
							
								
								
									
										18
									
								
								survey/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								survey/urls.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										56
									
								
								survey/views.py
									
									
									
									
									
										Normal 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 | ||||
| @@ -19,5 +19,8 @@ | ||||
| # Update Google Drive notifications daily | ||||
| 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 | ||||
| 30      *       *       *       *       rm -rf /tmp/* | ||||
|   | ||||
| @@ -13,6 +13,7 @@ def tfjm_context(request): | ||||
|             'HAS_OBSERVER': settings.HAS_OBSERVER, | ||||
|             'HAS_FINAL': settings.HAS_FINAL, | ||||
|             'HOME_PAGE_LINK': settings.HOME_PAGE_LINK, | ||||
|             'LIMESURVEY_URL': settings.LIMESURVEY_URL, | ||||
|             'LOGO_PATH': "tfjm/img/" + settings.LOGO_FILE, | ||||
|             'NB_ROUNDS': settings.NB_ROUNDS, | ||||
|             'ML_MANAGEMENT': settings.ML_MANAGEMENT, | ||||
|   | ||||
| @@ -3,16 +3,18 @@ | ||||
|  | ||||
| import os | ||||
|  | ||||
| from django.conf import settings | ||||
|  | ||||
| _client = None | ||||
|  | ||||
|  | ||||
| def get_sympa_client(): | ||||
|     global _client | ||||
|     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 | ||||
|             _client = Client("https://" + os.getenv("SYMPA_URL")) | ||||
|             _client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD")) | ||||
|             _client = Client("https://" + settings.SYMPA_URL) | ||||
|             _client.login(settings.SYMPA_EMAIL, settings.SYMPA_PASSWORD) | ||||
|         else: | ||||
|             _client = FakeSympaSoapClient() | ||||
|     return _client | ||||
|   | ||||
| @@ -74,6 +74,7 @@ INSTALLED_APPS = [ | ||||
|     'draw', | ||||
|     'registration', | ||||
|     'participation', | ||||
|     'survey', | ||||
| ] | ||||
|  | ||||
| if "test" not in sys.argv:  # pragma: no cover | ||||
| @@ -300,6 +301,12 @@ CHANNEL_LAYERS = { | ||||
| PHONENUMBER_DB_FORMAT = 'NATIONAL' | ||||
| 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 | ||||
| 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') | ||||
| @@ -322,6 +329,10 @@ GOOGLE_SERVICE_CLIENT = { | ||||
| # 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") | ||||
|  | ||||
| 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 | ||||
| FORBIDDEN_TRIGRAMS = [ | ||||
|     "BIT", | ||||
|   | ||||
| @@ -74,6 +74,9 @@ | ||||
|                 </li> | ||||
|             {% endif %} | ||||
|             {% 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"> | ||||
|                     <a class="nav-link" href="{% url "admin:index" %}"><i class="fas fa-cog"></i> {% trans "Administration" %}</a> | ||||
|                 </li> | ||||
|   | ||||
| @@ -44,6 +44,7 @@ urlpatterns = [ | ||||
|     path('draw/', include('draw.urls')), | ||||
|     path('participation/', include('participation.urls')), | ||||
|     path('registration/', include('registration.urls')), | ||||
|     path('survey/', include('survey.urls')), | ||||
|  | ||||
|     path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(), | ||||
|          name='photo_authorization'), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user