1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-22 01:18:27 +02:00

Compare commits

...

24 Commits

Author SHA1 Message Date
8aec72d712 Correction mot Coefficient 2025-05-31 17:38:45 +02:00
6a521b6121 Noms des fichiers en français 2025-05-31 12:18:12 +02:00
62abfa94d6 Correction liens bandeau Informations pour la finale 2025-05-29 21:49:59 +02:00
952315ea4d Correction publication des notes pour le dernier tour 2025-05-05 10:28:22 +02:00
2e613799c9 Remplacement de yuglify par uglify, plus récent 2025-04-28 23:32:49 +02:00
08805a6360 Correction non-affichage des colonnes d'observation sans observateur 2025-04-28 22:44:08 +02:00
6841659e41 Plus de AdminRegistration à indexer 2025-04-28 22:14:40 +02:00
a84ffcf0a3 Bouton pour rendre les solutions accessibles pour le second tour en 1 clic 2025-04-28 22:01:26 +02:00
203fc3cd54 On n'affiche pas les paiements pour la finale sur la liste des paiements d'un tournoi régional 2025-04-28 20:43:36 +02:00
60f5236dee Affichage du tournoi dans la liste des réponses à un questionnaire 2025-04-28 20:35:23 +02:00
ab459ecc17 On n'affiche pas les données de l'équipe observatrice quand on a pas 2025-04-28 20:34:06 +02:00
7ad7659d78 Use solution number instead of passage index in scale sheets 2025-04-28 20:06:53 +02:00
84eb08ec46 Correction formulaire saisie notes s'il n'y a pas d'observateur⋅rice 2025-04-26 19:20:54 +02:00
3750828883 Send mails using the runmailer_pg command 2025-04-25 00:00:25 +02:00
ba36ad4071 Update coefficients 2025-04-24 21:57:52 +02:00
626433c464 Prevent some errors 2025-04-24 21:29:08 +02:00
032b67ac51 Don't generate spreadhseet if there is no team in a pool 2025-04-23 20:40:14 +02:00
f3bd479fdc Fix final sheet layout for 4-teams pools 2025-04-22 23:17:20 +02:00
bc06cf4903 Fix draw issues with translated strings 2025-04-22 22:58:12 +02:00
6d43c4b97e annulé != terminé 2025-04-22 20:59:53 +02:00
0499885fc8 Fix problem names for 2025 2025-04-22 20:20:22 +02:00
63c96ff2d2 Refetch search query when the input is updated 2025-04-22 19:52:07 +02:00
efeb2628ad Fix notation sheet when there is no observer 2025-04-22 19:44:21 +02:00
56aad288f4 Simplify elasticsearch index to make it work better 2025-04-22 19:19:22 +02:00
31 changed files with 616 additions and 493 deletions

View File

@ -4,12 +4,10 @@ ENV PYTHONUNBUFFERED 1
ENV DJANGO_ALLOW_ASYNC_UNSAFE 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 \ 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 apk add --no-cache bash
RUN npm install -g yuglify
RUN mkdir /code /code/docs RUN mkdir /code /code/docs
WORKDIR /code WORKDIR /code
COPY requirements.txt /code/requirements.txt COPY requirements.txt /code/requirements.txt

View File

@ -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 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 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 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 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 à la poule suivante. Une fois que toutes les poules ont vu leurs problèmes

View File

