mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-10-31 05:29:52 +01:00 
			
		
		
		
	Compare commits
	
		
			27 Commits
		
	
	
		
			0499885fc8
			...
			dev
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8af11cd56f | ||
|  | 5c372f7582 | ||
|  | bd230ccaf6 | ||
|  | 46779488c1 | ||
|  | f49897cd5b | ||
|  | 399e223b33 | ||
|  | 004d54cb67 | ||
|  | 8aec72d712 | ||
|  | 6a521b6121 | ||
|  | 62abfa94d6 | ||
|  | 952315ea4d | ||
|  | 2e613799c9 | ||
|  | 08805a6360 | ||
|  | 6841659e41 | ||
|  | a84ffcf0a3 | ||
|  | 203fc3cd54 | ||
|  | 60f5236dee | ||
|  | ab459ecc17 | ||
|  | 7ad7659d78 | ||
|  | 84eb08ec46 | ||
|  | 3750828883 | ||
|  | ba36ad4071 | ||
|  | 626433c464 | ||
|  | 032b67ac51 | ||
|  | f3bd479fdc | ||
|  | bc06cf4903 | ||
|  | 6d43c4b97e | 
| @@ -26,9 +26,18 @@ py313: | ||||
|     - pip install tox --no-cache-dir | ||||
|   script: tox -e py313 | ||||
|  | ||||
| py314: | ||||
|   stage: test | ||||
|   image: python:3.14-alpine | ||||
|   before_script: | ||||
|     - apk add --no-cache libmagic | ||||
|     - apk add --no-cache gettext | ||||
|     - pip install tox --no-cache-dir | ||||
|   script: tox -e py314 | ||||
|  | ||||
| linters: | ||||
|   stage: quality-assurance | ||||
|   image: python:3-alpine | ||||
|   image: python:3.13-alpine | ||||
|   before_script: | ||||
|     - pip install tox --no-cache-dir | ||||
|   script: tox -e linters | ||||
| @@ -58,4 +67,3 @@ release-image: | ||||
|     - docker push $CONTAINER_RELEASE_IMAGE | ||||
|   rules: | ||||
|     - if: $CI_COMMIT_BRANCH == "main" | ||||
|  | ||||
|   | ||||
| @@ -4,12 +4,10 @@ ENV PYTHONUNBUFFERED 1 | ||||
| ENV DJANGO_ALLOW_ASYNC_UNSAFE 1 | ||||
|  | ||||
| RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libpq-dev libxml2-dev libxslt-dev \ | ||||
|     npm libmagic texlive texmf-dist-fontsrecommended texmf-dist-lang texmf-dist-latexextra | ||||
|     libmagic texlive texmf-dist-fontsrecommended texmf-dist-lang texmf-dist-latexextra uglify-js | ||||
|  | ||||
| RUN apk add --no-cache bash | ||||
|  | ||||
| RUN npm install -g yuglify | ||||
|  | ||||
| RUN mkdir /code /code/docs | ||||
| WORKDIR /code | ||||
| COPY requirements.txt /code/requirements.txt | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
| # -- Project information ----------------------------------------------------- | ||||
|  | ||||
| project = 'Plateforme du TFJM²' | ||||
| copyright = "2020-2024" | ||||
| copyright = "2020-2026" | ||||
| author = "Animath" | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ Présentation | ||||
| La plateforme d'inscription du TFJM² actuelle est née lors de l'édition 2020. Elle n'est | ||||
| pas la première à exister, elle succède à une précédente, moins fonctionnelle, dont les | ||||
| sources ont été perdues. Elle a été développée par Emmy D'Anello, bénévole pour Animath, | ||||
| qui la maintient au moins jusqu'en 2024. | ||||
| qui la maintient au moins jusqu'en 2026. | ||||
|  | ||||
| La plateforme est développée en Python, utilisant le framework web | ||||
| `Django <https://www.djangoproject.com/>`_. Elle est diponible librement sous licence GPLv3 | ||||
|   | ||||
| @@ -145,10 +145,38 @@ Paramètres des tournois | ||||
|  | ||||
| Il faut enfin paramétrer les différentes dates des tournois. | ||||
|  | ||||
| Pour cela, connectez-vous sur la plateforme (avec un compte administrateur⋅rice), et dans l'onglet | ||||
| Pour cela, connectez-vous sur la plateforme (avec un compte administrateurice), et dans l'onglet | ||||
| « Tournois », vous pouvez créer les différents tournois avec les différentes dates pour chaque tournoi. | ||||
| Plus d'information sur les différents paramètres dans la `section concernée | ||||
| <../orga.html#creer-un-tournoi>`_ | ||||
| <../orga.html#creer-un-tournoi>`_. | ||||
|  | ||||
|  | ||||
| Dossier Google Drive des feuilles de notes | ||||
| """""""""""""""""""""""""""""""""""""""""" | ||||
|  | ||||
| Les tableurs Google Sheets de notes sont créés automatiquement vers le Google Drive du TFJM². | ||||
| Pour que les tableurs se créent au bon endroit, il faut modifier l'identifiant du dossier où se créent | ||||
| ces tableurs. Il faut donc se rendre dans les variables d'environnement de la plateforme, et | ||||
| modifier la variable ``NOTES_DRIVE_FOLDER_ID`` pour mettre à jour l'identifiant du dossier. | ||||
| Pour le trouver, il suffit simplement de se rendre sur Google Drive et de récupérer l'identifiant | ||||
| présent à la fin de l'URL, après ``https://drive.google.com/drive/u/X/folders/``. | ||||
|  | ||||
| Ne pas oublier de partager le dossier en écriture à l'adresse | ||||
| ``plateforme-tfjm@plateforme-tfjm.iam.gserviceaccount.com``. | ||||
|  | ||||
|  | ||||
| Anciennes listes de diffusion | ||||
| """"""""""""""""""""""""""""" | ||||
|  | ||||
| Les listes Sympa doivent être fermées pour être correctement recréées. Un script permet | ||||
| de supprimer toutes les listes commençant par ``equipe``, ``orga`` ou ``jury`` : | ||||
|  | ||||
| .. code:: bash | ||||
|  | ||||
|    ./manage.py delete_old_sympa_lists | ||||
|  | ||||
| Attention : les listes closes ne sont pas supprimées. Rendez-vous sur la page | ||||
| `https://lists.tfjm.org/sympa/get_closed_lists`_ pour supprimer les listes ainsi fermées. | ||||
|  | ||||
|  | ||||
| À la fin du tournoi | ||||
|   | ||||
| @@ -178,7 +178,7 @@ Seuls les refus distincts comptent : refuser une deuxième fois un problème | ||||
| déjà refusé ne compte pas. Au-delà de ces refus gratuits, l'équipe se verra | ||||
| dotée d'une pénalité de 25 % sur le coefficient de l'oral de défense, par | ||||
| refus. Par exemple, si une équipe refuse 4 problèmes avec un coefficient | ||||
| sur l'oral de défense normalement à ``1.6``, son coefficient passera à ``1.2``. | ||||
| sur l'oral de défense normalement à ``1.5``, son coefficient passera à ``1.125``. | ||||
|  | ||||
| Une fois que toutes les équipes de la poule ont tiré leur problème, on passe | ||||
| à la poule suivante. Une fois que toutes les poules ont vu leurs problèmes | ||||
|   | ||||
| @@ -224,7 +224,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|  | ||||
|         # Update user interface | ||||
|         await self.channel_layer.group_send(f"tournament-{self.tournament.id}", | ||||
|                                             {'tid': self.tournament_id, 'type': 'draw.start', 'fmt': fmt, 'draw': draw}) | ||||
|                                             {'tid': self.tournament_id, 'type': 'draw.start', 'fmt': fmt}) | ||||
|         await self.channel_layer.group_send(f"tournament-{self.tournament.id}", | ||||
|                                             {'tid': self.tournament_id, 'type': 'draw.set_info', | ||||
|                                              'info': await self.tournament.draw.ainformation()}) | ||||
| @@ -235,7 +235,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|         await self.channel_layer.group_send(f"tournament-{self.tournament.id}", | ||||
|                                             {'tid': self.tournament_id, 'type': 'draw.notify', | ||||
|                                              'title': 'Tirage au sort du TFJM²', | ||||
|                                              'body': _("The draw of tournament {tournament} started!") | ||||
|                                              'body': str(_("The draw of tournament {tournament} started!")) | ||||
|                                             .format(tournament=self.tournament.name)}) | ||||
|  | ||||
|     async def draw_start(self, content) -> None: | ||||
| @@ -405,15 +405,15 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|                     await self.channel_layer.group_send( | ||||
|                         f"team-{dup.participation.team.trigram}", | ||||
|                         {'tid': self.tournament_id, 'type': 'draw.notify', 'title': 'Tirage au sort du TFJM²', | ||||
|                          'body': _("Your dice score is identical to the one of one or multiple teams. " | ||||
|                                    "Please relaunch it.")} | ||||
|                          'body': str(_("Your dice score is identical to the one of one or multiple teams. " | ||||
|                                        "Please relaunch it."))} | ||||
|                     ) | ||||
|                 # Alert the tournament | ||||
|                 await self.channel_layer.group_send( | ||||
|                     f"tournament-{self.tournament.id}", | ||||
|                     {'tid': self.tournament_id, 'type': 'draw.alert', | ||||
|                      'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format( | ||||
|                          teams=', '.join(td.participation.team.trigram for td in dups)), | ||||
|                      'message': str(_('Dices from teams {teams} are identical. Please relaunch your dices.').format( | ||||
|                         teams=', '.join(td.participation.team.trigram for td in dups))), | ||||
|                      'alert_type': 'warning'}) | ||||
|                 error = True | ||||
|  | ||||
| @@ -537,7 +537,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|         async for next_round in self.tournament.draw.round_set.filter(number__gte=2).all(): | ||||
|             await self.channel_layer.group_send(f"tournament-{self.tournament.id}", | ||||
|                                                 {'tid': self.tournament_id, 'type': 'draw.send_poules', | ||||
|                                                  'round': r.number, | ||||
|                                                  'round': next_round.number, | ||||
|                                                  'poules': [ | ||||
|                                                      { | ||||
|                                                          'letter': pool.get_letter_display(), | ||||
| @@ -612,8 +612,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|         # Notify the team that it can draw a problem | ||||
|         await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}", | ||||
|                                             {'tid': self.tournament_id, 'type': 'draw.notify', | ||||
|                                              'title': _("Your turn!"), | ||||
|                                              'body': _("It's your turn to draw a problem!")}) | ||||
|                                              'title': str(_("Your turn!")), | ||||
|                                              'body': str(_("It's your turn to draw a problem!"))}) | ||||
|  | ||||
|     async def select_problem(self, **kwargs): | ||||
|         """ | ||||
| @@ -752,8 +752,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|             # Notify the team that it can draw a problem | ||||
|             await self.channel_layer.group_send(f"team-{new_trigram}", | ||||
|                                                 {'tid': self.tournament_id, 'type': 'draw.notify', | ||||
|                                                  'title': _("Your turn!"), | ||||
|                                                  'body': _("It's your turn to draw a problem!")}) | ||||
|                                                  'title': str(_("Your turn!")), | ||||
|                                                  'body': str(_("It's your turn to draw a problem!"))}) | ||||
|         else: | ||||
|             # Pool is ended | ||||
|             await self.end_pool(pool) | ||||
| @@ -829,8 +829,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|                 # Notify the team that it can draw a dice | ||||
|                 await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", | ||||
|                                                     {'tid': self.tournament_id, 'type': 'draw.notify', | ||||
|                                                      'title': _("Your turn!"), | ||||
|                                                      'body': _("It's your turn to launch the dice!")}) | ||||
|                                                      'title': str(_("Your turn!")), | ||||
|                                                      'body': str(_("It's your turn to launch the dice!"))}) | ||||
|  | ||||
|             await self.channel_layer.group_send(f"tournament-{self.tournament.id}", | ||||
|                                                 {'tid': self.tournament_id, 'type': 'draw.dice_visibility', | ||||
| @@ -863,8 +863,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|                 # Notify the team that it can draw a dice | ||||
|                 await self.channel_layer.group_send(f"team-{participation.team.trigram}", | ||||
|                                                     {'tid': self.tournament_id, 'type': 'draw.notify', | ||||
|                                                      'title': _("Your turn!"), | ||||
|                                                      'body': _("It's your turn to launch the dice!")}) | ||||
|                                                      'title': str(_("Your turn!")), | ||||
|                                                      'body': str(_("It's your turn to launch the dice!"))}) | ||||
|  | ||||
|             # Reorder dices | ||||
|             await self.channel_layer.group_send(f"tournament-{self.tournament.id}", | ||||
| @@ -988,8 +988,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|         # Notify the team that it can draw a problem | ||||
|         await self.channel_layer.group_send(f"team-{new_trigram}", | ||||
|                                             {'tid': self.tournament_id, 'type': 'draw.notify', | ||||
|                                              'title': _("Your turn!"), | ||||
|                                              'body': _("It's your turn to draw a problem!")}) | ||||
|                                              'title': str(_("Your turn!")), | ||||
|                                              'body': str(_("It's your turn to draw a problem!"))}) | ||||
|  | ||||
|     @ensure_orga | ||||
|     async def export(self, **kwargs): | ||||
| @@ -1039,7 +1039,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|         # Send notification to everyone | ||||
|         await self.channel_layer.group_send(f"tournament-{self.tournament.id}", | ||||
|                                             {'tid': self.tournament_id, 'type': 'draw.notify', | ||||
|                                              'title': _("Draw") + " " + settings.APP_NAME, | ||||
|                                              'title': str(_("Draw")) + " " + settings.APP_NAME, | ||||
|                                              'body': str(_("The draw of the second round is starting!"))}) | ||||
|  | ||||
|         if settings.TFJM_APP == "TFJM": | ||||
| @@ -1092,8 +1092,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): | ||||
|                 # Notify the team that it can draw a problem | ||||
|                 await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", | ||||
|                                                     {'tid': self.tournament_id, 'type': 'draw.notify', | ||||
|                                                      'title': _("Your turn!"), | ||||
|                                                      'body': _("It's your turn to draw a problem!")}) | ||||
|                                                      'title': str(_("Your turn!")), | ||||
|                                                      'body': str(_("It's your turn to draw a problem!"))}) | ||||
|         else: | ||||
|             async for td in r2.team_draws.prefetch_related('participation__team'): | ||||
|                 await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", | ||||
|   | ||||
| @@ -221,9 +221,10 @@ document.addEventListener('DOMContentLoaded', () => { | ||||
|             elem.innerText = `${trigram} 🎲 ${result}` | ||||
|         } | ||||
|  | ||||
|         let nextTeam = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`).getAttribute("data-team") | ||||
|         if (nextTeam) { | ||||
|         let nextTeamDiv = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`) | ||||
|         if (nextTeamDiv) { | ||||
|             // If there is one team that does not have launched its dice, then we update the debug section | ||||
|             let nextTeam = nextTeamDiv.getAttribute("data-team") | ||||
|             let debugSpan = document.getElementById(`debug-dice-${tid}-team`) | ||||
|             if (debugSpan) | ||||
|                 debugSpan.innerText = nextTeam | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| # Copyright (C) 2023 by Animath | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
| import asyncio | ||||
| from random import shuffle | ||||
|  | ||||
| from asgiref.sync import sync_to_async | ||||
| @@ -712,15 +711,12 @@ class TestDraw(TestCase): | ||||
|                          {'tid': tid, 'type': 'export_visibility', 'visible': False}) | ||||
|  | ||||
|         # Cancel all steps and reset all | ||||
|         for i in range(1000): | ||||
|         for i in range(150): | ||||
|             await communicator.send_json_to({'tid': tid, 'type': 'cancel'}) | ||||
|  | ||||
|         # Purge receive queue | ||||
|         while True: | ||||
|             try: | ||||
|                 await communicator.receive_json_from() | ||||
|             except asyncio.TimeoutError: | ||||
|                 break | ||||
|         while (await communicator.receive_json_from())['type'] != "abort": | ||||
|             pass | ||||
|  | ||||
|         if await Draw.objects.filter(tournament_id=tid).aexists(): | ||||
|             print((await Draw.objects.filter(tournament_id=tid).aexists())) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ crond -l 0 | ||||
|  | ||||
| python manage.py migrate | ||||
| python manage.py update_index | ||||
| python manage.py runmailer_pg & | ||||
|  | ||||
| nginx | ||||
|  | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,7 +1,9 @@ | ||||
| # Copyright (C) 2020 by Animath | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib import admin | ||||
| from django.http import HttpRequest | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview | ||||
| @@ -51,9 +53,14 @@ class PassageInline(admin.TabularInline): | ||||
|     model = Passage | ||||
|     extra = 0 | ||||
|     ordering = ('position',) | ||||
|     autocomplete_fields = ('reporter', 'opponent', 'reviewer', 'observer',) | ||||
|     show_change_link = True | ||||
|  | ||||
|     def get_autocomplete_fields(self, request: HttpRequest) -> tuple[str]: | ||||
|         fields = ('reporter', 'opponent', 'reviewer',) | ||||
|         if settings.HAS_OBSERVER: | ||||
|             fields += ('observer',) | ||||
|         return fields | ||||
|  | ||||
|  | ||||
| class NoteInline(admin.TabularInline): | ||||
|     model = Note | ||||
| @@ -113,12 +120,9 @@ class PoolAdmin(admin.ModelAdmin): | ||||
|  | ||||
| @admin.register(Passage) | ||||
| class PassageAdmin(admin.ModelAdmin): | ||||
|     list_display = ('__str__', 'reporter_trigram', 'solution_number', 'opponent_trigram', 'reviewer_trigram', | ||||
|                     'observer_trigram', 'pool_abbr', 'position', 'tournament') | ||||
|     list_filter = ('pool__tournament', 'pool__round', 'pool__letter', 'solution_number',) | ||||
|     search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',) | ||||
|     ordering = ('pool__tournament', 'pool__round', 'pool__letter', 'position',) | ||||
|     autocomplete_fields = ('pool', 'reporter', 'opponent', 'reviewer', 'observer',) | ||||
|     inlines = (NoteInline,) | ||||
|  | ||||
|     @admin.display(description=_("reporter"), ordering='reporter__team__trigram') | ||||
| @@ -135,7 +139,7 @@ class PassageAdmin(admin.ModelAdmin): | ||||
|  | ||||
|     @admin.display(description=_("observer"), ordering='observer__team__trigram') | ||||
|     def observer_trigram(self, record: Passage): | ||||
|         return record.observer.team.trigram | ||||
|         return record.observer.team.trigram if record.observer else None | ||||
|  | ||||
|     @admin.display(description=_("pool"), ordering='pool__letter') | ||||
|     def pool_abbr(self, record): | ||||
| @@ -145,15 +149,23 @@ class PassageAdmin(admin.ModelAdmin): | ||||
|     def tournament(self, record: Passage): | ||||
|         return record.pool.tournament | ||||
|  | ||||
|     def get_list_display(self, request: HttpRequest) -> tuple[str]: | ||||
|         if settings.HAS_OBSERVER: | ||||
|             return ('__str__', 'reporter_trigram', 'solution_number', 'opponent_trigram', | ||||
|                     'reviewer_trigram', 'observer_trigram', 'pool_abbr', 'position', 'tournament') | ||||
|         else: | ||||
|             return ('__str__', 'reporter_trigram', 'solution_number', 'opponent_trigram', | ||||
|                     'reviewer_trigram', 'pool_abbr', 'position', 'tournament') | ||||
|  | ||||
|     def get_autocomplete_fields(self, request: HttpRequest) -> tuple[str]: | ||||
|         fields = ('pool', 'reporter', 'opponent', 'reviewer',) | ||||
|         if settings.HAS_OBSERVER: | ||||
|             fields += ('observer',) | ||||
|         return fields | ||||
|  | ||||
|  | ||||
| @admin.register(Note) | ||||
| class NoteAdmin(admin.ModelAdmin): | ||||
|     list_display = ('passage', 'pool', 'jury', 'reporter_writing', 'reporter_oral', | ||||
|                     'opponent_writing', 'opponent_oral', 'reviewer_writing', 'reviewer_oral', | ||||
|                     'observer_writing', 'observer_oral',) | ||||
|     list_filter = ('passage__pool__letter', 'passage__solution_number', 'jury', | ||||
|                    'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral', | ||||
|                    'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral') | ||||
|     search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__reporter__team__trigram',) | ||||
|     autocomplete_fields = ('jury', 'passage',) | ||||
|  | ||||
| @@ -161,6 +173,21 @@ class NoteAdmin(admin.ModelAdmin): | ||||
|     def pool(self, record): | ||||
|         return record.passage.pool.short_name | ||||
|  | ||||
|     def get_list_display(self, request: HttpRequest) -> tuple[str]: | ||||
|         fields = ('passage', 'pool', 'jury', 'reporter_writing', 'reporter_oral', | ||||
|                   'opponent_writing', 'opponent_oral', 'reviewer_writing', 'reviewer_oral',) | ||||
|         if settings.HAS_OBSERVER: | ||||
|             fields += ('observer_writing', 'observer_oral',) | ||||
|         return fields | ||||
|  | ||||
|     def get_list_filter(self, request: HttpRequest) -> tuple[str]: | ||||
|         fields = ('passage__pool__letter', 'passage__solution_number', 'jury', | ||||
|                   'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral', | ||||
|                   'reviewer_writing', 'reviewer_oral',) | ||||
|         if settings.HAS_OBSERVER: | ||||
|             fields += ('observer_writing', 'observer_oral',) | ||||
|         return fields | ||||
|  | ||||
|  | ||||
| @admin.register(Solution) | ||||
| class SolutionAdmin(admin.ModelAdmin): | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import re | ||||
| from crispy_forms.helper import FormHelper | ||||
| from crispy_forms.layout import Div, Field, HTML, Layout, Submit | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.core.validators import FileExtensionValidator | ||||
| @@ -14,7 +15,6 @@ from django.utils.translation import gettext_lazy as _ | ||||
| import pandas | ||||
| from pypdf import PdfReader | ||||
| from registration.models import VolunteerRegistration | ||||
| from tfjm import settings | ||||
|  | ||||
| from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview | ||||
|  | ||||
| @@ -405,6 +405,12 @@ class WrittenReviewForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class NoteForm(forms.ModelForm): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         if not settings.HAS_OBSERVER: | ||||
|             del self.fields['observer_writing'] | ||||
|             del self.fields['observer_oral'] | ||||
|  | ||||
|     class Meta: | ||||
|         model = Note | ||||
|         fields = ('reporter_writing', 'reporter_oral', 'opponent_writing', | ||||
|   | ||||
							
								
								
									
										24
									
								
								participation/management/commands/delete_old_sympa_lists.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								participation/management/commands/delete_old_sympa_lists.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| # 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 tfjm.lists import get_sympa_client | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     def handle(self, *args, **options): | ||||
|         """ | ||||
|         Supprime les listes de diffusion Sympa. | ||||
|         Toutes les listess commençant par "equipe", "orga" ou "jury" sont fermées. | ||||
|         Attention : la fermeture n'est pas définitive, il faut ensuite se rendre sur Sympa | ||||
|         pour supprimer les listes fermées. | ||||
|         """ | ||||
|         if not settings.ML_MANAGEMENT: | ||||
|             return | ||||
|  | ||||
|         sympa = get_sympa_client() | ||||
|  | ||||
|         for mailing_list in sympa.all_lists(): | ||||
|             address = mailing_list.list_address | ||||
|             if address.startswith("equipe") or address.startswith("orga") or address.startswith("jury"): | ||||
|                 sympa.delete_list(address) | ||||
| @@ -5,11 +5,13 @@ from pathlib import Path | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.management import BaseCommand | ||||
| from django.utils.translation import activate | ||||
| from participation.models import Solution, Tournament | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     def handle(self, *args, **kwargs): | ||||
|         activate(settings.PREFERRED_LANGUAGE_CODE) | ||||
|         base_dir = Path(__file__).parent.parent.parent.parent | ||||
|         base_dir /= "output" | ||||
|         if not base_dir.is_dir(): | ||||
|   | ||||
| @@ -440,6 +440,10 @@ class Tournament(models.Model): | ||||
|             return Participation.objects.filter(final=True) | ||||
|         return self.participation_set | ||||
|  | ||||
|     @property | ||||
|     def organizers_and_presidents(self): | ||||
|         return VolunteerRegistration.objects.filter(Q(admin=True) | Q(organized_tournaments=self) | Q(pools_presided__tournament=self)) | ||||
|  | ||||
|     @property | ||||
|     def solutions(self): | ||||
|         if self.final: | ||||
| @@ -932,10 +936,10 @@ class Participation(models.Model): | ||||
|                 'content': content, | ||||
|             }) | ||||
|         elif timezone.now() <= tournament.reviews_first_phase_limit + timedelta(hours=2): | ||||
|             reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reporter=self) | ||||
|             opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, opponent=self) | ||||
|             reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reviewer=self) | ||||
|             observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=1, observer=self) | ||||
|             reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, reporter=self) | ||||
|             opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, opponent=self) | ||||
|             reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, reviewer=self) | ||||
|             observer_passage = Passage.objects.filter(pool__tournament=tournament, pool__round=1, observer=self) | ||||
|             observer_passage = observer_passage.get() if observer_passage.exists() else None | ||||
|  | ||||
|             reporter_text = _("<p>The solutions draw is ended. You can check the result on " | ||||
| @@ -997,10 +1001,10 @@ class Participation(models.Model): | ||||
|                 'content': content, | ||||
|             }) | ||||
|         elif timezone.now() <= tournament.reviews_second_phase_limit + timedelta(hours=2): | ||||
|             reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reporter=self) | ||||
|             opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, opponent=self) | ||||
|             reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reviewer=self) | ||||
|             observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=2, observer=self) | ||||
|             reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, reporter=self) | ||||
|             opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, opponent=self) | ||||
|             reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, reviewer=self) | ||||
|             observer_passage = Passage.objects.filter(pool__tournament=tournament, pool__round=2, observer=self) | ||||
|             observer_passage = observer_passage.get() if observer_passage.exists() else None | ||||
|  | ||||
|             reporter_text = _("<p>For the second round, you will present " | ||||
| @@ -1061,10 +1065,10 @@ class Participation(models.Model): | ||||
|             }) | ||||
|         elif settings.NB_ROUNDS >= 3 \ | ||||
|                 and timezone.now() <= tournament.reviews_third_phase_limit + timedelta(hours=2): | ||||
|             reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reporter=self) | ||||
|             opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, opponent=self) | ||||
|             reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reviewer=self) | ||||
|             observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=3, observer=self) | ||||
|             reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, reporter=self) | ||||
|             opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, opponent=self) | ||||
|             reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, reviewer=self) | ||||
|             observer_passage = Passage.objects.filter(pool__tournament=tournament, pool__round=3, observer=self) | ||||
|             observer_passage = observer_passage.get() if observer_passage.exists() else None | ||||
|  | ||||
|             reporter_text = _("<p>For the third round, you will present " | ||||
| @@ -1254,6 +1258,10 @@ class Pool(models.Model): | ||||
|         passage_width = 6 + (2 if has_observer else 0) | ||||
|         passages = self.passages.all() | ||||
|  | ||||
|         if not pool_size or not passages.count(): | ||||
|             # Not initialized yet | ||||
|             return | ||||
|  | ||||
|         # Create tournament sheet if it does not exist | ||||
|         self.tournament.create_spreadsheet() | ||||
|  | ||||
| @@ -1623,6 +1631,10 @@ class Pool(models.Model): | ||||
|         worksheet.client.batch_update(spreadsheet.id, body) | ||||
|  | ||||
|     def update_juries_lines_spreadsheet(self): | ||||
|         if not self.participations.count() or not self.passages.count(): | ||||
|             # Not initialized yet | ||||
|             return | ||||
|  | ||||
|         translation.activate(settings.PREFERRED_LANGUAGE_CODE) | ||||
|  | ||||
|         gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT) | ||||
| @@ -1773,7 +1785,7 @@ class Passage(models.Model): | ||||
|  | ||||
|     @property | ||||
|     def coeff_reporter_oral(self) -> float: | ||||
|         coeff = 1.6 if settings.TFJM_APP == "TFJM" else 3 | ||||
|         coeff = 1.5 if settings.TFJM_APP == "TFJM" else 3 | ||||
|         coeff *= 1 - 0.25 * self.reporter_penalties | ||||
|         return coeff | ||||
|  | ||||
| @@ -1817,7 +1829,7 @@ class Passage(models.Model): | ||||
|  | ||||
|     @property | ||||
|     def coeff_reviewer_oral(self): | ||||
|         return 1 if settings.TFJM_APP == "TFJM" else 1.2 | ||||
|         return 1.2 | ||||
|  | ||||
|     @property | ||||
|     def average_reviewer(self) -> float: | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| # Copyright (C) 2020 by Animath | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.utils import formats | ||||
| from django.utils.safestring import mark_safe | ||||
| from django.utils.text import format_lazy | ||||
| @@ -106,8 +107,6 @@ class PoolTable(tables.Table): | ||||
|  | ||||
|  | ||||
| class PassageTable(tables.Table): | ||||
|     # FIXME Ne pas afficher l'équipe observatrice si non nécessaire | ||||
|  | ||||
|     reporter = tables.LinkColumn( | ||||
|         "participation:passage_detail", | ||||
|         args=[tables.A("id")], | ||||
| @@ -131,7 +130,9 @@ class PassageTable(tables.Table): | ||||
|             'class': 'table table-condensed table-striped text-center', | ||||
|         } | ||||
|         model = Passage | ||||
|         fields = ('reporter', 'opponent', 'reviewer', 'observer', 'solution_number', ) | ||||
|         fields = ('reporter', 'opponent', 'reviewer',) \ | ||||
|             + (('observer',) if settings.HAS_OBSERVER else ()) \ | ||||
|             + ('solution_number', ) | ||||
|  | ||||
|  | ||||
| class NoteTable(tables.Table): | ||||
| @@ -160,4 +161,6 @@ class NoteTable(tables.Table): | ||||
|         } | ||||
|         model = Note | ||||
|         fields = ('jury', 'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral', | ||||
|                   'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', 'update',) | ||||
|                   'reviewer_writing', 'reviewer_oral',) + \ | ||||
|                  (('observer_writing', 'observer_oral') if settings.HAS_OBSERVER else ()) + \ | ||||
|                  ('update',) | ||||
|   | ||||
| @@ -44,7 +44,7 @@ | ||||
|     \Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\ | ||||
| {% endif %} | ||||
| \vspace{3mm} | ||||
| {% trans "Round" %} {{ pool.round }} \;-- {% trans "Pool" %} {{ pool.get_letter_display }}{% if pool.participations.count == 5 %} \;-- {{ pool.get_room_display }}{% endif %} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_first_phase }}{% elif pool.round == 2 %}{{ pool.tournament.date_second_phase }}{% else %}{{ pool.tournament.date_third_phase }}{% endif %} | ||||
| {% trans "round"|capfirst %} {{ pool.round }} \;-- {% trans "pool"|capfirst %} {{ pool.get_letter_display }}{% if pool.participations.count == 5 %} \;-- {{ pool.get_room_display }}{% endif %} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_first_phase }}{% elif pool.round == 2 %}{{ pool.tournament.date_second_phase }}{% else %}{{ pool.tournament.date_third_phase }}{% endif %} | ||||
|  | ||||
|  | ||||
| \vspace{15mm} | ||||
| @@ -52,7 +52,7 @@ | ||||
|  | ||||
| \begin{tabular}{|p{40mm}{% for passage in passages.all %}{% if passages.count <= 3 %}|p{3cm}|p{3cm}{% else %}|p{2.8cm}|p{2.5cm}{% endif %}{% endfor %}|}\hline | ||||
| \multirow{2}{40mm}{\LARGE {% trans "Role" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{ \Large {% trans "Problem" %} {{ passage.solution_number }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}} | ||||
| {% for passage in passages.all %}& \hspace{4mm} {\Large {% trans "Writing"|upper %}} & \hspace{4mm} {\Large {% trans "Oral"|upper %}}{% endfor %} \\ \hline | ||||
| {% for passage in passages.all %}& \multicolumn{1}{c|}{\Large {% trans "Writing"|upper %}} & \multicolumn{1}{c|}{\Large {% trans "Oral"|upper %}}{% endfor %} \\ \hline | ||||
| \multirow{2}{35mm}{\LARGE {% trans "Reporter" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.reporter.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}} | ||||
| {% for passage in passages.all %} | ||||
| & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq {% if TFJM.APP == "TFJM" %}20{% else %}10{% endif %}$ | ||||
|   | ||||
| @@ -58,7 +58,7 @@ | ||||
|  | ||||
| %%%%%%%%%%%%%%%%%%%%%DEFENSEUR | ||||
| \begin{tabular}{|c|p{25mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline | ||||
| \multicolumn{4}{|l|}{The {\bf {% trans "Reporter" %}} \normalsize presents their ideas and major results for the solution of the problem.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline | ||||
| \multicolumn{4}{|l|}{The {\bf {% trans "Reporter" %}} \normalsize presents their ideas and major results for the solution of the problem.} {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline | ||||
|  | ||||
| %ECRIT | ||||
| \multirow{7}{3mm}{\bf \begin{turn}{90}WRITING\end{turn}} & \multirow{3}{20mm}{ {% trans "Scientific part" %}} & {% trans "Depth and difficulty of the elements presented" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
| @@ -86,7 +86,7 @@ | ||||
| %%%%%%%%%%%%%%%%%OPPOSANT⋅E | ||||
| \begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline | ||||
| \multicolumn{4}{|l|}{The {\bf {% trans "Opponent" %}} \normalsize provides a critical analysis of the solution and presentation.} | ||||
| {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline | ||||
| {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline | ||||
|  | ||||
| %ECRIT | ||||
| \multirow{6}{3mm}{\bf \begin{turn}{90}WRITING\end{turn}} &\multirow{4}{25mm}{ {% trans "Scientific part" %}} & {% trans "Critical thinking and perspective on the proposed solution" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
| @@ -108,7 +108,7 @@ | ||||
|  | ||||
| %%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR⋅RICE | ||||
| \begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline | ||||
| \multicolumn{4}{|l|}{The {\bf {% trans "Reviewer" %}} \normalsize evaluates the debate between the Reporter and the Opponent.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline | ||||
| \multicolumn{4}{|l|}{The {\bf {% trans "Reviewer" %}} \normalsize evaluates the debate between the Reporter and the Opponent.} {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline | ||||
|  | ||||
| %ECRIT | ||||
| \multirow{6}{3mm}{\bf \begin{turn}{90}WRITING\end{turn}} &\multirow{4}{25mm}{ {% trans "Scientific part" %}} & {% trans "Critical thinking and perspective on the proposed solution" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
| @@ -131,7 +131,7 @@ | ||||
| {% if TFJM.APP == "ETEAM" and pool.participations.count >= 4 %} | ||||
| %%%%%%%%%%%%%%%%%%%%%%OBSERVATEUR⋅RICE | ||||
| \begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline | ||||
| \multicolumn{4}{|l|}{The {\bf {% trans "Observer" %}} \normalsize makes useful remarks on crucial points missed by the other participants.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.observer.team.trigram }} {% endfor %}\\ \hline \hline | ||||
| \multicolumn{4}{|l|}{The {\bf {% trans "Observer" %}} \normalsize makes useful remarks on crucial points missed by the other participants.} {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.observer.team.trigram }} {% endfor %}\\ \hline \hline | ||||
|  | ||||
| %ECRIT | ||||
| \multirow{6}{3mm}{\bf \begin{turn}{90}WRITING\end{turn}} &\multirow{4}{25mm}{ {% trans "Scientific part" %}} & {% trans "Critical thinking and perspective on the proposed solution" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
|   | ||||
| @@ -52,7 +52,7 @@ | ||||
|  | ||||
| %%%%%%%%%%%%%%%%%%%%%DEFENSEUR | ||||
| \begin{tabular}{|c|p{24mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline | ||||
| \multicolumn{4}{|l|}{Læ {\bf D\'efenseur⋅se} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline | ||||
| \multicolumn{4}{|l|}{Læ {\bf D\'efenseur⋅se} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline | ||||
|  | ||||
| %ECRIT | ||||
| \multirow{7}{3mm}{\bf \begin{turn}{90}ÉCRIT\end{turn}} & \multirow{3}{24mm}{Partie scientifique} & Profondeur et difficulté des éléments présentés & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
| @@ -80,7 +80,7 @@ | ||||
| %%%%%%%%%%%%%%%%%OPPOSANT | ||||
| \begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline | ||||
| \multicolumn{4}{|l|}{L' {\bf Opposant⋅e} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.} | ||||
| {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline | ||||
| {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline | ||||
|  | ||||
| %ECRIT | ||||
| \multirow{6}{3mm}{\bf \begin{turn}{90}ÉCRIT\end{turn}} &\multirow{4}{24mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
| @@ -102,7 +102,7 @@ | ||||
|  | ||||
| %%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR.RICE | ||||
| \begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline | ||||
| \multicolumn{4}{|l|}{Læ {\bf Rapporteur⋅rice} \normalsize \'evalue le d\'ebat entre læ D\'efenseur⋅se et l'Opposant⋅e.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline | ||||
| \multicolumn{4}{|l|}{Læ {\bf Rapporteur⋅rice} \normalsize \'evalue le d\'ebat entre læ D\'efenseur⋅se et l'Opposant⋅e.} {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline | ||||
|  | ||||
| %ECRIT | ||||
| \multirow{6}{3mm}{\bf \begin{turn}{90}ÉCRIT\end{turn}} &\multirow{4}{24mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
| @@ -115,7 +115,7 @@ | ||||
| \multirow{9}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{5}{24mm}{Questions et discours de læ rapporteur⋅rice} & \footnotesize Faire prendre de la hauteur au débat (par les sujets abordés, la pertinence des questions posées, les points soulevés, gestion du temps) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
| && \footnotesize Créer un échange constructif entre les participants (formulation des questions, réaction aux réponses, articulation entre les questions, circulation de la parole) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} | ||||
| && Capacité à évaluer la qualité des échanges (Défenseur⋅se-Opposant⋅e et à trois) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} | ||||
| && Réponses aux questions de læ Rapporteur⋅rice et du jury (fond et capacité à faire avancer le débat) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} | ||||
| && Réponses aux questions du jury (fond et capacité à faire avancer le débat) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} | ||||
| & Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} | ||||
| &\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline | ||||
| \end{tabular} | ||||
|   | ||||
| @@ -23,45 +23,81 @@ | ||||
|                     <dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd> | ||||
|                 {% endif %} | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans 'remote'|capfirst %}</dt> | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "remote"|capfirst %}</dt> | ||||
|                 <dd class="col-sm-6">{{ tournament.remote|yesno }}</dd> | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans 'dates'|capfirst %}</dt> | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "dates"|capfirst %}</dt> | ||||
|                 <dd class="col-sm-6">{% trans "From" %} {{ tournament.date_start }} {% trans "to" %} {{ tournament.date_end }}</dd> | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans 'date of registration closing'|capfirst %}</dt> | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "date of registration closing"|capfirst %}</dt> | ||||
|                 <dd class="col-sm-6">{{ tournament.inscription_limit }}</dd> | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans 'date of maximal solution submission'|capfirst %}</dt> | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "date of maximal solution submission"|capfirst %}</dt> | ||||
|                 <dd class="col-sm-6">{{ tournament.solution_limit }}</dd> | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans 'date of the random draw'|capfirst %}</dt> | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "date of the random draw"|capfirst %}</dt> | ||||
|                 <dd class="col-sm-6">{{ tournament.solutions_draw }}</dd> | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the first round'|capfirst %}</dt> | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "date of maximal written reviews submission for the first round"|capfirst %}</dt> | ||||
|                 <dd class="col-sm-6">{{ tournament.reviews_first_phase_limit }}</dd> | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the second round'|capfirst %}</dt> | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "Solutions available for the second round" %}</dt> | ||||
|                 <dd class="col-sm-6"> | ||||
|                     {{ tournament.solutions_available_second_phase|yesno }} | ||||
|                     {% if user.is_authenticated and user.registration in tournament.organizers_and_presidents.all %} | ||||
|                         {% now 'Y-m-d' as today %} | ||||
|                         {% if not tournament.solutions_available_second_phase %} | ||||
|                             {% if today >= tournament.date_first_phase|date:"Y-m-d" %} | ||||
|                                 <a href="{% url 'participation:tournament_publish_solutions' pk=tournament.pk round=2 %}" class="btn btn-sm btn-info"><i class="fas fa-eye"></i> {% trans "Publish" %}</a> | ||||
|                             {% endif %} | ||||
|                         {% else %} | ||||
|                             {% if today <= tournament.date_second_phase|date:"Y-m-d" %} | ||||
|                                 <a href="{% url 'participation:tournament_publish_solutions' pk=tournament.pk round=2 %}?hide" class="btn btn-sm bg-danger"><i class="fas fa-eye-slash"></i> {% trans "Unpublish" %}</a> | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                 </dd> | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "date of maximal written reviews submission for the second round"|capfirst %}</dt> | ||||
|                 <dd class="col-sm-6">{{ tournament.reviews_second_phase_limit }}</dd> | ||||
|  | ||||
|                 {% if TFJM.APP == "ETEAM" %} | ||||
|                     <dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the third round'|capfirst %}</dt> | ||||
|                 {% if TFJM.NB_ROUNDS == 3 %} | ||||
|                     <dt class="col-sm-6 text-sm-end">{% trans "Solutions available for the third round" %}</dt> | ||||
|                     <dd class="col-sm-6"> | ||||
|                         {{ tournament.solutions_available_third_phase|yesno }} | ||||
|                         {% if tournament.solutions_available_second_phase and user.is_authenticated and user.registration in tournament.organizers_and_presidents.all %} | ||||
|                             {% now 'Y-m-d' as today %} | ||||
|                             {% if not tournament.solutions_available_third_phase %} | ||||
|                                 {% if today >= tournament.date_second_phase|date:"Y-m-d" %} | ||||
|                                     <a href="{% url 'participation:tournament_publish_solutions' pk=tournament.pk round=3 %}" class="btn btn-sm btn-info"><i class="fas fa-eye"></i> {% trans "Publish" %}</a> | ||||
|                                 {% endif %} | ||||
|                             {% else %} | ||||
|                                 {% if today <= tournament.date_third_phase|date:"Y-m-d" %} | ||||
|                                     <a href="{% url 'participation:tournament_publish_solutions' pk=tournament.pk round=3 %}?hide" class="btn btn-sm bg-danger"><i class="fas fa-eye-slash"></i> {% trans "Unpublish" %}</a> | ||||
|                                 {% endif %} | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                     </dd> | ||||
|  | ||||
|                     <dt class="col-sm-6 text-sm-end">{% trans "date of maximal written reviews submission for the third round"|capfirst %}</dt> | ||||
|                     <dd class="col-sm-6">{{ tournament.reviews_third_phase_limit }}</dd> | ||||
|                 {% endif %} | ||||
|  | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans 'description'|capfirst %}</dt> | ||||
|                 <dt class="col-sm-6 text-sm-end">{% trans "description"|capfirst %}</dt> | ||||
|                 <dd class="col-sm-6">{{ tournament.description }}</dd> | ||||
|  | ||||
|                 {% if TFJM.ML_MANAGEMENT %} | ||||
|                     <dt class="col-sm-6 text-sm-end">{% trans 'To contact organizers' %}</dt> | ||||
|                     <dt class="col-sm-6 text-sm-end">{% trans "To contact organizers" %}</dt> | ||||
|                     <dd class="col-sm-6"><a href="mailto:{{ tournament.organizers_email }}">{{ tournament.organizers_email }}</a></dd> | ||||
|  | ||||
|                     <dt class="col-sm-6 text-sm-end">{% trans 'To contact juries' %}</dt> | ||||
|                     {% if user.is_authenticated and user.registration.is_volunteer %} | ||||
|                         <dt class="col-sm-6 text-sm-end">{% trans "To contact juries" %}</dt> | ||||
|                         <dd class="col-sm-6"><a href="mailto:{{ tournament.jurys_email }}">{{ tournament.jurys_email }}</a></dd> | ||||
|  | ||||
|                     <dt class="col-sm-6 text-sm-end">{% trans 'To contact valid teams' %}</dt> | ||||
|                         <dt class="col-sm-6 text-sm-end">{% trans "To contact valid teams" %}</dt> | ||||
|                         <dd class="col-sm-6"><a href="mailto:{{ tournament.teams_email }}">{{ tournament.teams_email }}</a></dd> | ||||
|                     {% endif %} | ||||
|                 {% endif %} | ||||
|             </dl> | ||||
|         </div> | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotific | ||||
|     TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \ | ||||
|     TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \ | ||||
|     TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \ | ||||
|     TournamentPublishNotesView, TournamentUpdateView, WrittenReviewUploadView | ||||
|     TournamentPublishNotesView, TournamentPublishSolutionsView, TournamentUpdateView, WrittenReviewUploadView | ||||
|  | ||||
|  | ||||
| app_name = "participation" | ||||
| @@ -48,6 +48,8 @@ urlpatterns = [ | ||||
|          name="tournament_notation_sheets"), | ||||
|     path("tournament/<int:pk>/notation/notifications/", GSheetNotificationsView.as_view(), | ||||
|          name="tournament_gsheet_notifications"), | ||||
|     path("tournament/<int:pk>/publish-solutions/<int:round>/", TournamentPublishSolutionsView.as_view(), | ||||
|          name="tournament_publish_solutions"), | ||||
|     path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(), | ||||
|          name="tournament_publish_notes"), | ||||
|     path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(), | ||||
|   | ||||
| @@ -557,7 +557,7 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView): | ||||
|         if not self.get_object().valid: | ||||
|             raise PermissionDenied(_("The team is not validated yet.")) | ||||
|         if user.registration.is_admin or user.registration.participates \ | ||||
|                 and user.registration.team.participation \ | ||||
|                 and user.registration.team \ | ||||
|                 and user.registration.team.participation.pk == kwargs["pk"] \ | ||||
|                 or user.registration.is_volunteer \ | ||||
|                 and (self.get_object().tournament in user.registration.interesting_tournaments | ||||
| @@ -672,7 +672,7 @@ class TournamentPaymentsView(VolunteerMixin, SingleTableMixin, DetailView): | ||||
|         if self.object.final: | ||||
|             payments = Payment.objects.filter(final=True) | ||||
|         else: | ||||
|             payments = Payment.objects.filter(registrations__team__participation__tournament=self.get_object()) | ||||
|             payments = Payment.objects.filter(registrations__team__participation__tournament=self.get_object(), final=False) | ||||
|         return payments.annotate(team_id=F('registrations__team')).order_by('-valid', 'registrations__team__trigram') \ | ||||
|             .distinct().all() | ||||
|  | ||||
| @@ -747,12 +747,12 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView | ||||
|             return self.handle_no_permission() | ||||
|         tournament = self.get_object() | ||||
|         reg = request.user.registration | ||||
|         if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()): | ||||
|         if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.all(): | ||||
|             return self.handle_no_permission() | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         if int(kwargs["round"]) not in (1, 2): | ||||
|         if int(kwargs["round"]) not in range(1, settings.NB_ROUNDS + 1): | ||||
|             raise Http404 | ||||
|  | ||||
|         tournament = Tournament.objects.get(pk=kwargs["pk"]) | ||||
| @@ -767,6 +767,45 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView | ||||
|         return reverse_lazy("participation:tournament_detail", args=(kwargs['pk'],)) | ||||
|  | ||||
|  | ||||
| class TournamentPublishSolutionsView(VolunteerMixin, SingleObjectMixin, RedirectView): | ||||
|     """ | ||||
|     On rend les solutions du tour suivant accessibles aux équipes. | ||||
|     """ | ||||
|     model = Tournament | ||||
|  | ||||
|     def dispatch(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Les admins, orgas et PJ peuvent rendre les solutions accessibles. | ||||
|         """ | ||||
|         if not request.user.is_authenticated: | ||||
|             return self.handle_no_permission() | ||||
|         tournament = self.get_object() | ||||
|         reg = request.user.registration | ||||
|         if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.all(): | ||||
|             return self.handle_no_permission() | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         if int(kwargs["round"]) not in range(2, settings.NB_ROUNDS + 1): | ||||
|             raise Http404 | ||||
|  | ||||
|         tournament = Tournament.objects.get(pk=kwargs["pk"]) | ||||
|         publish_solutions = 'hide' not in request.GET | ||||
|         if int(kwargs['round']) == 2: | ||||
|             tournament.solutions_available_second_phase = publish_solutions | ||||
|         elif int(kwargs['round']) == 3: | ||||
|             tournament.solutions_available_third_phase = publish_solutions | ||||
|         tournament.save() | ||||
|         if 'hide' not in request.GET: | ||||
|             messages.success(request, _("Solutions are now available to teams!")) | ||||
|         else: | ||||
|             messages.warning(request, _("Solutions are not available to teams anymore.")) | ||||
|         return super().get(request, *args, **kwargs) | ||||
|  | ||||
|     def get_redirect_url(self, *args, **kwargs): | ||||
|         return reverse_lazy("participation:tournament_detail", args=(kwargs['pk'],)) | ||||
|  | ||||
|  | ||||
| class TournamentHarmonizeView(VolunteerMixin, DetailView): | ||||
|     """ | ||||
|     Harmonize the notes of a tournament. | ||||
| @@ -779,7 +818,7 @@ class TournamentHarmonizeView(VolunteerMixin, DetailView): | ||||
|             return self.handle_no_permission() | ||||
|         tournament = self.get_object() | ||||
|         reg = request.user.registration | ||||
|         if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()): | ||||
|         if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.all(): | ||||
|             return self.handle_no_permission() | ||||
|         if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1): | ||||
|             raise Http404 | ||||
| @@ -812,7 +851,7 @@ class TournamentHarmonizeNoteView(VolunteerMixin, DetailView): | ||||
|             return self.handle_no_permission() | ||||
|         tournament = self.get_object() | ||||
|         reg = request.user.registration | ||||
|         if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()): | ||||
|         if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.all(): | ||||
|             return self.handle_no_permission() | ||||
|         if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1) \ | ||||
|                 or self.kwargs['action'] not in ('add', 'remove') \ | ||||
| @@ -852,7 +891,7 @@ class SelectTeamFinalView(VolunteerMixin, DetailView): | ||||
|             return self.handle_no_permission() | ||||
|         tournament = self.get_object() | ||||
|         reg = request.user.registration | ||||
|         if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()): | ||||
|         if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.all(): | ||||
|             return self.handle_no_permission() | ||||
|         participation_qs = tournament.participations.filter(pk=self.kwargs["participation_id"]) | ||||
|         if not participation_qs.exists(): | ||||
| @@ -1003,17 +1042,14 @@ class SolutionsDownloadView(VolunteerMixin, View): | ||||
|                 return super().dispatch(request, *args, **kwargs) | ||||
|         elif 'tournament_id' in kwargs: | ||||
|             tournament = Tournament.objects.get(pk=kwargs["tournament_id"]) | ||||
|             if reg.is_volunteer \ | ||||
|                     and (tournament in reg.organized_tournaments.all() | ||||
|                          or reg.pools_presided.filter(tournament=tournament).exists()): | ||||
|             if reg.is_volunteer and reg in tournament.organizers_and_presidents.all(): | ||||
|                 return super().dispatch(request, *args, **kwargs) | ||||
|         else: | ||||
|             pool = Pool.objects.get(pk=kwargs["pool_id"]) | ||||
|             tournament = pool.tournament | ||||
|             if reg.is_volunteer \ | ||||
|                     and (reg in tournament.organizers.all() | ||||
|                          or reg in pool.juries.all() | ||||
|                          or reg.pools_presided.filter(tournament=tournament).exists()): | ||||
|                     and (reg in tournament.organizers_and_presidents.all() | ||||
|                          or reg in pool.juries.all()): | ||||
|                 return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|         return self.handle_no_permission() | ||||
| @@ -2001,7 +2037,7 @@ class PassageDetailView(LoginRequiredMixin, DetailView): | ||||
|         reg = request.user.registration | ||||
|         passage = self.get_object() | ||||
|         if reg.is_admin or reg.is_volunteer \ | ||||
|                 and (self.get_object().pool.tournament in reg.organized_tournaments.all() | ||||
|                 and (reg in self.get_object().pool.tournament.organizers_and_presidents.all() | ||||
|                      or reg in passage.pool.juries.all() | ||||
|                      or reg.pools_presided.filter(tournament=passage.pool.tournament).exists()) \ | ||||
|                 or reg.participates and reg.team \ | ||||
| @@ -2128,6 +2164,7 @@ class NoteUpdateView(VolunteerMixin, UpdateView): | ||||
|         form.fields['opponent_oral'].label += f" ({self.object.passage.opponent.team.trigram})" | ||||
|         form.fields['reviewer_writing'].label += f" ({self.object.passage.reviewer.team.trigram})" | ||||
|         form.fields['reviewer_oral'].label += f" ({self.object.passage.reviewer.team.trigram})" | ||||
|         if settings.HAS_OBSERVER: | ||||
|             form.fields['observer_writing'].label += f" ({self.object.passage.observer.team.trigram})" | ||||
|             form.fields['observer_oral'].label += f" ({self.object.passage.observer.team.trigram})" | ||||
|         return form | ||||
|   | ||||
| @@ -66,7 +66,7 @@ Cochez la/les cases correspondantes.\\ | ||||
|  | ||||
| \fbox{\textcolor{white}{A}}  Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$ | ||||
| {% if tournament.unified_registration %} dans | ||||
| l'un des tournois d'Île-de-France (selon sélection : du 26 au 27 avril 2025, du 3 au 4 mai 2025, ou du 10 au 11 mai 2025) | ||||
| l'un des tournois d'Île-de-France (selon sélection : du 4 au 5 mai 2026, du 28 au 29 mars 2026, ou TBA 2026) | ||||
| {% else %} de | ||||
| {{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }}, | ||||
| {% endif %} \`a | ||||
|   | ||||
| @@ -68,7 +68,7 @@ Cochez la/les cases correspondantes.\\ | ||||
|  | ||||
|  \fbox{\textcolor{white}{A}}  Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$ | ||||
|  {% if tournament.unified_registration %} dans | ||||
|  l'un des tournois d'Île-de-France (selon sélection : du 26 au 27 avril 2025, du 3 au 4 mai 2025, ou du 10 au 11 mai 2025) | ||||
|  l'un des tournois d'Île-de-France (selon sélection : du 4 au 5 mai 2026, du 28 au 29 mars 2026, ou TBA 2026) | ||||
|  {% else %} de | ||||
|  {{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }}, | ||||
|  {% endif %} \`a | ||||
|   | ||||
| @@ -54,9 +54,9 @@ né\cdt{}e le {{ registration.birth_date|default:"\underline{\phantom{dd/mm/aaaa | ||||
| à participer au Tournoi Français des Jeunes Mathématiciennes et Mathématiciens ($\mathbb{TFJM}^2$) | ||||
| {% if tournament.unified_registration %} dans l'un des tournois d'Île-de-France selon sélection : | ||||
| \begin{itemize} | ||||
|     \item Île-de-France 1, du 26 au 27 avril 2025 ; | ||||
|     \item Île-de-France 2, du 3 au 4 mai 2025 ; | ||||
|     \item Île-de-France 3, du 10 au 11 mai 2025.  | ||||
|     \item Île-de-France 1, du 4 au 5 avril 2026 ; | ||||
|     \item Île-de-France 2, du 28 au 29 mars 2026 ; | ||||
|     \item Île-de-France 3, du TBA 2026.  | ||||
| \end{itemize} | ||||
| {% else %} | ||||
| organisé \`a : | ||||
| @@ -67,7 +67,7 @@ Iel se rendra au lieu indiqu\'e ci-dessus le samedi matin et quittera les lieux | ||||
| ses propres moyens et sous la responsabilité du/de la représentant\cdt{}e légal\cdt{}e. | ||||
|  | ||||
| {% if tournament.name == "Lyon" %} | ||||
| Un hébergement à titre gratuit sera organisée la nuit du 10 au 11 mai 2025. | ||||
| Un hébergement à titre gratuit sera organisée la nuit du {{ tournament.date_start }} au {{ tournament.date_end }}. | ||||
| Le/la participant\cdt{}e sera logé\cdt{}e soit dans les résidences de l'ENS de Lyon situées | ||||
| sur les campus de l'école soit dans l'hotel Ibis Gerland Mérieux situé 246 rue Marcel Mérieux – 69007 LYON. | ||||
| {% endif %} | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| {{ object.user.last_name }} | ||||
| {{ object.user.first_name }} | ||||
| {{ object.user.email }} | ||||
| @@ -726,10 +726,11 @@ class PhotoAuthorizationView(LoginRequiredMixin, View): | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         filename = kwargs["filename"] | ||||
|         path = f"media/authorization/photo/{filename}" | ||||
|         if not os.path.exists(path): | ||||
|             raise Http404 | ||||
|         student = ParticipantRegistration.objects.get(Q(photo_authorization__endswith=filename) | ||||
|         student_qs = ParticipantRegistration.objects.filter(Q(photo_authorization__endswith=filename) | ||||
|                                                             | Q(photo_authorization_final__endswith=filename)) | ||||
|         if not os.path.exists(path) or not student_qs.exists(): | ||||
|             raise Http404 | ||||
|         student = student_qs.get() | ||||
|         user = request.user | ||||
|         if not (student.user == user or user.registration.is_admin or user.registration.is_volunteer and student.team | ||||
|                 and student.team.participation.tournament in user.registration.organized_tournaments.all()): | ||||
|   | ||||
| @@ -1,28 +1,28 @@ | ||||
| channels[daphne]~=4.2.2 | ||||
| channels-redis~=4.2.1 | ||||
| citric~=1.4.0 | ||||
| crispy-bootstrap5~=2025.4 | ||||
| channels[daphne]~=4.3.1 | ||||
| channels-redis~=4.3.0 | ||||
| citric~=2.0.0 | ||||
| crispy-bootstrap5~=2025.6 | ||||
| Django>=5.2,<6.0 | ||||
| django-crispy-forms~=2.4 | ||||
| django-filter~=25.1 | ||||
| django-filter~=25.2 | ||||
| django-haystack~=3.3.0 | ||||
| django-mailer~=2.3.2 | ||||
| django-phonenumber-field~=8.1.0 | ||||
| django-pipeline~=4.0.0 | ||||
| django-polymorphic~=3.1.0 | ||||
| django-phonenumber-field~=8.3.0 | ||||
| django-pipeline~=4.1.0 | ||||
| django-polymorphic~=4.1.0 | ||||
| django-tables2~=2.7.5 | ||||
| djangorestframework~=3.16.0 | ||||
| djangorestframework~=3.16.1 | ||||
| django-rest-polymorphic~=0.1.10 | ||||
| elasticsearch~=7.17.9 | ||||
| gspread~=6.2.0 | ||||
| gspread~=6.2.1 | ||||
| gunicorn~=23.0.0 | ||||
| odfpy~=1.4.1 | ||||
| pandas~=2.2.3 | ||||
| phonenumbers~=9.0.3 | ||||
| psycopg~=3.2.6 | ||||
| pypdf~=5.4.0 | ||||
| pandas~=2.3.3 | ||||
| phonenumbers~=9.0.17 | ||||
| psycopg~=3.2.12 | ||||
| pypdf~=6.1.3 | ||||
| python-magic~=0.4.27 | ||||
| requests~=2.32.3 | ||||
| sympasoap~=1.1 | ||||
| uvicorn~=0.34.2 | ||||
| requests~=2.32.5 | ||||
| sympasoap~=1.1.3 | ||||
| uvicorn~=0.38.0 | ||||
| websockets~=15.0.1 | ||||
| @@ -70,7 +70,7 @@ class Survey(models.Model): | ||||
|             teams = Team.objects.filter(participation__valid=True) | ||||
|             if self.tournament: | ||||
|                 teams = teams.filter(participation__tournament=self.tournament) | ||||
|             return teams.all() | ||||
|             return teams.order_by('participation__tournament__name', 'trigram').all() | ||||
|         else: | ||||
|             if self.invite_coaches: | ||||
|                 registrations = ParticipantRegistration.objects.filter(team__participation__valid=True) | ||||
| @@ -78,7 +78,7 @@ class Survey(models.Model): | ||||
|                 registrations = StudentRegistration.objects.filter(team__participation__valid=True) | ||||
|             if self.tournament: | ||||
|                 registrations = registrations.filter(team__participation__tournament=self.tournament) | ||||
|             return registrations.all() | ||||
|             return registrations.order_by('team__participation__tournament__name', 'team__trigram').all() | ||||
|  | ||||
|     @property | ||||
|     def completed(self): | ||||
|   | ||||
| @@ -48,6 +48,7 @@ | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>{% trans "participant"|capfirst %}</th> | ||||
|                 <th>{% trans "tournament"|capfirst %}</th> | ||||
|                 <th>{% trans "completed"|capfirst %}</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
| @@ -56,8 +57,10 @@ | ||||
|                 <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> | ||||
|                         <td>{{ participant.participation.tournament.name }}</td> | ||||
|                     {% else %} | ||||
|                         <td>{{ participant.user.first_name }} {{ participant.user.last_name }} ({% trans "team" %} {{ participant.team.trigram }})</td> | ||||
|                         <td>{{ participant.team.participation.tournament.name }}</td> | ||||
|                     {% endif %} | ||||
|                     {% if participant in survey.completed.all %} | ||||
|                         <td>{% trans "Yes" %}</td> | ||||
|   | ||||
| @@ -1,9 +1,4 @@ | ||||
| # min   hour    day     month   weekday command | ||||
| # Send pending mails | ||||
| *       *       *       *       *       cd /code && python manage.py send_mail -c 1 | ||||
| *       *       *       *       *       cd /code && python manage.py retry_deferred -c 1 | ||||
| 0       0       *       *       *       cd /code && python manage.py purge_mail_log 7 -c 1 | ||||
|  | ||||
| # Update search index | ||||
| */2     *       *       *       *       cd /code && python manage.py update_index &> /dev/null | ||||
|  | ||||
|   | ||||
| @@ -213,6 +213,7 @@ STATICFILES_FINDERS = ( | ||||
|  | ||||
| PIPELINE = { | ||||
|     'DISABLE_WRAPPER': True, | ||||
|     'JS_COMPRESSOR': 'pipeline.compressors.uglifyjs.UglifyJSCompressor', | ||||
|     'JAVASCRIPT': { | ||||
|         'main': { | ||||
|             'source_filenames': ( | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|         </div> | ||||
|  | ||||
|         <div id="sidebar-card" class="collapse d-lg-block"> | ||||
|             <div class="card-body"> | ||||
|             <div class="card-body px-2 py-1"> | ||||
|                 {% for information in user.registration.important_informations %} | ||||
|                     <div class="card my-2"> | ||||
|                         <div class="card-header bg-dark-subtle"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user