mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-06-21 23:18:25 +02:00
Compare commits
121 Commits
docs
...
08805a6360
Author | SHA1 | Date | |
---|---|---|---|
08805a6360
|
|||
6841659e41
|
|||
a84ffcf0a3
|
|||
203fc3cd54
|
|||
60f5236dee
|
|||
ab459ecc17
|
|||
7ad7659d78
|
|||
84eb08ec46
|
|||
3750828883
|
|||
ba36ad4071
|
|||
626433c464
|
|||
032b67ac51
|
|||
f3bd479fdc
|
|||
bc06cf4903
|
|||
6d43c4b97e
|
|||
0499885fc8
|
|||
63c96ff2d2
|
|||
efeb2628ad
|
|||
56aad288f4
|
|||
b33a69410a
|
|||
0a80e03b58
|
|||
73b94d5578
|
|||
97eea3b11a
|
|||
702c8d8c9e
|
|||
ca0601fb24
|
|||
d315c8371a
|
|||
7488d3eae1
|
|||
cfaf7c4287
|
|||
e3c216e44e
|
|||
73012bd61e
|
|||
bdf181e7e4
|
|||
c57ad854fe
|
|||
a2e5ab5f6a
|
|||
758a2c9a00
|
|||
fb10df77e5
|
|||
905b96fbcf
|
|||
be2e258948
|
|||
882570800c
|
|||
df31968a77
|
|||
df6fb3b3f3
|
|||
3807fbcf45
|
|||
8433390e19
|
|||
ec85f62ab6
|
|||
74b2a0c095
|
|||
67958335ab
|
|||
20410cc17f
|
|||
a5aff5ff21
|
|||
196dbc8275
|
|||
0847e5a308
|
|||
e5aa3ef059
|
|||
e1b4e1bb6b
|
|||
ecc59a6c8c
|
|||
b053a47a19
|
|||
ab2e49e8fb
|
|||
fe399c869d
|
|||
9de8a2ed0e
|
|||
d24f8cab16
|
|||
6cdf6331db
|
|||
65c6158b52
|
|||
4a5f48a834
|
|||
4ab706d219
|
|||
70f2be8b17
|
|||
4317947501
|
|||
f327a4c9c4
|
|||
1b24e90635
|
|||
338f0d456a
|
|||
2c4de8cec3
|
|||
6b7d52c79b
|
|||
f398bedcf3
|
|||
fdffe2331f
|
|||
42425c392d
|
|||
18f3ce4023
|
|||
620bbe7817
|
|||
12205f953b
|
|||
696863f6c3
|
|||
748720df50
|
|||
40db20a471
|
|||
2e99b3ea8e
|
|||
9721898731
|
|||
5c3b3d26c8
|
|||
d13ae89267
|
|||
44302a9ff4
|
|||
8b3f3af2b9
|
|||
dd397ae7c0
|
|||
3f2a757414
|
|||
d20d5f6266
|
|||
05a6570bed
|
|||
2a298a3ee4
|
|||
05c6333c5e
|
|||
d84db949c6
|
|||
2627b3a9b8
|
|||
2c8f6f22f2
|
|||
e258e6a337
|
|||
109748ffc6 | |||
4201a2dbe6 | |||
17c7d0ccc3
|
|||
dd45f77a5e
|
|||
eacebf1aa6
|
|||
21d4ac9d8d | |||
7c83ae8730 | |||
1977ffdbc9 | |||
a0a282df15
|
|||
603ee76664
|
|||
147cbff7f5
|
|||
8878ae8d8d
|
|||
4c8347072c
|
|||
73ea3d1717
|
|||
e026f49f8d
|
|||
ea03bd314b
|
|||
c12972b718
|
|||
2a775cedc1
|
|||
9bf3b7dff0
|
|||
cf92c78d03
|
|||
38ceef7a54
|
|||
ec2fa43e20
|
|||
85b3da09f6
|
|||
2c15774185
|
|||
08ad4f3888
|
|||
872009894d
|
|||
fd7fe90fce
|
|||
2ad538f5cc
|
@ -1,25 +1,31 @@
|
|||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- quality-assurance
|
- quality-assurance
|
||||||
|
- build
|
||||||
|
- release
|
||||||
|
|
||||||
py311:
|
variables:
|
||||||
stage: test
|
CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||||
image: python:3.11-alpine
|
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
|
||||||
before_script:
|
|
||||||
- apk add --no-cache libmagic
|
|
||||||
- apk add --no-cache gettext git # Useful for django-haystack, remove when the newer versions are in PyPI
|
|
||||||
- pip install tox --no-cache-dir
|
|
||||||
script: tox -e py311
|
|
||||||
|
|
||||||
py312:
|
py312:
|
||||||
stage: test
|
stage: test
|
||||||
image: python:3.12-alpine
|
image: python:3.12-alpine
|
||||||
before_script:
|
before_script:
|
||||||
- apk add --no-cache libmagic
|
- apk add --no-cache libmagic
|
||||||
- apk add --no-cache gettext git # Useful for django-haystack, remove when the newer versions are in PyPI
|
- apk add --no-cache gettext
|
||||||
- pip install tox --no-cache-dir
|
- pip install tox --no-cache-dir
|
||||||
script: tox -e py312
|
script: tox -e py312
|
||||||
|
|
||||||
|
py313:
|
||||||
|
stage: test
|
||||||
|
image: python:3.13-alpine
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache libmagic
|
||||||
|
- apk add --no-cache gettext
|
||||||
|
- pip install tox --no-cache-dir
|
||||||
|
script: tox -e py313
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
stage: quality-assurance
|
stage: quality-assurance
|
||||||
image: python:3-alpine
|
image: python:3-alpine
|
||||||
@ -27,3 +33,29 @@ linters:
|
|||||||
- pip install tox --no-cache-dir
|
- pip install tox --no-cache-dir
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
|
||||||
|
build-image:
|
||||||
|
image: docker
|
||||||
|
stage: build
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
before_script:
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
||||||
|
script:
|
||||||
|
- docker build --pull -t $CONTAINER_TEST_IMAGE .
|
||||||
|
- docker push $CONTAINER_TEST_IMAGE
|
||||||
|
|
||||||
|
release-image:
|
||||||
|
image: docker
|
||||||
|
stage: release
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
before_script:
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
||||||
|
script:
|
||||||
|
- docker pull $CONTAINER_TEST_IMAGE
|
||||||
|
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
|
||||||
|
- docker push $CONTAINER_RELEASE_IMAGE
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main"
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
FROM python:3.12-alpine
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
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 libxml2-dev libxslt-dev npm postgresql-dev libmagic texlive texmf-dist-latexextra
|
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
|
||||||
|
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
@ -36,4 +37,4 @@ RUN ln -s /code/.bashrc /root/.bashrc
|
|||||||
ENTRYPOINT ["/code/entrypoint.sh"]
|
ENTRYPOINT ["/code/entrypoint.sh"]
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["./manage.py", "shell_plus", "--ipython"]
|
CMD ["./manage.py", "shell"]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Copyright (C) 2024 by Animath
|
# Copyright (C) 2024 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.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.utils.translation import activate
|
from django.utils.translation import activate
|
||||||
from participation.models import Team, Tournament
|
from participation.models import Team, Tournament
|
||||||
@ -18,7 +19,7 @@ class Command(BaseCommand):
|
|||||||
help = "Create chat channels for tournaments and teams."
|
help = "Create chat channels for tournaments and teams."
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
activate('fr')
|
activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||||
|
|
||||||
# Création de canaux généraux, d'annonces, d'aide jurys et orgas, etc.
|
# Création de canaux généraux, d'annonces, d'aide jurys et orgas, etc.
|
||||||
# Le canal d'annonces est accessibles à tous⋅tes, mais seul⋅es les admins peuvent y écrire.
|
# Le canal d'annonces est accessibles à tous⋅tes, mais seul⋅es les admins peuvent y écrire.
|
||||||
|
17
chat/static/tfjm/chat_eteam.webmanifest
Normal file
17
chat/static/tfjm/chat_eteam.webmanifest
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"background_color": "white",
|
||||||
|
"description": "Chat for ETEAM",
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/static/tfjm/img/eteam.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "ETEAM Chat",
|
||||||
|
"short_name": "ETEAM Chat",
|
||||||
|
"start_url": "/chat/fullscreen",
|
||||||
|
"theme_color": "black"
|
||||||
|
}
|
@ -6,7 +6,11 @@
|
|||||||
|
|
||||||
{% block extracss %}
|
{% block extracss %}
|
||||||
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
|
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
|
||||||
<link rel="manifest" href="{% static "tfjm/chat.webmanifest" %}">
|
{% if TFJM.APP == "TFJM" %}
|
||||||
|
<link rel="manifest" href="{% static "tfjm/chat_tfjm.webmanifest" %}">
|
||||||
|
{% elif TFJM.APP == "ETEAM" %}
|
||||||
|
<link rel="manifest" href="{% static "tfjm/chat_eteam.webmanifest" %}">
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content-title %}{% endblock %}
|
{% block content-title %}{% endblock %}
|
||||||
|
@ -6,23 +6,35 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<title>
|
{% if TFJM.APP == "TFJM" %}
|
||||||
{% trans "TFJM² Chat" %}
|
<title>{% trans "TFJM² Chat" %}</title>
|
||||||
</title>
|
<meta name="description" content="{% trans "TFJM² Chat" %}">
|
||||||
<meta name="description" content="{% trans "TFJM² Chat" %}">
|
{% elif TFJM.APP == "ETEAM" %}
|
||||||
|
<title>{% trans "ETEAM Chat" %}</title>
|
||||||
|
<meta name="description" content="{% trans "ETEAM Chat" %}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Favicon #}
|
{# Favicon #}
|
||||||
<link rel="shortcut icon" href="{% static "favicon.ico" %}">
|
<link rel="shortcut icon" href="{% static "favicon.ico" %}">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
{# Bootstrap + Font Awesome CSS #}
|
{# Bootstrap CSS #}
|
||||||
{% stylesheet 'bootstrap_fontawesome' %}
|
<link href="{% static "bootstrap/css/bootstrap.min.css" %}" rel="stylesheet" type="text/css">
|
||||||
|
{# Fontawesome CSS #}
|
||||||
|
<link href="{% static "fontawesome/css/all.min.css" %}" rel="stylesheet" type="text/css">
|
||||||
|
<link href="{% static "fontawesome/css/v4-shims.css" %}">
|
||||||
|
{# bootstrap-select CSS #}
|
||||||
|
<link href="{% static "bootstrap-select/css/bootstrap-select.min.css" %}" rel="stylesheet" type="text/css">
|
||||||
|
|
||||||
{# Bootstrap JavaScript #}
|
{# Bootstrap JavaScript #}
|
||||||
{% javascript 'bootstrap' %}
|
<script type="application/javascript" src="{% static "bootstrap/js/bootstrap.bundle.min.js" %}" charset="utf-8"></script>
|
||||||
|
|
||||||
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
|
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
|
||||||
<link rel="manifest" href="{% static "tfjm/chat.webmanifest" %}">
|
{% if TFJM.APP == "TFJM" %}
|
||||||
|
<link rel="manifest" href="{% static "tfjm/chat_tfjm.webmanifest" %}">
|
||||||
|
{% elif TFJM.APP == "ETEAM" %}
|
||||||
|
<link rel="manifest" href="{% static "tfjm/chat_eteam.webmanifest" %}">
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex w-100 h-100 flex-column">
|
<body class="d-flex w-100 h-100 flex-column">
|
||||||
{% include "chat/content.html" with fullscreen=True %}
|
{% include "chat/content.html" with fullscreen=True %}
|
||||||
|
@ -7,22 +7,29 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<title>
|
<title>
|
||||||
{% trans "TFJM² Chat" %} - {% trans "Log in" %}
|
{% trans "Chat" %} - {% trans "Log in" %}
|
||||||
</title>
|
</title>
|
||||||
<meta name="description" content="{% trans "TFJM² Chat" %}">
|
<meta name="description" content="{% trans "Chat" %}">
|
||||||
|
|
||||||
{# Favicon #}
|
{# Favicon #}
|
||||||
<link rel="shortcut icon" href="{% static "favicon.ico" %}">
|
<link rel="shortcut icon" href="{% static "favicon.ico" %}">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
{# Bootstrap CSS #}
|
{# Bootstrap CSS #}
|
||||||
{% stylesheet 'bootstrap_fontawesome' %}
|
<link href="{% static "bootstrap/css/bootstrap.min.css" %}" rel="stylesheet" type="text/css">
|
||||||
|
{# Fontawesome CSS #}
|
||||||
|
<link href="{% static "fontawesome/css/all.min.css" %}" rel="stylesheet" type="text/css">
|
||||||
|
<link href="{% static "fontawesome/css/v4-shims.css" %}">
|
||||||
|
|
||||||
{# Bootstrap JavaScript #}
|
{# Bootstrap JavaScript #}
|
||||||
{% javascript 'bootstrap' %}
|
<script type="application/javascript" src="{% static "bootstrap/js/bootstrap.bundle.min.js" %}" charset="utf-8"></script>
|
||||||
|
|
||||||
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
|
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
|
||||||
<link rel="manifest" href="{% static "tfjm/chat.webmanifest" %}">
|
{% if TFJM.APP == "TFJM" %}
|
||||||
|
<link rel="manifest" href="{% static "tfjm/chat_tfjm.webmanifest" %}">
|
||||||
|
{% elif TFJM.APP == "ETEAM" %}
|
||||||
|
<link rel="manifest" href="{% static "tfjm/chat_eteam.webmanifest" %}">
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex w-100 h-100 flex-column">
|
<body class="d-flex w-100 h-100 flex-column">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
211
docs/dev/transition.rst
Normal file
211
docs/dev/transition.rst
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
Transition d'années
|
||||||
|
===================
|
||||||
|
|
||||||
|
Entre deux sessions du TFJM², certaines opérations doivent être effectuées chaque année,
|
||||||
|
afin de réinitialiser les données et de passer à l'année suivante.
|
||||||
|
|
||||||
|
Réinitialisation de la base de données
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Conservation des autorisations de droit à l'image
|
||||||
|
"""""""""""""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
La base de données du TFJM² est supprimée chaque année, avant chaque tournoi. Il n'y a
|
||||||
|
pas de conservation de données personnelles à l'exception des autorisations de droit
|
||||||
|
à l'image qui doivent être conservées pour des raisons légales pendant 5 ans.
|
||||||
|
|
||||||
|
Elles doivent alors être stockées sur Owncloud. Pour cela, il faut commencer par créer
|
||||||
|
un dossier dans Owncloud, qui stockera lesdites autorisations.
|
||||||
|
|
||||||
|
Rendez-vous ensuite dans le conteneur Docker et exécuter le script :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
./manage.py export_photo_authorizations
|
||||||
|
|
||||||
|
Cela a pour effet de générer un dossier dans ``output/photo_authorizations``, qui contient
|
||||||
|
un dossier par équipe avec les différentes autorisations de droit à l'image.
|
||||||
|
|
||||||
|
Il faut maintenant récupérer ce dossier. Sortir du conteneur, et exécuter dans ``/srv/TFJM`` :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
sudo docker cp tfjm-inscription-1:/code/output/photo_authorizations .
|
||||||
|
sudo mv photo_authorizations/* "data/owncloud/data/Emmy/files/Autorisations de droit à l'image/Autorisations de droit à l'image 2024/"
|
||||||
|
sudo chown -R www-data:root "data/owncloud/data/Emmy/files/Autorisations de droit à l'image/Autorisations de droit à l'image 2024"
|
||||||
|
sudo rmdir photo_authorizations
|
||||||
|
|
||||||
|
Il faut enfin réactualiser Owncloud. Exécuter en tant que www-data :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
sudo docker compose exec -u www-data cloud php occ files:scan Emmy
|
||||||
|
|
||||||
|
Vérifiez enfin que les fichiers sont bien accessibles dans l'interface Web.
|
||||||
|
Ne pas oublier enfin de partager le dossier.
|
||||||
|
|
||||||
|
|
||||||
|
Sauvegarde de secours
|
||||||
|
"""""""""""""""""""""
|
||||||
|
|
||||||
|
Si les données doivent être supprimées, il peut être utile de réaliser une sauvegarde à conserver
|
||||||
|
quelques mois.
|
||||||
|
|
||||||
|
.. danger::
|
||||||
|
|
||||||
|
Cette sauvegarde ne doit être faite qu'à des fins utiles et supprimée dès que plus nécessaire.
|
||||||
|
|
||||||
|
Sauvegardez alors le dossier ``/srv/TFJM/data/inscription/media`` et exportez la base de données :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
sudo cp -r data/inscription/media data/inscription/media-2024
|
||||||
|
sudo docker compose exec -u postgres postgres pg_dump inscription_tfjm | sudo tee inscription_tfjm_bkp_2024.sql > /dev/null
|
||||||
|
|
||||||
|
|
||||||
|
Réinitialisation effective
|
||||||
|
""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Il est désormais possible de réinitialiser la base de données, après avoir éteint le serveur :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
sudo docker compose stop inscription
|
||||||
|
sudo rm -r data/inscription/media/*
|
||||||
|
sudo docker compose exec -u postgres postgres dropdb inscription_tfjm
|
||||||
|
sudo docker compose exec -u postgres postgres createdb -O inscription_tfjm inscription_tfjm
|
||||||
|
|
||||||
|
Redémarrez enfin le serveur (les migrations seront créées automatiquement)
|
||||||
|
et créez un nouveau compte administrateur⋅rice :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
sudo docker compose up -d inscription
|
||||||
|
sudo docker compose exec inscription bash
|
||||||
|
./manage.py createsuperuser
|
||||||
|
|
||||||
|
Vérifiez finalement le bon fonctionnement du site.
|
||||||
|
|
||||||
|
|
||||||
|
Sites Django
|
||||||
|
""""""""""""
|
||||||
|
|
||||||
|
Après avoir réinitialisé les données, il faut mettre à jour le site Django, qui permettra
|
||||||
|
d'avoir notamment des noms de domaine correct dans les mails envoyés.
|
||||||
|
|
||||||
|
Se connecter alors sur le site réouvert, puis dans la partie « Administration », chercher la
|
||||||
|
section « Sites » et modifier l'unique site présent. Vous pouvez ensuite effectuer les modifications
|
||||||
|
à réaliser.
|
||||||
|
|
||||||
|
|
||||||
|
Nouveaux paramètres pour la nouvelle année
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
Certains paramètres doivent être modifiés pour prendre en compte la nouvelle année.
|
||||||
|
|
||||||
|
Dates d'inscription
|
||||||
|
"""""""""""""""""""
|
||||||
|
|
||||||
|
Les inscriptions sont permises uniquement entre l'ouverture et la fermeture, afin d'éviter
|
||||||
|
d'avoir des personnes s'inscrivant en dehors du TFJM².
|
||||||
|
|
||||||
|
Pour cela, dans votre projet local, rendez-vous dans ``tfjm/settings.py`` et cherchez
|
||||||
|
le paramètre ``REGISTRATION_DATES`` (pour le TFJM²). Modifiez alors les sous-paramètres
|
||||||
|
``open`` et ``close`` pour définir les dates pendant lesquelles les inscriptions des
|
||||||
|
participant⋅es sont permises pour cette nouvelle année. Elles doivent être au format ISO.
|
||||||
|
|
||||||
|
Exemple pour l'année 2025 où les inscriptions ouvrent au 8 janvier midi pour fermer
|
||||||
|
le 2 mars à 22h :
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
REGISTRATION_DATES = dict(
|
||||||
|
open=datetime.fromisoformat("2025-01-15T12:00:00+0100"),
|
||||||
|
close=datetime.fromisoformat("2025-03-02T22:00:00+0100"),
|
||||||
|
)
|
||||||
|
|
||||||
|
Il faudra ensuite commiter la modification et redémarrer le serveur pour que la modification
|
||||||
|
prenne effet.
|
||||||
|
|
||||||
|
|
||||||
|
Noms des problèmes
|
||||||
|
""""""""""""""""""
|
||||||
|
|
||||||
|
Toujours dans la configuration dans ``tfjm/settings.py``, la liste des problèmes doit être
|
||||||
|
modifiée pour que leurs noms s'affichent correctement lors du tirage au sort.
|
||||||
|
|
||||||
|
Cherchez le paramètre ``PROBLEMS`` et mettez alors à jour la liste, dans l'ordre, des noms
|
||||||
|
des problèmes.
|
||||||
|
|
||||||
|
À nouveau, il est nécessaire de commiter la modification et redémarrer le serveur.
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
« 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>`_
|
||||||
|
|
||||||
|
|
||||||
|
À la fin du tournoi
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Lorsque le tournoi est terminé, il faut récupérer les informations à stocker de façon pérenne,
|
||||||
|
notamment les solutions des équipes, les résultats ainsi que les autorisation de droit à l'image
|
||||||
|
comme indiqué précédemment.
|
||||||
|
|
||||||
|
Conservation des autorisations de droit à l'image
|
||||||
|
"""""""""""""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Se référer à la section plus haut.
|
||||||
|
|
||||||
|
|
||||||
|
Conservation des solutions des équipes
|
||||||
|
""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Le processus est très similaire à la conservation des autorisations de droit à l'image.
|
||||||
|
Il faut d'abord, dans le conteneur, lancer le script dédié pour récupérer les solutions
|
||||||
|
dans ``/code/output/solutions`` :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
./manage.py export_solutions
|
||||||
|
|
||||||
|
On sort du conteneur et on récupère les solutions pour les déplacer dans Owncloud :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
sudo docker cp tfjm-inscription-1:/code/output/solutions .
|
||||||
|
sudo mv solutions/* "data/owncloud/data/Emmy/files/Solutions écrites 2024/"
|
||||||
|
sudo chown -R www-data:root "data/owncloud/data/Emmy/files/Solutions écrites 2024"
|
||||||
|
sudo rmdir solutions
|
||||||
|
|
||||||
|
Il faut enfin réactualiser Owncloud. Exécuter en tant que www-data :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
sudo docker compose exec -u www-data cloud php occ files:scan Emmy
|
||||||
|
|
||||||
|
Vérifiez enfin que les fichiers sont bien accessibles dans l'interface Web.
|
||||||
|
Ne pas oublier enfin de partager le dossier.
|
||||||
|
|
||||||
|
|
||||||
|
Génération de la page de résultats Wordpress
|
||||||
|
""""""""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Pour finir, il est possible de récupérer les notes pour chaque tournoi afin de générer
|
||||||
|
la page Wordpress dans la section *Éditions précédentes*.
|
||||||
|
|
||||||
|
Il suffit de lancer le script ``./manage.py export_results``, qui donne le texte brut pour
|
||||||
|
Wordpress à ajouter sur la page de l'édition qui vient de se terminer dans l'onglet
|
||||||
|
*Éditions précédentes*.
|
||||||
|
|
||||||
|
Pensez à bien inclure sur cette page le lien vers les problèmes de l'année, ainsi que le
|
||||||
|
lien vers le dossier partagé dans le Owncloud concernant les solutions des équipes.
|
||||||
|
|
||||||
|
Assurez-vous de mettre à jour la page *Éditions précédentes* afin d'inclure le lien vers
|
||||||
|
la page nouvellement créée.
|
@ -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
|
||||||
|
@ -21,3 +21,4 @@ administrateur⋅rice.
|
|||||||
|
|
||||||
dev/index
|
dev/index
|
||||||
dev/install
|
dev/install
|
||||||
|
dev/transition
|
||||||
|
@ -122,6 +122,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
|
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
|
||||||
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
||||||
|
|
||||||
|
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||||
|
|
||||||
match content['type']:
|
match content['type']:
|
||||||
case 'set_language':
|
case 'set_language':
|
||||||
# Update the translation language
|
# Update the translation language
|
||||||
@ -183,7 +185,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Create the draw
|
# Create the draw
|
||||||
draw = await Draw.objects.acreate(tournament=self.tournament)
|
draw = await Draw.objects.acreate(tournament=self.tournament)
|
||||||
r1 = None
|
r1 = None
|
||||||
for i in [1, 2]:
|
for i in range(1, settings.NB_ROUNDS + 1):
|
||||||
# Create the round
|
# Create the round
|
||||||
r = await Round.objects.acreate(draw=draw, number=i)
|
r = await Round.objects.acreate(draw=draw, number=i)
|
||||||
if i == 1:
|
if i == 1:
|
||||||
@ -222,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()})
|
||||||
@ -233,8 +235,8 @@ 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': "Le tirage au sort du tournoi de "
|
'body': str(_("The draw of tournament {tournament} started!"))
|
||||||
f"{self.tournament.name} a commencé !"})
|
.format(tournament=self.tournament.name)})
|
||||||
|
|
||||||
async def draw_start(self, content) -> None:
|
async def draw_start(self, content) -> None:
|
||||||
"""
|
"""
|
||||||
@ -403,21 +405,21 @@ 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': 'Votre score de dé est identique à celui de une ou plusieurs équipes. '
|
'body': str(_("Your dice score is identical to the one of one or multiple teams. "
|
||||||
'Veuillez le relancer.'}
|
"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
|
||||||
|
|
||||||
return error
|
return error
|
||||||
|
|
||||||
async def process_dice_select_poules(self):
|
async def process_dice_select_poules(self): # noqa: C901
|
||||||
"""
|
"""
|
||||||
Called when all teams launched their dice.
|
Called when all teams launched their dice.
|
||||||
Place teams into pools and order their passage.
|
Place teams into pools and order their passage.
|
||||||
@ -448,7 +450,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# We can add a joker team if there is not already a team in the pool that was in the same pool
|
# We can add a joker team if there is not already a team in the pool that was in the same pool
|
||||||
# in the first round, and such that the number of such jokers is exactly the free space of the current pool.
|
# in the first round, and such that the number of such jokers is exactly the free space of the current pool.
|
||||||
# Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool.
|
# Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool.
|
||||||
if not self.tournament.final:
|
if not self.tournament.final and settings.TFJM_APP == "TFJM":
|
||||||
tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,))
|
tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,))
|
||||||
jokers = [td for td in tds if td.passage_index == 4]
|
jokers = [td for td in tds if td.passage_index == 4]
|
||||||
round2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
round2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
||||||
@ -502,12 +504,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
await self.tournament.draw.current_round.asave()
|
await self.tournament.draw.current_round.asave()
|
||||||
|
|
||||||
# Display dice result in the header of the information alert
|
# Display dice result in the header of the information alert
|
||||||
msg = "Les résultats des dés sont les suivants : "
|
trigrams = ", ".join(f"<strong>{td.participation.team.trigram}</strong> ({td.passage_dice})" for td in tds)
|
||||||
msg += ", ".join(f"<strong>{td.participation.team.trigram}</strong> ({td.passage_dice})" for td in tds)
|
msg = _("The dice results are the following: {trigrams}. "
|
||||||
msg += ". L'ordre de passage et les compositions des différentes poules sont affiché⋅es sur le côté. "
|
"The passage order and the compositions of the different pools are displayed on the side. "
|
||||||
msg += "Les ordres de passage pour le premier tour sont déterminés à partir des scores des dés, "
|
"The passage orders for the first round are determined from the dice scores, in increasing order. "
|
||||||
msg += "dans l'ordre croissant. Pour le deuxième tour, les ordres de passage sont déterminés à partir "
|
"For the second round, the passage orders are determined from the passage orders of the first round.") \
|
||||||
msg += "des ordres de passage du premier tour."
|
.format(trigrams=trigrams)
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
@ -531,18 +533,18 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||||
'visible': True})
|
'visible': True})
|
||||||
|
|
||||||
# First send the second pool to have the good team order
|
# First send the pools of next rounds to have the good team order
|
||||||
r2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
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': r2.number,
|
'round': next_round.number,
|
||||||
'poules': [
|
'poules': [
|
||||||
{
|
{
|
||||||
'letter': pool.get_letter_display(),
|
'letter': pool.get_letter_display(),
|
||||||
'teams': await pool.atrigrams(),
|
'teams': await pool.atrigrams(),
|
||||||
}
|
}
|
||||||
async for pool in r2.pool_set.order_by('letter').all()
|
async for pool in next_round.pool_set.order_by('letter').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': r.number,
|
||||||
@ -610,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': "À votre tour !",
|
'title': str(_("Your turn!")),
|
||||||
'body': "C'est à vous de tirer un nouveau problème !"})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
|
|
||||||
async def select_problem(self, **kwargs):
|
async def select_problem(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -631,7 +633,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
.prefetch_related('team').aget()
|
.prefetch_related('team').aget()
|
||||||
# Ensure that the user can draws a problem at this time
|
# Ensure that the user can draws a problem at this time
|
||||||
if participation.id != td.participation_id:
|
if participation.id != td.participation_id:
|
||||||
return await self.alert("This is not your turn.", 'danger')
|
return await self.alert(_("This is not your turn."), 'danger')
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Choose a random problem
|
# Choose a random problem
|
||||||
@ -702,19 +704,20 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
.prefetch_related('team').aget()
|
.prefetch_related('team').aget()
|
||||||
# Ensure that the user can accept a problem at this time
|
# Ensure that the user can accept a problem at this time
|
||||||
if participation.id != td.participation_id:
|
if participation.id != td.participation_id:
|
||||||
return await self.alert("This is not your turn.", 'danger')
|
return await self.alert(_("This is not your turn."), 'danger')
|
||||||
|
|
||||||
td.accepted = td.purposed
|
td.accepted = td.purposed
|
||||||
td.purposed = None
|
td.purposed = None
|
||||||
await td.asave()
|
await td.asave()
|
||||||
|
|
||||||
trigram = td.participation.team.trigram
|
trigram = td.participation.team.trigram
|
||||||
msg = f"L'équipe <strong>{trigram}</strong> a accepté le problème <strong>{td.accepted} : " \
|
msg = _("The team <strong>{trigram}</strong> accepted the problem <string>{problem}</strong>: "
|
||||||
f"{settings.PROBLEMS[td.accepted - 1]}</strong>. "
|
"{problem_name}. ").format(trigram=trigram, problem=td.accepted,
|
||||||
|
problem_name=settings.PROBLEMS[td.accepted - 1])
|
||||||
if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2:
|
if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2:
|
||||||
msg += "Une équipe peut encore l'accepter."
|
msg += _("One team more can accept this problem.")
|
||||||
else:
|
else:
|
||||||
msg += "Plus personne ne peut l'accepter."
|
msg += _("No team can accept this problem anymore.")
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
@ -749,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': "À votre tour !",
|
'title': str(_("Your turn!")),
|
||||||
'body': "C'est à vous de tirer un nouveau problème !"})
|
'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)
|
||||||
@ -808,8 +811,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'problems': [td.accepted async for td in pool.team_draws],
|
'problems': [td.accepted async for td in pool.team_draws],
|
||||||
})
|
})
|
||||||
|
|
||||||
msg += f"<br><br>Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \
|
msg += "<br><br>" + _("The draw of the pool {pool} is ended. The summary is below.") \
|
||||||
f"Le tableau récapitulatif est en bas."
|
.format(pool=f"{pool.get_letter_display()}{r.number}")
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
@ -826,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': "À votre tour !",
|
'title': str(_("Your turn!")),
|
||||||
'body': "C'est à vous de lancer le dé !"})
|
'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',
|
||||||
@ -843,11 +846,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
"""
|
"""
|
||||||
msg = self.tournament.draw.last_message
|
msg = self.tournament.draw.last_message
|
||||||
|
|
||||||
if r.number == 1 and not self.tournament.final:
|
if r.number < settings.NB_ROUNDS and not self.tournament.final and settings.TFJM_APP == "TFJM":
|
||||||
# Next round
|
# Next round
|
||||||
r2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
next_round = await self.tournament.draw.round_set.filter(number=r.number + 1).aget()
|
||||||
self.tournament.draw.current_round = r2
|
self.tournament.draw.current_round = next_round
|
||||||
msg += "<br><br>Le tirage au sort du tour 1 est terminé."
|
msg += "<br><br>" + _("The draw of the round {round} is ended.").format(round=r.number)
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
@ -860,26 +863,26 @@ 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': "À votre tour !",
|
'title': str(_("Your turn!")),
|
||||||
'body': "C'est à vous de lancer le dé !"})
|
'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}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
||||||
'round': r2.number,
|
'round': next_round.number,
|
||||||
'poules': [
|
'poules': [
|
||||||
{
|
{
|
||||||
'letter': pool.get_letter_display(),
|
'letter': pool.get_letter_display(),
|
||||||
'teams': await pool.atrigrams(),
|
'teams': await pool.atrigrams(),
|
||||||
}
|
}
|
||||||
async for pool in r2.pool_set.order_by('letter').all()
|
async for pool in next_round.pool_set.order_by('letter').all()
|
||||||
]})
|
]})
|
||||||
|
|
||||||
# The passage order for the second round is already determined by the first round
|
# The passage order for the second round is already determined by the first round
|
||||||
# Start the first pool of the second round
|
# Start the first pool of the second round
|
||||||
p1: Pool = await r2.pool_set.filter(letter=1).aget()
|
p1: Pool = await next_round.pool_set.filter(letter=1).aget()
|
||||||
r2.current_pool = p1
|
next_round.current_pool = p1
|
||||||
await r2.asave()
|
await next_round.asave()
|
||||||
|
|
||||||
async for td in p1.teamdraw_set.prefetch_related('participation__team').all():
|
async for td in p1.teamdraw_set.prefetch_related('participation__team').all():
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
@ -888,9 +891,9 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||||
'visible': True})
|
'visible': True})
|
||||||
elif r.number == 1 and self.tournament.final:
|
elif r.number == 1 and (self.tournament.final or not settings.HAS_FINAL):
|
||||||
# For the final tournament, we wait for a manual update between the two rounds.
|
# For the final tournament, we wait for a manual update between the two rounds.
|
||||||
msg += "<br><br>Le tirage au sort du tour 1 est terminé."
|
msg += "<br><br>" + _("The draw of the first round is ended.")
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
@ -919,7 +922,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
.prefetch_related('team').aget()
|
.prefetch_related('team').aget()
|
||||||
# Ensure that the user can reject a problem at this time
|
# Ensure that the user can reject a problem at this time
|
||||||
if participation.id != td.participation_id:
|
if participation.id != td.participation_id:
|
||||||
return await self.alert("This is not your turn.", 'danger')
|
return await self.alert(_("This is not your turn."), 'danger')
|
||||||
|
|
||||||
# Add the problem to the rejected problems list
|
# Add the problem to the rejected problems list
|
||||||
problem = td.purposed
|
problem = td.purposed
|
||||||
@ -929,19 +932,20 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
td.purposed = None
|
td.purposed = None
|
||||||
await td.asave()
|
await td.asave()
|
||||||
|
|
||||||
remaining = len(settings.PROBLEMS) - 5 - len(td.rejected)
|
remaining = len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT - len(td.rejected)
|
||||||
|
|
||||||
# Update messages
|
# Update messages
|
||||||
trigram = td.participation.team.trigram
|
trigram = td.participation.team.trigram
|
||||||
msg = f"L'équipe <strong>{trigram}</strong> a refusé le problème <strong>{problem} : " \
|
msg = _("The team <strong>{trigram}</strong> refused the problem <strong>{problem}</strong>: "
|
||||||
f"{settings.PROBLEMS[problem - 1]}</strong>. "
|
"{problem_name}.").format(trigram=trigram, problem=problem,
|
||||||
|
problem_name=settings.PROBLEMS[problem - 1]) + " "
|
||||||
if remaining >= 0:
|
if remaining >= 0:
|
||||||
msg += f"Il lui reste {remaining} refus sans pénalité."
|
msg += _("It remains {remaining} refusals without penalty.").format(remaining=remaining)
|
||||||
else:
|
else:
|
||||||
if already_refused:
|
if already_refused:
|
||||||
msg += "Cela n'ajoute pas de pénalité."
|
msg += _("This problem was already refused by this team.")
|
||||||
else:
|
else:
|
||||||
msg += "Cela ajoute une pénalité de 25 % sur le coefficient de l'oral de la défense."
|
msg += _("It adds a 25% penalty on the coefficient of the oral defense.")
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
@ -984,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': "À votre tour !",
|
'title': str(_("Your turn!")),
|
||||||
'body': "C'est à vous de tirer un nouveau problème !"})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
|
|
||||||
@ensure_orga
|
@ensure_orga
|
||||||
async def export(self, **kwargs):
|
async def export(self, **kwargs):
|
||||||
@ -1017,44 +1021,49 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
||||||
return await self.alert(_("The draw has not started yet."), 'danger')
|
return await self.alert(_("The draw has not started yet."), 'danger')
|
||||||
|
|
||||||
if not self.tournament.final:
|
if not self.tournament.final and settings.TFJM_APP == "TFJM":
|
||||||
return await self.alert(_("This is only available for the final tournament."), 'danger')
|
return await self.alert(_("This is only available for the final tournament."), 'danger')
|
||||||
|
|
||||||
r2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
r2 = await self.tournament.draw.round_set.filter(number=self.tournament.draw.current_round.number + 1).aget()
|
||||||
self.tournament.draw.current_round = r2
|
self.tournament.draw.current_round = r2
|
||||||
msg = "Le tirage au sort pour le tour 2 va commencer. " \
|
if settings.TFJM_APP == "TFJM":
|
||||||
"L'ordre de passage est déterminé à partir du classement du premier tour, " \
|
msg = str(_("The draw of the round {round} is starting. "
|
||||||
"de sorte à mélanger les équipes entre les deux jours."
|
"The passage order is determined from the ranking of the first round, "
|
||||||
|
"in order to mix the teams between the two days.").format(round=r2.number))
|
||||||
|
else:
|
||||||
|
msg = str(_("The draw of the round {round} is starting. "
|
||||||
|
"The passage order is another time randomly drawn.").format(round=r2.number))
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
# 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': 'Tirage au sort du TFJM²',
|
'title': str(_("Draw")) + " " + settings.APP_NAME,
|
||||||
'body': "Le tirage au sort pour le second tour de la finale a commencé !"})
|
'body': str(_("The draw of the second round is starting!"))})
|
||||||
|
|
||||||
# Set the first pool of the second round as the active pool
|
if settings.TFJM_APP == "TFJM":
|
||||||
pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
|
# Set the first pool of the second round as the active pool
|
||||||
r2.current_pool = pool
|
pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
|
||||||
await r2.asave()
|
r2.current_pool = pool
|
||||||
|
await r2.asave()
|
||||||
|
|
||||||
# Fetch notes from the first round
|
# Fetch notes from the first round
|
||||||
notes = dict()
|
notes = dict()
|
||||||
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
|
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
|
||||||
notes[participation] = sum([await pool.aaverage(participation)
|
notes[participation] = sum([await pool.aaverage(participation)
|
||||||
async for pool in self.tournament.pools.filter(participations=participation)
|
async for pool in self.tournament.pools.filter(participations=participation)
|
||||||
.prefetch_related('passages')])
|
.prefetch_related('passages')])
|
||||||
# Sort notes in a decreasing order
|
# Sort notes in a decreasing order
|
||||||
ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x])
|
ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x])
|
||||||
# Define pools and passage orders from the ranking of the first round
|
# Define pools and passage orders from the ranking of the first round
|
||||||
async for pool in r2.pool_set.order_by('letter').all():
|
async for pool in r2.pool_set.order_by('letter').all():
|
||||||
for i in range(pool.size):
|
for i in range(pool.size):
|
||||||
participation = ordered_participations.pop(0)
|
participation = ordered_participations.pop(0)
|
||||||
td = await TeamDraw.objects.aget(round=r2, participation=participation)
|
td = await TeamDraw.objects.aget(round=r2, participation=participation)
|
||||||
td.pool = pool
|
td.pool = pool
|
||||||
td.passage_index = i
|
td.passage_index = i
|
||||||
await td.asave()
|
await td.asave()
|
||||||
|
|
||||||
# Send pools to users
|
# Send pools to users
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
@ -1074,16 +1083,22 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
f"tournament-{self.tournament.id}",
|
f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
|
{'tid': self.tournament_id, 'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
|
||||||
|
|
||||||
async for td in r2.current_pool.team_draws.prefetch_related('participation__team'):
|
if settings.TFJM_APP == "TFJM":
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
async for td in r2.current_pool.team_draws.prefetch_related('participation__team'):
|
||||||
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
'visible': True})
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||||
|
'visible': True})
|
||||||
|
|
||||||
# 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': "À votre tour !",
|
'title': str(_("Your turn!")),
|
||||||
'body': "C'est à vous de tirer un nouveau problème !"})
|
'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}",
|
||||||
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||||
|
'visible': True})
|
||||||
|
|
||||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||||
@ -1098,7 +1113,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.set_active',
|
{'tid': self.tournament_id, 'type': 'draw.set_active',
|
||||||
'round': r2.number,
|
'round': r2.number,
|
||||||
'pool': r2.current_pool.get_letter_display()})
|
'pool': r2.current_pool.get_letter_display() if r2.current_pool else None})
|
||||||
|
|
||||||
@ensure_orga
|
@ensure_orga
|
||||||
async def cancel_last_step(self, **kwargs):
|
async def cancel_last_step(self, **kwargs):
|
||||||
@ -1372,32 +1387,21 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'round': r.number,
|
'round': r.number,
|
||||||
'team': td.participation.team.trigram,
|
'team': td.participation.team.trigram,
|
||||||
'problem': td.accepted})
|
'problem': td.accepted})
|
||||||
elif r.number == 2:
|
elif r.number >= 2 and settings.TFJM_APP == "TFJM":
|
||||||
if not self.tournament.final:
|
if not self.tournament.final:
|
||||||
# Go to the previous round
|
# Go to the previous round
|
||||||
r1 = await self.tournament.draw.round_set \
|
previous_round = await self.tournament.draw.round_set \
|
||||||
.prefetch_related('current_pool__current_team__participation__team').aget(number=1)
|
.prefetch_related('current_pool__current_team__participation__team').aget(number=r.number - 1)
|
||||||
self.tournament.draw.current_round = r1
|
self.tournament.draw.current_round = previous_round
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
async for td in r1.team_draws.prefetch_related('participation__team').all():
|
async for td in previous_round.team_draws.prefetch_related('participation__team').all():
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
||||||
'team': td.participation.team.trigram,
|
'team': td.participation.team.trigram,
|
||||||
'result': td.choice_dice})
|
'result': td.choice_dice})
|
||||||
|
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
previous_pool = previous_round.current_pool
|
||||||
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
|
||||||
'round': r1.number,
|
|
||||||
'poules': [
|
|
||||||
{
|
|
||||||
'letter': pool.get_letter_display(),
|
|
||||||
'teams': await pool.atrigrams(),
|
|
||||||
}
|
|
||||||
async for pool in r1.pool_set.order_by('letter').all()
|
|
||||||
]})
|
|
||||||
|
|
||||||
previous_pool = r1.current_pool
|
|
||||||
|
|
||||||
td = previous_pool.current_team
|
td = previous_pool.current_team
|
||||||
td.purposed = td.accepted
|
td.purposed = td.accepted
|
||||||
@ -1417,14 +1421,14 @@ 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.set_problem',
|
{'tid': self.tournament_id, 'type': 'draw.set_problem',
|
||||||
'round': r1.number,
|
'round': previous_round.number,
|
||||||
'team': td.participation.team.trigram,
|
'team': td.participation.team.trigram,
|
||||||
'problem': td.accepted})
|
'problem': td.accepted})
|
||||||
else:
|
else:
|
||||||
# Don't continue the final tournament
|
# Don't continue the final tournament
|
||||||
r1 = await self.tournament.draw.round_set \
|
previous_round = await self.tournament.draw.round_set \
|
||||||
.prefetch_related('current_pool__current_team__participation__team').aget(number=1)
|
.prefetch_related('current_pool__current_team__participation__team').aget(number=1)
|
||||||
self.tournament.draw.current_round = r1
|
self.tournament.draw.current_round = previous_round
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
async for td in r.teamdraw_set.all():
|
async for td in r.teamdraw_set.all():
|
||||||
@ -1446,7 +1450,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
async for td in r1.team_draws.prefetch_related('participation__team').all():
|
async for td in previous_round.team_draws.prefetch_related('participation__team').all():
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
||||||
'team': td.participation.team.trigram,
|
'team': td.participation.team.trigram,
|
||||||
@ -1460,17 +1464,31 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'visible': True})
|
'visible': True})
|
||||||
else:
|
else:
|
||||||
# Go to the dice order
|
# Go to the dice order
|
||||||
async for r0 in self.tournament.draw.round_set.all():
|
async for td in r.teamdraw_set.all():
|
||||||
async for td in r0.teamdraw_set.all():
|
td.pool = None
|
||||||
td.pool = None
|
td.passage_index = None
|
||||||
td.passage_index = None
|
td.choose_index = None
|
||||||
td.choose_index = None
|
td.choice_dice = None
|
||||||
td.choice_dice = None
|
await td.asave()
|
||||||
await td.asave()
|
|
||||||
|
|
||||||
r.current_pool = None
|
r.current_pool = None
|
||||||
await r.asave()
|
await r.asave()
|
||||||
|
|
||||||
|
await self.channel_layer.group_send(
|
||||||
|
f"tournament-{self.tournament.id}",
|
||||||
|
{
|
||||||
|
'tid': self.tournament_id,
|
||||||
|
'type': 'draw.send_poules',
|
||||||
|
'round': r.number,
|
||||||
|
'poules': [
|
||||||
|
{
|
||||||
|
'letter': pool.get_letter_display(),
|
||||||
|
'teams': await pool.atrigrams(),
|
||||||
|
}
|
||||||
|
async for pool in r.pool_set.order_by('letter').all()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
round_tds = {td.id: td async for td in r.team_draws.prefetch_related('participation__team')}
|
round_tds = {td.id: td async for td in r.team_draws.prefetch_related('participation__team')}
|
||||||
|
|
||||||
# Reset the last dice
|
# Reset the last dice
|
||||||
@ -1540,8 +1558,45 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'team': last_td.participation.team.trigram,
|
'team': last_td.participation.team.trigram,
|
||||||
'result': None})
|
'result': None})
|
||||||
break
|
break
|
||||||
else:
|
elif r.number == 1:
|
||||||
|
# Cancel the draw if it is the first round
|
||||||
await self.abort()
|
await self.abort()
|
||||||
|
else:
|
||||||
|
# Go back to the first round after resetting all
|
||||||
|
previous_round = await self.tournament.draw.round_set \
|
||||||
|
.prefetch_related('current_pool__current_team__participation__team').aget(number=r.number - 1)
|
||||||
|
self.tournament.draw.current_round = previous_round
|
||||||
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
|
async for td in previous_round.team_draws.prefetch_related('participation__team').all():
|
||||||
|
await self.channel_layer.group_send(
|
||||||
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
||||||
|
'team': td.participation.team.trigram,
|
||||||
|
'result': td.choice_dice})
|
||||||
|
|
||||||
|
previous_pool = previous_round.current_pool
|
||||||
|
|
||||||
|
td = previous_pool.current_team
|
||||||
|
td.purposed = td.accepted
|
||||||
|
td.accepted = None
|
||||||
|
await td.asave()
|
||||||
|
|
||||||
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||||
|
'visible': False})
|
||||||
|
|
||||||
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
||||||
|
'visible': True})
|
||||||
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
||||||
|
'visible': True})
|
||||||
|
|
||||||
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
|
{'tid': self.tournament_id, 'type': 'draw.set_problem',
|
||||||
|
'round': previous_round.number,
|
||||||
|
'team': td.participation.team.trigram,
|
||||||
|
'problem': td.accepted})
|
||||||
|
|
||||||
async def draw_alert(self, content):
|
async def draw_alert(self, content):
|
||||||
"""
|
"""
|
||||||
|
27
draw/migrations/0004_alter_round_number.py
Normal file
27
draw/migrations/0004_alter_round_number.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-07 12:46
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("draw", "0003_alter_teamdraw_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="round",
|
||||||
|
name="number",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[(1, "Round 1"), (2, "Round 2")],
|
||||||
|
help_text="The number of the round, 1 or 2 (or 3 for ETEAM)",
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(2),
|
||||||
|
],
|
||||||
|
verbose_name="number",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,69 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-13 08:53
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("draw", "0004_alter_round_number"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="round",
|
||||||
|
name="number",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[(1, "Round 1"), (2, "Round 2"), (3, "Round 3")],
|
||||||
|
help_text="The number of the round, 1 or 2 (or 3 for ETEAM)",
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(3),
|
||||||
|
],
|
||||||
|
verbose_name="number",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="teamdraw",
|
||||||
|
name="accepted",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Problem #1"),
|
||||||
|
(2, "Problem #2"),
|
||||||
|
(3, "Problem #3"),
|
||||||
|
(4, "Problem #4"),
|
||||||
|
(5, "Problem #5"),
|
||||||
|
(6, "Problem #6"),
|
||||||
|
(7, "Problem #7"),
|
||||||
|
(8, "Problem #8"),
|
||||||
|
(9, "Problem #9"),
|
||||||
|
(10, "Problem #10"),
|
||||||
|
],
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
verbose_name="accepted problem",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="teamdraw",
|
||||||
|
name="purposed",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Problem #1"),
|
||||||
|
(2, "Problem #2"),
|
||||||
|
(3, "Problem #3"),
|
||||||
|
(4, "Problem #4"),
|
||||||
|
(5, "Problem #5"),
|
||||||
|
(6, "Problem #6"),
|
||||||
|
(7, "Problem #7"),
|
||||||
|
(8, "Problem #8"),
|
||||||
|
(9, "Problem #9"),
|
||||||
|
(10, "Problem #10"),
|
||||||
|
],
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
verbose_name="purposed problem",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
27
draw/migrations/0006_alter_round_current_pool.py
Normal file
27
draw/migrations/0006_alter_round_current_pool.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-07-09 11:07
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("draw", "0005_alter_round_number_alter_teamdraw_accepted_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="round",
|
||||||
|
name="current_pool",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
help_text="The current pool where teams select their problems.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="draw.pool",
|
||||||
|
verbose_name="current pool",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
119
draw/models.py
119
draw/models.py
@ -5,6 +5,7 @@ import os
|
|||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
@ -81,7 +82,7 @@ class Draw(models.Model):
|
|||||||
elif self.current_round.current_pool.current_team is None:
|
elif self.current_round.current_pool.current_team is None:
|
||||||
return 'DICE_ORDER_POULE'
|
return 'DICE_ORDER_POULE'
|
||||||
elif self.current_round.current_pool.current_team.accepted is not None:
|
elif self.current_round.current_pool.current_team.accepted is not None:
|
||||||
if self.current_round.number == 1:
|
if self.current_round.number < settings.NB_ROUNDS:
|
||||||
# The last step can be the last problem acceptation after the first round
|
# The last step can be the last problem acceptation after the first round
|
||||||
# only for the final between the two rounds
|
# only for the final between the two rounds
|
||||||
return 'WAITING_FINAL'
|
return 'WAITING_FINAL'
|
||||||
@ -110,58 +111,61 @@ class Draw(models.Model):
|
|||||||
# Waiting for dices to determine pools and passage order
|
# Waiting for dices to determine pools and passage order
|
||||||
if self.current_round.number == 1:
|
if self.current_round.number == 1:
|
||||||
# Specific information for the first round
|
# Specific information for the first round
|
||||||
s += """Nous allons commencer le tirage des problèmes.<br>
|
s += _("We are going to start the problem draw.<br>"
|
||||||
Vous pouvez à tout moment poser toute question si quelque chose
|
"You can ask any question if something is not clear or wrong.<br><br>"
|
||||||
n'est pas clair ou ne va pas.<br><br>
|
"We are going to first draw the pools and the passage order for the first round "
|
||||||
Nous allons d'abord tirer les poules et l'ordre de passage
|
"with all the teams, then for each pool, we will draw the draw order and the problems.")
|
||||||
pour le premier tour avec toutes les équipes puis pour chaque poule,
|
s += "<br><br>"
|
||||||
nous tirerons l'ordre de tirage pour le tour et les problèmes.<br><br>"""
|
s += _("The captains, you can now all throw a 100-sided dice, by clicking on the big dice button. "
|
||||||
s += """
|
"The pools and the passage order during the first round will be the increasing order "
|
||||||
Les capitaines, vous pouvez désormais toustes lancer un dé 100,
|
"of the dices, ie. the smallest dice will be the first to pass in pool A.")
|
||||||
en cliquant sur le gros bouton. Les poules et l'ordre de passage
|
|
||||||
lors du premier tour sera l'ordre croissant des dés, c'est-à-dire
|
|
||||||
que le plus petit lancer sera le premier à passer dans la poule A."""
|
|
||||||
case 'DICE_ORDER_POULE':
|
case 'DICE_ORDER_POULE':
|
||||||
# Waiting for dices to determine the choice order
|
# Waiting for dices to determine the choice order
|
||||||
s += f"""Nous passons au tirage des problèmes pour la poule
|
s += _("We are going to start the problem draw for the pool <strong>{pool}</strong>, "
|
||||||
<strong>{self.current_round.current_pool}</strong>, entre les équipes
|
"between the teams <strong>{teams}</strong>. "
|
||||||
<strong>{', '.join(td.participation.team.trigram
|
"The captains can throw a 100-sided dice by clicking on the big dice button "
|
||||||
for td in self.current_round.current_pool.teamdraw_set.all())}</strong>.
|
"to determine the order of draw. The team with the highest score will draw first.") \
|
||||||
Les capitaines peuvent lancer un dé 100 en cliquant sur le gros bouton
|
.format(pool=self.current_round.current_pool,
|
||||||
pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra
|
teams=', '.join(td.participation.team.trigram
|
||||||
tirer en premier."""
|
for td in self.current_round.current_pool.teamdraw_set.all()))
|
||||||
case 'WAITING_DRAW_PROBLEM':
|
case 'WAITING_DRAW_PROBLEM':
|
||||||
# Waiting for a problem draw
|
# Waiting for a problem draw
|
||||||
td = self.current_round.current_pool.current_team
|
td = self.current_round.current_pool.current_team
|
||||||
s += f"""C'est au tour de l'équipe <strong>{td.participation.team.trigram}</strong>
|
s += _("The team <strong>{trigram}</strong> is going to draw a problem. "
|
||||||
de choisir son problème. Cliquez sur l'urne au milieu pour tirer un problème au sort."""
|
"Click on the urn in the middle to draw a problem.") \
|
||||||
|
.format(trigram=td.participation.team.trigram)
|
||||||
case 'WAITING_CHOOSE_PROBLEM':
|
case 'WAITING_CHOOSE_PROBLEM':
|
||||||
# Waiting for the team that can accept or reject the problem
|
# Waiting for the team that can accept or reject the problem
|
||||||
td = self.current_round.current_pool.current_team
|
td = self.current_round.current_pool.current_team
|
||||||
s += f"""L'équipe <strong>{td.participation.team.trigram}</strong> a tiré le problème
|
s += _("The team <strong>{trigram}</strong> drew the problem <strong>{problem}: "
|
||||||
<strong>{td.purposed} : {settings.PROBLEMS[td.purposed - 1]}</strong>. """
|
"{problem_name}</strong>.") \
|
||||||
|
.format(trigram=td.participation.team.trigram,
|
||||||
|
problem=td.purposed, problem_name=settings.PROBLEMS[td.purposed - 1]) + " "
|
||||||
if td.purposed in td.rejected:
|
if td.purposed in td.rejected:
|
||||||
# The problem was previously rejected
|
# The problem was previously rejected
|
||||||
s += """Elle a déjà refusé ce problème auparavant, elle peut donc le refuser sans pénalité et
|
s += _("It already refused this problem before, so it can refuse it without penalty and "
|
||||||
tirer un nouveau problème immédiatement, ou bien revenir sur son choix."""
|
"draw a new problem immediately, or change its mind.")
|
||||||
else:
|
else:
|
||||||
# The problem can be rejected
|
# The problem can be rejected
|
||||||
s += "Elle peut décider d'accepter ou de refuser ce problème. "
|
s += _("It can decide to accept or refuse this problem.") + " "
|
||||||
if len(td.rejected) >= len(settings.PROBLEMS) - 5:
|
if len(td.rejected) >= len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT:
|
||||||
s += "Refuser ce problème ajoutera une nouvelle pénalité de 25 % sur le coefficient de l'oral de la défense."
|
s += _("Refusing this problem will add a new 25% penalty "
|
||||||
|
"on the coefficient of the oral defense.")
|
||||||
else:
|
else:
|
||||||
s += f"Il reste {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité."
|
s += _("There are still {remaining} refusals without penalty.").format(
|
||||||
|
remaining=len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT - len(td.rejected))
|
||||||
case 'WAITING_FINAL':
|
case 'WAITING_FINAL':
|
||||||
# We are between the two rounds of the final tournament
|
# We are between the two rounds of the final tournament
|
||||||
s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !"
|
s += _("The draw for the second round will take place at the end of the first round. Good luck!")
|
||||||
case 'DRAW_ENDED':
|
case 'DRAW_ENDED':
|
||||||
# The draw is ended
|
# The draw is ended
|
||||||
s += "Le tirage au sort est terminé. Les solutions des autres équipes peuvent être trouvées dans l'onglet « Ma participation »."
|
s += _("The draw is ended. The solutions of the other teams can be found in the tab "
|
||||||
|
"\"My participation\".")
|
||||||
|
|
||||||
s += "<br><br>" if s else ""
|
s += "<br><br>" if s else ""
|
||||||
s += """Pour plus de détails sur le déroulement du tirage au sort,
|
rules_link = settings.RULES_LINK
|
||||||
le règlement est accessible sur
|
s += _("For more details on the draw, the rules are available on "
|
||||||
<a class="alert-link" href="https://tfjm.org/reglement">https://tfjm.org/reglement</a>."""
|
"<a class=\"alert-link\" href=\"{link}\">{link}</a>.").format(link=rules_link)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
async def ainformation(self) -> str:
|
async def ainformation(self) -> str:
|
||||||
@ -193,15 +197,15 @@ class Round(models.Model):
|
|||||||
choices=[
|
choices=[
|
||||||
(1, _('Round 1')),
|
(1, _('Round 1')),
|
||||||
(2, _('Round 2')),
|
(2, _('Round 2')),
|
||||||
],
|
(3, _('Round 3'))],
|
||||||
verbose_name=_('number'),
|
verbose_name=_('number'),
|
||||||
help_text=_("The number of the round, 1 or 2"),
|
help_text=_("The number of the round, 1 or 2 (or 3 for ETEAM)"),
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(2)],
|
validators=[MinValueValidator(1), MaxValueValidator(3)],
|
||||||
)
|
)
|
||||||
|
|
||||||
current_pool = models.ForeignKey(
|
current_pool = models.ForeignKey(
|
||||||
'Pool',
|
'Pool',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
related_name='+',
|
related_name='+',
|
||||||
@ -230,6 +234,13 @@ class Round(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.get_number_display()
|
return self.get_number_display()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.number is not None and self.number > settings.NB_ROUNDS:
|
||||||
|
raise ValidationError({'number': _("The number of the round must be between 1 and {nb}.")
|
||||||
|
.format(nb=settings.NB_ROUNDS)})
|
||||||
|
|
||||||
|
return super().clean()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('round')
|
verbose_name = _('round')
|
||||||
verbose_name_plural = _('rounds')
|
verbose_name_plural = _('rounds')
|
||||||
@ -389,11 +400,11 @@ class Pool(models.Model):
|
|||||||
]
|
]
|
||||||
elif self.size == 5:
|
elif self.size == 5:
|
||||||
table = [
|
table = [
|
||||||
[0, 2, 3],
|
[0, 2, 3, 4],
|
||||||
[1, 3, 4],
|
[1, 3, 4, 0],
|
||||||
[2, 4, 0],
|
[2, 4, 0, 1],
|
||||||
[3, 0, 1],
|
[3, 0, 1, 2],
|
||||||
[4, 1, 2],
|
[4, 1, 2, 3],
|
||||||
]
|
]
|
||||||
|
|
||||||
for i, line in enumerate(table):
|
for i, line in enumerate(table):
|
||||||
@ -405,15 +416,21 @@ class Pool(models.Model):
|
|||||||
passage_pool = pool2
|
passage_pool = pool2
|
||||||
passage_position = 1 + i // 2
|
passage_position = 1 + i // 2
|
||||||
|
|
||||||
|
reporter = tds[line[0]].participation
|
||||||
|
opponent = tds[line[1]].participation
|
||||||
|
reviewer = tds[line[2]].participation
|
||||||
|
observer = tds[line[3]].participation if self.size >= 4 and settings.HAS_OBSERVER else None
|
||||||
|
|
||||||
# Create the passage
|
# Create the passage
|
||||||
await Passage.objects.acreate(
|
await Passage.objects.acreate(
|
||||||
pool=passage_pool,
|
pool=passage_pool,
|
||||||
position=passage_position,
|
position=passage_position,
|
||||||
solution_number=tds[line[0]].accepted,
|
solution_number=tds[line[0]].accepted,
|
||||||
defender=tds[line[0]].participation,
|
reporter=reporter,
|
||||||
opponent=tds[line[1]].participation,
|
opponent=opponent,
|
||||||
reporter=tds[line[2]].participation,
|
reviewer=reviewer,
|
||||||
defender_penalties=tds[line[0]].penalty_int,
|
observer=observer,
|
||||||
|
reporter_penalties=tds[line[0]].penalty_int,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update Google Sheets
|
# Update Google Sheets
|
||||||
@ -524,15 +541,15 @@ class TeamDraw(models.Model):
|
|||||||
@property
|
@property
|
||||||
def penalty_int(self):
|
def penalty_int(self):
|
||||||
"""
|
"""
|
||||||
The number of penalties, which is the number of rejected problems after the P - 5 free rejects,
|
The number of penalties, which is the number of rejected problems after the P - 5 free rejects
|
||||||
where P is the number of problems.
|
(P - 6 for ETEAM), where P is the number of problems.
|
||||||
"""
|
"""
|
||||||
return max(0, len(self.rejected) - (len(settings.PROBLEMS) - 5))
|
return max(0, len(self.rejected) - (len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def penalty(self):
|
def penalty(self):
|
||||||
"""
|
"""
|
||||||
The penalty multiplier on the defender oral, in percentage, which is a malus of 25% for each penalty.
|
The penalty multiplier on the reporter oral, in percentage, which is a malus of 25% for each penalty.
|
||||||
"""
|
"""
|
||||||
return 25 * self.penalty_int
|
return 25 * self.penalty_int
|
||||||
|
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
await Notification.requestPermission()
|
await Notification.requestPermission()
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const TFJM = JSON.parse(document.getElementById('TFJM_settings').textContent)
|
||||||
|
const RECOMMENDED_SOLUTIONS_COUNT = TFJM.RECOMMENDED_SOLUTIONS_COUNT
|
||||||
|
|
||||||
const problems_count = JSON.parse(document.getElementById('problems_count').textContent)
|
const problems_count = JSON.parse(document.getElementById('problems_count').textContent)
|
||||||
|
|
||||||
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
|
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
|
||||||
@ -218,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
|
||||||
@ -308,7 +312,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* Set the different pools for the given round, and update the interface.
|
* Set the different pools for the given round, and update the interface.
|
||||||
* @param tid The tournament id
|
* @param tid The tournament id
|
||||||
* @param round The round number, as integer (1 or 2)
|
* @param round The round number, as integer (1 or 2, or 3 for ETEAM)
|
||||||
* @param poules The list of poules, which are represented with their letters and trigrams,
|
* @param poules The list of poules, which are represented with their letters and trigrams,
|
||||||
* [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}]
|
* [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}]
|
||||||
*/
|
*/
|
||||||
@ -430,7 +434,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* Update the table for the given round and the given pool, where there will be the chosen problems.
|
* Update the table for the given round and the given pool, where there will be the chosen problems.
|
||||||
* @param tid The tournament id
|
* @param tid The tournament id
|
||||||
* @param round The round number, as integer (1 or 2)
|
* @param round The round number, as integer (1 or 2, or 3 for ETEAM)
|
||||||
* @param poule The current pool, which id represented with its letter and trigrams,
|
* @param poule The current pool, which id represented with its letter and trigrams,
|
||||||
* {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}
|
* {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}
|
||||||
*/
|
*/
|
||||||
@ -518,45 +522,45 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
teamTd.innerText = team
|
teamTd.innerText = team
|
||||||
teamTr.append(teamTd)
|
teamTr.append(teamTd)
|
||||||
|
|
||||||
let defenderTd = document.createElement('td')
|
let reporterTd = document.createElement('td')
|
||||||
defenderTd.classList.add('text-center')
|
reporterTd.classList.add('text-center')
|
||||||
defenderTd.innerText = 'Déf'
|
reporterTd.innerText = 'Déf'
|
||||||
|
|
||||||
let opponentTd = document.createElement('td')
|
let opponentTd = document.createElement('td')
|
||||||
opponentTd.classList.add('text-center')
|
opponentTd.classList.add('text-center')
|
||||||
opponentTd.innerText = 'Opp'
|
opponentTd.innerText = 'Opp'
|
||||||
|
|
||||||
let reporterTd = document.createElement('td')
|
let reviewerTd = document.createElement('td')
|
||||||
reporterTd.classList.add('text-center')
|
reviewerTd.classList.add('text-center')
|
||||||
reporterTd.innerText = 'Rap'
|
reviewerTd.innerText = 'Rap'
|
||||||
|
|
||||||
// Put the cells in their right places, according to the pool size and the row number.
|
// Put the cells in their right places, according to the pool size and the row number.
|
||||||
if (poule.teams.length === 3) {
|
if (poule.teams.length === 3) {
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
teamTr.append(defenderTd, reporterTd, opponentTd)
|
teamTr.append(reporterTd, reviewerTd, opponentTd)
|
||||||
break
|
break
|
||||||
case 1:
|
case 1:
|
||||||
teamTr.append(opponentTd, defenderTd, reporterTd)
|
teamTr.append(opponentTd, reporterTd, reviewerTd)
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
teamTr.append(reporterTd, opponentTd, defenderTd)
|
teamTr.append(reviewerTd, opponentTd, reporterTd)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else if (poule.teams.length === 4) {
|
} else if (poule.teams.length === 4) {
|
||||||
let emptyTd = document.createElement('td')
|
let emptyTd = document.createElement('td')
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
teamTr.append(defenderTd, emptyTd, reporterTd, opponentTd)
|
teamTr.append(reporterTd, emptyTd, reviewerTd, opponentTd)
|
||||||
break
|
break
|
||||||
case 1:
|
case 1:
|
||||||
teamTr.append(opponentTd, defenderTd, emptyTd, reporterTd)
|
teamTr.append(opponentTd, reporterTd, emptyTd, reviewerTd)
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
teamTr.append(reporterTd, opponentTd, defenderTd, emptyTd)
|
teamTr.append(reviewerTd, opponentTd, reporterTd, emptyTd)
|
||||||
break
|
break
|
||||||
case 3:
|
case 3:
|
||||||
teamTr.append(emptyTd, reporterTd, opponentTd, defenderTd)
|
teamTr.append(emptyTd, reviewerTd, opponentTd, reporterTd)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else if (poule.teams.length === 5) {
|
} else if (poule.teams.length === 5) {
|
||||||
@ -564,19 +568,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let emptyTd2 = document.createElement('td')
|
let emptyTd2 = document.createElement('td')
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
teamTr.append(defenderTd, emptyTd, opponentTd, reporterTd, emptyTd2)
|
teamTr.append(reporterTd, emptyTd, opponentTd, reviewerTd, emptyTd2)
|
||||||
break
|
break
|
||||||
case 1:
|
case 1:
|
||||||
teamTr.append(emptyTd, defenderTd, reporterTd, emptyTd2, opponentTd)
|
teamTr.append(emptyTd, reporterTd, reviewerTd, emptyTd2, opponentTd)
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
teamTr.append(opponentTd, emptyTd, defenderTd, emptyTd2, reporterTd)
|
teamTr.append(opponentTd, emptyTd, reporterTd, emptyTd2, reviewerTd)
|
||||||
break
|
break
|
||||||
case 3:
|
case 3:
|
||||||
teamTr.append(reporterTd, opponentTd, emptyTd, defenderTd, emptyTd2)
|
teamTr.append(reviewerTd, opponentTd, emptyTd, reporterTd, emptyTd2)
|
||||||
break
|
break
|
||||||
case 4:
|
case 4:
|
||||||
teamTr.append(emptyTd, reporterTd, emptyTd2, opponentTd, defenderTd)
|
teamTr.append(emptyTd, reviewerTd, emptyTd2, opponentTd, reporterTd)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -587,7 +591,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* Highlight the team that is currently choosing its problem.
|
* Highlight the team that is currently choosing its problem.
|
||||||
* @param tid The tournament id
|
* @param tid The tournament id
|
||||||
* @param round The current round number, as integer (1 or 2)
|
* @param round The current round number, as integer (1 or 2, or 3 for ETEAM)
|
||||||
* @param pool The current pool letter (A, B, C or D) (null if non-relevant)
|
* @param pool The current pool letter (A, B, C or D) (null if non-relevant)
|
||||||
* @param team The current team trigram (null if non-relevant)
|
* @param team The current team trigram (null if non-relevant)
|
||||||
*/
|
*/
|
||||||
@ -624,7 +628,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* Update the recap and the table when a team accepts a problem.
|
* Update the recap and the table when a team accepts a problem.
|
||||||
* @param tid The tournament id
|
* @param tid The tournament id
|
||||||
* @param round The current round, as integer (1 or 2)
|
* @param round The current round, as integer (1 or 2, or 3 for ETEAM)
|
||||||
* @param team The current team trigram
|
* @param team The current team trigram
|
||||||
* @param problem The accepted problem, as integer
|
* @param problem The accepted problem, as integer
|
||||||
*/
|
*/
|
||||||
@ -648,7 +652,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* Update the recap when a team rejects a problem.
|
* Update the recap when a team rejects a problem.
|
||||||
* @param tid The tournament id
|
* @param tid The tournament id
|
||||||
* @param round The current round, as integer (1 or 2)
|
* @param round The current round, as integer (1 or 2, or 3 for ETEAM)
|
||||||
* @param team The current team trigram
|
* @param team The current team trigram
|
||||||
* @param rejected The full list of rejected problems
|
* @param rejected The full list of rejected problems
|
||||||
*/
|
*/
|
||||||
@ -658,15 +662,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
|
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
|
||||||
|
|
||||||
let penaltyDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-penalty`)
|
let penaltyDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-penalty`)
|
||||||
if (rejected.length > problems_count - 5) {
|
if (rejected.length > problems_count - RECOMMENDED_SOLUTIONS_COUNT) {
|
||||||
// If more than P - 5 problems were rejected, add a penalty of 25% of the coefficient of the oral defender
|
// If more than P - 5 problems were rejected, add a penalty of 25% of the coefficient of the oral reporter
|
||||||
|
// This is P - 6 for the ETEAM
|
||||||
if (penaltyDiv === null) {
|
if (penaltyDiv === null) {
|
||||||
penaltyDiv = document.createElement('div')
|
penaltyDiv = document.createElement('div')
|
||||||
penaltyDiv.id = `recap-${tid}-round-${round}-team-${team}-penalty`
|
penaltyDiv.id = `recap-${tid}-round-${round}-team-${team}-penalty`
|
||||||
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
|
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
|
||||||
recapDiv.parentNode.append(penaltyDiv)
|
recapDiv.parentNode.append(penaltyDiv)
|
||||||
}
|
}
|
||||||
penaltyDiv.textContent = `❌ ${25 * (rejected.length - (problems_count - 5))} %`
|
penaltyDiv.textContent = `❌ ${25 * (rejected.length - (problems_count - RECOMMENDED_SOLUTIONS_COUNT))} %`
|
||||||
} else {
|
} else {
|
||||||
// Eventually remove this div
|
// Eventually remove this div
|
||||||
if (penaltyDiv !== null)
|
if (penaltyDiv !== null)
|
||||||
@ -678,7 +683,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
* For a 5-teams pool, we may reorder the pool if two teams select the same problem.
|
* For a 5-teams pool, we may reorder the pool if two teams select the same problem.
|
||||||
* Then, we redraw the table and set the accepted problems.
|
* Then, we redraw the table and set the accepted problems.
|
||||||
* @param tid The tournament id
|
* @param tid The tournament id
|
||||||
* @param round The current round, as integer (1 or 2)
|
* @param round The current round, as integer (1 or 2, or 3 for ETEAM)
|
||||||
* @param poule The pool represented by its letter
|
* @param poule The pool represented by its letter
|
||||||
* @param teams The teams list represented by their trigrams, ["ABC", "DEF", "GHI", "JKL", "MNO"]
|
* @param teams The teams list represented by their trigrams, ["ABC", "DEF", "GHI", "JKL", "MNO"]
|
||||||
* @param problems The accepted problems in the same order than the teams, [1, 1, 2, 2, 3]
|
* @param problems The accepted problems in the same order than the teams, [1, 1, 2, 2, 3]
|
||||||
@ -696,6 +701,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let problem = problems[i]
|
let problem = problems[i]
|
||||||
|
|
||||||
setProblemAccepted(tid, round, team, problem)
|
setProblemAccepted(tid, round, team, problem)
|
||||||
|
|
||||||
|
let recapTeam = document.getElementById(`recap-${tid}-round-${round}-team-${team}`)
|
||||||
|
recapTeam.style.order = i.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@
|
|||||||
📁 {% trans "Export" %}
|
📁 {% trans "Export" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% if tournament.final %}
|
{% if tournament.final or not TFJM.HAS_FINAL %}
|
||||||
{# Volunteers can continue the second round for the final tournament #}
|
{# Volunteers can continue the second round for the final tournament #}
|
||||||
<div id="continue-{{ tournament.id }}"
|
<div id="continue-{{ tournament.id }}"
|
||||||
class="card-footer text-center{% if tournament.draw.get_state != 'WAITING_FINAL' %} d-none{% endif %}">
|
class="card-footer text-center{% if tournament.draw.get_state != 'WAITING_FINAL' %} d-none{% endif %}">
|
||||||
@ -307,71 +307,71 @@
|
|||||||
<td class="text-center">{{ td.participation.team.trigram }}</td>
|
<td class="text-center">{{ td.participation.team.trigram }}</td>
|
||||||
{% if pool.size == 3 %}
|
{% if pool.size == 3 %}
|
||||||
{% if forloop.counter == 1 %}
|
{% if forloop.counter == 1 %}
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
{% elif forloop.counter == 2 %}
|
{% elif forloop.counter == 2 %}
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
{% elif forloop.counter == 3 %}
|
{% elif forloop.counter == 3 %}
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif pool.size == 4 %}
|
{% elif pool.size == 4 %}
|
||||||
{% if forloop.counter == 1 %}
|
{% if forloop.counter == 1 %}
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
{% elif forloop.counter == 2 %}
|
{% elif forloop.counter == 2 %}
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
{% elif forloop.counter == 3 %}
|
{% elif forloop.counter == 3 %}
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
{% elif forloop.counter == 4 %}
|
{% elif forloop.counter == 4 %}
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif pool.size == 5 %}
|
{% elif pool.size == 5 %}
|
||||||
{% if forloop.counter == 1 %}
|
{% if forloop.counter == 1 %}
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center"></td>
|
||||||
{% elif forloop.counter == 2 %}
|
{% elif forloop.counter == 2 %}
|
||||||
<td></td>
|
<td class="text-center"></td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
{% elif forloop.counter == 3 %}
|
{% elif forloop.counter == 3 %}
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center"></td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
{% elif forloop.counter == 4 %}
|
{% elif forloop.counter == 4 %}
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center"></td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
{% elif forloop.counter == 5 %}
|
{% elif forloop.counter == 5 %}
|
||||||
<td></td>
|
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
|
||||||
<td class="text-center">Rap</td>
|
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
|
||||||
<td class="text-center">Opp</td>
|
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
|
||||||
<td></td>
|
<td class="text-center"></td>
|
||||||
<td class="text-center">Déf</td>
|
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -71,7 +71,7 @@ class TestDraw(TestCase):
|
|||||||
resp = await communicator.receive_json_from()
|
resp = await communicator.receive_json_from()
|
||||||
self.assertEqual(resp['type'], 'alert')
|
self.assertEqual(resp['type'], 'alert')
|
||||||
self.assertEqual(resp['alert_type'], 'danger')
|
self.assertEqual(resp['alert_type'], 'danger')
|
||||||
self.assertEqual(resp['message'], "The sum must be equal to the number of teams: expected 12, got 3")
|
self.assertEqual(resp['message'], "La somme doit être égale au nombre d'équipes : attendu 12, obtenu 3")
|
||||||
self.assertFalse(await Draw.objects.filter(tournament=self.tournament).aexists())
|
self.assertFalse(await Draw.objects.filter(tournament=self.tournament).aexists())
|
||||||
|
|
||||||
# Now start the draw
|
# Now start the draw
|
||||||
@ -113,7 +113,7 @@ class TestDraw(TestCase):
|
|||||||
resp = await communicator.receive_json_from()
|
resp = await communicator.receive_json_from()
|
||||||
self.assertEqual(resp['type'], 'alert')
|
self.assertEqual(resp['type'], 'alert')
|
||||||
self.assertEqual(resp['alert_type'], 'danger')
|
self.assertEqual(resp['alert_type'], 'danger')
|
||||||
self.assertEqual(resp['message'], "The draw is already started.")
|
self.assertEqual(resp['message'], "Le tirage a déjà commencé.")
|
||||||
|
|
||||||
draw: Draw = await Draw.objects.prefetch_related(
|
draw: Draw = await Draw.objects.prefetch_related(
|
||||||
'current_round__current_pool__current_team__participation__team').aget(tournament=self.tournament)
|
'current_round__current_pool__current_team__participation__team').aget(tournament=self.tournament)
|
||||||
@ -135,7 +135,7 @@ class TestDraw(TestCase):
|
|||||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': team.trigram})
|
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': team.trigram})
|
||||||
resp = await communicator.receive_json_from()
|
resp = await communicator.receive_json_from()
|
||||||
self.assertEqual(resp['type'], 'alert')
|
self.assertEqual(resp['type'], 'alert')
|
||||||
self.assertEqual(resp['message'], "You've already launched the dice.")
|
self.assertEqual(resp['message'], "Vous avez déjà lancé le dé.")
|
||||||
|
|
||||||
# Force exactly one duplicate
|
# Force exactly one duplicate
|
||||||
await td.arefresh_from_db()
|
await td.arefresh_from_db()
|
||||||
@ -207,7 +207,7 @@ class TestDraw(TestCase):
|
|||||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': trigram})
|
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': trigram})
|
||||||
resp = await communicator.receive_json_from()
|
resp = await communicator.receive_json_from()
|
||||||
self.assertEqual(resp['type'], 'alert')
|
self.assertEqual(resp['type'], 'alert')
|
||||||
self.assertEqual(resp['message'], "You've already launched the dice.")
|
self.assertEqual(resp['message'], "Vous avez déjà lancé le dé.")
|
||||||
|
|
||||||
# Force exactly one duplicate
|
# Force exactly one duplicate
|
||||||
await td.arefresh_from_db()
|
await td.arefresh_from_db()
|
||||||
@ -254,7 +254,7 @@ class TestDraw(TestCase):
|
|||||||
await communicator.send_json_to({'tid': tid, 'type': 'dice', 'trigram': None})
|
await communicator.send_json_to({'tid': tid, 'type': 'dice', 'trigram': None})
|
||||||
resp = await communicator.receive_json_from()
|
resp = await communicator.receive_json_from()
|
||||||
self.assertEqual(resp['type'], 'alert')
|
self.assertEqual(resp['type'], 'alert')
|
||||||
self.assertEqual(resp['message'], "This is not the time for this.")
|
self.assertEqual(resp['message'], "Ce n'est pas le moment pour cela.")
|
||||||
|
|
||||||
# Draw a problem
|
# Draw a problem
|
||||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||||
@ -277,7 +277,7 @@ class TestDraw(TestCase):
|
|||||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||||
resp = await communicator.receive_json_from()
|
resp = await communicator.receive_json_from()
|
||||||
self.assertEqual(resp['type'], 'alert')
|
self.assertEqual(resp['type'], 'alert')
|
||||||
self.assertEqual(resp['message'], "This is not the time for this.")
|
self.assertEqual(resp['message'], "Ce n'est pas le moment pour cela.")
|
||||||
|
|
||||||
# Reject the first problem
|
# Reject the first problem
|
||||||
await communicator.send_json_to({'tid': tid, 'type': 'reject'})
|
await communicator.send_json_to({'tid': tid, 'type': 'reject'})
|
||||||
|
@ -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
@ -1,10 +1,12 @@
|
|||||||
# 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, Synthesis, Team, Tournament, Tweak
|
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
|
||||||
|
|
||||||
|
|
||||||
class ParticipationInline(admin.StackedInline):
|
class ParticipationInline(admin.StackedInline):
|
||||||
@ -32,8 +34,8 @@ class SolutionInline(admin.TabularInline):
|
|||||||
show_change_link = True
|
show_change_link = True
|
||||||
|
|
||||||
|
|
||||||
class SynthesisInline(admin.TabularInline):
|
class WrittenReviewInline(admin.TabularInline):
|
||||||
model = Synthesis
|
model = WrittenReview
|
||||||
extra = 0
|
extra = 0
|
||||||
ordering = ('passage__solution_number', 'type',)
|
ordering = ('passage__solution_number', 'type',)
|
||||||
autocomplete_fields = ('passage',)
|
autocomplete_fields = ('passage',)
|
||||||
@ -51,9 +53,14 @@ class PassageInline(admin.TabularInline):
|
|||||||
model = Passage
|
model = Passage
|
||||||
extra = 0
|
extra = 0
|
||||||
ordering = ('position',)
|
ordering = ('position',)
|
||||||
autocomplete_fields = ('defender', 'opponent', 'reporter',)
|
|
||||||
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
|
||||||
@ -95,7 +102,7 @@ class ParticipationAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('team__name', 'team__trigram',)
|
search_fields = ('team__name', 'team__trigram',)
|
||||||
list_filter = ('valid', 'tournament',)
|
list_filter = ('valid', 'tournament',)
|
||||||
autocomplete_fields = ('team', 'tournament',)
|
autocomplete_fields = ('team', 'tournament',)
|
||||||
inlines = (SolutionInline, SynthesisInline,)
|
inlines = (SolutionInline, WrittenReviewInline,)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Pool)
|
@admin.register(Pool)
|
||||||
@ -113,25 +120,26 @@ class PoolAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Passage)
|
@admin.register(Passage)
|
||||||
class PassageAdmin(admin.ModelAdmin):
|
class PassageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('__str__', 'defender_trigram', 'solution_number', 'opponent_trigram', 'reporter_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', 'defender', 'opponent', 'reporter',)
|
|
||||||
inlines = (NoteInline,)
|
inlines = (NoteInline,)
|
||||||
|
|
||||||
@admin.display(description=_("defender"), ordering='defender__team__trigram')
|
@admin.display(description=_("reporter"), ordering='reporter__team__trigram')
|
||||||
def defender_trigram(self, record: Passage):
|
def reporter_trigram(self, record: Passage):
|
||||||
return record.defender.team.trigram
|
return record.reporter.team.trigram
|
||||||
|
|
||||||
@admin.display(description=_("opponent"), ordering='opponent__team__trigram')
|
@admin.display(description=_("opponent"), ordering='opponent__team__trigram')
|
||||||
def opponent_trigram(self, record: Passage):
|
def opponent_trigram(self, record: Passage):
|
||||||
return record.opponent.team.trigram
|
return record.opponent.team.trigram
|
||||||
|
|
||||||
@admin.display(description=_("reporter"), ordering='reporter__team__trigram')
|
@admin.display(description=_("reviewer"), ordering='reviewer__team__trigram')
|
||||||
def reporter_trigram(self, record: Passage):
|
def reviewer_trigram(self, record: Passage):
|
||||||
return record.reporter.team.trigram
|
return record.reviewer.team.trigram
|
||||||
|
|
||||||
|
@admin.display(description=_("observer"), ordering='observer__team__trigram')
|
||||||
|
def observer_trigram(self, record: Passage):
|
||||||
|
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):
|
||||||
@ -141,21 +149,45 @@ 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', 'defender_writing', 'defender_oral',
|
search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__reporter__team__trigram',)
|
||||||
'opponent_writing', 'opponent_oral', 'reporter_writing', 'reporter_oral',)
|
|
||||||
list_filter = ('passage__pool__letter', 'passage__solution_number', 'jury',
|
|
||||||
'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
|
||||||
'reporter_writing', 'reporter_oral')
|
|
||||||
search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__defender__team__trigram',)
|
|
||||||
autocomplete_fields = ('jury', 'passage',)
|
autocomplete_fields = ('jury', 'passage',)
|
||||||
|
|
||||||
@admin.display(description=_("pool"))
|
@admin.display(description=_("pool"))
|
||||||
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):
|
||||||
@ -173,19 +205,19 @@ class SolutionAdmin(admin.ModelAdmin):
|
|||||||
return Tournament.final_tournament() if record.final_solution else record.participation.tournament
|
return Tournament.final_tournament() if record.final_solution else record.participation.tournament
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Synthesis)
|
@admin.register(WrittenReview)
|
||||||
class SynthesisAdmin(admin.ModelAdmin):
|
class WrittenReviewAdmin(admin.ModelAdmin):
|
||||||
list_display = ('participation', 'type', 'defender', 'passage',)
|
list_display = ('participation', 'type', 'reporter', 'passage',)
|
||||||
list_filter = ('participation__tournament', 'type', 'passage__solution_number',)
|
list_filter = ('participation__tournament', 'type', 'passage__solution_number',)
|
||||||
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||||
autocomplete_fields = ('participation', 'passage',)
|
autocomplete_fields = ('participation', 'passage',)
|
||||||
|
|
||||||
@admin.display(description=_("defender"))
|
@admin.display(description=_("reporter"))
|
||||||
def defender(self, record: Synthesis):
|
def reporter(self, record: WrittenReview):
|
||||||
return record.passage.defender
|
return record.passage.reporter
|
||||||
|
|
||||||
@admin.display(description=_("problem"))
|
@admin.display(description=_("problem"))
|
||||||
def problem(self, record: Synthesis):
|
def problem(self, record: WrittenReview):
|
||||||
return record.passage.solution_number
|
return record.passage.solution_number
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
from ..models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview
|
||||||
|
|
||||||
|
|
||||||
class NoteSerializer(serializers.ModelSerializer):
|
class NoteSerializer(serializers.ModelSerializer):
|
||||||
@ -38,9 +38,9 @@ class SolutionSerializer(serializers.ModelSerializer):
|
|||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class SynthesisSerializer(serializers.ModelSerializer):
|
class WrittenReviewSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Synthesis
|
model = WrittenReview
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
@ -58,8 +58,9 @@ class TournamentSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Tournament
|
model = Tournament
|
||||||
fields = ('id', 'pk', 'name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
|
fields = ('id', 'pk', 'name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
|
||||||
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
|
'inscription_limit', 'solution_limit', 'solutions_draw', 'reviews_first_phase_limit',
|
||||||
'solutions_available_second_phase', 'syntheses_second_phase_limit',
|
'solutions_available_second_phase', 'reviews_second_phase_limit',
|
||||||
|
'solutions_available_third_phase', 'reviews_third_phase_limit',
|
||||||
'description', 'organizers', 'final', 'participations',)
|
'description', 'organizers', 'final', 'participations',)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import NoteViewSet, ParticipationViewSet, PassageViewSet, PoolViewSet, \
|
from .views import NoteViewSet, ParticipationViewSet, PassageViewSet, PoolViewSet, \
|
||||||
SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet, TweakViewSet
|
SolutionViewSet, TeamViewSet, TournamentViewSet, TweakViewSet, WrittenReviewViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_participation_urls(router, path):
|
def register_participation_urls(router, path):
|
||||||
@ -13,8 +13,8 @@ def register_participation_urls(router, path):
|
|||||||
router.register(path + "/participation", ParticipationViewSet)
|
router.register(path + "/participation", ParticipationViewSet)
|
||||||
router.register(path + "/passage", PassageViewSet)
|
router.register(path + "/passage", PassageViewSet)
|
||||||
router.register(path + "/pool", PoolViewSet)
|
router.register(path + "/pool", PoolViewSet)
|
||||||
|
router.register(path + "/review", WrittenReviewViewSet)
|
||||||
router.register(path + "/solution", SolutionViewSet)
|
router.register(path + "/solution", SolutionViewSet)
|
||||||
router.register(path + "/synthesis", SynthesisViewSet)
|
|
||||||
router.register(path + "/team", TeamViewSet)
|
router.register(path + "/team", TeamViewSet)
|
||||||
router.register(path + "/tournament", TournamentViewSet)
|
router.register(path + "/tournament", TournamentViewSet)
|
||||||
router.register(path + "/tweak", TweakViewSet)
|
router.register(path + "/tweak", TweakViewSet)
|
||||||
|
@ -4,16 +4,16 @@ from django_filters.rest_framework import DjangoFilterBackend
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from .serializers import NoteSerializer, ParticipationSerializer, PassageSerializer, PoolSerializer, \
|
from .serializers import NoteSerializer, ParticipationSerializer, PassageSerializer, PoolSerializer, \
|
||||||
SolutionSerializer, SynthesisSerializer, TeamSerializer, TournamentSerializer, TweakSerializer
|
SolutionSerializer, TeamSerializer, TournamentSerializer, TweakSerializer, WrittenReviewSerializer
|
||||||
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
|
from ..models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
|
||||||
|
|
||||||
|
|
||||||
class NoteViewSet(ModelViewSet):
|
class NoteViewSet(ModelViewSet):
|
||||||
queryset = Note.objects.all()
|
queryset = Note.objects.all()
|
||||||
serializer_class = NoteSerializer
|
serializer_class = NoteSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['jury', 'passage', 'defender_writing', 'defender_oral', 'opponent_writing',
|
filterset_fields = ['jury', 'passage', 'reporter_writing', 'reporter_oral', 'opponent_writing',
|
||||||
'opponent_oral', 'reporter_writing', 'reporter_oral', ]
|
'opponent_oral', 'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', ]
|
||||||
|
|
||||||
|
|
||||||
class ParticipationViewSet(ModelViewSet):
|
class ParticipationViewSet(ModelViewSet):
|
||||||
@ -27,7 +27,7 @@ class PassageViewSet(ModelViewSet):
|
|||||||
queryset = Passage.objects.all()
|
queryset = Passage.objects.all()
|
||||||
serializer_class = PassageSerializer
|
serializer_class = PassageSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['pool', 'solution_number', 'defender', 'opponent', 'reporter', 'pool_tournament', ]
|
filterset_fields = ['pool', 'solution_number', 'reporter', 'opponent', 'reviewer', 'observer', 'pool_tournament', ]
|
||||||
|
|
||||||
|
|
||||||
class PoolViewSet(ModelViewSet):
|
class PoolViewSet(ModelViewSet):
|
||||||
@ -44,9 +44,9 @@ class SolutionViewSet(ModelViewSet):
|
|||||||
filterset_fields = ['participation', 'number', 'problem', 'final_solution', ]
|
filterset_fields = ['participation', 'number', 'problem', 'final_solution', ]
|
||||||
|
|
||||||
|
|
||||||
class SynthesisViewSet(ModelViewSet):
|
class WrittenReviewViewSet(ModelViewSet):
|
||||||
queryset = Synthesis.objects.all()
|
queryset = WrittenReview.objects.all()
|
||||||
serializer_class = SynthesisSerializer
|
serializer_class = WrittenReviewSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['participation', 'number', 'passage', 'type', ]
|
filterset_fields = ['participation', 'number', 'passage', 'type', ]
|
||||||
|
|
||||||
@ -64,8 +64,9 @@ class TournamentViewSet(ModelViewSet):
|
|||||||
serializer_class = TournamentSerializer
|
serializer_class = TournamentSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
|
filterset_fields = ['name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
|
||||||
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
|
'inscription_limit', 'solution_limit', 'solutions_draw', 'reviews_first_phase_limit',
|
||||||
'solutions_available_second_phase', 'syntheses_second_phase_limit',
|
'solutions_available_second_phase', 'reviews_second_phase_limit',
|
||||||
|
'solutions_available_third_phase', 'reviews_third_phase_limit',
|
||||||
'description', 'organizers', 'final', ]
|
'description', 'organizers', 'final', ]
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.models.signals import post_save, pre_save
|
from django.db.models.signals import post_save, pre_save
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class ParticipationConfig(AppConfig):
|
class ParticipationConfig(AppConfig):
|
||||||
@ -10,6 +11,7 @@ class ParticipationConfig(AppConfig):
|
|||||||
The participation app contains the data about the teams, solutions, ...
|
The participation app contains the data about the teams, solutions, ...
|
||||||
"""
|
"""
|
||||||
name = 'participation'
|
name = 'participation'
|
||||||
|
verbose_name = _("participations")
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from participation import signals
|
from participation import signals
|
||||||
|
@ -5,8 +5,9 @@ from io import StringIO
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Div, Field, 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
|
||||||
@ -15,7 +16,7 @@ import pandas
|
|||||||
from pypdf import PdfReader
|
from pypdf import PdfReader
|
||||||
from registration.models import VolunteerRegistration
|
from registration.models import VolunteerRegistration
|
||||||
|
|
||||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview
|
||||||
|
|
||||||
|
|
||||||
class TeamForm(forms.ModelForm):
|
class TeamForm(forms.ModelForm):
|
||||||
@ -74,6 +75,33 @@ class ParticipationForm(forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
Form to update the problem of a team participation.
|
Form to update the problem of a team participation.
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if settings.SINGLE_TOURNAMENT:
|
||||||
|
del self.fields['tournament']
|
||||||
|
self.helper = FormHelper()
|
||||||
|
idf_warning_banner = f"""
|
||||||
|
<div class=\"alert alert-warning\">
|
||||||
|
<h5 class=\"alert-heading\">{_("IMPORTANT")}</h4>
|
||||||
|
{_("""For the tournaments in the region "Île-de-France": registration is
|
||||||
|
unified for each tournament. By choosing a tournament "Île-de-France",
|
||||||
|
you're accepting that your team may be selected for one of these tournaments.
|
||||||
|
In case of date conflict, please write them in your motivation letter.""")}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
unified_registration_tournament_ids = ",".join(
|
||||||
|
str(tournament.id) for tournament in Tournament.objects.filter(
|
||||||
|
unified_registration=True).all())
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
'tournament',
|
||||||
|
Div(
|
||||||
|
HTML(idf_warning_banner),
|
||||||
|
css_id="idf_warning_banner",
|
||||||
|
data_tid_unified=unified_registration_tournament_ids,
|
||||||
|
),
|
||||||
|
'final',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Participation
|
model = Participation
|
||||||
fields = ('tournament', 'final',)
|
fields = ('tournament', 'final',)
|
||||||
@ -104,7 +132,7 @@ class RequestValidationForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
engagement = forms.BooleanField(
|
engagement = forms.BooleanField(
|
||||||
label=_("I engage myself to participate to the whole TFJM²."),
|
label=_("I engage myself to participate to the whole tournament."),
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -125,6 +153,15 @@ class ValidateParticipationForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class TournamentForm(forms.ModelForm):
|
class TournamentForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if settings.NB_ROUNDS < 3:
|
||||||
|
del self.fields['date_third_phase']
|
||||||
|
del self.fields['solutions_available_third_phase']
|
||||||
|
del self.fields['reviews_third_phase_limit']
|
||||||
|
if not settings.PAYMENT_MANAGEMENT:
|
||||||
|
del self.fields['price']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tournament
|
model = Tournament
|
||||||
exclude = ('notes_sheet_id', )
|
exclude = ('notes_sheet_id', )
|
||||||
@ -134,12 +171,15 @@ class TournamentForm(forms.ModelForm):
|
|||||||
'inscription_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
|
'inscription_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
|
||||||
'solution_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
|
'solution_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
|
||||||
'solutions_draw': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
|
'solutions_draw': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
|
||||||
'syntheses_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
|
'date_first_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||||
format='%Y-%m-%d %H:%M'),
|
'reviews_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
|
||||||
'solutions_available_second_phase': forms.DateTimeInput(attrs={'type': 'datetime-local'},
|
format='%Y-%m-%d %H:%M'),
|
||||||
format='%Y-%m-%d %H:%M'),
|
'date_second_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||||
'syntheses_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
|
'reviews_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
|
||||||
format='%Y-%m-%d %H:%M'),
|
format='%Y-%m-%d %H:%M'),
|
||||||
|
'date_third_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||||
|
'reviews_third_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
|
||||||
|
format='%Y-%m-%d %H:%M'),
|
||||||
'organizers': forms.SelectMultiple(attrs={
|
'organizers': forms.SelectMultiple(attrs={
|
||||||
'class': 'selectpicker',
|
'class': 'selectpicker',
|
||||||
'data-live-search': 'true',
|
'data-live-search': 'true',
|
||||||
@ -283,25 +323,26 @@ class UploadNotesForm(forms.Form):
|
|||||||
line = [s for s in line if s == s]
|
line = [s for s in line if s == s]
|
||||||
# Strip cases
|
# Strip cases
|
||||||
line = [str(s).strip() for s in line if str(s)]
|
line = [str(s).strip() for s in line if str(s)]
|
||||||
if line and line[0] == 'Problème':
|
if line and line[0] in ["Problème", "Problem"]:
|
||||||
pool_size = len(line) - 1
|
pool_size = len(line) - 1
|
||||||
line_length = 2 + 6 * pool_size
|
line_length = 2 + (8 if df.iat[1, 8] == "Observer" else 6) * pool_size
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if pool_size == 0 or len(line) < line_length:
|
if pool_size == 0 or len(line) < line_length:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = line[0]
|
name = line[0]
|
||||||
if name.lower() in ["rôle", "juré⋅e", "juré?e", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]:
|
if name.lower() in ["rôle", "juré⋅e", "juré?e", "moyenne", "coefficient", "sous-total", "équipe", "equipe",
|
||||||
|
"role", "juree", "average", "coefficient", "subtotal", "team"]:
|
||||||
continue
|
continue
|
||||||
notes = line[2:line_length]
|
notes = line[2:line_length]
|
||||||
print(name, notes)
|
|
||||||
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
|
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
|
||||||
continue
|
continue
|
||||||
notes = list(map(lambda x: int(float(x)), notes))
|
notes = list(map(lambda x: int(float(x)), notes))
|
||||||
print(notes)
|
|
||||||
|
|
||||||
max_notes = pool_size * [20, 20, 10, 10, 10, 10]
|
max_notes = pool_size * [20 if settings.TFJM_APP == "TFJM" else 10,
|
||||||
|
20 if settings.TFJM_APP == "TFJM" else 10,
|
||||||
|
10, 10, 10, 10, 10, 10]
|
||||||
for n, max_n in zip(notes, max_notes):
|
for n, max_n in zip(notes, max_notes):
|
||||||
if n > max_n:
|
if n > max_n:
|
||||||
self.add_error('file',
|
self.add_error('file',
|
||||||
@ -325,21 +366,21 @@ class UploadNotesForm(forms.Form):
|
|||||||
class PassageForm(forms.ModelForm):
|
class PassageForm(forms.ModelForm):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
if "defender" in cleaned_data and "opponent" in cleaned_data and "reporter" in cleaned_data \
|
if "reporter" in cleaned_data and "opponent" in cleaned_data and "reviewer" in cleaned_data \
|
||||||
and len({cleaned_data["defender"], cleaned_data["opponent"], cleaned_data["reporter"]}) < 3:
|
and len({cleaned_data["reporter"], cleaned_data["opponent"], cleaned_data["reviewer"]}) < 3:
|
||||||
self.add_error(None, _("The defender, the opponent and the reporter must be different."))
|
self.add_error(None, _("The reporter, the opponent and the reviewer must be different."))
|
||||||
if "defender" in self.cleaned_data and "solution_number" in self.cleaned_data \
|
if "reporter" in self.cleaned_data and "solution_number" in self.cleaned_data \
|
||||||
and not Solution.objects.filter(participation=cleaned_data["defender"],
|
and not Solution.objects.filter(participation=cleaned_data["reporter"],
|
||||||
problem=cleaned_data["solution_number"]).exists():
|
problem=cleaned_data["solution_number"]).exists():
|
||||||
self.add_error("solution_number", _("This defender did not work on this problem."))
|
self.add_error("solution_number", _("This reporter did not work on this problem."))
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Passage
|
model = Passage
|
||||||
fields = ('position', 'solution_number', 'defender', 'opponent', 'reporter', 'defender_penalties',)
|
fields = ('position', 'solution_number', 'reporter', 'opponent', 'reviewer', 'opponent', 'reporter_penalties',)
|
||||||
|
|
||||||
|
|
||||||
class SynthesisForm(forms.ModelForm):
|
class WrittenReviewForm(forms.ModelForm):
|
||||||
def clean_file(self):
|
def clean_file(self):
|
||||||
if "file" in self.files:
|
if "file" in self.files:
|
||||||
file = self.files["file"]
|
file = self.files["file"]
|
||||||
@ -355,16 +396,22 @@ class SynthesisForm(forms.ModelForm):
|
|||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
"""
|
"""
|
||||||
Don't save a synthesis with this way. Use a view instead
|
Don't save a written review with this way. Use a view instead
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Synthesis
|
model = WrittenReview
|
||||||
fields = ('file',)
|
fields = ('file',)
|
||||||
|
|
||||||
|
|
||||||
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 = ('defender_writing', 'defender_oral', 'opponent_writing',
|
fields = ('reporter_writing', 'reporter_oral', 'opponent_writing',
|
||||||
'opponent_oral', 'reporter_writing', 'reporter_oral', )
|
'opponent_oral', 'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', )
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Copyright (C) 2021 by Animath
|
# Copyright (C) 2021 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.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import activate
|
from django.utils.translation import activate
|
||||||
@ -9,7 +10,7 @@ from participation.models import Tournament
|
|||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
activate('fr')
|
activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||||
|
|
||||||
tournaments = Tournament.objects.order_by('-date_start', 'name')
|
tournaments = Tournament.objects.order_by('-date_start', 'name')
|
||||||
for tournament in tournaments:
|
for tournament in tournaments:
|
||||||
|
@ -5,16 +5,16 @@ 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('fr')
|
|
||||||
|
|
||||||
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():
|
||||||
|
base_dir.mkdir()
|
||||||
|
base_dir /= "solutions"
|
||||||
if not base_dir.is_dir():
|
if not base_dir.is_dir():
|
||||||
base_dir.mkdir()
|
base_dir.mkdir()
|
||||||
base_dir /= "Par équipe"
|
base_dir /= "Par équipe"
|
||||||
|
@ -1,8 +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.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from participation.models import Team, Tournament
|
from participation.models import Team, Tournament
|
||||||
from registration.models import ParticipantRegistration, VolunteerRegistration
|
from registration.models import ParticipantRegistration, VolunteerRegistration
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
@ -13,6 +14,9 @@ class Command(BaseCommand):
|
|||||||
"""
|
"""
|
||||||
Create Sympa mailing lists and register teams.
|
Create Sympa mailing lists and register teams.
|
||||||
"""
|
"""
|
||||||
|
if not settings.ML_MANAGEMENT:
|
||||||
|
return
|
||||||
|
|
||||||
sympa = get_sympa_client()
|
sympa = get_sympa_client()
|
||||||
|
|
||||||
sympa.create_list("equipes", "Equipes du TFJM2", "hotline",
|
sympa.create_list("equipes", "Equipes du TFJM2", "hotline",
|
||||||
@ -33,7 +37,7 @@ class Command(BaseCommand):
|
|||||||
"education", raise_error=False)
|
"education", raise_error=False)
|
||||||
|
|
||||||
for tournament in Tournament.objects.all():
|
for tournament in Tournament.objects.all():
|
||||||
slug = tournament.name.lower().replace(" ", "-")
|
slug = slugify(tournament.name)
|
||||||
sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline",
|
sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline",
|
||||||
f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}"
|
f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}"
|
||||||
" du TFJM2.", "education", raise_error=False)
|
" du TFJM2.", "education", raise_error=False)
|
||||||
@ -51,7 +55,7 @@ class Command(BaseCommand):
|
|||||||
for team in Team.objects.filter(participation__valid=True).all():
|
for team in Team.objects.filter(participation__valid=True).all():
|
||||||
team.create_mailing_list()
|
team.create_mailing_list()
|
||||||
sympa.unsubscribe(team.email, "equipes-non-valides", True)
|
sympa.unsubscribe(team.email, "equipes-non-valides", True)
|
||||||
sympa.subscribe(team.email, f"equipes-{team.participation.tournament.name.lower().replace(' ', '-')}",
|
sympa.subscribe(team.email, f"equipes-{slugify(team.participation.tournament.name)}",
|
||||||
True, f"Equipe {team.name}")
|
True, f"Equipe {team.name}")
|
||||||
|
|
||||||
for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all():
|
for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all():
|
||||||
@ -59,16 +63,16 @@ class Command(BaseCommand):
|
|||||||
sympa.subscribe(team.email, "equipes-non-valides", True, f"Equipe {team.name}")
|
sympa.subscribe(team.email, "equipes-non-valides", True, f"Equipe {team.name}")
|
||||||
|
|
||||||
for participant in ParticipantRegistration.objects.filter(team__isnull=False).all():
|
for participant in ParticipantRegistration.objects.filter(team__isnull=False).all():
|
||||||
sympa.subscribe(participant.user.email, f"equipe-{participant.team.trigram.lower()}",
|
sympa.subscribe(participant.user.email, f"equipe-{slugify(participant.team.trigram)}",
|
||||||
True, f"{participant}")
|
True, f"{participant}")
|
||||||
|
|
||||||
for volunteer in VolunteerRegistration.objects.all():
|
for volunteer in VolunteerRegistration.objects.all():
|
||||||
for organized_tournament in volunteer.organized_tournaments.all():
|
for organized_tournament in volunteer.organized_tournaments.all():
|
||||||
slug = organized_tournament.name.lower().replace(" ", "-")
|
slug = slugify(organized_tournament.name)
|
||||||
sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True)
|
sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True)
|
||||||
|
|
||||||
for jury_in in volunteer.jury_in.all():
|
for jury_in in volunteer.jury_in.all():
|
||||||
slug = jury_in.tournament.name.lower().replace(" ", "-")
|
slug = slugify(jury_in.tournament.name)
|
||||||
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
|
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
|
||||||
|
|
||||||
for admin in VolunteerRegistration.objects.filter(admin=True).all():
|
for admin in VolunteerRegistration.objects.filter(admin=True).all():
|
||||||
|
@ -12,7 +12,7 @@ from ...models import Passage, Tournament
|
|||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
activate('fr')
|
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)
|
||||||
try:
|
try:
|
||||||
spreadsheet = gc.open("Tableau des deuxièmes", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
|
spreadsheet = gc.open("Tableau des deuxièmes", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
|
||||||
@ -51,25 +51,25 @@ class Command(BaseCommand):
|
|||||||
team3, score3 = sorted_notes[2]
|
team3, score3 = sorted_notes[2]
|
||||||
|
|
||||||
pool1 = tournament.pools.filter(round=1, participations=team2).first()
|
pool1 = tournament.pools.filter(round=1, participations=team2).first()
|
||||||
defender_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, defender=team2)
|
|
||||||
opponent_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, opponent=team2)
|
|
||||||
reporter_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, reporter=team2)
|
reporter_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, reporter=team2)
|
||||||
|
opponent_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, opponent=team2)
|
||||||
|
reviewer_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, reviewer=team2)
|
||||||
pool2 = tournament.pools.filter(round=2, participations=team2).first()
|
pool2 = tournament.pools.filter(round=2, participations=team2).first()
|
||||||
defender_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, defender=team2)
|
|
||||||
opponent_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, opponent=team2)
|
|
||||||
reporter_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, reporter=team2)
|
reporter_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, reporter=team2)
|
||||||
|
opponent_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, opponent=team2)
|
||||||
|
reviewer_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, reviewer=team2)
|
||||||
|
|
||||||
line.append(team2.team.trigram)
|
line.append(team2.team.trigram)
|
||||||
line.append(str(pool1.jury_president or ""))
|
line.append(str(pool1.jury_president or ""))
|
||||||
line.append(f"Pb. {defender_passage_1.solution_number}")
|
line.append(f"Pb. {reporter_passage_1.solution_number}")
|
||||||
line.extend([defender_passage_1.average_defender_writing, defender_passage_1.average_defender_oral,
|
line.extend([reporter_passage_1.average_reporter_writing, reporter_passage_1.average_reporter_oral,
|
||||||
opponent_passage_1.average_opponent_writing, opponent_passage_1.average_opponent_oral,
|
opponent_passage_1.average_opponent_writing, opponent_passage_1.average_opponent_oral,
|
||||||
reporter_passage_1.average_reporter_writing, reporter_passage_1.average_reporter_oral])
|
reviewer_passage_1.average_reviewer_writing, reviewer_passage_1.average_reviewer_oral])
|
||||||
line.append(str(pool2.jury_president or ""))
|
line.append(str(pool2.jury_president or ""))
|
||||||
line.append(f"Pb. {defender_passage_2.solution_number}")
|
line.append(f"Pb. {reporter_passage_2.solution_number}")
|
||||||
line.extend([defender_passage_2.average_defender_writing, defender_passage_2.average_defender_oral,
|
line.extend([reporter_passage_2.average_reporter_writing, reporter_passage_2.average_reporter_oral,
|
||||||
opponent_passage_2.average_opponent_writing, opponent_passage_2.average_opponent_oral,
|
opponent_passage_2.average_opponent_writing, opponent_passage_2.average_opponent_oral,
|
||||||
reporter_passage_2.average_reporter_writing, reporter_passage_2.average_reporter_oral])
|
reviewer_passage_2.average_reviewer_writing, reviewer_passage_2.average_reviewer_oral])
|
||||||
line.extend([score2, f"{score1:.1f} ({team1.team.trigram})",
|
line.extend([score2, f"{score1:.1f} ({team1.team.trigram})",
|
||||||
f"{score3:.1f} ({team3.team.trigram})"])
|
f"{score3:.1f} ({team3.team.trigram})"])
|
||||||
|
|
||||||
|
@ -15,6 +15,12 @@ from ...models import Tournament
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
"""
|
||||||
|
Création de notifications Google Drive pour récupérer les modifications sur les tableurs de notes.
|
||||||
|
|
||||||
|
Documentation de l'API : https://developers.google.com/calendar/api/guides/push?hl=fr
|
||||||
|
"""
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
|
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
|
||||||
|
31
participation/migrations/0014_alter_team_trigram.py
Normal file
31
participation/migrations/0014_alter_team_trigram.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-07 12:46
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0013_alter_pool_options_pool_room"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="team",
|
||||||
|
name="trigram",
|
||||||
|
field=models.CharField(
|
||||||
|
help_text="The code must be composed of 3 uppercase letters.",
|
||||||
|
max_length=3,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.RegexValidator("^[A-Z]{3}$"),
|
||||||
|
django.core.validators.RegexValidator(
|
||||||
|
"^(?!BIT$|CNO$|CRO$|CUL$|FTG$|FCK$|FUC$|FUK$|FYS$|HIV$|IST$|MST$|KKK$|KYS$|SEX$)",
|
||||||
|
message="This team code is forbidden.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verbose_name="code",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-07 13:51
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0014_alter_team_trigram"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="solutions_available_second_phase",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="solutions_available_second_phase",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="check this case when solutions for the second round become available",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="solutions_available_third_phase",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="check this case when solutions for the third round become available",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="syntheses_third_phase_limit",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
verbose_name="limit date to upload the syntheses for the third phase",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-07 14:01
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0015_tournament_solutions_available_third_phase_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="date_first_phase",
|
||||||
|
field=models.DateField(
|
||||||
|
default=datetime.date.today, verbose_name="first phase date"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="date_second_phase",
|
||||||
|
field=models.DateField(
|
||||||
|
default=datetime.date.today, verbose_name="first second date"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="date_third_phase",
|
||||||
|
field=models.DateField(
|
||||||
|
default=datetime.date.today, verbose_name="third phase date"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,77 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-13 08:53
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0016_tournament_date_first_phase_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="passage",
|
||||||
|
name="solution_number",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Problem #1"),
|
||||||
|
(2, "Problem #2"),
|
||||||
|
(3, "Problem #3"),
|
||||||
|
(4, "Problem #4"),
|
||||||
|
(5, "Problem #5"),
|
||||||
|
(6, "Problem #6"),
|
||||||
|
(7, "Problem #7"),
|
||||||
|
(8, "Problem #8"),
|
||||||
|
(9, "Problem #9"),
|
||||||
|
(10, "Problem #10"),
|
||||||
|
],
|
||||||
|
verbose_name="defended solution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="pool",
|
||||||
|
name="round",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[(1, "Round 1"), (2, "Round 2"), (3, "Round 3")],
|
||||||
|
verbose_name="round",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="solution",
|
||||||
|
name="problem",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Problem #1"),
|
||||||
|
(2, "Problem #2"),
|
||||||
|
(3, "Problem #3"),
|
||||||
|
(4, "Problem #4"),
|
||||||
|
(5, "Problem #5"),
|
||||||
|
(6, "Problem #6"),
|
||||||
|
(7, "Problem #7"),
|
||||||
|
(8, "Problem #8"),
|
||||||
|
(9, "Problem #9"),
|
||||||
|
(10, "Problem #10"),
|
||||||
|
],
|
||||||
|
verbose_name="problem",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="team",
|
||||||
|
name="trigram",
|
||||||
|
field=models.CharField(
|
||||||
|
help_text="The code must be composed of 4 uppercase letters.",
|
||||||
|
max_length=4,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.RegexValidator("^[A-Z]{3}[A-Z]*$"),
|
||||||
|
django.core.validators.RegexValidator(
|
||||||
|
"^(?!BIT$|CNO$|CRO$|CUL$|FTG$|FCK$|FUC$|FUK$|FYS$|HIV$|IST$|MST$|KKK$|KYS$|SEX$)",
|
||||||
|
message="This team code is forbidden.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verbose_name="code",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
91
participation/migrations/0018_rename_reporter_to_reviewer.py
Normal file
91
participation/migrations/0018_rename_reporter_to_reviewer.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-07-05 08:53
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"participation",
|
||||||
|
"0017_alter_passage_solution_number_alter_pool_round_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="note",
|
||||||
|
old_name="reporter_oral",
|
||||||
|
new_name="reviewer_oral",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="note",
|
||||||
|
old_name="reporter_writing",
|
||||||
|
new_name="reviewer_writing",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="passage",
|
||||||
|
old_name="reporter",
|
||||||
|
new_name="reviewer",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="reviewer_oral",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="reviewer oral note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="reviewer_writing",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="reviewer writing note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="passage",
|
||||||
|
name="reviewer",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="+",
|
||||||
|
to="participation.participation",
|
||||||
|
verbose_name="reviewer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="synthesis",
|
||||||
|
name="type",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[(1, "opponent"), (2, "reviewer")]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,86 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-07-05 09:47
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0018_rename_reporter_to_reviewer"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="note",
|
||||||
|
name="observer_oral",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(-10, -10),
|
||||||
|
(-9, -9),
|
||||||
|
(-8, -8),
|
||||||
|
(-7, -7),
|
||||||
|
(-6, -6),
|
||||||
|
(-5, -5),
|
||||||
|
(-4, -4),
|
||||||
|
(-3, -3),
|
||||||
|
(-2, -2),
|
||||||
|
(-1, -1),
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="observer oral note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="note",
|
||||||
|
name="observer_writing",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="observer writing note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="passage",
|
||||||
|
name="observer",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="participation.participation",
|
||||||
|
verbose_name="observer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="synthesis",
|
||||||
|
name="type",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[(1, "opponent"), (2, "reviewer"), (3, "observer")]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,75 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-07-06 19:19
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0019_note_observer_oral_note_observer_writing_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name="Synthesis",
|
||||||
|
new_name="WrittenReview",
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="writtenreview",
|
||||||
|
options={
|
||||||
|
"ordering": ("passage__pool__round", "type"),
|
||||||
|
"verbose_name": "written review",
|
||||||
|
"verbose_name_plural": "written reviews",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="tournament",
|
||||||
|
old_name="syntheses_first_phase_limit",
|
||||||
|
new_name="reviews_first_phase_limit",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="tournament",
|
||||||
|
old_name="syntheses_second_phase_limit",
|
||||||
|
new_name="reviews_second_phase_limit",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="tournament",
|
||||||
|
old_name="syntheses_third_phase_limit",
|
||||||
|
new_name="reviews_third_phase_limit",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="reviews_first_phase_limit",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
verbose_name="limit date to upload the written reviews for the first phase",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="reviews_second_phase_limit",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
verbose_name="limit date to upload the written reviews for the second phase",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="reviews_third_phase_limit",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
verbose_name="limit date to upload the written reviews for the third phase",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="writtenreview",
|
||||||
|
name="passage",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="written_reviews",
|
||||||
|
to="participation.passage",
|
||||||
|
verbose_name="passage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,133 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-07-06 20:00
|
||||||
|
import django
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0020_rename_synthesis_writtenreview_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="note",
|
||||||
|
old_name="defender_oral",
|
||||||
|
new_name="reporter_oral",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="note",
|
||||||
|
old_name="defender_writing",
|
||||||
|
new_name="reporter_writing",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="passage",
|
||||||
|
old_name="defender",
|
||||||
|
new_name="reporter",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="passage",
|
||||||
|
old_name="defender_penalties",
|
||||||
|
new_name="reporter_penalties",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="passage",
|
||||||
|
name="solution_number",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Problem #1"),
|
||||||
|
(2, "Problem #2"),
|
||||||
|
(3, "Problem #3"),
|
||||||
|
(4, "Problem #4"),
|
||||||
|
(5, "Problem #5"),
|
||||||
|
(6, "Problem #6"),
|
||||||
|
(7, "Problem #7"),
|
||||||
|
(8, "Problem #8"),
|
||||||
|
(9, "Problem #9"),
|
||||||
|
(10, "Problem #10"),
|
||||||
|
],
|
||||||
|
verbose_name="reported solution",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="reporter_oral",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
(11, 11),
|
||||||
|
(12, 12),
|
||||||
|
(13, 13),
|
||||||
|
(14, 14),
|
||||||
|
(15, 15),
|
||||||
|
(16, 16),
|
||||||
|
(17, 17),
|
||||||
|
(18, 18),
|
||||||
|
(19, 19),
|
||||||
|
(20, 20),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="reporter oral note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="reporter_writing",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
(11, 11),
|
||||||
|
(12, 12),
|
||||||
|
(13, 13),
|
||||||
|
(14, 14),
|
||||||
|
(15, 15),
|
||||||
|
(16, 16),
|
||||||
|
(17, 17),
|
||||||
|
(18, 18),
|
||||||
|
(19, 19),
|
||||||
|
(20, 20),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="reporter writing note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="passage",
|
||||||
|
name="reporter",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="+",
|
||||||
|
to="participation.participation",
|
||||||
|
verbose_name="reporter",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="passage",
|
||||||
|
name="reporter_penalties",
|
||||||
|
field=models.PositiveSmallIntegerField(
|
||||||
|
default=0,
|
||||||
|
help_text="Number of penalties for the reporter. The reporter will loose a 0.5 coefficient per penalty.",
|
||||||
|
verbose_name="penalties",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
44
participation/migrations/0022_alter_note_observer_oral.py
Normal file
44
participation/migrations/0022_alter_note_observer_oral.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-07-11 08:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0021_rename_defender_oral_note_reporter_oral_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="observer_oral",
|
||||||
|
field=models.SmallIntegerField(
|
||||||
|
choices=[
|
||||||
|
(-10, -10),
|
||||||
|
(-9, -9),
|
||||||
|
(-8, -8),
|
||||||
|
(-7, -7),
|
||||||
|
(-6, -6),
|
||||||
|
(-5, -5),
|
||||||
|
(-4, -4),
|
||||||
|
(-3, -3),
|
||||||
|
(-2, -2),
|
||||||
|
(-1, -1),
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 5),
|
||||||
|
(6, 6),
|
||||||
|
(7, 7),
|
||||||
|
(8, 8),
|
||||||
|
(9, 9),
|
||||||
|
(10, 10),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
verbose_name="observer oral note",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-01-14 18:06
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("participation", "0022_alter_note_observer_oral"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tournament",
|
||||||
|
name="unified_registration",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, verbose_name="unified registration"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,10 @@
|
|||||||
# 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 typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from participation.models import Note, Participation, Passage, Pool, Team, Tournament
|
from participation.models import Note, Participation, Passage, Pool, Team, Tournament
|
||||||
from registration.models import Payment
|
from registration.models import Payment
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
@ -13,6 +16,8 @@ def create_team_participation(instance, created, raw, **_):
|
|||||||
"""
|
"""
|
||||||
if not raw:
|
if not raw:
|
||||||
participation = Participation.objects.get_or_create(team=instance)[0]
|
participation = Participation.objects.get_or_create(team=instance)[0]
|
||||||
|
if settings.TFJM_APP == "ETEAM":
|
||||||
|
participation.tournament = Tournament.objects.first()
|
||||||
participation.save()
|
participation.save()
|
||||||
if not created:
|
if not created:
|
||||||
participation.team.create_mailing_list()
|
participation.team.create_mailing_list()
|
||||||
@ -22,7 +27,7 @@ def update_mailing_list(instance: Team, raw, **_):
|
|||||||
"""
|
"""
|
||||||
When a team name or trigram got updated, update mailing lists
|
When a team name or trigram got updated, update mailing lists
|
||||||
"""
|
"""
|
||||||
if instance.pk and not raw:
|
if instance.pk and not raw and settings.ML_MANAGEMENT:
|
||||||
old_team = Team.objects.get(pk=instance.pk)
|
old_team = Team.objects.get(pk=instance.pk)
|
||||||
if old_team.trigram != instance.trigram:
|
if old_team.trigram != instance.trigram:
|
||||||
# Delete old mailing list, create a new one
|
# Delete old mailing list, create a new one
|
||||||
@ -30,10 +35,10 @@ def update_mailing_list(instance: Team, raw, **_):
|
|||||||
instance.create_mailing_list()
|
instance.create_mailing_list()
|
||||||
# Subscribe all team members in the mailing list
|
# Subscribe all team members in the mailing list
|
||||||
for student in instance.students.all():
|
for student in instance.students.all():
|
||||||
get_sympa_client().subscribe(student.user.email, f"equipe-{instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(student.user.email, f"equipe-{slugify(instance.trigram)}", False,
|
||||||
f"{student.user.first_name} {student.user.last_name}")
|
f"{student.user.first_name} {student.user.last_name}")
|
||||||
for coach in instance.coaches.all():
|
for coach in instance.coaches.all():
|
||||||
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(coach.user.email, f"equipe-{slugify(instance.trigram)}", False,
|
||||||
f"{coach.user.first_name} {coach.user.last_name}")
|
f"{coach.user.first_name} {coach.user.last_name}")
|
||||||
|
|
||||||
|
|
||||||
@ -41,7 +46,7 @@ def create_payments(instance: Participation, created, raw, **_):
|
|||||||
"""
|
"""
|
||||||
When a participation got created, create an associated payment.
|
When a participation got created, create an associated payment.
|
||||||
"""
|
"""
|
||||||
if instance.valid and not raw:
|
if instance.valid and not raw and settings.PAYMENT_MANAGEMENT:
|
||||||
for student in instance.team.students.all():
|
for student in instance.team.students.all():
|
||||||
payment_qs = Payment.objects.filter(registrations=student, final=False)
|
payment_qs = Payment.objects.filter(registrations=student, final=False)
|
||||||
if payment_qs.exists():
|
if payment_qs.exists():
|
||||||
|
@ -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,19 +107,22 @@ class PoolTable(tables.Table):
|
|||||||
|
|
||||||
|
|
||||||
class PassageTable(tables.Table):
|
class PassageTable(tables.Table):
|
||||||
defender = tables.LinkColumn(
|
reporter = tables.LinkColumn(
|
||||||
"participation:passage_detail",
|
"participation:passage_detail",
|
||||||
args=[tables.A("id")],
|
args=[tables.A("id")],
|
||||||
verbose_name=_("defender").capitalize,
|
verbose_name=_("reporter").capitalize,
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_defender(self, value):
|
def render_reporter(self, value):
|
||||||
return value.team.trigram
|
return value.team.trigram
|
||||||
|
|
||||||
def render_opponent(self, value):
|
def render_opponent(self, value):
|
||||||
return value.team.trigram
|
return value.team.trigram
|
||||||
|
|
||||||
def render_reporter(self, value):
|
def render_reviewer(self, value):
|
||||||
|
return value.team.trigram
|
||||||
|
|
||||||
|
def render_observer(self, value):
|
||||||
return value.team.trigram
|
return value.team.trigram
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -126,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 = ('defender', 'opponent', 'reporter', 'solution_number', )
|
fields = ('reporter', 'opponent', 'reviewer',) \
|
||||||
|
+ (('observer',) if settings.HAS_OBSERVER else ()) \
|
||||||
|
+ ('solution_number', )
|
||||||
|
|
||||||
|
|
||||||
class NoteTable(tables.Table):
|
class NoteTable(tables.Table):
|
||||||
@ -154,5 +160,7 @@ class NoteTable(tables.Table):
|
|||||||
'class': 'table table-condensed table-striped text-center',
|
'class': 'table table-condensed table-striped text-center',
|
||||||
}
|
}
|
||||||
model = Note
|
model = Note
|
||||||
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
fields = ('jury', 'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral',
|
||||||
'reporter_writing', 'reporter_oral', 'update',)
|
'reviewer_writing', 'reviewer_oral',) + \
|
||||||
|
(('observer_writing', 'observer_oral') if settings.HAS_OBSERVER else ()) + \
|
||||||
|
('update',)
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
<div id="form-content">
|
<div id="form-content">
|
||||||
<h4>{% trans "Notes of" %} {{ note.jury }}</h4>
|
<h4>{% trans "Notes of" %} {{ note.jury }}</h4>
|
||||||
<h5>{% trans "Defense of" %} {{ note.passage.defender.team.trigram }}, {% trans "Pb." %} {{ note.passage.solution_number }}</h5>
|
<h5>{% trans "Defense of" %} {{ note.passage.reporter.team.trigram }}, {% trans "Pb." %} {{ note.passage.solution_number }}</h5>
|
||||||
<hr>
|
<hr>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
|
@ -25,27 +25,32 @@
|
|||||||
<dt class="col-sm-3">{% trans "Position:" %}</dt>
|
<dt class="col-sm-3">{% trans "Position:" %}</dt>
|
||||||
<dd class="col-sm-9">{{ passage.position }}</dd>
|
<dd class="col-sm-9">{{ passage.position }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Defender:" %}</dt>
|
<dt class="col-sm-3">{% trans "Reporter:" %}</dt>
|
||||||
<dd class="col-sm-9"><a href="{{ passage.defender.get_absolute_url }}">{{ passage.defender.team }}</a></dd>
|
<dd class="col-sm-9"><a href="{{ passage.reporter.get_absolute_url }}">{{ passage.reporter.team }}</a></dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Opponent:" %}</dt>
|
<dt class="col-sm-3">{% trans "Opponent:" %}</dt>
|
||||||
<dd class="col-sm-9"><a href="{{ passage.opponent.get_absolute_url }}">{{ passage.opponent.team }}</a></dd>
|
<dd class="col-sm-9"><a href="{{ passage.opponent.get_absolute_url }}">{{ passage.opponent.team }}</a></dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Reporter:" %}</dt>
|
<dt class="col-sm-3">{% trans "Reviewer:" %}</dt>
|
||||||
<dd class="col-sm-9"><a href="{{ passage.reporter.get_absolute_url }}">{{ passage.reporter.team }}</a></dd>
|
<dd class="col-sm-9"><a href="{{ passage.reviewer.get_absolute_url }}">{{ passage.reviewer.team }}</a></dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Defended solution:" %}</dt>
|
{% if passage.observer %}
|
||||||
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}</a></dd>
|
<dt class="col-sm-3">{% trans "Observer:" %}</dt>
|
||||||
|
<dd class="col-sm-9"><a href="{{ passage.observer.get_absolute_url }}">{{ passage.observer.team }}</a></dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Defender penalties count:" %}</dt>
|
<dt class="col-sm-3">{% trans "Reported solution:" %}</dt>
|
||||||
<dd class="col-sm-9">{{ passage.defender_penalties }}</dd>
|
<dd class="col-sm-9"><a href="{{ passage.reported_solution.file.url }}">{{ passage.reported_solution }}</a></dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">{% trans "Reporter penalties count:" %}</dt>
|
||||||
|
<dd class="col-sm-9">{{ passage.reporter_penalties }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Syntheses:" %}</dt>
|
<dt class="col-sm-3">{% trans "Syntheses:" %}</dt>
|
||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">
|
||||||
{% for synthesis in passage.syntheses.all %}
|
{% for review in passage.written_reviews.all %}
|
||||||
<a href="{{ synthesis.file.url }}">{{ synthesis }}{% if not forloop.last %}, {% endif %}</a>
|
<a href="{{ review.file.url }}">{{ review }}{% if not forloop.last %}, {% endif %}</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
{% trans "No synthesis was uploaded yet." %}
|
{% trans "No review was uploaded yet." %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
@ -58,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% elif user.registration.participates %}
|
{% elif user.registration.participates %}
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadSynthesisModal">{% trans "Upload synthesis" %}</button>
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadWrittenReviewModal">{% trans "Upload review" %}</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -74,16 +79,20 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-8">
|
<dt class="col-sm-8">
|
||||||
{% trans "Average points for the defender writing" %}
|
{% trans "Average points for the reporter writing" %}
|
||||||
({{ passage.defender.team.trigram }}) :
|
({{ passage.reporter.team.trigram }}) :
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_defender_writing|floatformat }}/20</dd>
|
<dd class="col-sm-4">
|
||||||
|
{{ passage.average_reporter_writing|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-8">
|
<dt class="col-sm-8">
|
||||||
{% trans "Average points for the defender oral" %}
|
{% trans "Average points for the reporter oral" %}
|
||||||
({{ passage.defender.team.trigram }}) :
|
({{ passage.reporter.team.trigram }}) :
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_defender_oral|floatformat }}/20</dd>
|
<dd class="col-sm-4">
|
||||||
|
{{ passage.average_reporter_oral|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-8">
|
<dt class="col-sm-8">
|
||||||
{% trans "Average points for the opponent writing" %}
|
{% trans "Average points for the opponent writing" %}
|
||||||
@ -98,38 +107,65 @@
|
|||||||
<dd class="col-sm-4">{{ passage.average_opponent_oral|floatformat }}/10</dd>
|
<dd class="col-sm-4">{{ passage.average_opponent_oral|floatformat }}/10</dd>
|
||||||
|
|
||||||
<dt class="col-sm-8">
|
<dt class="col-sm-8">
|
||||||
{% trans "Average points for the reporter writing" %}
|
{% trans "Average points for the reviewer writing" %}
|
||||||
({{ passage.reporter.team.trigram }}) :
|
({{ passage.reviewer.team.trigram }}) :
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_reporter_writing|floatformat }}/10</dd>
|
<dd class="col-sm-4">{{ passage.average_reviewer_writing|floatformat }}/10</dd>
|
||||||
|
|
||||||
<dt class="col-sm-8">
|
<dt class="col-sm-8">
|
||||||
{% trans "Average points for the reporter oral" %}
|
{% trans "Average points for the reviewer oral" %}
|
||||||
({{ passage.reporter.team.trigram }}) :
|
({{ passage.reviewer.team.trigram }}) :
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd>
|
<dd class="col-sm-4">{{ passage.average_reviewer_oral|floatformat }}/10</dd>
|
||||||
|
|
||||||
|
{% if passage.observer %}
|
||||||
|
<dt class="col-sm-8">
|
||||||
|
{% trans "Average points for the observer writing" %}
|
||||||
|
({{ passage.observer.team.trigram }}) :
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-4">{{ passage.average_observer_writing|floatformat }}/10</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-8">
|
||||||
|
{% trans "Average points for the observer oral" %}
|
||||||
|
({{ passage.observer.team.trigram }}) :
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-4">{{ passage.average_observer_oral|floatformat }}/10</dd>
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-8">
|
<dt class="col-sm-8">
|
||||||
{% trans "Defender points" %}
|
{% trans "Reporter points" %}
|
||||||
({{ passage.defender.team.trigram }}) :
|
({{ passage.reporter.team.trigram }}) :
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_defender|floatformat }}/52</dd>
|
<dd class="col-sm-4">
|
||||||
|
{{ passage.average_reporter|floatformat }}/{% if TFJM_APP == "TFJM" %}52{% else %}50{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-8">
|
<dt class="col-sm-8">
|
||||||
{% trans "Opponent points" %}
|
{% trans "Opponent points" %}
|
||||||
({{ passage.opponent.team.trigram }}) :
|
({{ passage.opponent.team.trigram }}) :
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_opponent|floatformat }}/29</dd>
|
<dd class="col-sm-4">
|
||||||
|
{{ passage.average_opponent|floatformat }}/{% if TFJM_APP == "TFJM" %}29{% else %}{% if passage.observer %}26{% else %}29{% endif %}{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-8">
|
<dt class="col-sm-8">
|
||||||
{% trans "Reporter points" %}
|
{% trans "reviewer points" %}
|
||||||
({{ passage.reporter.team.trigram }}) :
|
({{ passage.reviewer.team.trigram }}) :
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd>
|
<dd class="col-sm-4">{{ passage.average_reviewer|floatformat }}/{% if TFJM_APP == "TFJM" %}19{% else %}{% if passage.observer %}18{% else %}21{% endif %}{% endif %}</dd>
|
||||||
|
|
||||||
|
{% if passage.observer %}
|
||||||
|
<dt class="col-sm-8">
|
||||||
|
{% trans "observer points" %}
|
||||||
|
({{ passage.observer.team.trigram }}) :
|
||||||
|
</dt>
|
||||||
|
|
||||||
|
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/6</dd>
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -148,10 +184,10 @@
|
|||||||
{% include "base_modal.html" with modal_id=note.modal_name %}
|
{% include "base_modal.html" with modal_id=note.modal_name %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% elif user.registration.participates %}
|
{% elif user.registration.participates %}
|
||||||
{% trans "Upload synthesis" as modal_title %}
|
{% trans "Upload review" as modal_title %}
|
||||||
{% trans "Upload" as modal_button %}
|
{% trans "Upload" as modal_button %}
|
||||||
{% url "participation:upload_synthesis" pk=passage.pk as modal_action %}
|
{% url "participation:upload_written_review" pk=passage.pk as modal_action %}
|
||||||
{% include "base_modal.html" with modal_id="uploadSynthesis" modal_enctype="multipart/form-data" %}
|
{% include "base_modal.html" with modal_id="uploadWrittenReview" modal_enctype="multipart/form-data" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -165,8 +201,8 @@
|
|||||||
initModal("{{ note.modal_name }}", "{% url "participation:update_notes" pk=note.pk %}")
|
initModal("{{ note.modal_name }}", "{% url "participation:update_notes" pk=note.pk %}")
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% elif user.registration.participates %}
|
{% elif user.registration.participates %}
|
||||||
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
|
initModal("uploadWrittenReview", "{% url "participation:upload_written_review" pk=passage.pk %}")
|
||||||
{% endif %}
|
{% endif %}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -46,10 +46,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Defended solutions:" %}</dt>
|
<dt class="col-sm-3">{% trans "Reported solutions:" %}</dt>
|
||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">
|
||||||
{% for passage in pool.passages.all %}
|
{% for passage in pool.passages.all %}
|
||||||
<a href="{{ passage.defended_solution.file.url }}">{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %}
|
<a href="{{ passage.reported_solution.file.url }}">{{ passage.reporter.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<a href="{% url 'participation:pool_download_solutions' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
|
<a href="{% url 'participation:pool_download_solutions' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
|
||||||
<i class="fas fa-download"></i> {% trans "Download all" %}
|
<i class="fas fa-download"></i> {% trans "Download all" %}
|
||||||
@ -61,16 +61,16 @@
|
|||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for passage in pool.passages.all %}
|
{% for passage in pool.passages.all %}
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }} :
|
{{ passage.reporter.team.trigram }} — {{ passage.get_solution_number_display }} :
|
||||||
{% for synthesis in passage.syntheses.all %}
|
{% for review in passage.written_reviews.all %}
|
||||||
<a href="{{ synthesis.file.url }}">{{ synthesis.participation.team.trigram }} ({{ synthesis.get_type_display }})</a>{% if not forloop.last %}, {% endif %}
|
<a href="{{ review.file.url }}">{{ review.participation.team.trigram }} ({{ review.get_type_display }})</a>{% if not forloop.last %}, {% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
{% trans "No synthesis was uploaded yet." %}
|
{% trans "No review was uploaded yet." %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<a href="{% url 'participation:pool_download_syntheses' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
|
<a href="{% url 'participation:pool_download_written_reviews' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
|
||||||
<i class="fas fa-download"></i> {% trans "Download all" %}
|
<i class="fas fa-download"></i> {% trans "Download all" %}
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
|
@ -73,32 +73,36 @@
|
|||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not team.participation.tournament.remote %}
|
{% if not team.participation.tournament.remote %}
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Health sheets:" %}</dt>
|
{% if TFJM.HEALTH_SHEET_REQUIRED %}
|
||||||
<dd class="col-sm-6">
|
<dt class="col-sm-6 text-sm-end">{% trans "Health sheets:" %}</dt>
|
||||||
{% for student in team.students.all %}
|
<dd class="col-sm-6">
|
||||||
{% if student.under_18 %}
|
{% for student in team.students.all %}
|
||||||
{% if student.health_sheet %}
|
{% if student.under_18 %}
|
||||||
<a href="{{ student.health_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
{% if student.health_sheet %}
|
||||||
{% else %}
|
<a href="{{ student.health_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
||||||
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
|
{% else %}
|
||||||
|
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{% endfor %}
|
</dd>
|
||||||
</dd>
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Vaccine sheets:" %}</dt>
|
{% if TFJM.VACCINE_SHEET_REQUIRED %}
|
||||||
<dd class="col-sm-6">
|
<dt class="col-sm-6 text-sm-end">{% trans "Vaccine sheets:" %}</dt>
|
||||||
{% for student in team.students.all %}
|
<dd class="col-sm-6">
|
||||||
{% if student.under_18 %}
|
{% for student in team.students.all %}
|
||||||
{% if student.vaccine_sheet %}
|
{% if student.under_18 %}
|
||||||
<a href="{{ student.vaccine_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
{% if student.vaccine_sheet %}
|
||||||
{% else %}
|
<a href="{{ student.vaccine_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
||||||
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
|
{% else %}
|
||||||
|
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{% endfor %}
|
</dd>
|
||||||
</dd>
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Parental authorizations:" %}</dt>
|
<dt class="col-sm-6 text-sm-end">{% trans "Parental authorizations:" %}</dt>
|
||||||
<dd class="col-sm-6">
|
<dd class="col-sm-6">
|
||||||
@ -129,17 +133,19 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Motivation letter:" %}</dt>
|
{% if TFJM.MOTIVATION_LETTER_REQUIRED %}
|
||||||
<dd class="col-sm-6">
|
<dt class="col-sm-6 text-sm-end">{% trans "Motivation letter:" %}</dt>
|
||||||
{% if team.motivation_letter %}
|
<dd class="col-sm-6">
|
||||||
<a href="{{ team.motivation_letter.url }}">{% trans "Download" %}</a>
|
{% if team.motivation_letter %}
|
||||||
{% else %}
|
<a href="{{ team.motivation_letter.url }}">{% trans "Download" %}</a>
|
||||||
<em>{% trans "Not uploaded yet" %}</em>
|
{% else %}
|
||||||
{% endif %}
|
<em>{% trans "Not uploaded yet" %}</em>
|
||||||
{% if user.registration.team == team and not user.registration.team.participation.valid or user.registration.is_admin %}
|
{% endif %}
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadMotivationLetterModal">{% trans "Replace" %}</button>
|
{% if user.registration.team == team and not user.registration.team.participation.valid or user.registration.is_admin %}
|
||||||
{% endif %}
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadMotivationLetterModal">{% trans "Replace" %}</button>
|
||||||
</dd>
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if user.registration.is_volunteer %}
|
{% if user.registration.is_volunteer %}
|
||||||
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
|
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
|
||||||
@ -234,10 +240,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% trans "Upload motivation letter" as modal_title %}
|
{% if TFJM.MOTIVATION_LETTER_REQUIRED %}
|
||||||
{% trans "Upload" as modal_button %}
|
{% trans "Upload motivation letter" as modal_title %}
|
||||||
{% url "participation:upload_team_motivation_letter" pk=team.pk as modal_action %}
|
{% trans "Upload" as modal_button %}
|
||||||
{% include "base_modal.html" with modal_id="uploadMotivationLetter" modal_enctype="multipart/form-data" %}
|
{% url "participation:upload_team_motivation_letter" pk=team.pk as modal_action %}
|
||||||
|
{% include "base_modal.html" with modal_id="uploadMotivationLetter" modal_enctype="multipart/form-data" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% trans "Update team" as modal_title %}
|
{% trans "Update team" as modal_title %}
|
||||||
{% trans "Update" as modal_button %}
|
{% trans "Update" as modal_button %}
|
||||||
@ -253,7 +261,9 @@
|
|||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initModal("uploadMotivationLetter", "{% url "participation:upload_team_motivation_letter" pk=team.pk %}")
|
{% if TFJM.MOTIVATION_LETTER_REQUIRED %}
|
||||||
|
initModal("uploadMotivationLetter", "{% url "participation:upload_team_motivation_letter" pk=team.pk %}")
|
||||||
|
{% endif %}
|
||||||
initModal("updateTeam", "{% url "participation:update_team" pk=team.pk %}")
|
initModal("updateTeam", "{% url "participation:update_team" pk=team.pk %}")
|
||||||
initModal("leaveTeam", "{% url "participation:team_leave" %}")
|
initModal("leaveTeam", "{% url "participation:team_leave" %}")
|
||||||
})
|
})
|
||||||
|
88
participation/templates/participation/tex/final_sheet.tex
Normal file
88
participation/templates/participation/tex/final_sheet.tex
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
\documentclass[10pt,a4paper,landscape]{article}
|
||||||
|
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
\usepackage[utf8x]{inputenc}
|
||||||
|
\usepackage[french]{babel}
|
||||||
|
|
||||||
|
\usepackage[a4paper]{geometry}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amsfonts}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
\usepackage{amsthm}
|
||||||
|
\usepackage{hyperref}
|
||||||
|
\usepackage{color}
|
||||||
|
\usepackage{mathtools}
|
||||||
|
\usepackage{comment}
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{multirow}
|
||||||
|
\usepackage{footnote}
|
||||||
|
\usepackage{tabularx}
|
||||||
|
|
||||||
|
\addtolength{\textwidth}{6cm}
|
||||||
|
\addtolength{\oddsidemargin}{-3cm}
|
||||||
|
\addtolength{\textheight}{2cm}
|
||||||
|
\addtolength{\topmargin}{-0.5cm}
|
||||||
|
\setlength{\parindent}{0mm}
|
||||||
|
|
||||||
|
\DeclareUnicodeCharacter{22C5}{\textperiodcentered{}}
|
||||||
|
|
||||||
|
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
|
||||||
|
\renewcommand{\leq}{\leqslant}
|
||||||
|
\def\tfjmedition{~{{ tfjm_number }}}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\pagenumbering{gobble}
|
||||||
|
|
||||||
|
\centering
|
||||||
|
|
||||||
|
{% if TFJM.APP == "TFJM" %}
|
||||||
|
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\
|
||||||
|
{% else %}
|
||||||
|
\Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\
|
||||||
|
{% endif %}
|
||||||
|
\vspace{3mm}
|
||||||
|
{% 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}
|
||||||
|
|
||||||
|
|
||||||
|
\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 %}& \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 %}$
|
||||||
|
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq {% if TFJM.APP == "TFJM" %}20{% else %}10{% endif %}$
|
||||||
|
{% endfor %} & \hline
|
||||||
|
\multirow{2}{35mm}{\LARGE {% trans "Opponent" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.opponent.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 10$
|
||||||
|
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||||
|
{% endfor %} & \hline
|
||||||
|
\multirow{2}{35mm}{\LARGE {% trans "Reviewer" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.reviewer.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 10$
|
||||||
|
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||||
|
{% endfor %} & \hline
|
||||||
|
{% if TFJM.APP == "ETEAM" and pool.participations.count >= 4 %}
|
||||||
|
\multirow{2}{35mm}{\LARGE {% trans "Observer" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.observer.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 10$
|
||||||
|
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||||
|
{% endfor %} & \hline
|
||||||
|
{% endif %}
|
||||||
|
\end{tabular}
|
||||||
|
|
||||||
|
\vspace{15mm}
|
||||||
|
|
||||||
|
\LARGE {% trans "name"|capfirst %} {% trans "Juree"|lower %} :
|
||||||
|
{% if jury %}\underline{ {{ jury.user.first_name|safe }} {{ jury.user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %}
|
||||||
|
$\qquad$ {% trans "Signature" %} : \underline{\phantom{Phrase moins longue}}
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
%}
|
||||||
|
\end{document}
|
@ -1,74 +0,0 @@
|
|||||||
\documentclass[10pt,a4paper,landscape]{article}
|
|
||||||
|
|
||||||
\usepackage[T1]{fontenc}
|
|
||||||
\usepackage[utf8x]{inputenc}
|
|
||||||
\usepackage[french]{babel}
|
|
||||||
|
|
||||||
\usepackage[a4paper]{geometry}
|
|
||||||
\usepackage{graphicx}
|
|
||||||
\usepackage{amsmath}
|
|
||||||
\usepackage{amsfonts}
|
|
||||||
\usepackage{amssymb}
|
|
||||||
\usepackage{amsthm}
|
|
||||||
\usepackage{hyperref}
|
|
||||||
\usepackage{color}
|
|
||||||
\usepackage{mathtools}
|
|
||||||
\usepackage{comment}
|
|
||||||
\usepackage{array}
|
|
||||||
\usepackage{multirow}
|
|
||||||
\usepackage{footnote}
|
|
||||||
\usepackage{tabularx}
|
|
||||||
\usepackage{xintexpr}
|
|
||||||
|
|
||||||
\addtolength{\textwidth}{6cm}
|
|
||||||
\addtolength{\oddsidemargin}{-3cm}
|
|
||||||
\addtolength{\textheight}{2cm}
|
|
||||||
\addtolength{\topmargin}{-0.5cm}
|
|
||||||
\setlength{\parindent}{0mm}
|
|
||||||
|
|
||||||
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
|
|
||||||
\renewcommand{\leq}{\leqslant}
|
|
||||||
\def\tfjmedition{~{{ tfjm_number }}}
|
|
||||||
|
|
||||||
\begin{document}
|
|
||||||
\pagenumbering{gobble}
|
|
||||||
|
|
||||||
\centering
|
|
||||||
|
|
||||||
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\
|
|
||||||
\vspace{3mm}
|
|
||||||
Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{% if pool.participations.count == 5 %} \;-- {{ pool.get_room_display }}{% endif %} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_start }}{% else %}{{ pool.tournament.date_end }}{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
\vspace{15mm}
|
|
||||||
|
|
||||||
|
|
||||||
\begin{tabular}{|p{40mm}{% for passage in passages.all %}{% if passages.count == 3 %}|p{3cm}|p{3cm}{% else %}|p{2.5cm}|p{2.5cm}{% endif %}{% endfor %}|}\hline
|
|
||||||
\multirow{2}{40mm}{\LARGE R\^ole} {% for passage in passages.all %}& \multicolumn{2}{c|}{ \Large Probl\`eme {{ passage.solution_number }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
|
|
||||||
{% for passage in passages.all %}& \hspace{4mm} {\Large \'ECRIT} & \hspace{4mm} {\Large ORAL}{% endfor %} \\ \hline
|
|
||||||
\multirow{2}{35mm}{\LARGE D\'efenseur\textperiodcentered{}se} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.defender.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 20$
|
|
||||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 20$
|
|
||||||
{% endfor %} & \hline
|
|
||||||
\multirow{2}{35mm}{\LARGE Opposant\textperiodcentered{}e} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.opponent.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 10$
|
|
||||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
|
||||||
{% endfor %} & \hline
|
|
||||||
\multirow{2}{35mm}{\LARGE Rapporteur\textperiodcentered{}rice} {% 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 10$
|
|
||||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
|
||||||
{% endfor %} & \hline
|
|
||||||
\end{tabular}
|
|
||||||
|
|
||||||
\vspace{15mm}
|
|
||||||
|
|
||||||
\LARGE Nom jur\'e\textperiodcentered{}e :
|
|
||||||
{% if jury %}\underline{ {{ jury.user.first_name|safe }} {{ jury.user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %}
|
|
||||||
$\qquad$ Signature : \underline{\phantom{Phrase moins longue}}
|
|
||||||
|
|
||||||
\newpage
|
|
||||||
%}
|
|
||||||
\end{document}
|
|
151
participation/templates/participation/tex/scale_eteam.tex
Normal file
151
participation/templates/participation/tex/scale_eteam.tex
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
\documentclass[11pt,a4paper,landscape]{article}
|
||||||
|
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
\usepackage[utf8x]{inputenc}
|
||||||
|
\usepackage[english]{babel}
|
||||||
|
|
||||||
|
\usepackage[a4paper]{geometry}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amsfonts}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
\usepackage{amsthm}
|
||||||
|
\usepackage{hyperref}
|
||||||
|
\usepackage{color}
|
||||||
|
\usepackage{mathtools}
|
||||||
|
\usepackage{comment}
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{multirow}
|
||||||
|
\usepackage{footnote}
|
||||||
|
\usepackage{rotating}
|
||||||
|
|
||||||
|
\addtolength{\textwidth}{4cm}
|
||||||
|
\setlength{\parindent}{0mm}
|
||||||
|
|
||||||
|
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=1.2cm}
|
||||||
|
|
||||||
|
\DeclareUnicodeCharacter{22C5}{\textperiodcentered{}}
|
||||||
|
|
||||||
|
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
|
||||||
|
\pagestyle{empty}
|
||||||
|
\renewcommand{\leq}{\leqslant}
|
||||||
|
\def\tfjmedition{~{{ tfjm_number }}}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\thispagestyle{empty}
|
||||||
|
|
||||||
|
|
||||||
|
\begin{center}
|
||||||
|
{% if TFJM.APP == "TFJM" %}
|
||||||
|
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\
|
||||||
|
{% else %}
|
||||||
|
\Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\
|
||||||
|
{% endif %}
|
||||||
|
\end{center}
|
||||||
|
\vspace{3mm}
|
||||||
|
|
||||||
|
\begin{center}
|
||||||
|
\begin{itemize}
|
||||||
|
{% for passage in passages.all %}
|
||||||
|
\item {% trans "Reporter" %} {% trans "for passage" %} {{ forloop.counter }} : \underline{\texttt{~{{ passage.reporter.team.trigram }}~}} $\qquad$ {% trans "problem" %} \underline{~{{ passage.solution_number }}~}
|
||||||
|
{% endfor %}
|
||||||
|
\end{itemize}
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
\vspace{6mm}
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%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 %}& 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 }}}
|
||||||
|
&& {% trans "Presence, accuracy and correctness of proofs and algorithms" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Relevance, efficiency and elegance" %} & [0,1] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multirow{3}{20mm}{ {% trans "Formal aspects" %}}& {% trans "Clarity of reasoning (explanations, examples, illustrations, diagrams, etc.)" %} & [0,2]{{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Presentation (readability, compliance with the format, etc.)" %} & [0,1] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL WRITING" %} (/10)} {{ esp|safe }} \\ \hline \hline
|
||||||
|
|
||||||
|
%ORAL
|
||||||
|
\multirow{11}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{6}{20mm}{Oral presentation} & {% trans "Understanding of the material presented, knowledge and mastery of the mathematical subjects used during the presentation" %}} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Relevance of choices (proofs, examples, depth in relation to the written solution)" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Pedagogy and clarity of speech (explanations, illustrations, etc.)" %} & [0,1] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Brevity and cleanliness of the presentation" %} & [0,1] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multirow{3}{20mm}{ {% trans "Debates " %}} & {% trans "Correct answers to the questions asked" %} & [0,2] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Ability to move the debate forward (explaining the limits of one's knowledge, conjectures, live research, etc.)" %} & [0,2] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multirow{2}{20mm}{ {% trans "Penalty" %}} & {% trans "Ethical behaviour" %} & [--3,0] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Correspondence to the written material" %} & [--3,0] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL ORAL" %} (/10)} {{ esp|safe }} \\ \hline
|
||||||
|
|
||||||
|
\end{tabular}
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%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 %}& 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 }}}
|
||||||
|
&& {% trans "Validity of errors and positive points raised" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Identifying and prioritizing the most important errors and positive points" %} & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Formal aspects" %} & {% trans "Presentation (readability, compliance with the format, etc.)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL WRITING" %} (/10)} {{ esp|safe }} \\ \hline \hline
|
||||||
|
|
||||||
|
%ORAL
|
||||||
|
\multirow{9}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{6}{20mm}{ {% trans "Discussion" %}} & {% trans "Relevance of questions (importance of the topics covered, points raised)" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Questioning skills (formulation of questions, reaction to answers, articulation between questions, time management)" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Ability to assess the quality of the Defender's presentation (presentation and answers to the Opponent) (0-2)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Understanding" %} & {% trans "Answers to the questions of the Reporter and the jury (substance and ability to move the debate forward)" %} & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Penalty" %} & {% trans "Ethical behavior" %} & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL ORAL" %} (/10)} {{ esp|safe }}\\ \hline
|
||||||
|
\end{tabular}
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%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 %}& 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 }}}
|
||||||
|
&& {% trans "Validity of errors and positive points raised" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Identifying and prioritizing the most important errors and positive points" %} & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Formal aspects" %} & {% trans "Presentation (readability, compliance with the format, etc.)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL WRITING" %} (/10)} {{ esp|safe }} \\ \hline \hline
|
||||||
|
|
||||||
|
%ORAL
|
||||||
|
\multirow{12}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{8}{20mm}{ {% trans "Discussion" %}} & {% trans "Taking the debate to a higher level (through the topics covered, the relevance of the questions asked, the points raised, time management)" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Creating a constructive dialogue between the participants (formulation of questions, reaction to answers, articulation between questions, speaking time)" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Ability to assess the quality of the exchanges (Reporter-Opponent, and three-way)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Understanding" %} & {% trans "Answers to the jury's questions (substance and ability to move the debate forward)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Penalty" %} & {% trans "Ethical behavior" %} & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL ORAL" %} (/10)} {{ esp|safe }}\\ \hline
|
||||||
|
\end{tabular}
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
{% 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 %}& 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 }}}
|
||||||
|
&& {% trans "Validity of errors and positive points raised" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
|
&& {% trans "Identifying and prioritizing the most important errors and positive points" %} & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Formal aspects" %} & {% trans "Presentation (readability, compliance with the format, etc.)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL WRITING" %} (/10)} {{ esp|safe }} \\ \hline \hline
|
||||||
|
|
||||||
|
%ORAL
|
||||||
|
\multirow{6}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & {% trans "Scientific part" %} & {% trans "Significance of the remarks and questions (positive mark only if the other players omitted crucial matter)" %} & [--5,5] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Formal aspects" %} & {% trans "Relevance of the remarks and questions (positive mark only if the other players omitted crucial matter)" %} & [--5,5] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
& {% trans "Penalty" %} & {% trans "Ethical behavior" %} & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
|
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL ORAL" %} (/10)} {{ esp|safe }}\\ \hline
|
||||||
|
\end{tabular}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
\end{document}
|
@ -17,13 +17,15 @@
|
|||||||
\usepackage{array}
|
\usepackage{array}
|
||||||
\usepackage{multirow}
|
\usepackage{multirow}
|
||||||
\usepackage{footnote}
|
\usepackage{footnote}
|
||||||
\usepackage{xintexpr}
|
\usepackage{rotating}
|
||||||
|
|
||||||
\addtolength{\textwidth}{4cm}
|
\addtolength{\textwidth}{4cm}
|
||||||
\setlength{\parindent}{0mm}
|
\setlength{\parindent}{0mm}
|
||||||
|
|
||||||
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=1.2cm}
|
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=1.2cm}
|
||||||
|
|
||||||
|
\DeclareUnicodeCharacter{22C5}{\textperiodcentered{}}
|
||||||
|
|
||||||
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
|
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
|
||||||
\pagestyle{empty}
|
\pagestyle{empty}
|
||||||
\renewcommand{\leq}{\leqslant}
|
\renewcommand{\leq}{\leqslant}
|
||||||
@ -41,7 +43,7 @@
|
|||||||
\begin{center}
|
\begin{center}
|
||||||
\begin{itemize}
|
\begin{itemize}
|
||||||
{% for passage in passages.all %}
|
{% for passage in passages.all %}
|
||||||
\item D\'efenseur\textperiodcentered{}se au passage {{ forloop.counter }} : \underline{\texttt{~{{ passage.defender.team.trigram }}~}} $\qquad$ probl\`eme \underline{~{{ passage.solution_number }}~}
|
\item D\'efenseur⋅se au passage {{ forloop.counter }} : \underline{\texttt{~{{ passage.reporter.team.trigram }}~}} $\qquad$ probl\`eme \underline{~{{ passage.solution_number }}~}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
\end{itemize}
|
\end{itemize}
|
||||||
\end{center}
|
\end{center}
|
||||||
@ -50,24 +52,24 @@
|
|||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%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|}{Læ {\bf D\'efenseur\textperiodcentered{}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.defender.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
|
%ECRIT
|
||||||
\multirow{6}{3mm}{\centering \bf\'E\\ C\\ R\\ I\\ T} & \multirow{3}{20mm}{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 }}}
|
||||||
&& Présence, exactitude et justesse des démonstrations et algorithmes & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
&& Présence, exactitude et justesse des démonstrations et algorithmes & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Pertinence, efficacité et élégance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
&& Pertinence, efficacité et élégance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&\multirow{3}{20mm}{Forme}& Clarté du raisonnement (explications, exemples, illustrations, schémas, etc.) & [0,3]{{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
&\multirow{3}{24mm}{Forme}& Clarté du raisonnement (explications, exemples, illustrations, schémas, etc.) & [0,3]{{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
&& Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/20)} {{ esp|safe }} \\ \hline \hline
|
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/20)} {{ esp|safe }} \\ \hline \hline
|
||||||
|
|
||||||
%ORAL
|
%ORAL
|
||||||
\multirow{8}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{4}{20mm}{Présentation orale} & Compréhension du matériel présenté, connaissance et maîtrise des sujets mathématiques utilisés \emph{lors de la présentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
\multirow{11}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{6}{24mm}{Présentation orale} & Compréhension du matériel présenté, connaissance et maîtrise des sujets mathématiques utilisés \emph{lors de la présentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Pertinence des choix (démonstrations, exemples, profondeur au regard de la solution écrite) & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
&& Pertinence des choix (démonstrations, exemples, profondeur au regard de la solution écrite) & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Pédagogie et clarté du discours (explications, illustrations, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
&& Pédagogie et clarté du discours (explications, illustrations, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Brieveté et propreté de la présentation & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
&& Brieveté et propreté de la présentation & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&\multirow{2}{20mm}{Débats} & Réponses correctes aux questions posées & [0,5] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
&\multirow{3}{24mm}{Débats} & Réponses correctes aux questions posées & [0,5] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Capacité de faire avancer le débat (expliquer les limites de ses connaissances, des conjectures, rechercher en direct, etc.) & [0,4] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
&& Capacité de faire avancer le débat (expliquer les limites de ses connaissances, des conjectures, rechercher en direct, etc.) & [0,4] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&\multirow{2}{20mm}{Malus} & Attitude irrespectueuse ? & [--6,0] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
&\multirow{2}{24mm}{Malus} & Attitude irrespectueuse ? & [--6,0] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Non-conformité de la présentation avec le matériel écrit ? & [--6,0] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
&& Non-conformité de la présentation avec le matériel écrit ? & [--6,0] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/20)} {{ esp|safe }} \\ \hline
|
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/20)} {{ esp|safe }} \\ \hline
|
||||||
|
|
||||||
@ -77,21 +79,21 @@
|
|||||||
|
|
||||||
%%%%%%%%%%%%%%%%%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\textperiodcentered{}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{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{3}{20mm}{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 }}}
|
||||||
&& Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
&& Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
&& Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
& Forme & Pr\'esentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
& Forme & Pr\'esentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }} \\ \hline \hline
|
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }} \\ \hline \hline
|
||||||
|
|
||||||
%ORAL
|
%ORAL
|
||||||
\multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{3}{20mm}{Questions et discours de l'opposant\textperiodcentered{}e} & Pertinence des questions (importance des sujets abordés, des points soulevés) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
\multirow{10}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{5}{24mm}{Questions et discours de l'opposant⋅e} & Pertinence des questions (importance des sujets abordés, des points soulevés) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Gestion de l'échange (formulation des questions, réaction aux réponses, articulation entre les questions, gestion du temps) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
&& Gestion de l'échange (formulation des questions, réaction aux réponses, articulation entre les questions, gestion du temps) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Capacité à évaluer la qualité de la prestation de læ Défenseur⋅se (présentation et réponses à l'Opposant⋅e) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
&& Capacité à évaluer la qualité de la prestation de læ Défenseur⋅se (présentation et réponses à l'Opposant⋅e) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&& Réponses aux questions de læ Rapporteur\textperiodcentered{}rice et du jury (fond et capacité à faire avancer le débat) & [0,3] {{ 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,3] {{ 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}
|
||||||
@ -100,20 +102,20 @@
|
|||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%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|}{Læ {\bf Rapporteur\textperiodcentered{}rice} \normalsize \'evalue le d\'ebat entre læ D\'efenseur\textperiodcentered{}se et l'Opposant\textperiodcentered{}e.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reporter.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
|
%ECRIT
|
||||||
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{3}{20mm}{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 }}}
|
||||||
&& Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
&& Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||||
&& Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
&& Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
& Forme & Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
& Forme & Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }}\\ \hline \hline
|
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }}\\ \hline \hline
|
||||||
|
|
||||||
%ORAL
|
%ORAL
|
||||||
\multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{3}{20mm}{Questions et discours de læ rapporteur\textperiodcentered{}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\textperiodcentered{}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}
|
@ -18,44 +18,86 @@
|
|||||||
<dt class="col-sm-6 text-sm-end">{% trans 'place'|capfirst %}</dt>
|
<dt class="col-sm-6 text-sm-end">{% trans 'place'|capfirst %}</dt>
|
||||||
<dd class="col-sm-6">{{ tournament.place }}</dd>
|
<dd class="col-sm-6">{{ tournament.place }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans 'price'|capfirst %}</dt>
|
{% if TFJM.PAYMENT_MANAGEMENT %}
|
||||||
<dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd>
|
<dt class="col-sm-6 text-sm-end">{% trans 'price'|capfirst %}</dt>
|
||||||
|
<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>
|
<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 syntheses 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.syntheses_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 when solutions of round 2 are available'|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 }}</dd>
|
<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 syntheses submission for the second round'|capfirst %}</dt>
|
<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.syntheses_second_phase_limit }}</dd>
|
<dd class="col-sm-6">{{ tournament.reviews_second_phase_limit }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans 'description'|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>
|
||||||
<dd class="col-sm-6">{{ tournament.description }}</dd>
|
<dd class="col-sm-6">{{ tournament.description }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans 'To contact organizers' %}</dt>
|
{% if TFJM.ML_MANAGEMENT %}
|
||||||
<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 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 %}
|
||||||
<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 %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -75,13 +117,15 @@
|
|||||||
<div id="teams_table">
|
<div id="teams_table">
|
||||||
{% render_table teams %}
|
{% render_table teams %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
|
{% if TFJM.PAYMENT_MANAGEMENT %}
|
||||||
<div class="text-center">
|
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
|
||||||
<a href="{% url "participation:tournament_payments" pk=tournament.pk %}" class="btn btn-secondary">
|
<div class="text-center">
|
||||||
<i class="fas fa-money-bill-wave"></i> {% trans "Access to payments list" %}
|
<a href="{% url "participation:tournament_payments" pk=tournament.pk %}" class="btn btn-secondary">
|
||||||
</a>
|
<i class="fas fa-money-bill-wave"></i> {% trans "Access to payments list" %}
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if pools.data %}
|
{% if pools.data %}
|
||||||
@ -143,6 +187,12 @@
|
|||||||
<i class="fas fa-ranking-star"></i>
|
<i class="fas fa-ranking-star"></i>
|
||||||
{% trans "Harmonize" %} - {% trans "Day" %} 2
|
{% trans "Harmonize" %} - {% trans "Day" %} 2
|
||||||
</a>
|
</a>
|
||||||
|
{% if TFJM.NB_ROUNDS >= 3 %}
|
||||||
|
<a href="{% url 'participation:tournament_harmonize' pk=tournament.pk round=3 %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-ranking-star"></i>
|
||||||
|
{% trans "Harmonize" %} - {% trans "Day" %} 3
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
@ -169,10 +219,23 @@
|
|||||||
{% trans "Unpublish notes for second round" %}
|
{% trans "Unpublish notes for second round" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if TFJM.NB_ROUNDS >= 3 %}
|
||||||
|
{% if not available_notes_3 %}
|
||||||
|
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=3 %}" class="btn btn-sm btn-info">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
{% trans "Publish notes for third round" %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=3 %}?hide" class="btn btn-sm btn-danger">
|
||||||
|
<i class="fas fa-eye-slash"></i>
|
||||||
|
{% trans "Unpublish notes for third round" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</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 %}
|
||||||
@ -181,25 +244,26 @@
|
|||||||
<h3>{% trans "Files available for download" %}</h3>
|
<h3>{% trans "Files available for download" %}</h3>
|
||||||
|
|
||||||
<div class="alert alert-warning fade show files-to-download-collapse" id="files-to-download-popup">
|
<div class="alert alert-warning fade show files-to-download-collapse" id="files-to-download-popup">
|
||||||
<h4>IMPORTANT</h4>
|
<h4>{% trans "IMPORTANT" %}</h4>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Les fichiers accessibles ci-dessous peuvent contenir des informations personnelles.
|
{% blocktrans trimmed %}
|
||||||
Par conformité avec le droit européen et par respect de la confidentialité des données
|
The files accessible below may contain personal information.
|
||||||
des participant⋅es, vous ne devez utiliser ces données que dans un cadre strictement
|
In compliance with European law and out of respect for the confidentiality of participants data,
|
||||||
nécessaire en lien avec l'organisation du tournoi.
|
you may only use this data for purposes strictly necessary to the organization of the tournament.
|
||||||
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
De plus, il est de votre responsabilité de supprimer ces fichiers une fois que vous
|
{% blocktrans trimmed %}
|
||||||
n'en avez plus besoin, notamment à la fin du tournoi.
|
Moreover, it is your responsibility to delete these files once you no longer need them, especially at the end of the tournament.
|
||||||
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<button class="btn btn-warning" data-bs-toggle="collapse" href=".files-to-download-collapse"
|
<button class="btn btn-warning" data-bs-toggle="collapse" href=".files-to-download-collapse"
|
||||||
role="button" aria-expanded="false" aria-controls="files-to-download files-to-download-popup">
|
role="button" aria-expanded="false" aria-controls="files-to-download files-to-download-popup">
|
||||||
Je m'engage à ne pas divulguer les données des participant⋅es
|
{% trans "I agree not to divulge participants data and to delete them at the end of the tournament." %}
|
||||||
et de les supprimer à l'issue du tournoi
|
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -209,48 +273,48 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}">
|
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}">
|
||||||
Tableur de données des participant⋅es des équipes validées
|
{% trans "Validated team participant data spreadsheet" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}?all">
|
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}?all">
|
||||||
Tableur de données des participant⋅es de toutes les équipes
|
{% trans "All teams participant data spreadsheet" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "participation:tournament_authorizations" tournament_id=tournament.id %}">
|
<a href="{% url "participation:tournament_authorizations" tournament_id=tournament.id %}">
|
||||||
Archive de toutes les autorisations triées par équipe et par personne
|
{% trans "Archive of all authorisations sorted by team and person" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}">
|
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}">
|
||||||
Archive de toutes les solutions envoyées triées par équipe
|
{% trans "Archive of all submitted solutions sorted by team" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=problem">
|
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=problem">
|
||||||
Archive de toutes les solutions envoyées triées par problème
|
{% trans "Archive of all sent solutions sorted by problem" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=pool">
|
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=pool">
|
||||||
Archive de toutes les solutions envoyées triées par poule
|
{% trans "Archive of all sent solutions sorted by pool" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "participation:tournament_syntheses" tournament_id=tournament.id %}?sort_by=pool">
|
<a href="{% url "participation:tournament_written_reviews" tournament_id=tournament.id %}?sort_by=pool">
|
||||||
Archive de toutes les notes de synthèse triées par poule et par passage
|
{% trans "Archive of all summary notes sorted by pool and passage" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://docs.google.com/spreadsheets/d/{{ tournament.notes_sheet_id }}/edit">
|
<a href="https://docs.google.com/spreadsheets/d/{{ tournament.notes_sheet_id }}/edit">
|
||||||
<i class="fas fa-table"></i>
|
<i class="fas fa-table"></i>
|
||||||
Tableur de notes sur Google Sheets
|
{% trans "Note spreadsheet on Google Sheets" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "participation:tournament_notation_sheets" tournament_id=tournament.id %}">
|
<a href="{% url "participation:tournament_notation_sheets" tournament_id=tournament.id %}">
|
||||||
Archive de toutes les feuilles de notes à imprimer triées par poule
|
{% trans "Archive of all printable note sheets sorted by pool" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,15 +1,37 @@
|
|||||||
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
||||||
|
|
||||||
{% load crispy_forms_filters i18n %}
|
{% load crispy_forms_filters crispy_forms_tags i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div id="form-content">
|
<div id="form-content">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
{{ participation_form|crispy }}
|
{% crispy participation_form %}
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-success" type="submit">{% trans "Update" %}</button>
|
<button class="btn btn-success" type="submit">{% trans "Update" %}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
const tournamentSelect = document.getElementById('id_tournament')
|
||||||
|
const idfWarningBanner = document.getElementById('idf_warning_banner')
|
||||||
|
const unifiedRegistrationTournamentIds = idfWarningBanner.getAttribute('data-tid-unified').split(',')
|
||||||
|
if (idfWarningBanner.getAttribute('data-tid-unified') !== "") {
|
||||||
|
function updateIDFWarningBannerVisibility() {
|
||||||
|
const tid = tournamentSelect.value
|
||||||
|
if (unifiedRegistrationTournamentIds.includes(tid))
|
||||||
|
idfWarningBanner.classList.remove('d-none')
|
||||||
|
else
|
||||||
|
idfWarningBanner.classList.add('d-none')
|
||||||
|
}
|
||||||
|
|
||||||
|
tournamentSelect.addEventListener('change', updateIDFWarningBannerVisibility)
|
||||||
|
updateIDFWarningBannerVisibility()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
idfWarningBanner.classList.add('d-none')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
|
||||||
|
|
||||||
{% load crispy_forms_filters i18n static %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<form method="post" enctype="multipart/form-data">
|
|
||||||
<div id="form-content">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
{% trans "Templates:" %}
|
|
||||||
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.pdf" %}"> PDF</a> —
|
|
||||||
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.tex" %}"> TEX</a> —
|
|
||||||
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.odt" %}"> ODT</a> —
|
|
||||||
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a>
|
|
||||||
</div>
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
|
|
||||||
</form>
|
|
||||||
{% endblock content %}
|
|
@ -0,0 +1,25 @@
|
|||||||
|
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
||||||
|
|
||||||
|
{% load crispy_forms_filters i18n static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<div id="form-content">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% trans "Templates:" %}
|
||||||
|
{% if TFJM.APP == "TFJM" %}
|
||||||
|
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.pdf" %}"> PDF</a> —
|
||||||
|
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.tex" %}"> TEX</a> —
|
||||||
|
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.odt" %}"> ODT</a> —
|
||||||
|
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a>
|
||||||
|
{% elif TFJM.APP == "ETEAM" %}
|
||||||
|
<a class="alert-link" href="{% static "eteam/Written_review.pdf" %}"> PDF</a> —
|
||||||
|
<a class="alert-link" href="{% static "eteam/Written_review.tex" %}"> TEX</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
@ -1,3 +1,2 @@
|
|||||||
{{ object.name }}
|
{{ object.name }}
|
||||||
{{ object.place }}
|
|
||||||
{{ object.description }}
|
{{ object.description }}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{{ object.link }}
|
|
||||||
{{ object.participation.team.name }}
|
|
||||||
{{ object.participation.team.trigram }}
|
|
||||||
{{ object.participation.problem }}
|
|
||||||
{{ object.participation.get_problem_display }}
|
|
@ -674,7 +674,7 @@ class TestPayment(TestCase):
|
|||||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||||
data={'type': "bank_transfer",
|
data={'type': "bank_transfer",
|
||||||
'additional_information': "This is a bank transfer",
|
'additional_information': "This is a bank transfer",
|
||||||
'receipt': open("tfjm/static/Fiche_sanitaire.pdf", "rb")})
|
'receipt': open("tfjm/static/tfjm/Fiche_sanitaire.pdf", "rb")})
|
||||||
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
||||||
payment.refresh_from_db()
|
payment.refresh_from_db()
|
||||||
self.assertIsNone(payment.valid)
|
self.assertIsNone(payment.valid)
|
||||||
@ -735,7 +735,7 @@ class TestPayment(TestCase):
|
|||||||
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
|
||||||
data={'type': "scholarship",
|
data={'type': "scholarship",
|
||||||
'additional_information': "I don't have to pay because I have a scholarship",
|
'additional_information': "I don't have to pay because I have a scholarship",
|
||||||
'receipt': open("tfjm/static/Fiche_sanitaire.pdf", "rb")})
|
'receipt': open("tfjm/static/tfjm/Fiche_sanitaire.pdf", "rb")})
|
||||||
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
|
||||||
payment.refresh_from_db()
|
payment.refresh_from_db()
|
||||||
self.assertIsNone(payment.valid)
|
self.assertIsNone(payment.valid)
|
||||||
|
@ -8,11 +8,11 @@ from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotific
|
|||||||
PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
|
PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
|
||||||
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \
|
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \
|
||||||
ScaleNotationSheetTemplateView, SelectTeamFinalView, \
|
ScaleNotationSheetTemplateView, SelectTeamFinalView, \
|
||||||
SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
|
SolutionsDownloadView, SolutionUploadView, \
|
||||||
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
|
TournamentPublishNotesView, TournamentPublishSolutionsView, TournamentUpdateView, WrittenReviewUploadView
|
||||||
|
|
||||||
|
|
||||||
app_name = "participation"
|
app_name = "participation"
|
||||||
@ -42,12 +42,14 @@ urlpatterns = [
|
|||||||
name="tournament_authorizations"),
|
name="tournament_authorizations"),
|
||||||
path("tournament/<int:tournament_id>/solutions/", SolutionsDownloadView.as_view(),
|
path("tournament/<int:tournament_id>/solutions/", SolutionsDownloadView.as_view(),
|
||||||
name="tournament_solutions"),
|
name="tournament_solutions"),
|
||||||
path("tournament/<int:tournament_id>/syntheses/", SolutionsDownloadView.as_view(),
|
path("tournament/<int:tournament_id>/written_reviews/", SolutionsDownloadView.as_view(),
|
||||||
name="tournament_syntheses"),
|
name="tournament_written_reviews"),
|
||||||
path("tournament/<int:tournament_id>/notation/sheets/", NotationSheetsArchiveView.as_view(),
|
path("tournament/<int:tournament_id>/notation/sheets/", NotationSheetsArchiveView.as_view(),
|
||||||
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(),
|
||||||
@ -60,7 +62,7 @@ urlpatterns = [
|
|||||||
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
|
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
|
||||||
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
|
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
|
||||||
path("pools/<int:pool_id>/solutions/", SolutionsDownloadView.as_view(), name="pool_download_solutions"),
|
path("pools/<int:pool_id>/solutions/", SolutionsDownloadView.as_view(), name="pool_download_solutions"),
|
||||||
path("pools/<int:pool_id>/syntheses/", SolutionsDownloadView.as_view(), name="pool_download_syntheses"),
|
path("pools/<int:pool_id>/written_reviews/", SolutionsDownloadView.as_view(), name="pool_download_written_reviews"),
|
||||||
path("pools/<int:pk>/notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"),
|
path("pools/<int:pk>/notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"),
|
||||||
path("pools/<int:pk>/notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"),
|
path("pools/<int:pk>/notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"),
|
||||||
path("pools/<int:pool_id>/notation/sheets/", NotationSheetsArchiveView.as_view(), name="pool_notation_sheets"),
|
path("pools/<int:pool_id>/notation/sheets/", NotationSheetsArchiveView.as_view(), name="pool_notation_sheets"),
|
||||||
@ -71,6 +73,6 @@ urlpatterns = [
|
|||||||
path("pools/<int:pk>/upload-notes/template/", PoolNotesTemplateView.as_view(), name="pool_notes_template"),
|
path("pools/<int:pk>/upload-notes/template/", PoolNotesTemplateView.as_view(), name="pool_notes_template"),
|
||||||
path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"),
|
path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"),
|
||||||
path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),
|
path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),
|
||||||
path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"),
|
path("pools/passages/<int:pk>/written_review/", WrittenReviewUploadView.as_view(), name="upload_written_review"),
|
||||||
path("pools/passages/notes/<int:pk>/", NoteUpdateView.as_view(), name="update_notes"),
|
path("pools/passages/notes/<int:pk>/", NoteUpdateView.as_view(), name="update_notes"),
|
||||||
]
|
]
|
||||||
|
@ -22,9 +22,10 @@ from django.db import transaction
|
|||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.http import FileResponse, Http404, HttpResponse
|
from django.http import FileResponse, Http404, HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone, translation
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.timezone import localtime
|
from django.utils.timezone import localtime
|
||||||
@ -46,9 +47,9 @@ from tfjm.lists import get_sympa_client
|
|||||||
from tfjm.views import AdminMixin, VolunteerMixin
|
from tfjm.views import AdminMixin, VolunteerMixin
|
||||||
|
|
||||||
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
|
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
|
||||||
PoolForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
|
PoolForm, RequestValidationForm, SolutionForm, TeamForm, TournamentForm, UploadNotesForm, \
|
||||||
UploadNotesForm, ValidateParticipationForm
|
ValidateParticipationForm, WrittenReviewForm
|
||||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
|
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
|
||||||
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
|
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
|
||||||
|
|
||||||
|
|
||||||
@ -88,7 +89,7 @@ class CreateTeamView(LoginRequiredMixin, CreateView):
|
|||||||
registration.save()
|
registration.save()
|
||||||
|
|
||||||
# Subscribe the user mail address to the team mailing list
|
# Subscribe the user mail address to the team mailing list
|
||||||
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(user.email, f"equipe-{slugify(form.instance.trigram)}", False,
|
||||||
f"{user.first_name} {user.last_name}")
|
f"{user.first_name} {user.last_name}")
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@ -130,7 +131,7 @@ class JoinTeamView(LoginRequiredMixin, FormView):
|
|||||||
registration.save()
|
registration.save()
|
||||||
|
|
||||||
# Subscribe to the team mailing list
|
# Subscribe to the team mailing list
|
||||||
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(user.email, f"equipe-{slugify(form.instance.trigram)}", False,
|
||||||
f"{user.first_name} {user.last_name}")
|
f"{user.first_name} {user.last_name}")
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@ -229,10 +230,11 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
|||||||
self.object.participation.save()
|
self.object.participation.save()
|
||||||
|
|
||||||
mail_context = dict(team=self.object, domain=Site.objects.first().domain)
|
mail_context = dict(team=self.object, domain=Site.objects.first().domain)
|
||||||
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
||||||
send_mail("[TFJM²] Validation d'équipe", mail_plain, settings.DEFAULT_FROM_EMAIL,
|
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
||||||
[self.object.participation.tournament.organizers_email], html_message=mail_html)
|
send_mail(f"[{settings.APP_NAME}] {_('Team validation')}", mail_plain, settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[self.object.participation.tournament.organizers_email], html_message=mail_html)
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -255,7 +257,8 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
|||||||
|
|
||||||
domain = Site.objects.first().domain
|
domain = Site.objects.first().domain
|
||||||
for registration in self.object.participants.all():
|
for registration in self.object.participants.all():
|
||||||
if registration.is_student and self.object.participation.tournament.price:
|
if settings.PAYMENT_MANAGEMENT and \
|
||||||
|
registration.is_student and self.object.participation.tournament.price:
|
||||||
payment = Payment.objects.get(registrations=registration, final=False)
|
payment = Payment.objects.get(registrations=registration, final=False)
|
||||||
else:
|
else:
|
||||||
payment = None
|
payment = None
|
||||||
@ -263,18 +266,21 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
|||||||
message=form.cleaned_data["message"])
|
message=form.cleaned_data["message"])
|
||||||
mail_context_html = dict(domain=domain, registration=registration, team=self.object, payment=payment,
|
mail_context_html = dict(domain=domain, registration=registration, team=self.object, payment=payment,
|
||||||
message=form.cleaned_data["message"].replace('\n', '<br>'))
|
message=form.cleaned_data["message"].replace('\n', '<br>'))
|
||||||
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context_plain)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
mail_html = render_to_string("participation/mails/team_validated.html", mail_context_html)
|
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context_plain)
|
||||||
registration.user.email_user("[TFJM²] Équipe validée", mail_plain, html_message=mail_html)
|
mail_html = render_to_string("participation/mails/team_validated.html", mail_context_html)
|
||||||
|
registration.user.email_user(f"[{settings.APP_NAME}] {_('Team validated')}", mail_plain,
|
||||||
|
html_message=mail_html)
|
||||||
elif "invalidate" in self.request.POST:
|
elif "invalidate" in self.request.POST:
|
||||||
self.object.participation.valid = None
|
self.object.participation.valid = None
|
||||||
self.object.participation.save()
|
self.object.participation.save()
|
||||||
mail_context_plain = dict(team=self.object, message=form.cleaned_data["message"])
|
mail_context_plain = dict(team=self.object, message=form.cleaned_data["message"])
|
||||||
mail_context_html = dict(team=self.object, message=form.cleaned_data["message"].replace('\n', '<br>'))
|
mail_context_html = dict(team=self.object, message=form.cleaned_data["message"].replace('\n', '<br>'))
|
||||||
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context_plain)
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context_html)
|
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context_plain)
|
||||||
send_mail("[TFJM²] Équipe non validée", mail_plain, None, [self.object.email],
|
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context_html)
|
||||||
html_message=mail_html)
|
send_mail(f"[{settings.APP_NAME}] {_('Team not validated')}", mail_plain,
|
||||||
|
None, [self.object.email], html_message=mail_html)
|
||||||
else:
|
else:
|
||||||
form.add_error(None, _("You must specify if you validate the registration or not."))
|
form.add_error(None, _("You must specify if you validate the registration or not."))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
@ -311,6 +317,7 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
instance=self.object.participation)
|
instance=self.object.participation)
|
||||||
if not self.request.user.registration.is_volunteer:
|
if not self.request.user.registration.is_volunteer:
|
||||||
del context["participation_form"].fields['final']
|
del context["participation_form"].fields['final']
|
||||||
|
context["participation_form"].helper.layout.remove('final')
|
||||||
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
|
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -319,6 +326,7 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
|
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
|
||||||
if not self.request.user.registration.is_volunteer:
|
if not self.request.user.registration.is_volunteer:
|
||||||
del participation_form.fields['final']
|
del participation_form.fields['final']
|
||||||
|
participation_form.helper.layout.remove('final')
|
||||||
if not participation_form.is_valid():
|
if not participation_form.is_valid():
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
@ -515,7 +523,7 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
|
|||||||
team = request.user.registration.team
|
team = request.user.registration.team
|
||||||
request.user.registration.team = None
|
request.user.registration.team = None
|
||||||
request.user.registration.save()
|
request.user.registration.save()
|
||||||
get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False)
|
get_sympa_client().unsubscribe(request.user.email, f"equipe-{slugify(team.trigram)}", False)
|
||||||
if team.students.count() + team.coaches.count() == 0:
|
if team.students.count() + team.coaches.count() == 0:
|
||||||
team.delete()
|
team.delete()
|
||||||
return redirect(reverse_lazy("index"))
|
return redirect(reverse_lazy("index"))
|
||||||
@ -549,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
|
||||||
@ -624,8 +632,9 @@ class TournamentDetailView(MultiTableMixin, DetailView):
|
|||||||
context["notes"] = sorted_notes
|
context["notes"] = sorted_notes
|
||||||
context["available_notes_1"] = all(pool.results_available for pool in self.object.pools.filter(round=1).all())
|
context["available_notes_1"] = all(pool.results_available for pool in self.object.pools.filter(round=1).all())
|
||||||
context["available_notes_2"] = all(pool.results_available for pool in self.object.pools.filter(round=2).all())
|
context["available_notes_2"] = all(pool.results_available for pool in self.object.pools.filter(round=2).all())
|
||||||
|
context["available_notes_3"] = all(pool.results_available for pool in self.object.pools.filter(round=3).all())
|
||||||
|
|
||||||
if not self.object.final and notes and context["available_notes_2"] \
|
if settings.HAS_FINAL and not self.object.final and notes and context["available_notes_2"] \
|
||||||
and not self.request.user.is_anonymous and self.request.user.registration.is_volunteer:
|
and not self.request.user.is_anonymous and self.request.user.registration.is_volunteer:
|
||||||
context["team_selectable_for_final"] = next(participation for participation, _note in sorted_notes
|
context["team_selectable_for_final"] = next(participation for participation, _note in sorted_notes
|
||||||
if not participation.final)
|
if not participation.final)
|
||||||
@ -663,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()
|
||||||
|
|
||||||
@ -683,7 +692,7 @@ class TournamentExportCSVView(VolunteerMixin, DetailView):
|
|||||||
)
|
)
|
||||||
writer = csv.DictWriter(resp, ('Tournoi', 'Équipe', 'Trigramme', 'Sélectionnée',
|
writer = csv.DictWriter(resp, ('Tournoi', 'Équipe', 'Trigramme', 'Sélectionnée',
|
||||||
'Nom', 'Prénom', 'Email', 'Type', 'Genre', 'Date de naissance',
|
'Nom', 'Prénom', 'Email', 'Type', 'Genre', 'Date de naissance',
|
||||||
'Adresse', 'Code postal', 'Ville', 'Téléphone',
|
'Adresse', 'Code postal', 'Ville', 'Pays', 'Téléphone',
|
||||||
'Classe', 'Établissement',
|
'Classe', 'Établissement',
|
||||||
'Nom responsable légal⋅e', 'Téléphone responsable légal⋅e',
|
'Nom responsable légal⋅e', 'Téléphone responsable légal⋅e',
|
||||||
'Email responsable légal⋅e',
|
'Email responsable légal⋅e',
|
||||||
@ -711,6 +720,7 @@ class TournamentExportCSVView(VolunteerMixin, DetailView):
|
|||||||
'Adresse': registration.address,
|
'Adresse': registration.address,
|
||||||
'Code postal': registration.zip_code,
|
'Code postal': registration.zip_code,
|
||||||
'Ville': registration.city,
|
'Ville': registration.city,
|
||||||
|
'Pays': registration.country,
|
||||||
'Téléphone': registration.phone_number,
|
'Téléphone': registration.phone_number,
|
||||||
'Classe': registration.get_student_class_display() if registration.is_student
|
'Classe': registration.get_student_class_display() if registration.is_student
|
||||||
else registration.last_degree,
|
else registration.last_degree,
|
||||||
@ -737,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):
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
tournament = Tournament.objects.get(pk=kwargs["pk"])
|
tournament = Tournament.objects.get(pk=kwargs["pk"])
|
||||||
@ -757,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.
|
||||||
@ -769,9 +818,9 @@ 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 (1, 2):
|
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1):
|
||||||
raise Http404
|
raise Http404
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@ -802,9 +851,10 @@ 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 (1, 2) or self.kwargs['action'] not in ('add', 'remove') \
|
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1) \
|
||||||
|
or self.kwargs['action'] not in ('add', 'remove') \
|
||||||
or self.kwargs['trigram'] not in [p.team.trigram
|
or self.kwargs['trigram'] not in [p.team.trigram
|
||||||
for p in tournament.participations.filter(valid=True).all()]:
|
for p in tournament.participations.filter(valid=True).all()]:
|
||||||
raise Http404
|
raise Http404
|
||||||
@ -826,7 +876,7 @@ class TournamentHarmonizeNoteView(VolunteerMixin, DetailView):
|
|||||||
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
|
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
|
||||||
spreadsheet = gc.open_by_key(tournament.notes_sheet_id)
|
spreadsheet = gc.open_by_key(tournament.notes_sheet_id)
|
||||||
worksheet = spreadsheet.worksheet("Classement final")
|
worksheet = spreadsheet.worksheet("Classement final")
|
||||||
column = 3 if kwargs['round'] == 1 else 5
|
column = 3 if kwargs['round'] == 1 else 5 if kwargs['round'] == 2 else 8
|
||||||
row = worksheet.find(f"{participation.team.name} ({participation.team.trigram})", in_column=1).row
|
row = worksheet.find(f"{participation.team.name} ({participation.team.trigram})", in_column=1).row
|
||||||
worksheet.update_cell(row, column, new_diff)
|
worksheet.update_cell(row, column, new_diff)
|
||||||
|
|
||||||
@ -841,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():
|
||||||
@ -972,7 +1022,7 @@ class PoolUpdateView(VolunteerMixin, UpdateView):
|
|||||||
|
|
||||||
class SolutionsDownloadView(VolunteerMixin, View):
|
class SolutionsDownloadView(VolunteerMixin, View):
|
||||||
"""
|
"""
|
||||||
Download all solutions or syntheses as a ZIP archive.
|
Download all solutions or written reviews as a ZIP archive.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
@ -992,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()
|
||||||
@ -1013,11 +1060,12 @@ class SolutionsDownloadView(VolunteerMixin, View):
|
|||||||
if 'team_id' in kwargs:
|
if 'team_id' in kwargs:
|
||||||
team = Team.objects.get(pk=kwargs["team_id"])
|
team = Team.objects.get(pk=kwargs["team_id"])
|
||||||
solutions = Solution.objects.filter(participation=team.participation).all()
|
solutions = Solution.objects.filter(participation=team.participation).all()
|
||||||
syntheses = Synthesis.objects.filter(participation=team.participation).all()
|
written_reviews = WrittenReview.objects.filter(participation=team.participation).all()
|
||||||
filename = _("Solutions of team {trigram}.zip") if is_solution else _("Syntheses of team {trigram}.zip")
|
filename = _("Solutions of team {trigram}.zip") if is_solution \
|
||||||
|
else _("Written reviews of team {trigram}.zip")
|
||||||
filename = filename.format(trigram=team.trigram)
|
filename = filename.format(trigram=team.trigram)
|
||||||
|
|
||||||
def prefix(s: Solution | Synthesis) -> str:
|
def prefix(s: Solution | WrittenReview) -> str:
|
||||||
return ""
|
return ""
|
||||||
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"])
|
||||||
@ -1030,11 +1078,12 @@ class SolutionsDownloadView(VolunteerMixin, View):
|
|||||||
for sol in pool.solutions:
|
for sol in pool.solutions:
|
||||||
sol.pool = pool
|
sol.pool = pool
|
||||||
solutions.append(sol)
|
solutions.append(sol)
|
||||||
syntheses = Synthesis.objects.filter(passage__pool__tournament=tournament).all()
|
written_reviews = WrittenReview.objects.filter(passage__pool__tournament=tournament).all()
|
||||||
filename = _("Solutions of {tournament}.zip") if is_solution else _("Syntheses of {tournament}.zip")
|
filename = _("Solutions of {tournament}.zip") if is_solution \
|
||||||
|
else _("Written reviews of {tournament}.zip")
|
||||||
filename = filename.format(tournament=tournament.name)
|
filename = filename.format(tournament=tournament.name)
|
||||||
|
|
||||||
def prefix(s: Solution | Synthesis) -> str:
|
def prefix(s: Solution | WrittenReview) -> str:
|
||||||
pool = s.pool if is_solution else s.passage.pool
|
pool = s.pool if is_solution else s.passage.pool
|
||||||
p = f"Poule {pool.short_name}/"
|
p = f"Poule {pool.short_name}/"
|
||||||
if not is_solution:
|
if not is_solution:
|
||||||
@ -1045,27 +1094,28 @@ class SolutionsDownloadView(VolunteerMixin, View):
|
|||||||
solutions = Solution.objects.filter(participation__tournament=tournament).all()
|
solutions = Solution.objects.filter(participation__tournament=tournament).all()
|
||||||
else:
|
else:
|
||||||
solutions = Solution.objects.filter(final_solution=True).all()
|
solutions = Solution.objects.filter(final_solution=True).all()
|
||||||
syntheses = Synthesis.objects.filter(passage__pool__tournament=tournament).all()
|
written_reviews = WrittenReview.objects.filter(passage__pool__tournament=tournament).all()
|
||||||
filename = _("Solutions of {tournament}.zip") if is_solution else _("Syntheses of {tournament}.zip")
|
filename = _("Solutions of {tournament}.zip") if is_solution \
|
||||||
|
else _("Written reviews of {tournament}.zip")
|
||||||
filename = filename.format(tournament=tournament.name)
|
filename = filename.format(tournament=tournament.name)
|
||||||
|
|
||||||
def prefix(s: Solution | Synthesis) -> str:
|
def prefix(s: Solution | WrittenReview) -> str:
|
||||||
return f"{s.participation.team.trigram}/" if sort_by == "team" else f"Problème {s.problem}/"
|
return f"{s.participation.team.trigram}/" if sort_by == "team" else f"Problème {s.problem}/"
|
||||||
else:
|
else:
|
||||||
pool = Pool.objects.get(pk=kwargs["pool_id"])
|
pool = Pool.objects.get(pk=kwargs["pool_id"])
|
||||||
solutions = pool.solutions
|
solutions = pool.solutions
|
||||||
syntheses = Synthesis.objects.filter(passage__pool=pool).all()
|
written_reviews = WrittenReview.objects.filter(passage__pool=pool).all()
|
||||||
filename = _("Solutions for pool {pool} of tournament {tournament}.zip") \
|
filename = _("Solutions for pool {pool} of tournament {tournament}.zip") \
|
||||||
if is_solution else _("Syntheses for pool {pool} of tournament {tournament}.zip")
|
if is_solution else _("Written reviews for pool {pool} of tournament {tournament}.zip")
|
||||||
filename = filename.format(pool=pool.short_name,
|
filename = filename.format(pool=pool.short_name,
|
||||||
tournament=pool.tournament.name)
|
tournament=pool.tournament.name)
|
||||||
|
|
||||||
def prefix(s: Solution | Synthesis) -> str:
|
def prefix(s: Solution | WrittenReview) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
zf = ZipFile(output, "w")
|
zf = ZipFile(output, "w")
|
||||||
for s in (solutions if is_solution else syntheses):
|
for s in (solutions if is_solution else written_reviews):
|
||||||
if s.file.storage.exists(s.file.path):
|
if s.file.storage.exists(s.file.path):
|
||||||
zf.write("media/" + s.file.name, prefix(s) + f"{s}.pdf")
|
zf.write("media/" + s.file.name, prefix(s) + f"{s}.pdf")
|
||||||
|
|
||||||
@ -1134,17 +1184,20 @@ class PoolJuryView(VolunteerMixin, FormView, DetailView):
|
|||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
# Send welcome mail
|
# Send welcome mail
|
||||||
subject = "[TFJM²] " + str(_("New TFJM² jury account"))
|
subject = f"[{settings.APP_NAME}] " + str(_("New jury account"))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
message = render_to_string('registration/mails/add_organizer.txt', dict(user=user,
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
inviter=self.request.user,
|
message = render_to_string('registration/mails/add_organizer.txt',
|
||||||
password=password,
|
dict(user=user,
|
||||||
domain=site.domain))
|
inviter=self.request.user,
|
||||||
html = render_to_string('registration/mails/add_organizer.html', dict(user=user,
|
password=password,
|
||||||
inviter=self.request.user,
|
domain=site.domain))
|
||||||
password=password,
|
html = render_to_string('registration/mails/add_organizer.html',
|
||||||
domain=site.domain))
|
dict(user=user,
|
||||||
user.email_user(subject, message, html_message=html)
|
inviter=self.request.user,
|
||||||
|
password=password,
|
||||||
|
domain=site.domain))
|
||||||
|
user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
# Add the user in the jury
|
# Add the user in the jury
|
||||||
self.object.juries.add(reg)
|
self.object.juries.add(reg)
|
||||||
@ -1251,7 +1304,7 @@ class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
|
|||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
for vr, notes in parsed_notes.items():
|
for vr, notes in parsed_notes.items():
|
||||||
notes_count = 6
|
notes_count = 6 + (2 if pool.participations.count() >= 4 and settings.HAS_OBSERVER else 0)
|
||||||
for i, passage in enumerate(pool.passages.all()):
|
for i, passage in enumerate(pool.passages.all()):
|
||||||
note = Note.objects.get_or_create(jury=vr, passage=passage)[0]
|
note = Note.objects.get_or_create(jury=vr, passage=passage)[0]
|
||||||
passage_notes = notes[notes_count * i:notes_count * (i + 1)]
|
passage_notes = notes[notes_count * i:notes_count * (i + 1)]
|
||||||
@ -1286,8 +1339,11 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs): # noqa: C901
|
def render_to_response(self, context, **response_kwargs): # noqa: C901
|
||||||
|
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||||
|
|
||||||
pool_size = self.object.passages.count()
|
pool_size = self.object.passages.count()
|
||||||
passage_width = 6
|
has_observer = self.object.participations.count() >= 4 and settings.HAS_OBSERVER
|
||||||
|
passage_width = 6 + (2 if has_observer else 0)
|
||||||
line_length = pool_size * passage_width
|
line_length = pool_size * passage_width
|
||||||
|
|
||||||
def getcol(number: int) -> str:
|
def getcol(number: int) -> str:
|
||||||
@ -1472,78 +1528,95 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
header_pb = TableRow()
|
header_pb = TableRow()
|
||||||
table.addElement(header_pb)
|
table.addElement(header_pb)
|
||||||
problems_tc = TableCell(valuetype="string", stylename=title_style_topleft)
|
problems_tc = TableCell(valuetype="string", stylename=title_style_topleft)
|
||||||
problems_tc.addElement(P(text="Problème"))
|
problems_tc.addElement(P(text=_("Problem")))
|
||||||
problems_tc.setAttribute('numbercolumnsspanned', "2")
|
problems_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
header_pb.addElement(problems_tc)
|
header_pb.addElement(problems_tc)
|
||||||
header_pb.addElement(CoveredTableCell())
|
header_pb.addElement(CoveredTableCell())
|
||||||
for passage in self.object.passages.all():
|
for passage in self.object.passages.all():
|
||||||
tc = TableCell(valuetype="string", stylename=title_style_topleftright)
|
tc = TableCell(valuetype="string", stylename=title_style_topleftright)
|
||||||
tc.addElement(P(text=f"Problème {passage.solution_number}"))
|
tc.addElement(P(text=_("Problem #{problem}").format(problem=passage.solution_number)))
|
||||||
tc.setAttribute('numbercolumnsspanned', "6")
|
tc.setAttribute('numbercolumnsspanned', str(passage_width))
|
||||||
header_pb.addElement(tc)
|
header_pb.addElement(tc)
|
||||||
header_pb.addElement(CoveredTableCell(numbercolumnsrepeated=5))
|
header_pb.addElement(CoveredTableCell(numbercolumnsrepeated=passage_width - 1))
|
||||||
|
|
||||||
# Add roles on the second line of the table
|
# Add roles on the second line of the table
|
||||||
header_role = TableRow()
|
header_role = TableRow()
|
||||||
table.addElement(header_role)
|
table.addElement(header_role)
|
||||||
role_tc = TableCell(valuetype="string", stylename=title_style_left)
|
role_tc = TableCell(valuetype="string", stylename=title_style_left)
|
||||||
role_tc.addElement(P(text="Rôle"))
|
role_tc.addElement(P(text=_("Role")))
|
||||||
role_tc.setAttribute('numbercolumnsspanned', "2")
|
role_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
header_role.addElement(role_tc)
|
header_role.addElement(role_tc)
|
||||||
header_role.addElement(CoveredTableCell())
|
header_role.addElement(CoveredTableCell())
|
||||||
for i in range(pool_size):
|
for i in range(pool_size):
|
||||||
defender_tc = TableCell(valuetype="string", stylename=title_style_left)
|
reporter_tc = TableCell(valuetype="string", stylename=title_style_left)
|
||||||
defender_tc.addElement(P(text="Défenseur⋅se"))
|
reporter_tc.addElement(P(text=_("Reporter")))
|
||||||
defender_tc.setAttribute('numbercolumnsspanned', "2")
|
|
||||||
header_role.addElement(defender_tc)
|
|
||||||
header_role.addElement(CoveredTableCell())
|
|
||||||
|
|
||||||
opponent_tc = TableCell(valuetype="string", stylename=title_style)
|
|
||||||
opponent_tc.addElement(P(text="Opposant⋅e"))
|
|
||||||
opponent_tc.setAttribute('numbercolumnsspanned', "2")
|
|
||||||
header_role.addElement(opponent_tc)
|
|
||||||
header_role.addElement(CoveredTableCell())
|
|
||||||
|
|
||||||
reporter_tc = TableCell(valuetype="string",
|
|
||||||
stylename=title_style_right)
|
|
||||||
reporter_tc.addElement(P(text="Rapporteur⋅rice"))
|
|
||||||
reporter_tc.setAttribute('numbercolumnsspanned', "2")
|
reporter_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
header_role.addElement(reporter_tc)
|
header_role.addElement(reporter_tc)
|
||||||
header_role.addElement(CoveredTableCell())
|
header_role.addElement(CoveredTableCell())
|
||||||
|
|
||||||
|
opponent_tc = TableCell(valuetype="string", stylename=title_style)
|
||||||
|
opponent_tc.addElement(P(text=_("Opponent")))
|
||||||
|
opponent_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
|
header_role.addElement(opponent_tc)
|
||||||
|
header_role.addElement(CoveredTableCell())
|
||||||
|
|
||||||
|
reviewer_tc = TableCell(valuetype="string",
|
||||||
|
stylename=title_style if has_observer else title_style_right)
|
||||||
|
reviewer_tc.addElement(P(text=_("Reviewer")))
|
||||||
|
reviewer_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
|
header_role.addElement(reviewer_tc)
|
||||||
|
header_role.addElement(CoveredTableCell())
|
||||||
|
|
||||||
|
if has_observer:
|
||||||
|
observer_tc = TableCell(valuetype="string", stylename=title_style_right)
|
||||||
|
observer_tc.addElement(P(text=_("Observer")))
|
||||||
|
observer_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
|
header_role.addElement(observer_tc)
|
||||||
|
header_role.addElement(CoveredTableCell())
|
||||||
|
|
||||||
# Add maximum notes on the third line
|
# Add maximum notes on the third line
|
||||||
header_notes = TableRow()
|
header_notes = TableRow()
|
||||||
table.addElement(header_notes)
|
table.addElement(header_notes)
|
||||||
jury_tc = TableCell(valuetype="string", value="Juré⋅e", stylename=title_style_botleft)
|
jury_tc = TableCell(valuetype="string", value=_("Juree"), stylename=title_style_botleft)
|
||||||
jury_tc.addElement(P(text="Juré⋅e"))
|
jury_tc.addElement(P(text=_("Juree")))
|
||||||
jury_tc.setAttribute('numbercolumnsspanned', "2")
|
jury_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
header_notes.addElement(jury_tc)
|
header_notes.addElement(jury_tc)
|
||||||
header_notes.addElement(CoveredTableCell())
|
header_notes.addElement(CoveredTableCell())
|
||||||
|
|
||||||
for i in range(pool_size):
|
for i in range(pool_size):
|
||||||
defender_w_tc = TableCell(valuetype="string", stylename=title_style_botleft)
|
reporter_w_tc = TableCell(valuetype="string", stylename=title_style_botleft)
|
||||||
defender_w_tc.addElement(P(text="Écrit (/20)"))
|
reporter_w_tc.addElement(P(text=f"{_('Writing')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
|
||||||
header_notes.addElement(defender_w_tc)
|
header_notes.addElement(reporter_w_tc)
|
||||||
|
|
||||||
defender_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
reporter_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
||||||
defender_o_tc.addElement(P(text="Oral (/20)"))
|
reporter_o_tc.addElement(P(text=f"{_('Oral')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
|
||||||
header_notes.addElement(defender_o_tc)
|
header_notes.addElement(reporter_o_tc)
|
||||||
|
|
||||||
opponent_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
opponent_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
||||||
opponent_w_tc.addElement(P(text="Écrit (/10)"))
|
opponent_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
|
||||||
header_notes.addElement(opponent_w_tc)
|
header_notes.addElement(opponent_w_tc)
|
||||||
|
|
||||||
opponent_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
opponent_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
||||||
opponent_o_tc.addElement(P(text="Oral (/10)"))
|
opponent_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
|
||||||
header_notes.addElement(opponent_o_tc)
|
header_notes.addElement(opponent_o_tc)
|
||||||
|
|
||||||
reporter_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
reviewer_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
||||||
reporter_w_tc.addElement(P(text="Écrit (/10)"))
|
reviewer_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
|
||||||
header_notes.addElement(reporter_w_tc)
|
header_notes.addElement(reviewer_w_tc)
|
||||||
|
|
||||||
reporter_o_tc = TableCell(valuetype="string", stylename=title_style_botright)
|
reviewer_o_tc = TableCell(valuetype="string",
|
||||||
reporter_o_tc.addElement(P(text="Oral (/10)"))
|
stylename=title_style_bot if has_observer else title_style_botright)
|
||||||
header_notes.addElement(reporter_o_tc)
|
reviewer_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
|
||||||
|
header_notes.addElement(reviewer_o_tc)
|
||||||
|
|
||||||
|
if has_observer:
|
||||||
|
observer_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
||||||
|
observer_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
|
||||||
|
header_notes.addElement(observer_w_tc)
|
||||||
|
|
||||||
|
observer_o_tc = TableCell(valuetype="string", stylename=title_style_botright)
|
||||||
|
observer_o_tc.addElement(P(text=f"{_('Oral')} (/10)"))
|
||||||
|
header_notes.addElement(observer_o_tc)
|
||||||
|
|
||||||
# Add a notation line for each jury
|
# Add a notation line for each jury
|
||||||
for jury in self.object.juries.all():
|
for jury in self.object.juries.all():
|
||||||
@ -1574,7 +1647,7 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
average_row = TableRow()
|
average_row = TableRow()
|
||||||
table.addElement(average_row)
|
table.addElement(average_row)
|
||||||
average_tc = TableCell(valuetype="string", stylename=title_style_topleftright)
|
average_tc = TableCell(valuetype="string", stylename=title_style_topleftright)
|
||||||
average_tc.addElement(P(text="Moyenne"))
|
average_tc.addElement(P(text=_("Average")))
|
||||||
average_tc.setAttribute('numbercolumnsspanned', "2")
|
average_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
average_row.addElement(average_tc)
|
average_row.addElement(average_tc)
|
||||||
average_row.addElement(CoveredTableCell())
|
average_row.addElement(CoveredTableCell())
|
||||||
@ -1593,52 +1666,62 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
coeff_row = TableRow()
|
coeff_row = TableRow()
|
||||||
table.addElement(coeff_row)
|
table.addElement(coeff_row)
|
||||||
coeff_tc = TableCell(valuetype="string", stylename=title_style_leftright)
|
coeff_tc = TableCell(valuetype="string", stylename=title_style_leftright)
|
||||||
coeff_tc.addElement(P(text="Coefficient"))
|
coeff_tc.addElement(P(text=_("Coefficient")))
|
||||||
coeff_tc.setAttribute('numbercolumnsspanned', "2")
|
coeff_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
coeff_row.addElement(coeff_tc)
|
coeff_row.addElement(coeff_tc)
|
||||||
coeff_row.addElement(CoveredTableCell())
|
coeff_row.addElement(CoveredTableCell())
|
||||||
for passage in self.object.passages.all():
|
for passage in self.object.passages.all():
|
||||||
defender_w_tc = TableCell(valuetype="float", value=1, stylename=style_left)
|
reporter_w_tc = TableCell(valuetype="float", value=passage.coeff_reporter_writing, stylename=style_left)
|
||||||
defender_w_tc.addElement(P(text="1"))
|
reporter_w_tc.addElement(P(text=str(passage.coeff_reporter_writing)))
|
||||||
coeff_row.addElement(defender_w_tc)
|
|
||||||
|
|
||||||
defender_o_tc = TableCell(valuetype="float", value=1.6 - 0.4 * passage.defender_penalties, stylename=style)
|
|
||||||
defender_o_tc.addElement(P(text=str(2 - 0.4 * passage.defender_penalties)))
|
|
||||||
coeff_row.addElement(defender_o_tc)
|
|
||||||
|
|
||||||
opponent_w_tc = TableCell(valuetype="float", value=0.9, stylename=style)
|
|
||||||
opponent_w_tc.addElement(P(text="1"))
|
|
||||||
coeff_row.addElement(opponent_w_tc)
|
|
||||||
|
|
||||||
opponent_o_tc = TableCell(valuetype="float", value=2, stylename=style)
|
|
||||||
opponent_o_tc.addElement(P(text="2"))
|
|
||||||
coeff_row.addElement(opponent_o_tc)
|
|
||||||
|
|
||||||
reporter_w_tc = TableCell(valuetype="float", value=0.9, stylename=style)
|
|
||||||
reporter_w_tc.addElement(P(text="1"))
|
|
||||||
coeff_row.addElement(reporter_w_tc)
|
coeff_row.addElement(reporter_w_tc)
|
||||||
|
|
||||||
reporter_o_tc = TableCell(valuetype="float", value=1, stylename=style_right)
|
reporter_o_tc = TableCell(valuetype="float", value=passage.coeff_reporter_oral, stylename=style)
|
||||||
reporter_o_tc.addElement(P(text="1"))
|
reporter_o_tc.addElement(P(text=str(passage.coeff_reporter_oral)))
|
||||||
coeff_row.addElement(reporter_o_tc)
|
coeff_row.addElement(reporter_o_tc)
|
||||||
|
|
||||||
|
opponent_w_tc = TableCell(valuetype="float", value=passage.coeff_opponent_writing, stylename=style)
|
||||||
|
opponent_w_tc.addElement(P(text=str(passage.coeff_opponent_writing)))
|
||||||
|
coeff_row.addElement(opponent_w_tc)
|
||||||
|
|
||||||
|
opponent_o_tc = TableCell(valuetype="float", value=passage.coeff_opponent_oral, stylename=style)
|
||||||
|
opponent_o_tc.addElement(P(text=str(passage.coeff_opponent_oral)))
|
||||||
|
coeff_row.addElement(opponent_o_tc)
|
||||||
|
|
||||||
|
reviewer_w_tc = TableCell(valuetype="float", value=passage.coeff_reviewer_writing, stylename=style)
|
||||||
|
reviewer_w_tc.addElement(P(text=str(passage.coeff_reviewer_writing)))
|
||||||
|
coeff_row.addElement(reviewer_w_tc)
|
||||||
|
|
||||||
|
reviewer_o_tc = TableCell(valuetype="float", value=passage.coeff_reviewer_oral,
|
||||||
|
stylename=style if has_observer else style_right)
|
||||||
|
reviewer_o_tc.addElement(P(text=str(passage.coeff_reviewer_oral)))
|
||||||
|
coeff_row.addElement(reviewer_o_tc)
|
||||||
|
|
||||||
|
if has_observer:
|
||||||
|
observer_w_tc = TableCell(valuetype="float", value=passage.coeff_observer_writing, stylename=style)
|
||||||
|
observer_w_tc.addElement(P(text=str(passage.coeff_observer_writing)))
|
||||||
|
coeff_row.addElement(observer_w_tc)
|
||||||
|
|
||||||
|
observer_o_tc = TableCell(valuetype="float", value=passage.coeff_observer_oral, stylename=style_right)
|
||||||
|
observer_o_tc.addElement(P(text=str(passage.coeff_observer_oral)))
|
||||||
|
coeff_row.addElement(observer_o_tc)
|
||||||
|
|
||||||
# Add the subtotal on the next line
|
# Add the subtotal on the next line
|
||||||
subtotal_row = TableRow()
|
subtotal_row = TableRow()
|
||||||
table.addElement(subtotal_row)
|
table.addElement(subtotal_row)
|
||||||
subtotal_tc = TableCell(valuetype="string", stylename=title_style_botleft)
|
subtotal_tc = TableCell(valuetype="string", stylename=title_style_botleft)
|
||||||
subtotal_tc.addElement(P(text="Sous-total"))
|
subtotal_tc.addElement(P(text=_("Subtotal")))
|
||||||
subtotal_tc.setAttribute('numbercolumnsspanned', "2")
|
subtotal_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
subtotal_row.addElement(subtotal_tc)
|
subtotal_row.addElement(subtotal_tc)
|
||||||
subtotal_row.addElement(CoveredTableCell())
|
subtotal_row.addElement(CoveredTableCell())
|
||||||
for i, passage in enumerate(self.object.passages.all()):
|
for i, passage in enumerate(self.object.passages.all()):
|
||||||
def_w_col = getcol(min_column + passage_width * i)
|
def_w_col = getcol(min_column + passage_width * i)
|
||||||
def_o_col = getcol(min_column + passage_width * i + 1)
|
def_o_col = getcol(min_column + passage_width * i + 1)
|
||||||
defender_tc = TableCell(valuetype="float", value=passage.average_defender, stylename=style_botleft)
|
reporter_tc = TableCell(valuetype="float", value=passage.average_reporter, stylename=style_botleft)
|
||||||
defender_tc.addElement(P(text=str(passage.average_defender)))
|
reporter_tc.addElement(P(text=str(passage.average_reporter)))
|
||||||
defender_tc.setAttribute('numbercolumnsspanned', "2")
|
reporter_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
defender_tc.setAttribute("formula", f"of:=[.{def_w_col}{max_row + 1}] * [.{def_w_col}{max_row + 2}]"
|
reporter_tc.setAttribute("formula", f"of:=[.{def_w_col}{max_row + 1}] * [.{def_w_col}{max_row + 2}]"
|
||||||
f" + [.{def_o_col}{max_row + 1}] * [.{def_o_col}{max_row + 2}]")
|
f" + [.{def_o_col}{max_row + 1}] * [.{def_o_col}{max_row + 2}]")
|
||||||
subtotal_row.addElement(defender_tc)
|
subtotal_row.addElement(reporter_tc)
|
||||||
subtotal_row.addElement(CoveredTableCell())
|
subtotal_row.addElement(CoveredTableCell())
|
||||||
|
|
||||||
opp_w_col = getcol(min_column + passage_width * i + 2)
|
opp_w_col = getcol(min_column + passage_width * i + 2)
|
||||||
@ -1653,14 +1736,26 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
|
|
||||||
rep_w_col = getcol(min_column + passage_width * i + 4)
|
rep_w_col = getcol(min_column + passage_width * i + 4)
|
||||||
rep_o_col = getcol(min_column + passage_width * i + 5)
|
rep_o_col = getcol(min_column + passage_width * i + 5)
|
||||||
reporter_tc = TableCell(valuetype="float", value=passage.average_reporter, stylename=style_botright)
|
reviewer_tc = TableCell(valuetype="float", value=passage.average_reviewer,
|
||||||
reporter_tc.addElement(P(text=str(passage.average_reporter)))
|
stylename=style_bot if has_observer else style_botright)
|
||||||
reporter_tc.setAttribute('numbercolumnsspanned', "2")
|
reviewer_tc.addElement(P(text=str(passage.average_reviewer)))
|
||||||
reporter_tc.setAttribute("formula", f"of:=[.{rep_w_col}{max_row + 1}] * [.{rep_w_col}{max_row + 2}]"
|
reviewer_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
|
reviewer_tc.setAttribute("formula", f"of:=[.{rep_w_col}{max_row + 1}] * [.{rep_w_col}{max_row + 2}]"
|
||||||
f" + [.{rep_o_col}{max_row + 1}] * [.{rep_o_col}{max_row + 2}]")
|
f" + [.{rep_o_col}{max_row + 1}] * [.{rep_o_col}{max_row + 2}]")
|
||||||
subtotal_row.addElement(reporter_tc)
|
subtotal_row.addElement(reviewer_tc)
|
||||||
subtotal_row.addElement(CoveredTableCell())
|
subtotal_row.addElement(CoveredTableCell())
|
||||||
|
|
||||||
|
if has_observer:
|
||||||
|
obs_w_col = getcol(min_column + passage_width * i + 6)
|
||||||
|
obs_o_col = getcol(min_column + passage_width * i + 7)
|
||||||
|
observer_tc = TableCell(valuetype="float", value=passage.average_observer, stylename=style_botright)
|
||||||
|
observer_tc.addElement(P(text=str(passage.average_observer)))
|
||||||
|
observer_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
|
observer_tc.setAttribute("formula", f"of:=[.{obs_w_col}{max_row + 1}] * [.{obs_w_col}{max_row + 2}]"
|
||||||
|
f" + [.{obs_o_col}{max_row + 1}] * [.{obs_o_col}{max_row + 2}]")
|
||||||
|
subtotal_row.addElement(observer_tc)
|
||||||
|
subtotal_row.addElement(CoveredTableCell())
|
||||||
|
|
||||||
table.addElement(TableRow())
|
table.addElement(TableRow())
|
||||||
|
|
||||||
if self.object.participations.count() == 5:
|
if self.object.participations.count() == 5:
|
||||||
@ -1678,17 +1773,17 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
scores_header = TableRow()
|
scores_header = TableRow()
|
||||||
table.addElement(scores_header)
|
table.addElement(scores_header)
|
||||||
team_tc = TableCell(valuetype="string", stylename=title_style_topbotleft)
|
team_tc = TableCell(valuetype="string", stylename=title_style_topbotleft)
|
||||||
team_tc.addElement(P(text="Équipe"))
|
team_tc.addElement(P(text=_("Team")))
|
||||||
team_tc.setAttribute('numbercolumnsspanned', "2")
|
team_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
scores_header.addElement(team_tc)
|
scores_header.addElement(team_tc)
|
||||||
problem_tc = TableCell(valuetype="string", stylename=title_style_topbot)
|
problem_tc = TableCell(valuetype="string", stylename=title_style_topbot)
|
||||||
problem_tc.addElement(P(text="Problème"))
|
problem_tc.addElement(P(text=_("Problem")))
|
||||||
scores_header.addElement(problem_tc)
|
scores_header.addElement(problem_tc)
|
||||||
total_tc = TableCell(valuetype="string", stylename=title_style_topbot)
|
total_tc = TableCell(valuetype="string", stylename=title_style_topbot)
|
||||||
total_tc.addElement(P(text="Total"))
|
total_tc.addElement(P(text=_("Total")))
|
||||||
scores_header.addElement(total_tc)
|
scores_header.addElement(total_tc)
|
||||||
rank_tc = TableCell(valuetype="string", stylename=title_style_topbotright)
|
rank_tc = TableCell(valuetype="string", stylename=title_style_topbotright)
|
||||||
rank_tc.addElement(P(text="Rang"))
|
rank_tc.addElement(P(text=_("Rank")))
|
||||||
scores_header.addElement(rank_tc)
|
scores_header.addElement(rank_tc)
|
||||||
|
|
||||||
sorted_participations = sorted(self.object.participations.all(), key=lambda p: -self.object.average(p))
|
sorted_participations = sorted(self.object.participations.all(), key=lambda p: -self.object.average(p))
|
||||||
@ -1698,36 +1793,42 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
|
|
||||||
team_tc = TableCell(valuetype="string",
|
team_tc = TableCell(valuetype="string",
|
||||||
stylename=style_botleft if passage.position == pool_size else style_left)
|
stylename=style_botleft if passage.position == pool_size else style_left)
|
||||||
team_tc.addElement(P(text=f"{passage.defender.team.name} ({passage.defender.team.trigram})"))
|
team_tc.addElement(P(text=f"{passage.reporter.team.name} ({passage.reporter.team.trigram})"))
|
||||||
team_tc.setAttribute('numbercolumnsspanned', "2")
|
team_tc.setAttribute('numbercolumnsspanned', "2")
|
||||||
team_row.addElement(team_tc)
|
team_row.addElement(team_tc)
|
||||||
|
|
||||||
problem_tc = TableCell(valuetype="string",
|
problem_tc = TableCell(valuetype="string",
|
||||||
stylename=style_bot if passage.position == pool_size else style)
|
stylename=style_bot if passage.position == pool_size else style)
|
||||||
problem_tc.addElement(P(text=f"Problème {passage.solution_number}"))
|
problem_tc.addElement(P(text=_("Problem #{problem}").format(problem=passage.solution_number)))
|
||||||
problem_tc.setAttribute("formula", f"of:=[.B{3 + passage_width * (passage.position - 1)}]")
|
problem_tc.setAttribute("formula", f"of:=[.B{3 + passage_width * (passage.position - 1)}]")
|
||||||
team_row.addElement(problem_tc)
|
team_row.addElement(problem_tc)
|
||||||
|
|
||||||
defender_pos = passage.position - 1
|
reporter_pos = passage.position - 1
|
||||||
opponent_pos = self.object.passages.get(opponent=passage.defender).position - 1
|
opponent_pos = self.object.passages.get(opponent=passage.reporter).position - 1
|
||||||
reporter_pos = self.object.passages.get(reporter=passage.defender).position - 1
|
reviewer_pos = self.object.passages.get(reviewer=passage.reporter).position - 1
|
||||||
|
observer_pos = self.object.passages.get(observer=passage.reporter).position - 1 \
|
||||||
|
if has_observer else None
|
||||||
|
|
||||||
score_tc = TableCell(valuetype="float", value=self.object.average(passage.defender),
|
score_tc = TableCell(valuetype="float", value=self.object.average(passage.reporter),
|
||||||
stylename=style_bot if passage.position == pool_size else style)
|
stylename=style_bot if passage.position == pool_size else style)
|
||||||
score_tc.addElement(P(text=self.object.average(passage.defender)))
|
score_tc.addElement(P(text=self.object.average(passage.reporter)))
|
||||||
formula = "of:="
|
formula = "of:="
|
||||||
formula += getcol(min_column + defender_pos * passage_width) + str(max_row + 3) # Defender
|
formula += getcol(min_column + reporter_pos * passage_width) + str(max_row + 3) # Reporter
|
||||||
formula += " + " + getcol(min_column + opponent_pos * passage_width + 2) + str(max_row + 3) # Opponent
|
formula += " + " + getcol(min_column + opponent_pos * passage_width + 2) + str(max_row + 3) # Opponent
|
||||||
formula += " + " + getcol(min_column + reporter_pos * passage_width + 4) + str(max_row + 3) # Reporter
|
formula += " + " + getcol(min_column + reviewer_pos * passage_width + 4) + str(max_row + 3) # Reviewer
|
||||||
|
if has_observer:
|
||||||
|
# Observer
|
||||||
|
formula += " + " + getcol(min_column + observer_pos * passage_width + 6) + str(max_row + 3)
|
||||||
score_tc.setAttribute("formula", formula)
|
score_tc.setAttribute("formula", formula)
|
||||||
team_row.addElement(score_tc)
|
team_row.addElement(score_tc)
|
||||||
|
|
||||||
score_col = 'C'
|
score_col = 'C'
|
||||||
rank_tc = TableCell(valuetype="float", value=sorted_participations.index(passage.defender) + 1,
|
rank_tc = TableCell(valuetype="float", value=sorted_participations.index(passage.reporter) + 1,
|
||||||
stylename=style_botright if passage.position == pool_size else style_right)
|
stylename=style_botright if passage.position == pool_size else style_right)
|
||||||
rank_tc.addElement(P(text=str(sorted_participations.index(passage.defender) + 1)))
|
rank_tc.addElement(P(text=str(sorted_participations.index(passage.reporter) + 1)))
|
||||||
rank_tc.setAttribute("formula", f"of:=RANK([.{score_col}{max_row + 5 + passage.position}]; "
|
rank_tc.setAttribute("formula", f"of:=RANK([.{score_col}{max_row + 5 + passage.position}]; "
|
||||||
f"[.{score_col}${max_row + 6}]:[.{score_col}${max_row + 5 + pool_size}])")
|
f"[.{score_col}${max_row + 6}]:"
|
||||||
|
f"[.{score_col}${max_row + 5 + pool_size}])")
|
||||||
team_row.addElement(rank_tc)
|
team_row.addElement(rank_tc)
|
||||||
|
|
||||||
table.addElement(TableRow())
|
table.addElement(TableRow())
|
||||||
@ -1752,8 +1853,8 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|||||||
|
|
||||||
return FileResponse(streaming_content=open("/tmp/notes.ods", "rb"),
|
return FileResponse(streaming_content=open("/tmp/notes.ods", "rb"),
|
||||||
content_type="application/vnd.oasis.opendocument.spreadsheet",
|
content_type="application/vnd.oasis.opendocument.spreadsheet",
|
||||||
filename=f"Feuille de notes - {self.object.tournament.name} "
|
filename=f"{_('Notation sheet')} - {self.object.tournament.name} "
|
||||||
f"- Poule {self.object.short_name}.ods")
|
f"- {_('Pool')} {self.object.short_name}.ods")
|
||||||
|
|
||||||
|
|
||||||
class NotationSheetTemplateView(VolunteerMixin, DetailView):
|
class NotationSheetTemplateView(VolunteerMixin, DetailView):
|
||||||
@ -1782,11 +1883,13 @@ class NotationSheetTemplateView(VolunteerMixin, DetailView):
|
|||||||
context['esp'] = passages.count() * '&'
|
context['esp'] = passages.count() * '&'
|
||||||
if self.request.user.registration in self.object.juries.all() and 'blank' not in self.request.GET:
|
if self.request.user.registration in self.object.juries.all() and 'blank' not in self.request.GET:
|
||||||
context['jury'] = self.request.user.registration
|
context['jury'] = self.request.user.registration
|
||||||
context['tfjm_number'] = timezone.now().year - 2010
|
context['tfjm_number'] = timezone.now().year - settings.FIRST_EDITION + 1
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
def render_to_response(self, context, **response_kwargs):
|
||||||
tex = render_to_string(self.template_name, context=context, request=self.request)
|
template_name = self.get_template_names()[0]
|
||||||
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
|
tex = render_to_string(template_name, context=context, request=self.request)
|
||||||
temp_dir = mkdtemp()
|
temp_dir = mkdtemp()
|
||||||
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
||||||
f.write(tex)
|
f.write(tex)
|
||||||
@ -1795,15 +1898,16 @@ class NotationSheetTemplateView(VolunteerMixin, DetailView):
|
|||||||
process.wait()
|
process.wait()
|
||||||
return FileResponse(streaming_content=open(os.path.join(temp_dir, "texput.pdf"), "rb"),
|
return FileResponse(streaming_content=open(os.path.join(temp_dir, "texput.pdf"), "rb"),
|
||||||
content_type="application/pdf",
|
content_type="application/pdf",
|
||||||
filename=self.template_name.split("/")[-1][:-3] + "pdf")
|
filename=template_name.split("/")[-1][:-3] + "pdf")
|
||||||
|
|
||||||
|
|
||||||
class ScaleNotationSheetTemplateView(NotationSheetTemplateView):
|
class ScaleNotationSheetTemplateView(NotationSheetTemplateView):
|
||||||
template_name = 'participation/tex/bareme.tex'
|
def get_template_names(self):
|
||||||
|
return [f"participation/tex/scale_{settings.TFJM_APP.lower()}.tex"]
|
||||||
|
|
||||||
|
|
||||||
class FinalNotationSheetTemplateView(NotationSheetTemplateView):
|
class FinalNotationSheetTemplateView(NotationSheetTemplateView):
|
||||||
template_name = 'participation/tex/finale.tex'
|
template_name = "participation/tex/final_sheet.tex"
|
||||||
|
|
||||||
|
|
||||||
class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
||||||
@ -1835,6 +1939,8 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||||
|
|
||||||
if 'pool_id' in kwargs:
|
if 'pool_id' in kwargs:
|
||||||
pool = self.get_object()
|
pool = self.get_object()
|
||||||
tournament = pool.tournament
|
tournament = pool.tournament
|
||||||
@ -1850,15 +1956,15 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
|||||||
with ZipFile(output, "w") as zf:
|
with ZipFile(output, "w") as zf:
|
||||||
for pool in pools:
|
for pool in pools:
|
||||||
prefix = f"{pool.short_name}/" if len(pools) > 1 else ""
|
prefix = f"{pool.short_name}/" if len(pools) > 1 else ""
|
||||||
for template_name in ['bareme', 'finale']:
|
for template_name in [f"scale_{settings.TFJM_APP.lower()}", "final_sheet"]:
|
||||||
juries = list(pool.juries.all()) + [None]
|
juries = list(pool.juries.all()) + [None]
|
||||||
|
|
||||||
for jury in juries:
|
for jury in juries:
|
||||||
if jury is not None and template_name == "bareme":
|
if jury is not None and template_name.startswith("scale"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
context = {'jury': jury, 'pool': pool,
|
context = {'jury': jury, 'pool': pool,
|
||||||
'tfjm_number': timezone.now().year - 2010}
|
'tfjm_number': timezone.now().year - settings.FIRST_EDITION + 1}
|
||||||
|
|
||||||
passages = pool.passages.all()
|
passages = pool.passages.all()
|
||||||
context['passages'] = passages
|
context['passages'] = passages
|
||||||
@ -1875,7 +1981,7 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
|||||||
os.path.join(temp_dir, "texput.tex"), ])
|
os.path.join(temp_dir, "texput.tex"), ])
|
||||||
process.wait()
|
process.wait()
|
||||||
|
|
||||||
sheet_name = f"Barème pour la poule {pool.short_name}" if template_name == "bareme" \
|
sheet_name = f"Barème pour la poule {pool.short_name}" if template_name.startswith("scale") \
|
||||||
else (f"Feuille de notation pour la poule {pool.short_name}"
|
else (f"Feuille de notation pour la poule {pool.short_name}"
|
||||||
f" - {str(jury) if jury else 'Vierge'}")
|
f" - {str(jury) if jury else 'Vierge'}")
|
||||||
|
|
||||||
@ -1890,6 +1996,13 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
|||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class GSheetNotificationsView(View):
|
class GSheetNotificationsView(View):
|
||||||
|
"""
|
||||||
|
Cette vue gère les notifications envoyées par Google Drive en cas de
|
||||||
|
modifications d'un tableur de notes sur Google Sheets.
|
||||||
|
|
||||||
|
Documentation de l'API : https://developers.google.com/calendar/api/guides/push?hl=fr
|
||||||
|
"""
|
||||||
|
|
||||||
async def post(self, request, *args, **kwargs):
|
async def post(self, request, *args, **kwargs):
|
||||||
if not await Tournament.objects.filter(pk=kwargs['pk']).aexists():
|
if not await Tournament.objects.filter(pk=kwargs['pk']).aexists():
|
||||||
return HttpResponse(status=404)
|
return HttpResponse(status=404)
|
||||||
@ -1924,11 +2037,11 @@ 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 \
|
||||||
and reg.team.participation in [passage.defender, passage.opponent, passage.reporter]:
|
and reg.team.participation in [passage.reporter, passage.opponent, passage.reviewer, passage.observer]:
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
@ -1949,12 +2062,12 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
|
|||||||
if 'notes' in context and not self.request.user.registration.is_admin:
|
if 'notes' in context and not self.request.user.registration.is_admin:
|
||||||
context['notes']._sequence.remove('update')
|
context['notes']._sequence.remove('update')
|
||||||
|
|
||||||
context['notes'].columns['defender_writing'].column.verbose_name += f" ({passage.defender.team.trigram})"
|
|
||||||
context['notes'].columns['defender_oral'].column.verbose_name += f" ({passage.defender.team.trigram})"
|
|
||||||
context['notes'].columns['opponent_writing'].column.verbose_name += f" ({passage.opponent.team.trigram})"
|
|
||||||
context['notes'].columns['opponent_oral'].column.verbose_name += f" ({passage.opponent.team.trigram})"
|
|
||||||
context['notes'].columns['reporter_writing'].column.verbose_name += f" ({passage.reporter.team.trigram})"
|
context['notes'].columns['reporter_writing'].column.verbose_name += f" ({passage.reporter.team.trigram})"
|
||||||
context['notes'].columns['reporter_oral'].column.verbose_name += f" ({passage.reporter.team.trigram})"
|
context['notes'].columns['reporter_oral'].column.verbose_name += f" ({passage.reporter.team.trigram})"
|
||||||
|
context['notes'].columns['opponent_writing'].column.verbose_name += f" ({passage.opponent.team.trigram})"
|
||||||
|
context['notes'].columns['opponent_oral'].column.verbose_name += f" ({passage.opponent.team.trigram})"
|
||||||
|
context['notes'].columns['reviewer_writing'].column.verbose_name += f" ({passage.reviewer.team.trigram})"
|
||||||
|
context['notes'].columns['reviewer_oral'].column.verbose_name += f" ({passage.reviewer.team.trigram})"
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -1975,9 +2088,9 @@ class PassageUpdateView(VolunteerMixin, UpdateView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
|
|
||||||
class SynthesisUploadView(LoginRequiredMixin, FormView):
|
class WrittenReviewUploadView(LoginRequiredMixin, FormView):
|
||||||
template_name = "participation/upload_synthesis.html"
|
template_name = "participation/upload_written_review.html"
|
||||||
form_class = SynthesisForm
|
form_class = WrittenReviewForm
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated or not request.user.registration.participates:
|
if not request.user.is_authenticated or not request.user.registration.participates:
|
||||||
@ -1989,7 +2102,8 @@ class SynthesisUploadView(LoginRequiredMixin, FormView):
|
|||||||
self.participation = self.request.user.registration.team.participation
|
self.participation = self.request.user.registration.team.participation
|
||||||
self.passage = qs.get()
|
self.passage = qs.get()
|
||||||
|
|
||||||
if self.participation not in [self.passage.opponent, self.passage.reporter]:
|
if self.participation \
|
||||||
|
and self.participation not in [self.passage.opponent, self.passage.reviewer, self.passage.observer]:
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
@ -2001,15 +2115,16 @@ class SynthesisUploadView(LoginRequiredMixin, FormView):
|
|||||||
It is discriminating whenever the team is selected for the final tournament or not.
|
It is discriminating whenever the team is selected for the final tournament or not.
|
||||||
"""
|
"""
|
||||||
form_syn = form.instance
|
form_syn = form.instance
|
||||||
form_syn.type = 1 if self.participation == self.passage.opponent else 2
|
form_syn.type = 1 if self.participation == self.passage.opponent \
|
||||||
syn_qs = Synthesis.objects.filter(participation=self.participation,
|
else 2 if self.participation == self.passage.reviewer else 3
|
||||||
passage=self.passage,
|
syn_qs = WrittenReview.objects.filter(participation=self.participation,
|
||||||
type=form_syn.type).all()
|
passage=self.passage,
|
||||||
|
type=form_syn.type).all()
|
||||||
|
|
||||||
deadline = self.passage.pool.tournament.syntheses_first_phase_limit if self.passage.pool.round == 1 \
|
deadline = self.passage.pool.tournament.reviews_first_phase_limit if self.passage.pool.round == 1 \
|
||||||
else self.passage.pool.tournament.syntheses_second_phase_limit
|
else self.passage.pool.tournament.reviews_second_phase_limit
|
||||||
if syn_qs.exists() and timezone.now() > deadline:
|
if syn_qs.exists() and timezone.now() > deadline:
|
||||||
form.add_error(None, _("You can't upload a synthesis after the deadline."))
|
form.add_error(None, _("You can't upload a written review after the deadline."))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
# Drop previous solution if existing
|
# Drop previous solution if existing
|
||||||
@ -2043,12 +2158,15 @@ class NoteUpdateView(VolunteerMixin, UpdateView):
|
|||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
form.fields['defender_writing'].label += f" ({self.object.passage.defender.team.trigram})"
|
|
||||||
form.fields['defender_oral'].label += f" ({self.object.passage.defender.team.trigram})"
|
|
||||||
form.fields['opponent_writing'].label += f" ({self.object.passage.opponent.team.trigram})"
|
|
||||||
form.fields['opponent_oral'].label += f" ({self.object.passage.opponent.team.trigram})"
|
|
||||||
form.fields['reporter_writing'].label += f" ({self.object.passage.reporter.team.trigram})"
|
form.fields['reporter_writing'].label += f" ({self.object.passage.reporter.team.trigram})"
|
||||||
form.fields['reporter_oral'].label += f" ({self.object.passage.reporter.team.trigram})"
|
form.fields['reporter_oral'].label += f" ({self.object.passage.reporter.team.trigram})"
|
||||||
|
form.fields['opponent_writing'].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_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
|
return form
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
@ -10,4 +10,4 @@ def register_registration_urls(router, path):
|
|||||||
"""
|
"""
|
||||||
router.register(path + "/payment", PaymentViewSet)
|
router.register(path + "/payment", PaymentViewSet)
|
||||||
router.register(path + "/registration", RegistrationViewSet)
|
router.register(path + "/registration", RegistrationViewSet)
|
||||||
router.register(path + "/volunteers", VolunteersViewSet)
|
router.register(path + "/volunteers", VolunteersViewSet, basename="volunteers")
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.models.signals import post_save, pre_save
|
from django.db.models.signals import post_save, pre_save
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class RegistrationConfig(AppConfig):
|
class RegistrationConfig(AppConfig):
|
||||||
@ -10,6 +11,7 @@ class RegistrationConfig(AppConfig):
|
|||||||
Registration app contains the detail about users only.
|
Registration app contains the detail about users only.
|
||||||
"""
|
"""
|
||||||
name = 'registration'
|
name = 'registration'
|
||||||
|
verbose_name = _("registrations")
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from registration import signals
|
from registration import signals
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.forms import UserCreationForm
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
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
|
||||||
@ -103,12 +104,15 @@ class StudentRegistrationForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["birth_date"].widget = forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
|
self.fields["birth_date"].widget = forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
|
||||||
|
if not settings.SUGGEST_ANIMATH:
|
||||||
|
del self.fields["give_contact_to_animath"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StudentRegistration
|
model = StudentRegistration
|
||||||
fields = ('team', 'student_class', 'birth_date', 'gender', 'address', 'zip_code', 'city', 'phone_number',
|
fields = ('team', 'student_class', 'birth_date', 'gender', 'address', 'zip_code', 'city', 'country',
|
||||||
'school', 'health_issues', 'housing_constraints', 'responsible_name', 'responsible_phone',
|
'phone_number', 'school', 'health_issues', 'housing_constraints',
|
||||||
'responsible_email', 'give_contact_to_animath', 'email_confirmed',)
|
'responsible_name', 'responsible_phone', 'responsible_email', 'give_contact_to_animath',
|
||||||
|
'email_confirmed',)
|
||||||
|
|
||||||
|
|
||||||
class PhotoAuthorizationForm(forms.ModelForm):
|
class PhotoAuthorizationForm(forms.ModelForm):
|
||||||
@ -247,9 +251,14 @@ class CoachRegistrationForm(forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
A coach can tell its professional activity.
|
A coach can tell its professional activity.
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if not settings.SUGGEST_ANIMATH:
|
||||||
|
del self.fields["give_contact_to_animath"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CoachRegistration
|
model = CoachRegistration
|
||||||
fields = ('team', 'gender', 'address', 'zip_code', 'city', 'phone_number',
|
fields = ('team', 'gender', 'address', 'zip_code', 'city', 'country', 'phone_number',
|
||||||
'last_degree', 'professional_activity', 'health_issues', 'housing_constraints',
|
'last_degree', 'professional_activity', 'health_issues', 'housing_constraints',
|
||||||
'give_contact_to_animath', 'email_confirmed',)
|
'give_contact_to_animath', 'email_confirmed',)
|
||||||
|
|
||||||
@ -258,6 +267,11 @@ class VolunteerRegistrationForm(forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
A volunteer can also tell its professional activity.
|
A volunteer can also tell its professional activity.
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if not settings.SUGGEST_ANIMATH:
|
||||||
|
del self.fields["give_contact_to_animath"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VolunteerRegistration
|
model = VolunteerRegistration
|
||||||
fields = ('professional_activity', 'admin', 'give_contact_to_animath', 'email_confirmed',)
|
fields = ('professional_activity', 'admin', 'give_contact_to_animath', 'email_confirmed',)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
from ...models import Payment
|
from ...models import Payment
|
||||||
@ -15,6 +16,9 @@ class Command(BaseCommand):
|
|||||||
help = "Vérifie si les paiements Hello Asso initiés sont validés ou non. Si oui, valide les inscriptions."
|
help = "Vérifie si les paiements Hello Asso initiés sont validés ou non. Si oui, valide les inscriptions."
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
if not settings.PAYMENT_MANAGEMENT:
|
||||||
|
return
|
||||||
|
|
||||||
for payment in Payment.objects.exclude(valid=True).filter(checkout_intent_id__isnull=False).all():
|
for payment in Payment.objects.exclude(valid=True).filter(checkout_intent_id__isnull=False).all():
|
||||||
checkout_intent = payment.get_checkout_intent()
|
checkout_intent = payment.get_checkout_intent()
|
||||||
if checkout_intent is not None and 'order' in checkout_intent:
|
if checkout_intent is not None and 'order' in checkout_intent:
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
# Copyright (C) 2024 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from participation.models import Team
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = """Cette commande permet d'exporter dans le dossier output/photo_authorizations l'ensemble des
|
||||||
|
autorisations de droit à l'image des participant⋅es, triées par équipe, incluant aussi celles de la finale."""
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
base_dir = Path(__file__).parent.parent.parent.parent
|
||||||
|
base_dir /= "output"
|
||||||
|
if not base_dir.is_dir():
|
||||||
|
base_dir.mkdir()
|
||||||
|
base_dir /= "photo_authorizations"
|
||||||
|
if not base_dir.is_dir():
|
||||||
|
base_dir.mkdir()
|
||||||
|
|
||||||
|
for team in Team.objects.filter(participation__valid=True).all():
|
||||||
|
team_dir = base_dir / f"{team.trigram} - {team.name}"
|
||||||
|
if not team_dir.is_dir():
|
||||||
|
team_dir.mkdir()
|
||||||
|
|
||||||
|
for participant in team.participants.all():
|
||||||
|
if participant.photo_authorization:
|
||||||
|
with participant.photo_authorization.file as file_input:
|
||||||
|
with open(team_dir / f"{participant}.pdf", 'wb') as file_output:
|
||||||
|
file_output.write(file_input.read())
|
||||||
|
|
||||||
|
if participant.photo_authorization_final:
|
||||||
|
with participant.photo_authorization_final.file as file_input:
|
||||||
|
with open(team_dir / f"{participant} (finale).pdf", 'wb') as file_output:
|
||||||
|
file_output.write(file_input.read())
|
@ -1,6 +1,7 @@
|
|||||||
# Copyright (C) 2024 by Animath
|
# Copyright (C) 2024 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.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
from ...models import Payment
|
from ...models import Payment
|
||||||
@ -13,5 +14,8 @@ class Command(BaseCommand):
|
|||||||
help = "Envoie un mail de rappel à toustes les participant⋅es qui n'ont pas encore payé ou déclaré de paiement."
|
help = "Envoie un mail de rappel à toustes les participant⋅es qui n'ont pas encore payé ou déclaré de paiement."
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
if not settings.PAYMENT_MANAGEMENT:
|
||||||
|
return
|
||||||
|
|
||||||
for payment in Payment.objects.filter(valid=False).filter(registrations__team__participation__valid=True).all():
|
for payment in Payment.objects.filter(valid=False).filter(registrations__team__participation__valid=True).all():
|
||||||
payment.send_remind_mail()
|
payment.send_remind_mail()
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-07 12:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"registration",
|
||||||
|
"0013_participantregistration_photo_authorization_final_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="participantregistration",
|
||||||
|
name="country",
|
||||||
|
field=models.CharField(
|
||||||
|
default="France", max_length=255, verbose_name="country"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-27 19:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registration", "0014_participantregistration_country"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="participantregistration",
|
||||||
|
name="gender",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("female", "Female"), ("male", "Male"), ("other", "Other")],
|
||||||
|
max_length=6,
|
||||||
|
verbose_name="gender",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -1,12 +1,14 @@
|
|||||||
# 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 datetime import date, datetime
|
from datetime import date
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
@ -49,7 +51,7 @@ class Registration(PolymorphicModel):
|
|||||||
The account got created or the email got changed.
|
The account got created or the email got changed.
|
||||||
Send an email that contains a link to validate the address.
|
Send an email that contains a link to validate the address.
|
||||||
"""
|
"""
|
||||||
subject = "[TFJM²] " + str(_("Activate your TFJM² account"))
|
subject = f"[{settings.APP_NAME}] " + str(_("Activate your account"))
|
||||||
token = email_validation_token.make_token(self.user)
|
token = email_validation_token.make_token(self.user)
|
||||||
uid = urlsafe_base64_encode(force_bytes(self.user.pk))
|
uid = urlsafe_base64_encode(force_bytes(self.user.pk))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
@ -165,7 +167,6 @@ class ParticipantRegistration(Registration):
|
|||||||
("male", _("Male")),
|
("male", _("Male")),
|
||||||
("other", _("Other")),
|
("other", _("Other")),
|
||||||
],
|
],
|
||||||
default="other",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
address = models.CharField(
|
address = models.CharField(
|
||||||
@ -183,6 +184,12 @@ class ParticipantRegistration(Registration):
|
|||||||
verbose_name=_("city"),
|
verbose_name=_("city"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
country = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_("country"),
|
||||||
|
default="France",
|
||||||
|
)
|
||||||
|
|
||||||
phone_number = PhoneNumberField(
|
phone_number = PhoneNumberField(
|
||||||
verbose_name=_("phone number"),
|
verbose_name=_("phone number"),
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -253,6 +260,8 @@ class ParticipantRegistration(Registration):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def registration_informations(self):
|
def registration_informations(self):
|
||||||
|
from survey.models import Survey
|
||||||
|
|
||||||
informations = []
|
informations = []
|
||||||
if not self.team:
|
if not self.team:
|
||||||
text = _("You are not in a team. You can <a href=\"{create_url}\">create one</a> "
|
text = _("You are not in a team. You can <a href=\"{create_url}\">create one</a> "
|
||||||
@ -293,6 +302,20 @@ class ParticipantRegistration(Registration):
|
|||||||
'content': content,
|
'content': content,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if self.team.participation.valid:
|
||||||
|
for survey in Survey.objects.filter(Q(tournament__isnull=True) | Q(tournament=self.team.participation.tournament),
|
||||||
|
Q(invite_team=False), Q(invite_coaches=True) | Q(invite_coaches=self.is_coach),
|
||||||
|
~Q(completed_registrations=self)):
|
||||||
|
text = _("Please answer to the survey \"{name}\". You can go to the survey on <a href=\"{survey_link}\">that link</a>, "
|
||||||
|
"using the token code you received by mail.")
|
||||||
|
content = format_lazy(text, name=survey.name, survey_link=f"{settings.LIMESURVEY_URL}/index.php/{survey.survey_id}")
|
||||||
|
informations.append({
|
||||||
|
'title': _("Required answer to survey"),
|
||||||
|
'type': "warning",
|
||||||
|
'priority': 12,
|
||||||
|
'content': content
|
||||||
|
})
|
||||||
|
|
||||||
informations.extend(self.team.important_informations())
|
informations.extend(self.team.important_informations())
|
||||||
|
|
||||||
return informations
|
return informations
|
||||||
@ -301,27 +324,27 @@ class ParticipantRegistration(Registration):
|
|||||||
"""
|
"""
|
||||||
The team is selected for final.
|
The team is selected for final.
|
||||||
"""
|
"""
|
||||||
translation.activate('fr')
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
subject = "[TFJM²] " + str(_("Team selected for the final tournament"))
|
subject = f"[{settings.APP_NAME}] " + str(_("Team selected for the final tournament"))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
from participation.models import Tournament
|
from participation.models import Tournament
|
||||||
tournament = Tournament.final_tournament()
|
tournament = Tournament.final_tournament()
|
||||||
payment = self.payments.filter(final=True).first() if self.is_student else None
|
payment = self.payments.filter(final=True).first() if self.is_student else None
|
||||||
message = loader.render_to_string('registration/mails/final_selection.txt',
|
message = loader.render_to_string('registration/mails/final_selection.txt',
|
||||||
{
|
{
|
||||||
'user': self.user,
|
'user': self.user,
|
||||||
'domain': site.domain,
|
'domain': site.domain,
|
||||||
'tournament': tournament,
|
'tournament': tournament,
|
||||||
'payment': payment,
|
'payment': payment,
|
||||||
})
|
})
|
||||||
html = loader.render_to_string('registration/mails/final_selection.html',
|
html = loader.render_to_string('registration/mails/final_selection.html',
|
||||||
{
|
{
|
||||||
'user': self.user,
|
'user': self.user,
|
||||||
'domain': site.domain,
|
'domain': site.domain,
|
||||||
'tournament': tournament,
|
'tournament': tournament,
|
||||||
'payment': payment,
|
'payment': payment,
|
||||||
})
|
})
|
||||||
self.user.email_user(subject, message, html_message=html)
|
self.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("participant registration")
|
verbose_name = _("participant registration")
|
||||||
@ -419,7 +442,7 @@ class StudentRegistration(ParticipantRegistration):
|
|||||||
'priority': 5,
|
'priority': 5,
|
||||||
'content': content,
|
'content': content,
|
||||||
})
|
})
|
||||||
if not self.health_sheet:
|
if settings.HEALTH_SHEET_REQUIRED and not self.health_sheet:
|
||||||
text = _("You have not uploaded your health sheet. "
|
text = _("You have not uploaded your health sheet. "
|
||||||
"You can do it by clicking on <a href=\"{health_url}\">this link</a>.")
|
"You can do it by clicking on <a href=\"{health_url}\">this link</a>.")
|
||||||
health_url = reverse_lazy("registration:upload_user_health_sheet", args=(self.id,))
|
health_url = reverse_lazy("registration:upload_user_health_sheet", args=(self.id,))
|
||||||
@ -430,7 +453,7 @@ class StudentRegistration(ParticipantRegistration):
|
|||||||
'priority': 5,
|
'priority': 5,
|
||||||
'content': content,
|
'content': content,
|
||||||
})
|
})
|
||||||
if not self.vaccine_sheet:
|
if settings.VACCINE_SHEET_REQUIRED and not self.vaccine_sheet:
|
||||||
text = _("You have not uploaded your vaccine sheet. "
|
text = _("You have not uploaded your vaccine sheet. "
|
||||||
"You can do it by clicking on <a href=\"{vaccine_url}\">this link</a>.")
|
"You can do it by clicking on <a href=\"{vaccine_url}\">this link</a>.")
|
||||||
vaccine_url = reverse_lazy("registration:upload_user_vaccine_sheet", args=(self.id,))
|
vaccine_url = reverse_lazy("registration:upload_user_vaccine_sheet", args=(self.id,))
|
||||||
@ -767,7 +790,7 @@ class Payment(models.Model):
|
|||||||
return checkout_intent
|
return checkout_intent
|
||||||
|
|
||||||
tournament = self.tournament
|
tournament = self.tournament
|
||||||
year = datetime.now().year
|
year = timezone.now().year
|
||||||
base_site = "https://" + Site.objects.first().domain
|
base_site = "https://" + Site.objects.first().domain
|
||||||
checkout_intent = helloasso.create_checkout_intent(
|
checkout_intent = helloasso.create_checkout_intent(
|
||||||
amount=100 * self.amount,
|
amount=100 * self.amount,
|
||||||
@ -795,35 +818,35 @@ class Payment(models.Model):
|
|||||||
return checkout_intent
|
return checkout_intent
|
||||||
|
|
||||||
def send_remind_mail(self):
|
def send_remind_mail(self):
|
||||||
translation.activate('fr')
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
subject = "[TFJM²] " + str(_("Reminder for your payment"))
|
subject = f"[{settings.APP_NAME}] " + str(_("Reminder for your payment"))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
for registration in self.registrations.all():
|
for registration in self.registrations.all():
|
||||||
message = loader.render_to_string('registration/mails/payment_reminder.txt',
|
message = loader.render_to_string('registration/mails/payment_reminder.txt',
|
||||||
dict(registration=registration, payment=self, domain=site.domain))
|
dict(registration=registration, payment=self, domain=site.domain))
|
||||||
html = loader.render_to_string('registration/mails/payment_reminder.html',
|
html = loader.render_to_string('registration/mails/payment_reminder.html',
|
||||||
dict(registration=registration, payment=self, domain=site.domain))
|
dict(registration=registration, payment=self, domain=site.domain))
|
||||||
registration.user.email_user(subject, message, html_message=html)
|
registration.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
def send_helloasso_payment_confirmation_mail(self):
|
def send_helloasso_payment_confirmation_mail(self):
|
||||||
translation.activate('fr')
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
subject = "[TFJM²] " + str(_("Payment confirmation"))
|
subject = f"[{settings.APP_NAME}] " + str(_("Payment confirmation"))
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
for registration in self.registrations.all():
|
for registration in self.registrations.all():
|
||||||
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
|
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
|
||||||
dict(registration=registration, payment=self, domain=site.domain))
|
dict(registration=registration, payment=self, domain=site.domain))
|
||||||
html = loader.render_to_string('registration/mails/payment_confirmation.html',
|
html = loader.render_to_string('registration/mails/payment_confirmation.html',
|
||||||
dict(registration=registration, payment=self, domain=site.domain))
|
dict(registration=registration, payment=self, domain=site.domain))
|
||||||
registration.user.email_user(subject, message, html_message=html)
|
registration.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
payer = self.get_checkout_intent()['order']['payer']
|
payer = self.get_checkout_intent()['order']['payer']
|
||||||
payer_name = f"{payer['firstName']} {payer['lastName']}"
|
payer_name = f"{payer['firstName']} {payer['lastName']}"
|
||||||
if not self.registrations.filter(user__email=payer['email']).exists():
|
if not self.registrations.filter(user__email=payer['email']).exists():
|
||||||
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
|
message = loader.render_to_string('registration/mails/payment_confirmation.txt',
|
||||||
dict(registration=payer_name, payment=self, domain=site.domain))
|
dict(registration=payer_name, payment=self, domain=site.domain))
|
||||||
html = loader.render_to_string('registration/mails/payment_confirmation.html',
|
html = loader.render_to_string('registration/mails/payment_confirmation.html',
|
||||||
dict(registration=payer_name, payment=self, domain=site.domain))
|
dict(registration=payer_name, payment=self, domain=site.domain))
|
||||||
send_mail(subject, message, None, [payer['email']], html_message=html)
|
send_mail(subject, message, None, [payer['email']], html_message=html)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("registration:update_payment", args=(self.pk,))
|
return reverse_lazy("registration:update_payment", args=(self.pk,))
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
|
|
||||||
from .models import Registration, VolunteerRegistration
|
from .models import Registration, VolunteerRegistration
|
||||||
@ -29,8 +30,8 @@ def send_email_link(instance, **_):
|
|||||||
registration.send_email_validation_link()
|
registration.send_email_validation_link()
|
||||||
|
|
||||||
if registration.participates and registration.team:
|
if registration.participates and registration.team:
|
||||||
get_sympa_client().unsubscribe(old_instance.email, f"equipe-{registration.team.trigram.lower()}", False)
|
get_sympa_client().unsubscribe(old_instance.email, f"equipe-{slugify(registration.team.trigram)}", False)
|
||||||
get_sympa_client().subscribe(instance.email, f"equipe-{registration.team.trigram.lower()}", False,
|
get_sympa_client().subscribe(instance.email, f"equipe-{slugify(registration.team.trigram)}", False,
|
||||||
f"{instance.first_name} {instance.last_name}")
|
f"{instance.first_name} {instance.last_name}")
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,8 +24,7 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
En cas de problème, merci de nous contacter soit par mail à l'adresse
|
En cas de problème, merci de nous contacter soit par mail à l'adresse
|
||||||
<a href="mailto:contact@tfjm.org">contact@tfjm.org</a>, soit sur la plateforme de chat accessible sur
|
<a href="mailto:contact@tfjm.org">contact@tfjm.org</a>.
|
||||||
<a href="https://element.tfjm.org/">https://element.tfjm.org/</a> en vous connectant avec les mêmes identifiants.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -9,30 +9,42 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% trans "Sign up" %}</h2>
|
{% now "c" as now %}
|
||||||
|
{% if now < TFJM.REGISTRATION_DATES.open.isoformat and not user.registration.is_admin %}
|
||||||
<form method="post">
|
<div class="alert alert-warning">
|
||||||
{% csrf_token %}
|
{% trans "Thank you for your great interest, but registrations are not opened yet!" %}
|
||||||
{{ form|crispy }}
|
{% trans "They will open on:" %} {{ TFJM.REGISTRATION_DATES.open|date:'DATETIME_FORMAT' }}.
|
||||||
<div id="registration_form"></div>
|
{% trans "Please come back at this time to register!" %}
|
||||||
|
|
||||||
<div class="py-2 text-muted">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
{% trans "By registering, you certify that you have read and accepted our" %}
|
|
||||||
<a href="{% url 'about' %}#politique-confidentialite">{% trans "privacy policy" %}</a>.
|
|
||||||
</div>
|
</div>
|
||||||
|
{% elif now > TFJM.REGISTRATION_DATES.close.isoformat and not user.registration.is_admin %}
|
||||||
<button class="btn btn-success" type="submit">
|
<div class="alert alert-danger">
|
||||||
{% trans "Sign up" %}
|
{% trans "Registrations are closed for this year. We hope to see you next year!" %}
|
||||||
</button>
|
{% trans "If needed, you can contact us by mail." %}
|
||||||
</form>
|
</div>
|
||||||
|
{% else %}
|
||||||
<div id="student_registration_form" class="d-none">
|
<form method="post">
|
||||||
{{ student_registration_form|crispy }}
|
{% csrf_token %}
|
||||||
</div>
|
{{ form|crispy }}
|
||||||
<div id="coach_registration_form" class="d-none">
|
<div id="registration_form"></div>
|
||||||
{{ coach_registration_form|crispy }}
|
|
||||||
</div>
|
<div class="py-2 text-muted">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
{% trans "By registering, you certify that you have read and accepted our" %}
|
||||||
|
<a href="{% url 'about' %}#politique-confidentialite">{% trans "privacy policy" %}</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-success" type="submit">
|
||||||
|
{% trans "Sign up" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="student_registration_form" class="d-none">
|
||||||
|
{{ student_registration_form|crispy }}
|
||||||
|
</div>
|
||||||
|
<div id="coach_registration_form" class="d-none">
|
||||||
|
{{ coach_registration_form|crispy }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
% Specials
|
% Specials
|
||||||
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
||||||
|
\newcommand{\cdt}{\kern-0.5pt\ensuremath\cdot\kern-0.5pt}
|
||||||
|
|
||||||
% Page formating
|
% Page formating
|
||||||
\hoffset -1in
|
\hoffset -1in
|
||||||
@ -37,7 +38,7 @@
|
|||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
|
|
||||||
\includegraphics[height=2cm]{/code/static/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
|
\includegraphics[height=2cm]{/code/static/tfjm/img/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
|
||||||
|
|
||||||
\vfill
|
\vfill
|
||||||
|
|
||||||
@ -56,19 +57,23 @@ Autorisation d'enregistrement et de diffusion de l'image ({{ tournament.name }})
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Je soussign\'e {{ registration|safe|default:"\dotfill" }}\\
|
Je soussign\'e\cdt{}e {{ registration|safe|default:"\dotfill" }}\\
|
||||||
demeurant au {{ registration.address|safe|default:"\dotfill" }}
|
demeurant au {{ registration.address|safe|default:"\dotfill" }}
|
||||||
|
|
||||||
\medskip
|
\medskip
|
||||||
Cochez la/les cases correspondantes.\\
|
Cochez la/les cases correspondantes.\\
|
||||||
\medskip
|
\medskip
|
||||||
|
|
||||||
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$ de {{ tournament.name }}
|
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$
|
||||||
du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }}, \`a me photographier ou \`a me
|
{% if tournament.unified_registration %} dans
|
||||||
filmer et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion sur son site et sur les sites
|
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)
|
||||||
partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit d’utiliser mon image sur tous ses supports
|
{% else %} de
|
||||||
d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la pr\'esente, cessionnaire des droits
|
{{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }},
|
||||||
pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de ces photographies.\\
|
{% endif %} \`a
|
||||||
|
me photographier ou \`a me filmer et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion
|
||||||
|
sur son site et sur les sites partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit d’utiliser mon
|
||||||
|
image sur tous ses supports d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la pr\'esente,
|
||||||
|
cessionnaire des droits pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de ces photographies.\\
|
||||||
|
|
||||||
\medskip
|
\medskip
|
||||||
Animath s'engage, conform\'ement aux dispositions l\'egales en vigueur relatives au droit \`a l'image, \`a ce que la
|
Animath s'engage, conform\'ement aux dispositions l\'egales en vigueur relatives au droit \`a l'image, \`a ce que la
|
||||||
@ -98,7 +103,7 @@ Animath, IHP, 11 rue Pierre et Marie Curie, 75231 Paris cedex 05.\\
|
|||||||
|
|
||||||
\bigskip
|
\bigskip
|
||||||
|
|
||||||
Signature pr\'ec\'ed\'ee de la mention \og lu et approuv\'e \fg{}
|
Signature pr\'ec\'ed\'ee de la mention « lu et approuv\'e »
|
||||||
|
|
||||||
\medskip
|
\medskip
|
||||||
|
|
||||||
@ -106,7 +111,7 @@ Signature pr\'ec\'ed\'ee de la mention \og lu et approuv\'e \fg{}
|
|||||||
|
|
||||||
\begin{minipage}[c]{0.5\textwidth}
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
|
||||||
\underline{Le participant :}\\
|
\underline{La/le participant\cdt{}e :}\\
|
||||||
|
|
||||||
Fait \`a :\\
|
Fait \`a :\\
|
||||||
le
|
le
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
% Specials
|
% Specials
|
||||||
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
||||||
|
\newcommand{\cdt}{\kern-0.5pt\ensuremath\cdot\kern-0.5pt}
|
||||||
|
|
||||||
% Page formating
|
% Page formating
|
||||||
\hoffset -1in
|
\hoffset -1in
|
||||||
@ -37,7 +38,7 @@
|
|||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
|
|
||||||
\includegraphics[height=2cm]{/code/static/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
|
\includegraphics[height=2cm]{/code/static/tfjm/img/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
|
||||||
|
|
||||||
\vfill
|
\vfill
|
||||||
|
|
||||||
@ -57,20 +58,25 @@ Autorisation d'enregistrement et de diffusion de l'image
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Je soussign\'e \dotfill (p\`ere, m\`ere, responsable l\'egal) \\
|
Je soussign\'e\cdt{}e \dotfill (p\`ere, m\`ere, responsable l\'egal) \\
|
||||||
agissant en qualit\'e de repr\'esentant de {{ registration|safe|default:"\dotfill" }}\\
|
agissant en qualit\'e de repr\'esentant\cdt{}e de {{ registration|safe|default:"\dotfill" }}\\
|
||||||
demeurant au {{ registration.address|safe|default:"\dotfill" }}
|
demeurant au {{ registration.address|safe|default:"\dotfill" }}
|
||||||
|
|
||||||
\medskip
|
\medskip
|
||||||
Cochez la/les cases correspondantes.\\
|
Cochez la/les cases correspondantes.\\
|
||||||
\medskip
|
\medskip
|
||||||
|
|
||||||
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$ de {{ tournament.name }}
|
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$
|
||||||
du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }}, \`a photographier ou \`a filmer
|
{% if tournament.unified_registration %} dans
|
||||||
l'enfant et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion sur son site et sur les sites
|
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)
|
||||||
partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit d’utiliser l'image de l'enfant sur tous ses
|
{% else %} de
|
||||||
supports d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la pr\'esente, cessionnaire des
|
{{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }},
|
||||||
droits pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de ces photographies.\\
|
{% endif %} \`a
|
||||||
|
photographier ou \`a filmer l'enfant et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion
|
||||||
|
sur son site et sur les sites partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit d’utiliser l'image
|
||||||
|
de l'enfant sur tous ses supports d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la
|
||||||
|
pr\'esente, cessionnaire des droits pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de
|
||||||
|
ces photographies.\\
|
||||||
|
|
||||||
\medskip
|
\medskip
|
||||||
Animath s'engage, conform\'ement aux dispositions l\'egales en vigueur relatives au droit \`a l'image, \`a ce que la
|
Animath s'engage, conform\'ement aux dispositions l\'egales en vigueur relatives au droit \`a l'image, \`a ce que la
|
||||||
@ -100,14 +106,14 @@ Animath, IHP, 11 rue Pierre et Marie Curie, 75231 Paris cedex 05.\\
|
|||||||
|
|
||||||
\bigskip
|
\bigskip
|
||||||
|
|
||||||
Signatures pr\'ec\'ed\'ees de la mention \og lu et approuv\'e \fg{}
|
Signatures pr\'ec\'ed\'ees de la mention « lu et approuv\'e »
|
||||||
|
|
||||||
\medskip
|
\medskip
|
||||||
|
|
||||||
|
|
||||||
\begin{minipage}[c]{0.5\textwidth}
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
|
||||||
\underline{Le responsable l\'egal :}\\
|
\underline{La/le responsable l\'egal\cdt{}e :}\\
|
||||||
|
|
||||||
Fait \`a :\\
|
Fait \`a :\\
|
||||||
le :
|
le :
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
% Specials
|
% Specials
|
||||||
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
||||||
|
\newcommand{\cdt}{\kern-0.5pt\ensuremath\cdot\kern-0.5pt}
|
||||||
|
|
||||||
% Page formating
|
% Page formating
|
||||||
\hoffset -1in
|
\hoffset -1in
|
||||||
@ -37,7 +38,7 @@
|
|||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
|
|
||||||
\includegraphics[height=2cm]{/code/static/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
|
\includegraphics[height=2cm]{/code/static/tfjm/img/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
|
||||||
|
|
||||||
\vfill
|
\vfill
|
||||||
|
|
||||||
@ -45,22 +46,39 @@
|
|||||||
\Large \bf Autorisation parentale pour les mineurs ({{ tournament.name }})
|
\Large \bf Autorisation parentale pour les mineurs ({{ tournament.name }})
|
||||||
\end{center}
|
\end{center}
|
||||||
|
|
||||||
Je soussigné(e) \hrulefill,\\
|
Je soussigné\cdt{}e \hrulefill,\\
|
||||||
responsable légal, demeurant \writingsep\hrulefill\\
|
responsable légal\cdt{}e, demeurant \writingsep\hrulefill\\
|
||||||
\writingsep\hrulefill,\\
|
\writingsep\hrulefill,\\
|
||||||
\writingsep autorise {{ registration|default:"\hrulefill" }},\\
|
\writingsep autorise {{ registration|default:"\hrulefill" }},\\
|
||||||
né(e) le {{ registration.birth_date }},
|
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$) organisé \`a :
|
à 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.
|
||||||
|
\end{itemize}
|
||||||
|
{% else %}
|
||||||
|
organisé \`a :
|
||||||
{{ tournament.place }}, du {{ tournament.date_start }} au {{ tournament.date_end }}.
|
{{ tournament.place }}, du {{ tournament.date_start }} au {{ tournament.date_end }}.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
Iel se rendra au lieu indiqu\'e ci-dessus le samedi matin et quittera les lieux l'après-midi du dimanche par
|
Iel se rendra au lieu indiqu\'e ci-dessus le samedi matin et quittera les lieux l'après-midi du dimanche par
|
||||||
ses propres moyens et sous la responsabilité du représentant légal.
|
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.
|
||||||
|
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 %}
|
||||||
|
|
||||||
\vspace{8ex}
|
\vspace{8ex}
|
||||||
|
|
||||||
Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{% now "Y" %},
|
Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{% now "Y" %}
|
||||||
|
|
||||||
|
\vspace{4ex}
|
||||||
|
|
||||||
|
Signature :
|
||||||
|
|
||||||
\vfill
|
\vfill
|
||||||
\vfill
|
\vfill
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
\documentclass[a4paper,11pt]{article}
|
||||||
|
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage{lmodern}
|
||||||
|
\usepackage[english]{babel}
|
||||||
|
|
||||||
|
\usepackage{fancyhdr}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
%\usepackage{anyfontsize}
|
||||||
|
\usepackage{fancybox}
|
||||||
|
\usepackage{eso-pic,graphicx}
|
||||||
|
\usepackage{xcolor}
|
||||||
|
|
||||||
|
|
||||||
|
% Specials
|
||||||
|
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
||||||
|
|
||||||
|
% Page formating
|
||||||
|
\hoffset -1in
|
||||||
|
\voffset -1in
|
||||||
|
\textwidth 180 mm
|
||||||
|
\textheight 250 mm
|
||||||
|
\oddsidemargin 15mm
|
||||||
|
\evensidemargin 15mm
|
||||||
|
\pagestyle{fancy}
|
||||||
|
|
||||||
|
% Headers and footers
|
||||||
|
\fancyfoot{}
|
||||||
|
\lhead{}
|
||||||
|
\rhead{}
|
||||||
|
\renewcommand{\headrulewidth}{0pt}
|
||||||
|
% \lfoot{\footnotesize Address}
|
||||||
|
% \rfoot{\footnotesize todo association}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\includegraphics[height=2cm]{/code/static/tfjm/img/eteam.png}\hfill{\fontsize{55pt}{55pt}ETEAM Tournament}
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
\begin{center}
|
||||||
|
\Large \bf Parental authorisation for minors
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
I, \hrulefill,\\
|
||||||
|
legal representative, residing at \writingsep\hrulefill\\
|
||||||
|
\writingsep\hrulefill,\\
|
||||||
|
\writingsep autorise {{ registration|default:"\hrulefill" }},\\
|
||||||
|
born on {{ registration.birth_date }},
|
||||||
|
to participate in the European Tournament of Enthusiastic Apprentice Mathematicians (ETEAM) organised in:
|
||||||
|
{{ tournament.place }}, from {{ tournament.date_start }} to {{ tournament.date_end }}.
|
||||||
|
|
||||||
|
The participant will travel to the abovementioned location on Monday morning and will leave the premises on Friday afternoon by independant means and under the responsibility of the legal representative.
|
||||||
|
|
||||||
|
|
||||||
|
\vspace{8ex}
|
||||||
|
|
||||||
|
Signature:
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
\end{document}
|
@ -0,0 +1,112 @@
|
|||||||
|
\documentclass[a4paper,11pt]{article}
|
||||||
|
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage{lmodern}
|
||||||
|
\usepackage[english]{babel}
|
||||||
|
|
||||||
|
\usepackage{fancyhdr}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
%\usepackage{anyfontsize}
|
||||||
|
\usepackage{fancybox}
|
||||||
|
\usepackage{eso-pic,graphicx}
|
||||||
|
\usepackage{xcolor}
|
||||||
|
|
||||||
|
|
||||||
|
% Specials
|
||||||
|
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
||||||
|
|
||||||
|
% Page formating
|
||||||
|
\hoffset -1in
|
||||||
|
\voffset -1in
|
||||||
|
\textwidth 180 mm
|
||||||
|
\textheight 250 mm
|
||||||
|
\oddsidemargin 15mm
|
||||||
|
\evensidemargin 15mm
|
||||||
|
\pagestyle{fancy}
|
||||||
|
|
||||||
|
% Headers and footers
|
||||||
|
\fancyfoot{}
|
||||||
|
\lhead{}
|
||||||
|
\rhead{}
|
||||||
|
\renewcommand{\headrulewidth}{0pt}
|
||||||
|
%\lfoot{\footnotesize Address}
|
||||||
|
%\rfoot{\footnotesize todo association}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\includegraphics[height=2cm]{/code/static/tfjm/img/eteam.png}\hfill{\fontsize{55pt}{55pt}{ETEAM Tournament}}
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
\begin{center}
|
||||||
|
|
||||||
|
|
||||||
|
\LARGE
|
||||||
|
Video and interview consent and release form
|
||||||
|
\end{center}
|
||||||
|
\normalsize
|
||||||
|
|
||||||
|
|
||||||
|
\thispagestyle{empty}
|
||||||
|
|
||||||
|
\bigskip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
I, {{ registration|safe|default:"\dotfill" }}\\
|
||||||
|
residing at {{ registration.address|safe|default:"\dotfill" }} {{ registration.zip_code|safe|default:"" }} {{ registration.city|safe|default:"" }}
|
||||||
|
{{ registration.country|safe|default:"" }},\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
Tick the appropriate box(es).\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
|
||||||
|
\fbox{\textcolor{white}{A}} Authorise the ETEAM organizers, for the ETEAM tournament from {{ tournament.date_start }} to {{ tournament.date_end }} in: {{ tournament.place }}, to photograph or film me and to distribute the photos and/or videos taken on this occasion on its website and on partner websites. I hereby grant ETEAM the right to use my image free of charge on all its information media: brochures, websites, social networks. ETEAM hereby becomes the assignee of the rights for these photographs. There is no time limit on the validity of this release nor are there any geographic limitations on where these materials may be distributed.\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
ETEAM commits itself, in accordance with the legal regulations in force relating to image rights, to ensuring that the publication and distribution of the image as well as the accompanying comments do not infringe on the private life, dignity and reputation of the person photographed.\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
\fbox{\textcolor{white}{A}} Authorise the broadcasting in the media (Press, Television, Internet) of photographs taken during any media coverage of this event.\\
|
||||||
|
\medskip
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
\fbox{\textcolor{white}{A}} By signing this form, I acknowledge that I have completely read and fully understand the above consent and release and agree to be bound thereby. I hereby release any and all claims against any person or organisation utilising this material for marketing, educational, promotional, and/or any other lawful purpose whatsoever.\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
\fbox{\textcolor{white}{A}} I agree to be kept informed of other activities organised by ETEAM and its partners.\\
|
||||||
|
\bigskip
|
||||||
|
|
||||||
|
Signature preceded by the words "read and approved"
|
||||||
|
\medskip
|
||||||
|
|
||||||
|
|
||||||
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
|
||||||
|
\underline{Legal representative:}\\
|
||||||
|
|
||||||
|
\end{minipage}
|
||||||
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
|
||||||
|
\underline{The participant:}\\
|
||||||
|
|
||||||
|
|
||||||
|
\end{minipage}
|
||||||
|
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
\vfill
|
||||||
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
% \footnotesize Address
|
||||||
|
\end{minipage}
|
||||||
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
\footnotesize
|
||||||
|
% \begin{flushright}
|
||||||
|
% todo association
|
||||||
|
% \end{flushright}
|
||||||
|
\end{minipage}
|
||||||
|
\end{document}
|
@ -0,0 +1,112 @@
|
|||||||
|
\documentclass[a4paper,11pt]{article}
|
||||||
|
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage{lmodern}
|
||||||
|
\usepackage[english]{babel}
|
||||||
|
|
||||||
|
\usepackage{fancyhdr}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
%\usepackage{anyfontsize}
|
||||||
|
\usepackage{fancybox}
|
||||||
|
\usepackage{eso-pic,graphicx}
|
||||||
|
\usepackage{xcolor}
|
||||||
|
|
||||||
|
|
||||||
|
% Specials
|
||||||
|
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
|
||||||
|
|
||||||
|
% Page formating
|
||||||
|
\hoffset -1in
|
||||||
|
\voffset -1in
|
||||||
|
\textwidth 180 mm
|
||||||
|
\textheight 250 mm
|
||||||
|
\oddsidemargin 15mm
|
||||||
|
\evensidemargin 15mm
|
||||||
|
\pagestyle{fancy}
|
||||||
|
|
||||||
|
% Headers and footers
|
||||||
|
\fancyfoot{}
|
||||||
|
\lhead{}
|
||||||
|
\rhead{}
|
||||||
|
\renewcommand{\headrulewidth}{0pt}
|
||||||
|
%\lfoot{\footnotesize Address}
|
||||||
|
%\rfoot{\footnotesize todo association}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\includegraphics[height=2cm]{/code/static/tfjm/img/eteam.png}\hfill{\fontsize{55pt}{55pt}{ETEAM Tournament}}
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
\begin{center}
|
||||||
|
|
||||||
|
|
||||||
|
\LARGE
|
||||||
|
Video and interview consent and release form
|
||||||
|
\end{center}
|
||||||
|
\normalsize
|
||||||
|
|
||||||
|
|
||||||
|
\thispagestyle{empty}
|
||||||
|
|
||||||
|
\bigskip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
I, {{ registration|safe|default:"\dotfill" }}\\
|
||||||
|
residing at {{ registration.address|safe|default:"\dotfill" }} {{ registration.zip_code|safe|default:"" }} {{ registration.city|safe|default:"" }}
|
||||||
|
{{ registration.country|safe|default:"" }},\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
Tick the appropriate box(es).\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
|
||||||
|
\fbox{\textcolor{white}{A}} Authorise the ETEAM organizers, for the ETEAM tournament from {{ tournament.date_start }} to {{ tournament.date_end }} in: {{ tournament.place }}, to photograph or film me and to distribute the photos and/or videos taken on this occasion on its website and on partner websites. I hereby grant ETEAM the right to use my image free of charge on all its information media: brochures, websites, social networks. ETEAM hereby becomes the assignee of the rights for these photographs. There is no time limit on the validity of this release nor are there any geographic limitations on where these materials may be distributed.\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
ETEAM commits itself, in accordance with the legal regulations in force relating to image rights, to ensuring that the publication and distribution of the image as well as the accompanying comments do not infringe on the private life, dignity and reputation of the person photographed.\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
\fbox{\textcolor{white}{A}} Authorise the broadcasting in the media (Press, Television, Internet) of photographs taken during any media coverage of this event.\\
|
||||||
|
\medskip
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
\fbox{\textcolor{white}{A}} By signing this form, I acknowledge that I have completely read and fully understand the above consent and release and agree to be bound thereby. I hereby release any and all claims against any person or organisation utilising this material for marketing, educational, promotional, and/or any other lawful purpose whatsoever.\\
|
||||||
|
|
||||||
|
\medskip
|
||||||
|
\fbox{\textcolor{white}{A}} I agree to be kept informed of other activities organised by ETEAM and its partners.\\
|
||||||
|
\bigskip
|
||||||
|
|
||||||
|
Signature preceded by the words "read and approved"
|
||||||
|
\medskip
|
||||||
|
|
||||||
|
|
||||||
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
|
||||||
|
\underline{Legal representative:}\\
|
||||||
|
|
||||||
|
\end{minipage}
|
||||||
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
|
||||||
|
\underline{The participant:}\\
|
||||||
|
|
||||||
|
|
||||||
|
\end{minipage}
|
||||||
|
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
\vfill
|
||||||
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
% \footnotesize Address
|
||||||
|
\end{minipage}
|
||||||
|
\begin{minipage}[c]{0.5\textwidth}
|
||||||
|
\footnotesize
|
||||||
|
% \begin{flushright}
|
||||||
|
% todo association
|
||||||
|
% \end{flushright}
|
||||||
|
\end{minipage}
|
||||||
|
\end{document}
|
@ -48,7 +48,10 @@
|
|||||||
<dd class="col-sm-6">{{ user_object.registration.get_gender_display }}</dd>
|
<dd class="col-sm-6">{{ user_object.registration.get_gender_display }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Address:" %}</dt>
|
<dt class="col-sm-6 text-sm-end">{% trans "Address:" %}</dt>
|
||||||
<dd class="col-sm-6">{{ user_object.registration.address }}, {{ user_object.registration.zip_code|stringformat:'05d' }} {{ user_object.registration.city }}</dd>
|
<dd class="col-sm-6">
|
||||||
|
{{ user_object.registration.address }},
|
||||||
|
{{ user_object.registration.zip_code|stringformat:'05d' }} {{ user_object.registration.city }} ({{ user_object.registration.country }})
|
||||||
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Phone number:" %}</dt>
|
<dt class="col-sm-6 text-sm-end">{% trans "Phone number:" %}</dt>
|
||||||
<dd class="col-sm-6">{{ user_object.registration.phone_number }}</dd>
|
<dd class="col-sm-6">{{ user_object.registration.phone_number }}</dd>
|
||||||
@ -86,25 +89,29 @@
|
|||||||
|
|
||||||
{% if user_object.registration.studentregistration %}
|
{% if user_object.registration.studentregistration %}
|
||||||
{% if user_object.registration.under_18 and user_object.registration.team.participation.tournament and not user_object.registration.team.participation.tournament.remote %}
|
{% if user_object.registration.under_18 and user_object.registration.team.participation.tournament and not user_object.registration.team.participation.tournament.remote %}
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Health sheet:" %}</dt>
|
{% if TFJM.HEALTH_SHEET_REQUIRED %}
|
||||||
<dd class="col-sm-6">
|
<dt class="col-sm-6 text-sm-end">{% trans "Health sheet:" %}</dt>
|
||||||
{% if user_object.registration.health_sheet %}
|
<dd class="col-sm-6">
|
||||||
<a href="{{ user_object.registration.health_sheet.url }}">{% trans "Download" %}</a>
|
{% if user_object.registration.health_sheet %}
|
||||||
{% endif %}
|
<a href="{{ user_object.registration.health_sheet.url }}">{% trans "Download" %}</a>
|
||||||
{% if user_object.registration.team and not user_object.registration.team.participation.valid %}
|
{% endif %}
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadHealthSheetModal">{% trans "Replace" %}</button>
|
{% if user_object.registration.team and not user_object.registration.team.participation.valid %}
|
||||||
{% endif %}
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadHealthSheetModal">{% trans "Replace" %}</button>
|
||||||
</dd>
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Vaccine sheet:" %}</dt>
|
{% if TFJM.VACCINE_SHEET_REQUIRED %}
|
||||||
<dd class="col-sm-6">
|
<dt class="col-sm-6 text-sm-end">{% trans "Vaccine sheet:" %}</dt>
|
||||||
{% if user_object.registration.vaccine_sheet %}
|
<dd class="col-sm-6">
|
||||||
<a href="{{ user_object.registration.vaccine_sheet.url }}">{% trans "Download" %}</a>
|
{% if user_object.registration.vaccine_sheet %}
|
||||||
{% endif %}
|
<a href="{{ user_object.registration.vaccine_sheet.url }}">{% trans "Download" %}</a>
|
||||||
{% if user_object.registration.team and not user_object.registration.team.participation.valid %}
|
{% endif %}
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadVaccineSheetModal">{% trans "Replace" %}</button>
|
{% if user_object.registration.team and not user_object.registration.team.participation.valid %}
|
||||||
{% endif %}
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadVaccineSheetModal">{% trans "Replace" %}</button>
|
||||||
</dd>
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Parental authorization:" %}</dt>
|
<dt class="col-sm-6 text-sm-end">{% trans "Parental authorization:" %}</dt>
|
||||||
<dd class="col-sm-6">
|
<dd class="col-sm-6">
|
||||||
@ -158,11 +165,13 @@
|
|||||||
<dd class="col-sm-6">{{ user_object.registration.is_admin|yesno }}</dd>
|
<dd class="col-sm-6">{{ user_object.registration.is_admin|yesno }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans "Grant Animath to contact me in the future about other actions:" %}</dt>
|
{% if TFJM.SUGGEST_ANIMATH %}
|
||||||
<dd class="col-sm-6">{{ user_object.registration.give_contact_to_animath|yesno }}</dd>
|
<dt class="col-sm-6 text-sm-end">{% trans "Grant Animath to contact me in the future about other actions:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ user_object.registration.give_contact_to_animath|yesno }}</dd>
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% if user_object.registration.participates and user_object.registration.team.participation.valid %}
|
{% if TFJM.PAYMENT_MANAGEMENT and user_object.registration.participates and user_object.registration.team.participation.valid %}
|
||||||
<hr>
|
<hr>
|
||||||
{% for payment in user_object.registration.payments.all %}
|
{% for payment in user_object.registration.payments.all %}
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@ -223,15 +232,19 @@
|
|||||||
{% include "base_modal.html" with modal_id="uploadPhotoAuthorization" modal_enctype="multipart/form-data" %}
|
{% include "base_modal.html" with modal_id="uploadPhotoAuthorization" modal_enctype="multipart/form-data" %}
|
||||||
|
|
||||||
{% if user_object.registration.under_18 %}
|
{% if user_object.registration.under_18 %}
|
||||||
{% trans "Upload health sheet" as modal_title %}
|
{% if TFJM.HEALTH_SHEET_REQUIRED %}
|
||||||
{% trans "Upload" as modal_button %}
|
{% trans "Upload health sheet" as modal_title %}
|
||||||
{% url "registration:upload_user_health_sheet" pk=user_object.registration.pk as modal_action %}
|
{% trans "Upload" as modal_button %}
|
||||||
{% include "base_modal.html" with modal_id="uploadHealthSheet" modal_enctype="multipart/form-data" %}
|
{% url "registration:upload_user_health_sheet" pk=user_object.registration.pk as modal_action %}
|
||||||
|
{% include "base_modal.html" with modal_id="uploadHealthSheet" modal_enctype="multipart/form-data" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% trans "Upload vaccine sheet" as modal_title %}
|
{% if TFJM.VACCINE_SHEET_REQUIRED %}
|
||||||
{% trans "Upload" as modal_button %}
|
{% trans "Upload vaccine sheet" as modal_title %}
|
||||||
{% url "registration:upload_user_vaccine_sheet" pk=user_object.registration.pk as modal_action %}
|
{% trans "Upload" as modal_button %}
|
||||||
{% include "base_modal.html" with modal_id="uploadVaccineSheet" modal_enctype="multipart/form-data" %}
|
{% url "registration:upload_user_vaccine_sheet" pk=user_object.registration.pk as modal_action %}
|
||||||
|
{% include "base_modal.html" with modal_id="uploadVaccineSheet" modal_enctype="multipart/form-data" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% trans "Upload parental authorization" as modal_title %}
|
{% trans "Upload parental authorization" as modal_title %}
|
||||||
{% trans "Upload" as modal_button %}
|
{% trans "Upload" as modal_button %}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{{ object.user.last_name }}
|
|
||||||
{{ object.user.first_name }}
|
|
||||||
{{ object.user.email }}
|
|
||||||
{{ object.type }}
|
|
||||||
{{ object.role }}
|
|
@ -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 }}
|
|
||||||
|
@ -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 }}
|
|
||||||
|
@ -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 }}
|
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
# 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 datetime import timedelta
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import TestCase
|
from django.test import override_settings, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
from django.utils.http import urlsafe_base64_encode
|
from django.utils.http import urlsafe_base64_encode
|
||||||
from participation.models import Team
|
from participation.models import Team
|
||||||
@ -114,6 +117,9 @@ class TestRegistration(TestCase):
|
|||||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||||
str(self.coach.registration.get_absolute_url()), 302, 200)
|
str(self.coach.registration.get_absolute_url()), 302, 200)
|
||||||
|
|
||||||
|
# Ensure that we are between registration dates
|
||||||
|
@override_settings(REGISTRATION_DATES={'open': timezone.now() - timedelta(days=1),
|
||||||
|
'close': timezone.now() + timedelta(days=1)})
|
||||||
def test_registration(self):
|
def test_registration(self):
|
||||||
"""
|
"""
|
||||||
Ensure that the signup form is working successfully.
|
Ensure that the signup form is working successfully.
|
||||||
@ -146,6 +152,7 @@ class TestRegistration(TestCase):
|
|||||||
address="1 Rue de Rivoli",
|
address="1 Rue de Rivoli",
|
||||||
zip_code=75001,
|
zip_code=75001,
|
||||||
city="Paris",
|
city="Paris",
|
||||||
|
country="France",
|
||||||
phone_number="0123456789",
|
phone_number="0123456789",
|
||||||
responsible_name="Toto",
|
responsible_name="Toto",
|
||||||
responsible_phone="0123456789",
|
responsible_phone="0123456789",
|
||||||
@ -194,6 +201,7 @@ class TestRegistration(TestCase):
|
|||||||
address="1 Rue de Rivoli",
|
address="1 Rue de Rivoli",
|
||||||
zip_code=75001,
|
zip_code=75001,
|
||||||
city="Paris",
|
city="Paris",
|
||||||
|
country="France",
|
||||||
phone_number="0123456789",
|
phone_number="0123456789",
|
||||||
professional_activity="God",
|
professional_activity="God",
|
||||||
last_degree="Master",
|
last_degree="Master",
|
||||||
@ -221,6 +229,52 @@ class TestRegistration(TestCase):
|
|||||||
response = self.client.get(reverse("registration:email_validation_resend", args=(user.pk,)))
|
response = self.client.get(reverse("registration:email_validation_resend", args=(user.pk,)))
|
||||||
self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200)
|
self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200)
|
||||||
|
|
||||||
|
def test_registration_dates(self):
|
||||||
|
"""
|
||||||
|
Test that registrations are working only between registration dates.
|
||||||
|
"""
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
# Test that registration between open and close dates are working
|
||||||
|
with override_settings(REGISTRATION_DATES={'open': timezone.now() - timedelta(days=2),
|
||||||
|
'close': timezone.now() + timedelta(days=2)}):
|
||||||
|
response = self.client.get(reverse("registration:signup"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn("<i class=\"fas fa-user-plus\"></i> Register", response.content.decode())
|
||||||
|
self.assertNotIn("registrations are not opened", response.content.decode())
|
||||||
|
self.assertNotIn("Registrations are closed", response.content.decode())
|
||||||
|
|
||||||
|
response = self.client.post(reverse("registration:signup"))
|
||||||
|
self.assertFormError(response.context['form'], None, [])
|
||||||
|
|
||||||
|
# Test that registration before open date is not working
|
||||||
|
with override_settings(REGISTRATION_DATES={'open': timezone.now() + timedelta(days=1),
|
||||||
|
'close': timezone.now() + timedelta(days=2)}):
|
||||||
|
response = self.client.get(reverse("registration:signup"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotIn("<i class=\"fas fa-user-plus\"></i> Register", response.content.decode())
|
||||||
|
self.assertIn("registrations are not opened", response.content.decode())
|
||||||
|
|
||||||
|
response = self.client.post(reverse("registration:signup"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFormError(response.context['form'], None,
|
||||||
|
"Registrations are not opened yet. They will open on the "
|
||||||
|
f"{settings.REGISTRATION_DATES['open']:%Y-%m-%d %H:%M}.")
|
||||||
|
|
||||||
|
# Test that registration after close date is not working
|
||||||
|
with override_settings(REGISTRATION_DATES={'open': timezone.now() - timedelta(days=2),
|
||||||
|
'close': timezone.now() - timedelta(days=1)}):
|
||||||
|
response = self.client.get(reverse("registration:signup"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotIn("<i class=\"fas fa-user-plus\"></i> Register", response.content.decode())
|
||||||
|
self.assertIn("Registrations are closed", response.content.decode())
|
||||||
|
|
||||||
|
response = self.client.post(reverse("registration:signup"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFormError(response.context['form'], None,
|
||||||
|
"Registrations for this year are closed since "
|
||||||
|
f"{settings.REGISTRATION_DATES['close']:%Y-%m-%d %H:%M}.")
|
||||||
|
|
||||||
def test_login(self):
|
def test_login(self):
|
||||||
"""
|
"""
|
||||||
With a registered user, try to log in
|
With a registered user, try to log in
|
||||||
@ -274,11 +328,13 @@ class TestRegistration(TestCase):
|
|||||||
for user, data in [(self.user, dict(professional_activity="Bot", admin=True)),
|
for user, data in [(self.user, dict(professional_activity="Bot", admin=True)),
|
||||||
(self.student, dict(student_class=11, school="Sky", birth_date="2001-01-01",
|
(self.student, dict(student_class=11, school="Sky", birth_date="2001-01-01",
|
||||||
gender="female", address="1 Rue de Rivoli", zip_code=75001,
|
gender="female", address="1 Rue de Rivoli", zip_code=75001,
|
||||||
city="Paris", responsible_name="Toto",
|
city="Paris", country="France",
|
||||||
|
responsible_name="Toto",
|
||||||
responsible_phone="0123456789",
|
responsible_phone="0123456789",
|
||||||
responsible_email="toto@example.com")),
|
responsible_email="toto@example.com")),
|
||||||
(self.coach, dict(professional_activity="God", last_degree="Médaille Fields", gender="male",
|
(self.coach, dict(professional_activity="God", last_degree="Médaille Fields", gender="male",
|
||||||
address="1 Rue de Rivoli", zip_code=75001, city="Paris"))]:
|
address="1 Rue de Rivoli", zip_code=75001,
|
||||||
|
city="Paris", country="France"))]:
|
||||||
response = self.client.get(reverse("registration:update_user", args=(user.pk,)))
|
response = self.client.get(reverse("registration:update_user", args=(user.pk,)))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -333,7 +389,7 @@ class TestRegistration(TestCase):
|
|||||||
|
|
||||||
response = self.client.post(reverse(f"registration:upload_user_{auth_type}",
|
response = self.client.post(reverse(f"registration:upload_user_{auth_type}",
|
||||||
args=(self.student.registration.pk,)), data={
|
args=(self.student.registration.pk,)), data={
|
||||||
auth_type: open("tfjm/static/Fiche_sanitaire.pdf", "rb"),
|
auth_type: open("tfjm/static/tfjm/Fiche_sanitaire.pdf", "rb"),
|
||||||
})
|
})
|
||||||
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
||||||
|
|
||||||
@ -356,7 +412,7 @@ class TestRegistration(TestCase):
|
|||||||
old_authoratization = self.student.registration.photo_authorization.path
|
old_authoratization = self.student.registration.photo_authorization.path
|
||||||
response = self.client.post(reverse("registration:upload_user_photo_authorization",
|
response = self.client.post(reverse("registration:upload_user_photo_authorization",
|
||||||
args=(self.student.registration.pk,)), data=dict(
|
args=(self.student.registration.pk,)), data=dict(
|
||||||
photo_authorization=open("tfjm/static/Fiche_sanitaire.pdf", "rb"),
|
photo_authorization=open("tfjm/static/tfjm/Fiche_sanitaire.pdf", "rb"),
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
||||||
self.assertFalse(os.path.isfile(old_authoratization))
|
self.assertFalse(os.path.isfile(old_authoratization))
|
||||||
|
@ -26,7 +26,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View
|
from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from magic import Magic
|
from magic import Magic
|
||||||
from participation.models import Passage, Solution, Synthesis, Tournament
|
from participation.models import Passage, Solution, Tournament, WrittenReview
|
||||||
from tfjm.tokens import email_validation_token
|
from tfjm.tokens import email_validation_token
|
||||||
from tfjm.views import UserMixin, UserRegistrationMixin, VolunteerMixin
|
from tfjm.views import UserMixin, UserRegistrationMixin, VolunteerMixin
|
||||||
|
|
||||||
@ -60,6 +60,22 @@ class SignupView(CreateView):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form(self, form_class=None):
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
if self.request.method in ("POST", "PUT") \
|
||||||
|
and (not self.request.user.is_authenticated or not self.request.user.registration.is_admin):
|
||||||
|
# Check that registrations are opened
|
||||||
|
now = timezone.now()
|
||||||
|
if now < settings.REGISTRATION_DATES['open']:
|
||||||
|
form.add_error(None, format_lazy(_("Registrations are not opened yet. "
|
||||||
|
"They will open on the {opening_date:%Y-%m-%d %H:%M}."),
|
||||||
|
opening_date=settings.REGISTRATION_DATES['open']))
|
||||||
|
elif now > settings.REGISTRATION_DATES['close']:
|
||||||
|
form.add_error(None, format_lazy(_("Registrations for this year are closed since "
|
||||||
|
"{closing_date:%Y-%m-%d %H:%M}."),
|
||||||
|
closing_date=settings.REGISTRATION_DATES['close']))
|
||||||
|
return form
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
role = form.cleaned_data["role"]
|
role = form.cleaned_data["role"]
|
||||||
@ -121,16 +137,17 @@ class AddOrganizerView(VolunteerMixin, CreateView):
|
|||||||
form.instance.set_password(password)
|
form.instance.set_password(password)
|
||||||
form.instance.save()
|
form.instance.save()
|
||||||
|
|
||||||
subject = "[TFJM²] " + str(_("New TFJM² organizer account"))
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
site = Site.objects.first()
|
subject = f"[{settings.APP_NAME}] " + str(_("New organizer account"))
|
||||||
message = render_to_string('registration/mails/add_organizer.txt', dict(user=registration.user,
|
site = Site.objects.first()
|
||||||
inviter=self.request.user,
|
message = render_to_string('registration/mails/add_organizer.txt', dict(user=registration.user,
|
||||||
password=password,
|
inviter=self.request.user,
|
||||||
domain=site.domain))
|
password=password,
|
||||||
html = render_to_string('registration/mails/add_organizer.html', dict(user=registration.user,
|
domain=site.domain))
|
||||||
inviter=self.request.user,
|
html = render_to_string('registration/mails/add_organizer.html', dict(user=registration.user,
|
||||||
password=password,
|
inviter=self.request.user,
|
||||||
domain=site.domain))
|
password=password,
|
||||||
|
domain=site.domain))
|
||||||
registration.user.email_user(subject, message, html_message=html)
|
registration.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
if registration.is_admin:
|
if registration.is_admin:
|
||||||
@ -436,13 +453,18 @@ class AuthorizationTemplateView(TemplateView):
|
|||||||
if not Tournament.objects.filter(name__iexact=self.request.GET.get("tournament_name")).exists():
|
if not Tournament.objects.filter(name__iexact=self.request.GET.get("tournament_name")).exists():
|
||||||
raise PermissionDenied("Ce tournoi n'existe pas.")
|
raise PermissionDenied("Ce tournoi n'existe pas.")
|
||||||
context["tournament"] = Tournament.objects.get(name__iexact=self.request.GET.get("tournament_name"))
|
context["tournament"] = Tournament.objects.get(name__iexact=self.request.GET.get("tournament_name"))
|
||||||
|
elif settings.SINGLE_TOURNAMENT:
|
||||||
|
# One single tournament (for ETEAM)
|
||||||
|
context["tournament"] = Tournament.objects.first()
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied("Merci d'indiquer un tournoi.")
|
raise PermissionDenied("Merci d'indiquer un tournoi.")
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
def render_to_response(self, context, **response_kwargs):
|
||||||
tex = render_to_string(self.template_name, context=context, request=self.request)
|
template_name = self.get_template_names()[0]
|
||||||
|
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||||
|
tex = render_to_string(template_name, context=context, request=self.request)
|
||||||
temp_dir = mkdtemp()
|
temp_dir = mkdtemp()
|
||||||
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
||||||
f.write(tex)
|
f.write(tex)
|
||||||
@ -451,20 +473,34 @@ class AuthorizationTemplateView(TemplateView):
|
|||||||
process.wait()
|
process.wait()
|
||||||
return FileResponse(open(os.path.join(temp_dir, "texput.pdf"), "rb"),
|
return FileResponse(open(os.path.join(temp_dir, "texput.pdf"), "rb"),
|
||||||
content_type="application/pdf",
|
content_type="application/pdf",
|
||||||
filename=self.template_name.split("/")[-1][:-3] + "pdf")
|
filename=template_name.split("/")[-1][:-3] + "pdf")
|
||||||
|
|
||||||
|
|
||||||
class AdultPhotoAuthorizationTemplateView(AuthorizationTemplateView):
|
class AdultPhotoAuthorizationTemplateView(AuthorizationTemplateView):
|
||||||
template_name = "registration/tex/Autorisation_droit_image_majeur.tex"
|
def get_template_names(self):
|
||||||
|
if settings.TFJM_APP == "TFJM":
|
||||||
|
return ["registration/tex/Autorisation_droit_image_majeur.tex"]
|
||||||
|
elif settings.TFJM_APP == "ETEAM":
|
||||||
|
return ["registration/tex/photo_authorization_eteam_adult.tex"]
|
||||||
|
|
||||||
|
|
||||||
class ChildPhotoAuthorizationTemplateView(AuthorizationTemplateView):
|
class ChildPhotoAuthorizationTemplateView(AuthorizationTemplateView):
|
||||||
template_name = "registration/tex/Autorisation_droit_image_mineur.tex"
|
def get_template_names(self):
|
||||||
|
if settings.TFJM_APP == "TFJM":
|
||||||
|
return ["registration/tex/Autorisation_droit_image_mineur.tex"]
|
||||||
|
elif settings.TFJM_APP == "ETEAM":
|
||||||
|
return ["registration/tex/photo_authorization_eteam_child.tex"]
|
||||||
|
|
||||||
|
|
||||||
class ParentalAuthorizationTemplateView(AuthorizationTemplateView):
|
class ParentalAuthorizationTemplateView(AuthorizationTemplateView):
|
||||||
template_name = "registration/tex/Autorisation_parentale.tex"
|
template_name = "registration/tex/Autorisation_parentale.tex"
|
||||||
|
|
||||||
|
def get_template_names(self):
|
||||||
|
if settings.TFJM_APP == "TFJM":
|
||||||
|
return ["registration/tex/Autorisation_parentale.tex"]
|
||||||
|
elif settings.TFJM_APP == "ETEAM":
|
||||||
|
return ["registration/tex/parental_authorization_eteam.tex"]
|
||||||
|
|
||||||
|
|
||||||
class InstructionsTemplateView(AuthorizationTemplateView):
|
class InstructionsTemplateView(AuthorizationTemplateView):
|
||||||
template_name = "registration/tex/Instructions.tex"
|
template_name = "registration/tex/Instructions.tex"
|
||||||
@ -690,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()):
|
||||||
@ -816,11 +853,12 @@ class SolutionView(LoginRequiredMixin, View):
|
|||||||
raise Http404
|
raise Http404
|
||||||
solution = Solution.objects.get(file__endswith=filename)
|
solution = Solution.objects.get(file__endswith=filename)
|
||||||
user = request.user
|
user = request.user
|
||||||
if user.registration.participates:
|
if user.registration.participates and user.registration.team.participation:
|
||||||
passage_participant_qs = Passage.objects.filter(Q(defender=user.registration.team.participation)
|
passage_participant_qs = Passage.objects.filter(Q(reporter=user.registration.team.participation)
|
||||||
| Q(opponent=user.registration.team.participation)
|
| Q(opponent=user.registration.team.participation)
|
||||||
| Q(reporter=user.registration.team.participation),
|
| Q(reviewer=user.registration.team.participation)
|
||||||
defender=solution.participation,
|
| Q(observer=user.registration.team.participation),
|
||||||
|
reporter=solution.participation,
|
||||||
solution_number=solution.problem)
|
solution_number=solution.problem)
|
||||||
else:
|
else:
|
||||||
passage_participant_qs = Passage.objects.none()
|
passage_participant_qs = Passage.objects.none()
|
||||||
@ -832,12 +870,13 @@ class SolutionView(LoginRequiredMixin, View):
|
|||||||
or user.registration.is_volunteer
|
or user.registration.is_volunteer
|
||||||
and Passage.objects.filter(Q(pool__juries=user.registration)
|
and Passage.objects.filter(Q(pool__juries=user.registration)
|
||||||
| Q(pool__tournament__in=user.registration.organized_tournaments.all()),
|
| Q(pool__tournament__in=user.registration.organized_tournaments.all()),
|
||||||
defender=solution.participation,
|
reporter=solution.participation,
|
||||||
solution_number=solution.problem).exists()
|
solution_number=solution.problem).exists()
|
||||||
or user.registration.participates and user.registration.team
|
or user.registration.participates and user.registration.team
|
||||||
and (solution.participation.team == user.registration.team or
|
and (solution.participation.team == user.registration.team or
|
||||||
any(passage.pool.round == 1
|
any(passage.pool.round == 1
|
||||||
or timezone.now() >= passage.pool.tournament.solutions_available_second_phase
|
or (passage.pool.round == 2 and passage.pool.tournament.solutions_available_second_phase)
|
||||||
|
or (passage.pool.round == 3 and passage.pool.tournament.solutions_available_third_phase)
|
||||||
for passage in passage_participant_qs.all()))):
|
for passage in passage_participant_qs.all()))):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
# Guess mime type of the file
|
# Guess mime type of the file
|
||||||
@ -849,30 +888,30 @@ class SolutionView(LoginRequiredMixin, View):
|
|||||||
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
|
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
|
||||||
|
|
||||||
|
|
||||||
class SynthesisView(LoginRequiredMixin, View):
|
class WrittenReviewView(LoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Display the sent synthesis.
|
Display the sent written reviews.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/syntheses/{filename}"
|
path = f"media/reviews/{filename}"
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise Http404
|
raise Http404
|
||||||
synthesis = Synthesis.objects.get(file__endswith=filename)
|
review = WrittenReview.objects.get(file__endswith=filename)
|
||||||
user = request.user
|
user = request.user
|
||||||
if not (user.registration.is_admin or user.registration.is_volunteer
|
if not (user.registration.is_admin or user.registration.is_volunteer
|
||||||
and (user.registration in synthesis.passage.pool.juries.all()
|
and (user.registration in review.passage.pool.juries.all()
|
||||||
or user.registration in synthesis.passage.pool.tournament.organizers.all()
|
or user.registration in review.passage.pool.tournament.organizers.all()
|
||||||
or user.registration.pools_presided.filter(tournament=synthesis.passage.pool.tournament).exists())
|
or user.registration.pools_presided.filter(tournament=review.passage.pool.tournament).exists())
|
||||||
or user.registration.participates and user.registration.team == synthesis.participation.team):
|
or user.registration.participates and user.registration.team == review.participation.team):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
# Guess mime type of the file
|
# Guess mime type of the file
|
||||||
mime = Magic(mime=True)
|
mime = Magic(mime=True)
|
||||||
mime_type = mime.from_file(path)
|
mime_type = mime.from_file(path)
|
||||||
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
||||||
# Replace file name
|
# Replace file name
|
||||||
true_file_name = str(synthesis) + f".{ext}"
|
true_file_name = str(review) + f".{ext}"
|
||||||
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
|
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,29 +1,28 @@
|
|||||||
channels[daphne]~=4.0.0
|
channels[daphne]~=4.2.2
|
||||||
channels-redis~=4.2.0
|
channels-redis~=4.2.1
|
||||||
crispy-bootstrap5~=2023.10
|
citric~=1.4.0
|
||||||
Django>=5.0.3,<6.0
|
crispy-bootstrap5~=2025.4
|
||||||
django-crispy-forms~=2.1
|
Django>=5.2,<6.0
|
||||||
django-extensions~=3.2.3
|
django-crispy-forms~=2.4
|
||||||
django-filter~=23.5
|
django-filter~=25.1
|
||||||
git+https://github.com/django-haystack/django-haystack.git#v3.3b2
|
django-haystack~=3.3.0
|
||||||
django-mailer~=2.3.1
|
django-mailer~=2.3.2
|
||||||
django-phonenumber-field~=7.3.0
|
django-phonenumber-field~=8.1.0
|
||||||
django-pipeline~=3.1.0
|
django-pipeline~=4.0.0
|
||||||
django-polymorphic~=3.1.0
|
django-polymorphic~=3.1.0
|
||||||
django-tables2~=2.7.0
|
django-tables2~=2.7.5
|
||||||
djangorestframework~=3.14.0
|
djangorestframework~=3.16.0
|
||||||
django-rest-polymorphic~=0.1.10
|
django-rest-polymorphic~=0.1.10
|
||||||
elasticsearch~=7.17.9
|
elasticsearch~=7.17.9
|
||||||
gspread~=6.1.0
|
gspread~=6.2.0
|
||||||
gunicorn~=21.2.0
|
gunicorn~=23.0.0
|
||||||
odfpy~=1.4.1
|
odfpy~=1.4.1
|
||||||
pandas~=2.2.1
|
pandas~=2.2.3
|
||||||
phonenumbers~=8.13.27
|
phonenumbers~=9.0.3
|
||||||
psycopg2-binary~=2.9.9
|
psycopg~=3.2.6
|
||||||
pypdf~=3.17.4
|
pypdf~=5.4.0
|
||||||
ipython~=8.20.0
|
|
||||||
python-magic~=0.4.27
|
python-magic~=0.4.27
|
||||||
requests~=2.31.0
|
requests~=2.32.3
|
||||||
sympasoap~=1.1
|
sympasoap~=1.1
|
||||||
uvicorn~=0.25.0
|
uvicorn~=0.34.2
|
||||||
websockets~=12.0
|
websockets~=15.0.1
|
0
survey/__init__.py
Normal file
0
survey/__init__.py
Normal file
13
survey/admin.py
Normal file
13
survey/admin.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Survey
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Survey)
|
||||||
|
class SurveyAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('survey_id', 'name', 'invite_team', 'invite_coaches', 'tournament',)
|
||||||
|
list_filter = ('invite_team', 'invite_coaches', 'tournament',)
|
||||||
|
search_fields = ('name',)
|
11
survey/apps.py
Normal file
11
survey/apps.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "survey"
|
||||||
|
verbose_name = _("surveys")
|
28
survey/forms.py
Normal file
28
survey/forms.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .models import Survey
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if 'survey_id' in self.initial:
|
||||||
|
self.fields['survey_id'].disabled = True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Survey
|
||||||
|
exclude = ('completed_registrations', 'completed_teams',)
|
||||||
|
widgets = {
|
||||||
|
'completed_registrations': forms.SelectMultiple(attrs={
|
||||||
|
'class': 'selectpicker',
|
||||||
|
'data-live-search': 'true',
|
||||||
|
'data-live-search-normalize': 'true',
|
||||||
|
'data-width': 'fit',
|
||||||
|
}),
|
||||||
|
'completed_teams': forms.SelectMultiple(attrs={
|
||||||
|
'class': 'selectpicker',
|
||||||
|
'data-live-search': 'true',
|
||||||
|
'data-live-search-normalize': 'true',
|
||||||
|
'data-width': 'fit',
|
||||||
|
}),
|
||||||
|
}
|
13
survey/management/commands/fetch_survey_completion_data.py
Normal file
13
survey/management/commands/fetch_survey_completion_data.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from ...models import Survey
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
for survey in Survey.objects.all():
|
||||||
|
survey.fetch_completion_data()
|
83
survey/migrations/0001_initial.py
Normal file
83
survey/migrations/0001_initial.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-19 21:12
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"participation",
|
||||||
|
"0023_tournament_unified_registration",
|
||||||
|
),
|
||||||
|
("registration", "0014_participantregistration_country"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Survey",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"survey_id",
|
||||||
|
models.IntegerField(
|
||||||
|
help_text="The numeric identifier of the Limesurvey.",
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="survey identifier",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=255, verbose_name="display name")),
|
||||||
|
(
|
||||||
|
"invite_team",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="When this field is checked, teams will get only one survey invitation instead of one per person.",
|
||||||
|
verbose_name="invite whole team",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"invite_coaches",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="When this field is checked, coaches will also be invited in the survey. No effect when the whole team is invited.",
|
||||||
|
verbose_name="invite coaches",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"completed_registrations",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="completed_surveys",
|
||||||
|
to="registration.participantregistration",
|
||||||
|
verbose_name="participants that completed the survey",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"completed_teams",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="completed_surveys",
|
||||||
|
to="participation.team",
|
||||||
|
verbose_name="teams that completed the survey",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tournament",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="When this field is filled, the survey participants will be restricted to this tournament members.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="participation.tournament",
|
||||||
|
verbose_name="tournament restriction",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "survey",
|
||||||
|
"verbose_name_plural": "surveys",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-19 22:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"participation",
|
||||||
|
"0023_tournament_unified_registration",
|
||||||
|
),
|
||||||
|
("registration", "0014_participantregistration_country"),
|
||||||
|
("survey", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="survey",
|
||||||
|
name="completed_registrations",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="completed_surveys",
|
||||||
|
to="registration.participantregistration",
|
||||||
|
verbose_name="participants that completed the survey",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="survey",
|
||||||
|
name="completed_teams",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="completed_surveys",
|
||||||
|
to="participation.team",
|
||||||
|
verbose_name="teams that completed the survey",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="survey",
|
||||||
|
name="tournament",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="When this field is filled, the survey participants will be restricted to this tournament members.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="surveys",
|
||||||
|
to="participation.tournament",
|
||||||
|
verbose_name="tournament restriction",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
0
survey/migrations/__init__.py
Normal file
0
survey/migrations/__init__.py
Normal file
137
survey/models.py
Normal file
137
survey/models.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from citric import Client
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from participation.models import Team, Tournament
|
||||||
|
from registration.models import ParticipantRegistration, StudentRegistration
|
||||||
|
|
||||||
|
|
||||||
|
class Survey(models.Model):
|
||||||
|
"""
|
||||||
|
Ce modèle représente un sondage LimeSurvey afin de faciliter l'import des
|
||||||
|
participant⋅es au sondage et d'effectuer le suivi.
|
||||||
|
"""
|
||||||
|
survey_id = models.IntegerField(
|
||||||
|
primary_key=True,
|
||||||
|
verbose_name=_("survey identifier"),
|
||||||
|
help_text=_("The numeric identifier of the Limesurvey."),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name=_("display name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_team = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("invite whole team"),
|
||||||
|
help_text=_("When this field is checked, teams will get only one survey invitation instead of one per person."),
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_coaches = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name=_("invite coaches"),
|
||||||
|
help_text=_("When this field is checked, coaches will also be invited in the survey. No effect when the whole team is invited."),
|
||||||
|
)
|
||||||
|
|
||||||
|
tournament = models.ForeignKey(
|
||||||
|
Tournament,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="surveys",
|
||||||
|
verbose_name=_("tournament restriction"),
|
||||||
|
help_text=_("When this field is filled, the survey participants will be restricted to this tournament members."),
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_registrations = models.ManyToManyField(
|
||||||
|
ParticipantRegistration,
|
||||||
|
blank=True,
|
||||||
|
related_name="completed_surveys",
|
||||||
|
verbose_name=_("participants that completed the survey"),
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_teams = models.ManyToManyField(
|
||||||
|
Team,
|
||||||
|
blank=True,
|
||||||
|
related_name="completed_surveys",
|
||||||
|
verbose_name=_("teams that completed the survey"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def participants(self):
|
||||||
|
if self.invite_team:
|
||||||
|
teams = Team.objects.filter(participation__valid=True)
|
||||||
|
if self.tournament:
|
||||||
|
teams = teams.filter(participation__tournament=self.tournament)
|
||||||
|
return teams.order_by('participation__tournament__name', 'trigram').all()
|
||||||
|
else:
|
||||||
|
if self.invite_coaches:
|
||||||
|
registrations = ParticipantRegistration.objects.filter(team__participation__valid=True)
|
||||||
|
else:
|
||||||
|
registrations = StudentRegistration.objects.filter(team__participation__valid=True)
|
||||||
|
if self.tournament:
|
||||||
|
registrations = registrations.filter(team__participation__tournament=self.tournament)
|
||||||
|
return registrations.order_by('team__participation__tournament__name', 'team__trigram').all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completed(self):
|
||||||
|
if self.invite_team:
|
||||||
|
return self.completed_teams
|
||||||
|
else:
|
||||||
|
return self.completed_registrations
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse_lazy("survey:survey_detail", args=(self.survey_id,))
|
||||||
|
|
||||||
|
def generate_participants_data(self):
|
||||||
|
participants_data = []
|
||||||
|
if self.invite_team:
|
||||||
|
for team in self.participants:
|
||||||
|
participant_data = {"firstname": team.name, "lastname": f"(équipe {team.trigram})", "email": team.email}
|
||||||
|
participants_data.append(participant_data)
|
||||||
|
else:
|
||||||
|
for reg in self.participants:
|
||||||
|
participant_data = {"firstname": reg.user.first_name, "lastname": reg.user.last_name, "email": reg.user.email}
|
||||||
|
participants_data.append(participant_data)
|
||||||
|
return participants_data
|
||||||
|
|
||||||
|
def invite_all(self):
|
||||||
|
participants_data = self.generate_participants_data()
|
||||||
|
with Client(f"{settings.LIMESURVEY_URL}/index.php/admin/remotecontrol", settings.LIMESURVEY_USER, settings.LIMESURVEY_PASSWORD) as client:
|
||||||
|
try:
|
||||||
|
current_participants = client.list_participants(self.survey_id, limit=10000)
|
||||||
|
except:
|
||||||
|
current_participants = []
|
||||||
|
current_participants_email = set(participant['participant_info']['email'] for participant in current_participants)
|
||||||
|
participants_data = [participant_data for participant_data in participants_data if participant_data['email'] not in current_participants_email]
|
||||||
|
try:
|
||||||
|
client.activate_tokens(self.survey_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
new_participants = client.add_participants(self.survey_id, participant_data=participants_data)
|
||||||
|
if new_participants:
|
||||||
|
client.invite_participants(self.survey_id, token_ids=[participant['tid'] for participant in new_participants])
|
||||||
|
return new_participants
|
||||||
|
|
||||||
|
def fetch_completion_data(self):
|
||||||
|
with Client(f"{settings.LIMESURVEY_URL}/index.php/admin/remotecontrol", settings.LIMESURVEY_USER, settings.LIMESURVEY_PASSWORD) as client:
|
||||||
|
participants = client.list_participants(self.survey_id, limit=10000, attributes=['completed'])
|
||||||
|
if self.invite_team:
|
||||||
|
team_names = [participant['participant_info']['firstname'] for participant in participants if participant['completed'] != 'N']
|
||||||
|
self.completed_teams.set(list(Team.objects.filter(name__in=team_names).values_list('id', flat=True)))
|
||||||
|
else:
|
||||||
|
mails = [participant['participant_info']['email'] for participant in participants if participant['completed'] != 'N']
|
||||||
|
self.completed_registrations.set(list(ParticipantRegistration.objects.filter(user__email__in=mails).values_list('id', flat=True)))
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("survey")
|
||||||
|
verbose_name_plural = _("surveys")
|
31
survey/tables.py
Normal file
31
survey/tables.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Copyright (C) 2025 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
from .models import Survey
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyTable(tables.Table):
|
||||||
|
survey_id = tables.LinkColumn(
|
||||||
|
'survey:survey_detail',
|
||||||
|
args=[tables.A('survey_id')],
|
||||||
|
verbose_name=lambda: _("survey identifier").capitalize(),
|
||||||
|
)
|
||||||
|
|
||||||
|
nb_completed = tables.Column(
|
||||||
|
verbose_name=_("completed").capitalize,
|
||||||
|
accessor='survey_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
def render_nb_completed(self, record):
|
||||||
|
return f"{record.completed.count()}/{record.participants.count()}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table-condensed table-striped',
|
||||||
|
}
|
||||||
|
model = Survey
|
||||||
|
fields = ('survey_id', 'name', 'invite_team', 'invite_coaches', 'tournament', 'nb_completed',)
|
||||||
|
order_by = ('survey_id',)
|
87
survey/templates/survey/survey_detail.html
Normal file
87
survey/templates/survey/survey_detail.html
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card bg-body shadow">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h4>
|
||||||
|
{% trans "survey"|capfirst %} {{ survey.survey_id }}
|
||||||
|
<a href="{{ TFJM.LIMESURVEY_URL }}/index.php/{{ survey.survey_id }}" target="_blank"><i class="fas fa-arrow-up-right-from-square"></i></a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "Name:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ survey.name }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "One answer per team:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ survey.invite_team|yesno }}</dd>
|
||||||
|
|
||||||
|
{% if not survey.invite_team %}
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "Coaches can answer the survey:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ survey.invite_coaches|yesno }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if survey.tournament %}
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "Tournament restriction:" %}</dt>
|
||||||
|
<dd class="col-sm-6">{{ survey.tournament }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<dt class="col-sm-6 text-sm-end">{% trans "Completion rate:" %}</dt>
|
||||||
|
<dd class="col-sm-6">
|
||||||
|
{{ survey.completed.count }}/{{ survey.participants.count }}
|
||||||
|
<a href="{% url "survey:survey_refresh_completed" pk=survey.pk %}"><i class="fas fa-arrow-rotate-right" alt="refresh"></i></a>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateSurveyModal">{% trans "Update" %}</button>
|
||||||
|
<a class="btn btn-secondary" href="{% url "survey:survey_invite" pk=survey.pk %}">{% trans "Send invites" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<table class="table table-condensed table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "participant"|capfirst %}</th>
|
||||||
|
<th>{% trans "tournament"|capfirst %}</th>
|
||||||
|
<th>{% trans "completed"|capfirst %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for participant in survey.participants %}
|
||||||
|
<tr class="{% if participant in survey.completed.all %}table-success{% else %}table-danger{% endif %}">
|
||||||
|
{% if survey.invite_team %}
|
||||||
|
<td>{% trans "Team" %} {{ participant.name }} ({{ participant.trigram }})</td>
|
||||||
|
<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>
|
||||||
|
{% else %}
|
||||||
|
<td>{% trans "No" %}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% trans "Update survey" as modal_title %}
|
||||||
|
{% trans "Update" as modal_button %}
|
||||||
|
{% url "survey:survey_update" pk=survey.pk as modal_action %}
|
||||||
|
{% include "base_modal.html" with modal_id="updateSurvey" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initModal("updateSurvey", "{% url "survey:survey_update" pk=survey.pk %}")
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
17
survey/templates/survey/survey_form.html
Normal file
17
survey/templates/survey/survey_form.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
||||||
|
|
||||||
|
{% load crispy_forms_filters i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
<div id="form-content">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
</div>
|
||||||
|
{% if object.pk %}
|
||||||
|
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user