@ -224,7 +224,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Update user interface # Update user interface
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", 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}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.set_info', {'tid': self.tournament_id, 'type': 'draw.set_info',
'info': await self.tournament.draw.ainformation()}) 'info': await self.tournament.draw.ainformation()})
@ -235,7 +235,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.notify', {'tid': self.tournament_id, 'type': 'draw.notify',
'title': 'Tirage au sort du TFJM²', '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)}) .format(tournament=self.tournament.name)})
async def draw_start(self, content) -> None: async def draw_start(self, content) -> None:
@ -405,15 +405,15 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
await self.channel_layer.group_send( await self.channel_layer.group_send(
f"team-{dup.participation.team.trigram}", f"team-{dup.participation.team.trigram}",
{'tid': self.tournament_id, 'type': 'draw.notify', 'title': 'Tirage au sort du TFJM²', {'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. " 'body': str(_("Your dice score is identical to the one of one or multiple teams. "
"Please relaunch it.")} "Please relaunch it."))}
) )
# Alert the tournament # Alert the tournament
await self.channel_layer.group_send( await self.channel_layer.group_send(
f"tournament-{self.tournament.id}", f"tournament-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.alert', {'tid': self.tournament_id, 'type': 'draw.alert',
'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format( 'message': str(_('Dices from teams {teams} are identical. Please relaunch your dices.').format(
teams=', '.join(td.participation.team.trigram for td in dups)), teams=', '.join(td.participation.team.trigram for td in dups))),
'alert_type': 'warning'}) 'alert_type': 'warning'})
error = True error = True
@ -537,7 +537,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
async for next_round in self.tournament.draw.round_set.filter(number__gte=2).all(): 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}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.send_poules', {'tid': self.tournament_id, 'type': 'draw.send_poules',
'round': r.number, 'round': next_round.number,
'poules': [ 'poules': [
{ {
'letter': pool.get_letter_display(), 'letter': pool.get_letter_display(),
@ -612,8 +612,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Notify the team that it can draw a problem # Notify the team that it can draw a problem
await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}", await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
{'tid': self.tournament_id, 'type': 'draw.notify', {'tid': self.tournament_id, 'type': 'draw.notify',
'title': _("Your turn!"), 'title': str(_("Your turn!")),
'body': _("It's your turn to draw a problem!")}) 'body': str(_("It's your turn to draw a problem!"))})
async def select_problem(self, **kwargs): async def select_problem(self, **kwargs):
""" """
@ -752,8 +752,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Notify the team that it can draw a problem # Notify the team that it can draw a problem
await self.channel_layer.group_send(f"team-{new_trigram}", await self.channel_layer.group_send(f"team-{new_trigram}",
{'tid': self.tournament_id, 'type': 'draw.notify', {'tid': self.tournament_id, 'type': 'draw.notify',
'title': _("Your turn!"), 'title': str(_("Your turn!")),
'body': _("It's your turn to draw a problem!")}) 'body': str(_("It's your turn to draw a problem!"))})
else: else:
# Pool is ended # Pool is ended
await self.end_pool(pool) await self.end_pool(pool)
@ -829,8 +829,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Notify the team that it can draw a dice # Notify the team that it can draw a dice
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
{'tid': self.tournament_id, 'type': 'draw.notify', {'tid': self.tournament_id, 'type': 'draw.notify',
'title': _("Your turn!"), 'title': str(_("Your turn!")),
'body': _("It's your turn to launch the dice!")}) 'body': str(_("It's your turn to launch the dice!"))})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.dice_visibility', {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
@ -863,8 +863,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Notify the team that it can draw a dice # Notify the team that it can draw a dice
await self.channel_layer.group_send(f"team-{participation.team.trigram}", await self.channel_layer.group_send(f"team-{participation.team.trigram}",
{'tid': self.tournament_id, 'type': 'draw.notify', {'tid': self.tournament_id, 'type': 'draw.notify',
'title': _("Your turn!"), 'title': str(_("Your turn!")),
'body': _("It's your turn to launch the dice!")}) 'body': str(_("It's your turn to launch the dice!"))})
# Reorder dices # Reorder dices
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", 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 # Notify the team that it can draw a problem
await self.channel_layer.group_send(f"team-{new_trigram}", await self.channel_layer.group_send(f"team-{new_trigram}",
{'tid': self.tournament_id, 'type': 'draw.notify', {'tid': self.tournament_id, 'type': 'draw.notify',
'title': _("Your turn!"), 'title': str(_("Your turn!")),
'body': _("It's your turn to draw a problem!")}) 'body': str(_("It's your turn to draw a problem!"))})
@ensure_orga @ensure_orga
async def export(self, **kwargs): async def export(self, **kwargs):
@ -1039,7 +1039,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Send notification to everyone # Send notification to everyone
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.notify', {'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!"))}) 'body': str(_("The draw of the second round is starting!"))})
if settings.TFJM_APP == "TFJM": if settings.TFJM_APP == "TFJM":
@ -1092,8 +1092,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Notify the team that it can draw a problem # Notify the team that it can draw a problem
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
{'tid': self.tournament_id, 'type': 'draw.notify', {'tid': self.tournament_id, 'type': 'draw.notify',
'title': _("Your turn!"), 'title': str(_("Your turn!")),
'body': _("It's your turn to draw a problem!")}) 'body': str(_("It's your turn to draw a problem!"))})
else: else:
async for td in r2.team_draws.prefetch_related('participation__team'): async for td in r2.team_draws.prefetch_related('participation__team'):
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",

View File

@ -221,9 +221,10 @@ document.addEventListener('DOMContentLoaded', () => {
elem.innerText = `${trigram} 🎲 ${result}` elem.innerText = `${trigram} 🎲 ${result}`
} }
let nextTeam = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`).getAttribute("data-team") let nextTeamDiv = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`)
if (nextTeam) { if (nextTeamDiv) {
// If there is one team that does not have launched its dice, then we update the debug section // 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`) let debugSpan = document.getElementById(`debug-dice-${tid}-team`)
if (debugSpan) if (debugSpan)
debugSpan.innerText = nextTeam debugSpan.innerText = nextTeam

View File

@ -4,6 +4,7 @@ crond -l 0
python manage.py migrate python manage.py migrate
python manage.py update_index python manage.py update_index
python manage.py runmailer_pg &
nginx nginx

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
# Copyright (C) 2020 by Animath # Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
@ -51,9 +53,14 @@ class PassageInline(admin.TabularInline):
model = Passage model = Passage
extra = 0 extra = 0
ordering = ('position',) ordering = ('position',)
autocomplete_fields = ('reporter', 'opponent', 'reviewer', 'observer',)
show_change_link = True 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): class NoteInline(admin.TabularInline):
model = Note model = Note
@ -113,12 +120,9 @@ class PoolAdmin(admin.ModelAdmin):
@admin.register(Passage) @admin.register(Passage)
class PassageAdmin(admin.ModelAdmin): 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',) list_filter = ('pool__tournament', 'pool__round', 'pool__letter', 'solution_number',)
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',) search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
ordering = ('pool__tournament', 'pool__round', 'pool__letter', 'position',) ordering = ('pool__tournament', 'pool__round', 'pool__letter', 'position',)
autocomplete_fields = ('pool', 'reporter', 'opponent', 'reviewer', 'observer',)
inlines = (NoteInline,) inlines = (NoteInline,)
@admin.display(description=_("reporter"), ordering='reporter__team__trigram') @admin.display(description=_("reporter"), ordering='reporter__team__trigram')
@ -135,7 +139,7 @@ class PassageAdmin(admin.ModelAdmin):
@admin.display(description=_("observer"), ordering='observer__team__trigram') @admin.display(description=_("observer"), ordering='observer__team__trigram')
def observer_trigram(self, record: Passage): 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') @admin.display(description=_("pool"), ordering='pool__letter')
def pool_abbr(self, record): def pool_abbr(self, record):
@ -145,15 +149,23 @@ class PassageAdmin(admin.ModelAdmin):
def tournament(self, record: Passage): def tournament(self, record: Passage):
return record.pool.tournament 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) @admin.register(Note)
class NoteAdmin(admin.ModelAdmin): 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',) search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__reporter__team__trigram',)
autocomplete_fields = ('jury', 'passage',) autocomplete_fields = ('jury', 'passage',)
@ -161,6 +173,21 @@ class NoteAdmin(admin.ModelAdmin):
def pool(self, record): def pool(self, record):
return record.passage.pool.short_name 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) @admin.register(Solution)
class SolutionAdmin(admin.ModelAdmin): class SolutionAdmin(admin.ModelAdmin):

View File

@ -7,6 +7,7 @@ import re
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, Field, HTML, Layout, Submit from crispy_forms.layout import Div, Field, HTML, Layout, Submit
from django import forms from django import forms
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
@ -14,7 +15,6 @@ from django.utils.translation import gettext_lazy as _
import pandas import pandas
from pypdf import PdfReader from pypdf import PdfReader
from registration.models import VolunteerRegistration from registration.models import VolunteerRegistration
from tfjm import settings
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview
@ -405,6 +405,12 @@ class WrittenReviewForm(forms.ModelForm):
class NoteForm(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: class Meta:
model = Note model = Note
fields = ('reporter_writing', 'reporter_oral', 'opponent_writing', fields = ('reporter_writing', 'reporter_oral', 'opponent_writing',

View File

@ -5,11 +5,13 @@ from pathlib import Path
from django.conf import settings from django.conf import settings
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.utils.translation import activate
from participation.models import Solution, Tournament from participation.models import Solution, Tournament
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
activate(settings.PREFERRED_LANGUAGE_CODE)
base_dir = Path(__file__).parent.parent.parent.parent base_dir = Path(__file__).parent.parent.parent.parent
base_dir /= "output" base_dir /= "output"
if not base_dir.is_dir(): if not base_dir.is_dir():

View File

@ -440,6 +440,10 @@ class Tournament(models.Model):
return Participation.objects.filter(final=True) return Participation.objects.filter(final=True)
return self.participation_set 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 @property
def solutions(self): def solutions(self):
if self.final: if self.final:
@ -932,10 +936,10 @@ class Participation(models.Model):
'content': content, 'content': content,
}) })
elif timezone.now() <= tournament.reviews_first_phase_limit + timedelta(hours=2): 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) reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, reporter=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, opponent=self) opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, opponent=self)
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reviewer=self) reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, reviewer=self)
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=1, observer=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 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 " reporter_text = _("<p>The solutions draw is ended. You can check the result on "
@ -997,10 +1001,10 @@ class Participation(models.Model):
'content': content, 'content': content,
}) })
elif timezone.now() <= tournament.reviews_second_phase_limit + timedelta(hours=2): 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) reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, reporter=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, opponent=self) opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, opponent=self)
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reviewer=self) reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, reviewer=self)
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=2, observer=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 observer_passage = observer_passage.get() if observer_passage.exists() else None
reporter_text = _("<p>For the second round, you will present " reporter_text = _("<p>For the second round, you will present "
@ -1061,10 +1065,10 @@ class Participation(models.Model):
}) })
elif settings.NB_ROUNDS >= 3 \ elif settings.NB_ROUNDS >= 3 \
and timezone.now() <= tournament.reviews_third_phase_limit + timedelta(hours=2): 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) reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, reporter=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, opponent=self) opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, opponent=self)
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reviewer=self) reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, reviewer=self)
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=3, observer=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 observer_passage = observer_passage.get() if observer_passage.exists() else None
reporter_text = _("<p>For the third round, you will present " 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) passage_width = 6 + (2 if has_observer else 0)
passages = self.passages.all() passages = self.passages.all()
if not pool_size or not passages.count():
# Not initialized yet
return
# Create tournament sheet if it does not exist # Create tournament sheet if it does not exist
self.tournament.create_spreadsheet() self.tournament.create_spreadsheet()
@ -1414,8 +1422,8 @@ class Pool(models.Model):
if has_observer: if has_observer:
merge_cells.append(f"{getcol(9 + i * passage_width)}2:{getcol(10 + i * passage_width)}2") merge_cells.append(f"{getcol(9 + i * passage_width)}2:{getcol(10 + i * passage_width)}2")
merge_cells.append(f"{getcol(9 + i * passage_width)}{max_row + 3}" merge_cells.append(f"{getcol(9 + i * passage_width)}{max_row + 3}"
f":{getcol(10 + i * passage_width)}{max_row + 3}") f":{getcol(10 + i * passage_width)}{max_row + 3}")
merge_cells.append(f"A{max_row + 1}:B{max_row + 1}") merge_cells.append(f"A{max_row + 1}:B{max_row + 1}")
merge_cells.append(f"A{max_row + 2}:B{max_row + 2}") merge_cells.append(f"A{max_row + 2}:B{max_row + 2}")
merge_cells.append(f"A{max_row + 3}:B{max_row + 3}") merge_cells.append(f"A{max_row + 3}:B{max_row + 3}")
@ -1623,6 +1631,10 @@ class Pool(models.Model):
worksheet.client.batch_update(spreadsheet.id, body) worksheet.client.batch_update(spreadsheet.id, body)
def update_juries_lines_spreadsheet(self): 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) translation.activate(settings.PREFERRED_LANGUAGE_CODE)
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT) gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
@ -1773,7 +1785,7 @@ class Passage(models.Model):
@property @property
def coeff_reporter_oral(self) -> float: 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 coeff *= 1 - 0.25 * self.reporter_penalties
return coeff return coeff
@ -1817,7 +1829,7 @@ class Passage(models.Model):
@property @property
def coeff_reviewer_oral(self): def coeff_reviewer_oral(self):
return 1 if settings.TFJM_APP == "TFJM" else 1.2 return 1.2
@property @property
def average_reviewer(self) -> float: def average_reviewer(self) -> float:

View File

@ -1,6 +1,7 @@
# Copyright (C) 2020 by Animath # Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.utils import formats from django.utils import formats
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import format_lazy from django.utils.text import format_lazy
@ -106,8 +107,6 @@ class PoolTable(tables.Table):
class PassageTable(tables.Table): class PassageTable(tables.Table):
# FIXME Ne pas afficher l'équipe observatrice si non nécessaire
reporter = tables.LinkColumn( reporter = tables.LinkColumn(
"participation:passage_detail", "participation:passage_detail",
args=[tables.A("id")], args=[tables.A("id")],
@ -131,7 +130,9 @@ class PassageTable(tables.Table):
'class': 'table table-condensed table-striped text-center', 'class': 'table table-condensed table-striped text-center',
} }
model = Passage 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): class NoteTable(tables.Table):
@ -160,4 +161,6 @@ class NoteTable(tables.Table):
} }
model = Note model = Note
fields = ('jury', 'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral', 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',)

View File

@ -44,7 +44,7 @@
\Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\ \Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\
{% endif %} {% endif %}
\vspace{3mm} \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} \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 \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 }}} \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 }}} \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 %} {% 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 %}$ & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq {% if TFJM.APP == "TFJM" %}20{% else %}10{% endif %}$

View File

@ -58,7 +58,7 @@
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR %%%%%%%%%%%%%%%%%%%%%DEFENSEUR
\begin{tabular}{|c|p{25mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline \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 %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 }}} \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 %%%%%%%%%%%%%%%%%OPPOSANT⋅E
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline \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.} \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 %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 }}} \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 %%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR⋅RICE
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline \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 %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 }}} \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 %} {% if TFJM.APP == "ETEAM" and pool.participations.count >= 4 %}
%%%%%%%%%%%%%%%%%%%%%%OBSERVATEUR⋅RICE %%%%%%%%%%%%%%%%%%%%%%OBSERVATEUR⋅RICE
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline \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 %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 }}} \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 }}}

View File

@ -52,7 +52,7 @@
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR %%%%%%%%%%%%%%%%%%%%%DEFENSEUR
\begin{tabular}{|c|p{24mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline \begin{tabular}{|c|p{24mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline
\multicolumn{4}{|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|}{{\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 %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 }}} \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 %%%%%%%%%%%%%%%%%OPPOSANT
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline \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.} \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 %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 }}} \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 %%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR.RICE
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline \begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
\multicolumn{4}{|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|}{{\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 %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 }}} \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 }}} \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 }}} && \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 }}} && 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 }}} & Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline &\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
\end{tabular} \end{tabular}

View File

@ -23,44 +23,80 @@
<dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd> <dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd>
{% endif %} {% 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> <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> <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> <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> <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> <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> <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> <dd class="col-sm-6">{{ tournament.reviews_second_phase_limit }}</dd>
{% if TFJM.APP == "ETEAM" %} {% if TFJM.NB_ROUNDS == 3 %}
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the third round'|capfirst %}</dt> <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> <dd class="col-sm-6">{{ tournament.reviews_third_phase_limit }}</dd>
{% endif %} {% 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> <dd class="col-sm-6">{{ tournament.description }}</dd>
{% if TFJM.ML_MANAGEMENT %} {% 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> <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 %}
<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 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> <dd class="col-sm-6"><a href="mailto:{{ tournament.teams_email }}">{{ tournament.teams_email }}</a></dd>
{% endif %}
{% endif %} {% endif %}
</dl> </dl>
</div> </div>
@ -199,7 +235,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% if user.registration.is_admin or user.registration in tournament.organizers.all %} {% if user.registration.is_admin or user.registration in tournament.organizers.all %}

View File

@ -1,3 +1,2 @@
{{ object.name }} {{ object.name }}
{{ object.place }}
{{ object.description }} {{ object.description }}

View File

@ -1,5 +0,0 @@
{{ object.link }}
{{ object.participation.team.name }}
{{ object.participation.team.trigram }}
{{ object.participation.problem }}
{{ object.participation.get_problem_display }}

View File

@ -12,7 +12,7 @@ from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotific
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \ TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \ TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \ TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \
TournamentPublishNotesView, TournamentUpdateView, WrittenReviewUploadView TournamentPublishNotesView, TournamentPublishSolutionsView, TournamentUpdateView, WrittenReviewUploadView
app_name = "participation" app_name = "participation"
@ -48,6 +48,8 @@ urlpatterns = [
name="tournament_notation_sheets"), name="tournament_notation_sheets"),
path("tournament/<int:pk>/notation/notifications/", GSheetNotificationsView.as_view(), path("tournament/<int:pk>/notation/notifications/", GSheetNotificationsView.as_view(),
name="tournament_gsheet_notifications"), 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(), path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(),
name="tournament_publish_notes"), name="tournament_publish_notes"),
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(), path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),

View File

@ -557,7 +557,7 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView):
if not self.get_object().valid: if not self.get_object().valid:
raise PermissionDenied(_("The team is not validated yet.")) raise PermissionDenied(_("The team is not validated yet."))
if user.registration.is_admin or user.registration.participates \ 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"] \ and user.registration.team.participation.pk == kwargs["pk"] \
or user.registration.is_volunteer \ or user.registration.is_volunteer \
and (self.get_object().tournament in user.registration.interesting_tournaments and (self.get_object().tournament in user.registration.interesting_tournaments
@ -672,7 +672,7 @@ class TournamentPaymentsView(VolunteerMixin, SingleTableMixin, DetailView):
if self.object.final: if self.object.final:
payments = Payment.objects.filter(final=True) payments = Payment.objects.filter(final=True)
else: 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') \ return payments.annotate(team_id=F('registrations__team')).order_by('-valid', 'registrations__team__trigram') \
.distinct().all() .distinct().all()
@ -747,12 +747,12 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView
return self.handle_no_permission() return self.handle_no_permission()
tournament = self.get_object() tournament = self.get_object()
reg = request.user.registration 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 self.handle_no_permission()
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get(self, 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 raise Http404
tournament = Tournament.objects.get(pk=kwargs["pk"]) 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'],)) 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): class TournamentHarmonizeView(VolunteerMixin, DetailView):
""" """
Harmonize the notes of a tournament. Harmonize the notes of a tournament.
@ -779,7 +818,7 @@ class TournamentHarmonizeView(VolunteerMixin, DetailView):
return self.handle_no_permission() return self.handle_no_permission()
tournament = self.get_object() tournament = self.get_object()
reg = request.user.registration 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 self.handle_no_permission()
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1): if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1):
raise Http404 raise Http404
@ -812,7 +851,7 @@ class TournamentHarmonizeNoteView(VolunteerMixin, DetailView):
return self.handle_no_permission() return self.handle_no_permission()
tournament = self.get_object() tournament = self.get_object()
reg = request.user.registration 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 self.handle_no_permission()
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1) \ if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1) \
or self.kwargs['action'] not in ('add', 'remove') \ or self.kwargs['action'] not in ('add', 'remove') \
@ -852,7 +891,7 @@ class SelectTeamFinalView(VolunteerMixin, DetailView):
return self.handle_no_permission() return self.handle_no_permission()
tournament = self.get_object() tournament = self.get_object()
reg = request.user.registration 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 self.handle_no_permission()
participation_qs = tournament.participations.filter(pk=self.kwargs["participation_id"]) participation_qs = tournament.participations.filter(pk=self.kwargs["participation_id"])
if not participation_qs.exists(): if not participation_qs.exists():
@ -1003,17 +1042,14 @@ class SolutionsDownloadView(VolunteerMixin, View):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
elif 'tournament_id' in kwargs: elif 'tournament_id' in kwargs:
tournament = Tournament.objects.get(pk=kwargs["tournament_id"]) tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
if reg.is_volunteer \ if reg.is_volunteer and reg in tournament.organizers_and_presidents.all():
and (tournament in reg.organized_tournaments.all()
or reg.pools_presided.filter(tournament=tournament).exists()):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
else: else:
pool = Pool.objects.get(pk=kwargs["pool_id"]) pool = Pool.objects.get(pk=kwargs["pool_id"])
tournament = pool.tournament tournament = pool.tournament
if reg.is_volunteer \ if reg.is_volunteer \
and (reg in tournament.organizers.all() and (reg in tournament.organizers_and_presidents.all()
or reg in pool.juries.all() or reg in pool.juries.all()):
or reg.pools_presided.filter(tournament=tournament).exists()):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission() return self.handle_no_permission()
@ -2001,7 +2037,7 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
reg = request.user.registration reg = request.user.registration
passage = self.get_object() passage = self.get_object()
if reg.is_admin or reg.is_volunteer \ 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 in passage.pool.juries.all()
or reg.pools_presided.filter(tournament=passage.pool.tournament).exists()) \ or reg.pools_presided.filter(tournament=passage.pool.tournament).exists()) \
or reg.participates and reg.team \ or reg.participates and reg.team \
@ -2128,8 +2164,9 @@ class NoteUpdateView(VolunteerMixin, UpdateView):
form.fields['opponent_oral'].label += f" ({self.object.passage.opponent.team.trigram})" 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_writing'].label += f" ({self.object.passage.reviewer.team.trigram})"
form.fields['reviewer_oral'].label += f" ({self.object.passage.reviewer.team.trigram})" form.fields['reviewer_oral'].label += f" ({self.object.passage.reviewer.team.trigram})"
form.fields['observer_writing'].label += f" ({self.object.passage.observer.team.trigram})" if settings.HAS_OBSERVER:
form.fields['observer_oral'].label += f" ({self.object.passage.observer.team.trigram})" 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 return form
def form_valid(self, form): def form_valid(self, form):

View File

@ -1,5 +0,0 @@
{{ object.user.last_name }}
{{ object.user.first_name }}
{{ object.user.email }}
{{ object.type }}
{{ object.role }}

View File

@ -1,11 +1,4 @@
{{ object.user.first_name }} {{ object.user.first_name }}
{{ object.user.last_name }} {{ object.user.last_name }}
{{ object.user.email }} {{ object.user.email }}
{{ object.type }}
{{ object.professional_activity }}
{{ object.address }}
{{ object.zip_code }}
{{ object.city }}
{{ object.phone_number }} {{ object.phone_number }}
{{ object.team.name }}
{{ object.team.trigram }}

View File

@ -1,16 +1,7 @@
{{ object.user.first_name }} {{ object.user.first_name }}
{{ object.user.last_name }} {{ object.user.last_name }}
{{ object.user.email }} {{ object.user.email }}
{{ object.type }}
{{ object.get_student_class_display }}
{{ object.school }}
{{ object.birth_date }}
{{ object.address }}
{{ object.zip_code }}
{{ object.city }}
{{ object.phone_number }} {{ object.phone_number }}
{{ object.responsible_name }} {{ object.responsible_name }}
{{ object.reponsible_phone }} {{ object.reponsible_phone }}
{{ object.reponsible_email }} {{ object.reponsible_email }}
{{ object.team.name }}
{{ object.team.trigram }}

View File

@ -1,5 +1,3 @@
{{ object.user.last_name }} {{ object.user.last_name }}
{{ object.user.first_name }} {{ object.user.first_name }}
{{ object.user.email }} {{ object.user.email }}
{{ object.type }}
{{ object.professional_activity }}

View File

@ -726,10 +726,11 @@ class PhotoAuthorizationView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
filename = kwargs["filename"] filename = kwargs["filename"]
path = f"media/authorization/photo/{filename}" path = f"media/authorization/photo/{filename}"
if not os.path.exists(path): 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 raise Http404
student = ParticipantRegistration.objects.get(Q(photo_authorization__endswith=filename) student = student_qs.get()
| Q(photo_authorization_final__endswith=filename))
user = request.user user = request.user
if not (student.user == user or user.registration.is_admin or user.registration.is_volunteer and student.team 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()): and student.team.participation.tournament in user.registration.organized_tournaments.all()):

View File

@ -70,7 +70,7 @@ class Survey(models.Model):
teams = Team.objects.filter(participation__valid=True) teams = Team.objects.filter(participation__valid=True)
if self.tournament: if self.tournament:
teams = teams.filter(participation__tournament=self.tournament) teams = teams.filter(participation__tournament=self.tournament)
return teams.all() return teams.order_by('participation__tournament__name', 'trigram').all()
else: else:
if self.invite_coaches: if self.invite_coaches:
registrations = ParticipantRegistration.objects.filter(team__participation__valid=True) registrations = ParticipantRegistration.objects.filter(team__participation__valid=True)
@ -78,7 +78,7 @@ class Survey(models.Model):
registrations = StudentRegistration.objects.filter(team__participation__valid=True) registrations = StudentRegistration.objects.filter(team__participation__valid=True)
if self.tournament: if self.tournament:
registrations = registrations.filter(team__participation__tournament=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 @property
def completed(self): def completed(self):

View File

@ -48,6 +48,7 @@
<thead> <thead>
<tr> <tr>
<th>{% trans "participant"|capfirst %}</th> <th>{% trans "participant"|capfirst %}</th>
<th>{% trans "tournament"|capfirst %}</th>
<th>{% trans "completed"|capfirst %}</th> <th>{% trans "completed"|capfirst %}</th>
</tr> </tr>
</thead> </thead>
@ -56,8 +57,10 @@
<tr class="{% if participant in survey.completed.all %}table-success{% else %}table-danger{% endif %}"> <tr class="{% if participant in survey.completed.all %}table-success{% else %}table-danger{% endif %}">
{% if survey.invite_team %} {% if survey.invite_team %}
<td>{% trans "Team" %} {{ participant.name }} ({{ participant.trigram }})</td> <td>{% trans "Team" %} {{ participant.name }} ({{ participant.trigram }})</td>
<td>{{ participant.participation.tournament.name }}</td>
{% else %} {% else %}
<td>{{ participant.user.first_name }} {{ participant.user.last_name }} ({% trans "team" %} {{ participant.team.trigram }})</td> <td>{{ participant.user.first_name }} {{ participant.user.last_name }} ({% trans "team" %} {{ participant.team.trigram }})</td>
<td>{{ participant.team.participation.tournament.name }}</td>
{% endif %} {% endif %}
{% if participant in survey.completed.all %} {% if participant in survey.completed.all %}
<td>{% trans "Yes" %}</td> <td>{% trans "Yes" %}</td>

View File

@ -1,9 +1,4 @@
# min hour day month weekday command # 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 # Update search index
*/2 * * * * cd /code && python manage.py update_index &> /dev/null */2 * * * * cd /code && python manage.py update_index &> /dev/null

View File

@ -213,6 +213,7 @@ STATICFILES_FINDERS = (
PIPELINE = { PIPELINE = {
'DISABLE_WRAPPER': True, 'DISABLE_WRAPPER': True,
'JS_COMPRESSOR': 'pipeline.compressors.uglifyjs.UglifyJSCompressor',
'JAVASCRIPT': { 'JAVASCRIPT': {
'main': { 'main': {
'source_filenames': ( 'source_filenames': (
@ -389,14 +390,14 @@ if TFJM_APP == "TFJM":
) )
PROBLEMS = [ PROBLEMS = [
"Triominos", "Une bonne humeur contagieuse",
"Rassemblements mathématiques", "Drôles de toboggans",
"Tournoi de ping-pong", "Plats à tarte gradués",
"Dépollution de la Seine", "Transformation de papillons",
"Électron libre", "Gerrymandering",
"Pièces truquées", "Le cauchemar de la ligne 20-25",
"Drôles de cookies", "Taxes routières",
"Création d'un jeu", "Points colorés sur un cercle",
] ]
elif TFJM_APP == "ETEAM": elif TFJM_APP == "ETEAM":
PREFERRED_LANGUAGE_CODE = 'en' PREFERRED_LANGUAGE_CODE = 'en'

View File

@ -1,9 +1,9 @@
function initModal(target, url, content_id = 'form-content') { function initModal(target, url, content_id = 'form-content', always_refetch = false) {
document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]') document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]')
.forEach(elem => elem.addEventListener('click', () => { .forEach(elem => elem.addEventListener('click', () => {
let modalBody = document.querySelector("#" + target + "Modal div.modal-body") let modalBody = document.querySelector("#" + target + "Modal div.modal-body")
if (!modalBody.innerHTML.trim()) { if (!modalBody.innerHTML.trim() || always_refetch) {
if (url instanceof Function) url = url() if (url instanceof Function) url = url()
fetch(url, {headers: {'CONTENT-ONLY': '1'}}) fetch(url, {headers: {'CONTENT-ONLY': '1'}})

View File

@ -106,7 +106,7 @@
{% if user.is_authenticated and user.registration.is_admin %} {% if user.is_authenticated and user.registration.is_admin %}
initModal("search", initModal("search",
() => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value), () => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value),
"search-results") "search-results", true)
{% endif %} {% endif %}
{% if not user.is_authenticated %} {% if not user.is_authenticated %}

View File

@ -23,7 +23,7 @@
</div> </div>
<div id="sidebar-card" class="collapse d-lg-block"> <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 %} {% for information in user.registration.important_informations %}
<div class="card my-2"> <div class="card my-2">
<div class="card-header bg-dark-subtle"> <div class="card-header bg-dark-subtle">