mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-02-26 17:46:27 +00:00
Compare commits
No commits in common. "2f4755ffc7e849456fb752352b7bd4e4f5ad55d4" and "ff414ea0468ec9c84f23a7a7586fcec95b2b323f" have entirely different histories.
2f4755ffc7
...
ff414ea046
@ -2,6 +2,14 @@ stages:
|
|||||||
- test
|
- test
|
||||||
- quality-assurance
|
- quality-assurance
|
||||||
|
|
||||||
|
py39:
|
||||||
|
stage: test
|
||||||
|
image: python:3.9-alpine
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache libmagic
|
||||||
|
- pip install tox --no-cache-dir
|
||||||
|
script: tox -e py39
|
||||||
|
|
||||||
py310:
|
py310:
|
||||||
stage: test
|
stage: test
|
||||||
image: python:3.10-alpine
|
image: python:3.10-alpine
|
||||||
|
@ -3,7 +3,7 @@ FROM python:3.11-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 postgresql-dev libmagic texlive texmf-dist-latexextra
|
RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libxml2-dev libxslt-dev postgresql-dev libmagic texlive
|
||||||
|
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
@ -13,8 +13,6 @@ COPY requirements.txt /code/requirements.txt
|
|||||||
COPY docs/requirements.txt /code/docs/requirements.txt
|
COPY docs/requirements.txt /code/docs/requirements.txt
|
||||||
RUN pip install -r requirements.txt --no-cache-dir
|
RUN pip install -r requirements.txt --no-cache-dir
|
||||||
RUN pip install -r docs/requirements.txt --no-cache-dir
|
RUN pip install -r docs/requirements.txt --no-cache-dir
|
||||||
# FIXME Remove this line when all dependencies will be ready
|
|
||||||
RUN pip install "Django>=4.2,<5.0"
|
|
||||||
|
|
||||||
COPY . /code/
|
COPY . /code/
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import include, path
|
from django.conf.urls import include, url
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from .viewsets import UserViewSet
|
from .viewsets import UserViewSet
|
||||||
@ -29,6 +29,6 @@ app_name = 'api'
|
|||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
# Additionally, we include login URLs for the browsable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
url('^', include(router.urls)),
|
||||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
]
|
]
|
@ -34,13 +34,13 @@ def pre_save_object(sender, instance, **kwargs):
|
|||||||
instance._previous = None
|
instance._previous = None
|
||||||
|
|
||||||
|
|
||||||
def save_object(sender, instance, raw, **kwargs):
|
def save_object(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
Each time a model is saved, an entry in the table `Changelog` is added in the database
|
Each time a model is saved, an entry in the table `Changelog` is added in the database
|
||||||
in order to store each modification made
|
in order to store each modification made
|
||||||
"""
|
"""
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal") or raw:
|
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
64
apps/participation/admin.py
Normal file
64
apps/participation/admin.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Copyright (C) 2020 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Team)
|
||||||
|
class TeamAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'trigram', 'valid',)
|
||||||
|
search_fields = ('name', 'trigram',)
|
||||||
|
list_filter = ('participation__valid',)
|
||||||
|
|
||||||
|
def valid(self, team):
|
||||||
|
return team.participation.valid
|
||||||
|
|
||||||
|
valid.short_description = _('valid')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Participation)
|
||||||
|
class ParticipationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('team', 'valid',)
|
||||||
|
search_fields = ('team__name', 'team__trigram',)
|
||||||
|
list_filter = ('valid',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Pool)
|
||||||
|
class PoolAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('participations__team__name', 'participations__team__trigram',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Passage)
|
||||||
|
class PassageAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Note)
|
||||||
|
class NoteAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('jury',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Solution)
|
||||||
|
class SolutionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('participation',)
|
||||||
|
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Synthesis)
|
||||||
|
class SynthesisAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('participation',)
|
||||||
|
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Tournament)
|
||||||
|
class TournamentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name',)
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Tweak)
|
||||||
|
class TweakAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('participation', 'pool', 'diff',)
|
@ -6,17 +6,12 @@ from io import StringIO
|
|||||||
import re
|
import re
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from crispy_forms.bootstrap import InlineField
|
|
||||||
from crispy_forms.helper import FormHelper
|
|
||||||
from crispy_forms.layout import Div, Fieldset, Submit
|
|
||||||
from django import forms
|
from django import forms
|
||||||
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
|
||||||
from django.db.models import CharField, Value
|
|
||||||
from django.db.models.functions import Concat
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from pypdf import PdfReader
|
from pypdf import PdfFileReader
|
||||||
|
|
||||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
||||||
|
|
||||||
@ -28,7 +23,7 @@ class TeamForm(forms.ModelForm):
|
|||||||
def clean_name(self):
|
def clean_name(self):
|
||||||
if "name" in self.cleaned_data:
|
if "name" in self.cleaned_data:
|
||||||
name = self.cleaned_data["name"]
|
name = self.cleaned_data["name"]
|
||||||
if Team.objects.filter(name=name).exclude(pk=self.instance.pk).exists():
|
if not self.instance.pk and Team.objects.filter(name=name).exists():
|
||||||
raise ValidationError(_("This name is already used."))
|
raise ValidationError(_("This name is already used."))
|
||||||
return name
|
return name
|
||||||
|
|
||||||
@ -38,7 +33,7 @@ class TeamForm(forms.ModelForm):
|
|||||||
if not re.match("[A-Z]{3}", trigram):
|
if not re.match("[A-Z]{3}", trigram):
|
||||||
raise ValidationError(_("The trigram must be composed of three uppercase letters."))
|
raise ValidationError(_("The trigram must be composed of three uppercase letters."))
|
||||||
|
|
||||||
if Team.objects.filter(trigram=trigram).exclude(pk=self.instance.pk).exists():
|
if not self.instance.pk and Team.objects.filter(trigram=trigram).exists():
|
||||||
raise ValidationError(_("This trigram is already used."))
|
raise ValidationError(_("This trigram is already used."))
|
||||||
return trigram
|
return trigram
|
||||||
|
|
||||||
@ -156,7 +151,7 @@ class SolutionForm(forms.ModelForm):
|
|||||||
raise ValidationError(_("The uploaded file size must be under 5 Mo."))
|
raise ValidationError(_("The uploaded file size must be under 5 Mo."))
|
||||||
if file.content_type != "application/pdf":
|
if file.content_type != "application/pdf":
|
||||||
raise ValidationError(_("The uploaded file must be a PDF file."))
|
raise ValidationError(_("The uploaded file must be a PDF file."))
|
||||||
pdf_reader = PdfReader(file)
|
pdf_reader = PdfFileReader(file)
|
||||||
pages = len(pdf_reader.pages)
|
pages = len(pdf_reader.pages)
|
||||||
if pages > 30:
|
if pages > 30:
|
||||||
raise ValidationError(_("The PDF file must not have more than 30 pages."))
|
raise ValidationError(_("The PDF file must not have more than 30 pages."))
|
||||||
@ -175,7 +170,7 @@ class SolutionForm(forms.ModelForm):
|
|||||||
class PoolForm(forms.ModelForm):
|
class PoolForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Pool
|
model = Pool
|
||||||
fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'juries',)
|
fields = ('tournament', 'round', 'bbb_url', 'results_available', 'juries',)
|
||||||
widgets = {
|
widgets = {
|
||||||
"juries": forms.SelectMultiple(attrs={
|
"juries": forms.SelectMultiple(attrs={
|
||||||
'class': 'selectpicker',
|
'class': 'selectpicker',
|
||||||
@ -203,48 +198,6 @@ class PoolTeamsForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AddJuryForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.helper = FormHelper()
|
|
||||||
self.helper.form_class = 'form-inline'
|
|
||||||
self.helper.layout = Fieldset(
|
|
||||||
_("Add new jury"),
|
|
||||||
Div(
|
|
||||||
Div(
|
|
||||||
InlineField('first_name', autofocus="autofocus"),
|
|
||||||
css_class='col-xl-3',
|
|
||||||
),
|
|
||||||
Div(
|
|
||||||
InlineField('last_name'),
|
|
||||||
css_class='col-xl-3',
|
|
||||||
),
|
|
||||||
Div(
|
|
||||||
InlineField('email'),
|
|
||||||
css_class='col-xl-5',
|
|
||||||
),
|
|
||||||
Div(
|
|
||||||
Submit('submit', _("Add")),
|
|
||||||
css_class='col-xl-1',
|
|
||||||
),
|
|
||||||
css_class='row',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
"""
|
|
||||||
Ensure that the email address is unique.
|
|
||||||
"""
|
|
||||||
email = self.data["email"]
|
|
||||||
if User.objects.filter(email=email).exists():
|
|
||||||
self.add_error("email", _("This email address is already used."))
|
|
||||||
return email
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = ('first_name', 'last_name', 'email',)
|
|
||||||
|
|
||||||
|
|
||||||
class UploadNotesForm(forms.Form):
|
class UploadNotesForm(forms.Form):
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label=_("CSV file:"),
|
label=_("CSV file:"),
|
||||||
@ -262,57 +215,40 @@ class UploadNotesForm(forms.Form):
|
|||||||
file = cleaned_data['file']
|
file = cleaned_data['file']
|
||||||
with file:
|
with file:
|
||||||
try:
|
try:
|
||||||
data: bytes = file.read()
|
csvfile = csv.reader(StringIO(file.read().decode()))
|
||||||
try:
|
|
||||||
content = data.decode()
|
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# This is not UTF-8, grrrr
|
self.add_error('file', _("This file contains non-UTF-8 content. "
|
||||||
content = data.decode('latin1')
|
|
||||||
csvfile = csv.reader(StringIO(content))
|
|
||||||
self.process(csvfile, cleaned_data)
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
self.add_error('file', _("This file contains non-UTF-8 and non-ISO-8859-1 content. "
|
|
||||||
"Please send your sheet as a CSV file."))
|
"Please send your sheet as a CSV file."))
|
||||||
|
|
||||||
|
self.process(csvfile, cleaned_data)
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
def process(self, csvfile: Iterable[str], cleaned_data: dict):
|
def process(self, csvfile: Iterable[str], cleaned_data: dict):
|
||||||
parsed_notes = {}
|
parsed_notes = {}
|
||||||
valid_lengths = [1 + 6 * 3, 1 + 7 * 4, 1 + 6 * 5] # Per pool sizes
|
|
||||||
pool_size = 0
|
|
||||||
line_length = 0
|
|
||||||
for line in csvfile:
|
for line in csvfile:
|
||||||
line = [s.strip() for s in line if s]
|
line = [s for s in line if s]
|
||||||
if line and line[0] == 'Problème':
|
if len(line) < 19:
|
||||||
pool_size = len(line) - 1
|
|
||||||
if pool_size < 3 or pool_size > 5:
|
|
||||||
self.add_error('file', _("Can't determine the pool size. Are you sure your file is correct?"))
|
|
||||||
return
|
|
||||||
line_length = valid_lengths[pool_size - 3]
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if pool_size == 0 or len(line) < line_length:
|
|
||||||
continue
|
|
||||||
|
|
||||||
name = line[0]
|
name = line[0]
|
||||||
if name.lower() in ["rôle", "juré", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]:
|
notes = line[1:19]
|
||||||
continue
|
if not all(s.isnumeric() for s in notes):
|
||||||
notes = line[1:line_length]
|
|
||||||
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
|
|
||||||
continue
|
continue
|
||||||
notes = list(map(int, notes))
|
notes = list(map(int, notes))
|
||||||
|
if max(notes) < 3 or min(notes) < 0:
|
||||||
|
continue
|
||||||
|
|
||||||
max_notes = pool_size * ([20, 16, 9, 10, 9, 10] + ([4] if pool_size == 4 else []))
|
max_notes = 3 * [20, 16, 9, 10, 9, 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',
|
||||||
_("The following note is higher of the maximum expected value:")
|
_("The following note is higher of the maximum expected value:")
|
||||||
+ str(n) + " > " + str(max_n))
|
+ str(n) + " > " + str(max_n))
|
||||||
|
|
||||||
# Search by "{first_name} {last_name}"
|
first_name, last_name = tuple(name.split(' ', 1))
|
||||||
jury = User.objects.annotate(full_name=Concat('first_name', Value(' '), 'last_name',
|
|
||||||
output_field=CharField())) \
|
jury = User.objects.filter(first_name=first_name, last_name=last_name,
|
||||||
.filter(full_name=name.replace('’', '\''), registration__volunteerregistration__isnull=False)
|
registration__volunteerregistration__isnull=False)
|
||||||
if jury.count() != 1:
|
if jury.count() != 1:
|
||||||
self.add_error('file', _("The following user was not found:") + " " + name)
|
self.add_error('file', _("The following user was not found:") + " " + name)
|
||||||
continue
|
continue
|
||||||
@ -340,7 +276,7 @@ class PassageForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Passage
|
model = Passage
|
||||||
fields = ('position', 'solution_number', 'defender', 'opponent', 'reporter', 'observer', 'defender_penalties',)
|
fields = ('solution_number', 'defender', 'opponent', 'reporter', 'defender_penalties',)
|
||||||
|
|
||||||
|
|
||||||
class SynthesisForm(forms.ModelForm):
|
class SynthesisForm(forms.ModelForm):
|
||||||
@ -351,10 +287,6 @@ class SynthesisForm(forms.ModelForm):
|
|||||||
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
|
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
|
||||||
if file.content_type != "application/pdf":
|
if file.content_type != "application/pdf":
|
||||||
raise ValidationError(_("The uploaded file must be a PDF file."))
|
raise ValidationError(_("The uploaded file must be a PDF file."))
|
||||||
pdf_reader = PdfReader(file)
|
|
||||||
pages = len(pdf_reader.pages)
|
|
||||||
if pages > 2:
|
|
||||||
raise ValidationError(_("The PDF file must not have more than 2 pages."))
|
|
||||||
return self.cleaned_data["file"]
|
return self.cleaned_data["file"]
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
@ -371,4 +303,4 @@ class NoteForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Note
|
model = Note
|
||||||
fields = ('defender_writing', 'defender_oral', 'opponent_writing',
|
fields = ('defender_writing', 'defender_oral', 'opponent_writing',
|
||||||
'opponent_oral', 'reporter_writing', 'reporter_oral', 'observer_oral', )
|
'opponent_oral', 'reporter_writing', 'reporter_oral', )
|
@ -23,7 +23,7 @@ class Command(BaseCommand):
|
|||||||
token = response['access_token']
|
token = response['access_token']
|
||||||
|
|
||||||
organization = "animath"
|
organization = "animath"
|
||||||
form_slug = "tfjm-2023-tournois-regionaux"
|
form_slug = "tfjm-2022-tournois-regionaux"
|
||||||
from_date = "2000-01-01"
|
from_date = "2000-01-01"
|
||||||
url = f"https://api.helloasso.com/v5/organizations/{organization}/forms/Event/{form_slug}/payments" \
|
url = f"https://api.helloasso.com/v5/organizations/{organization}/forms/Event/{form_slug}/payments" \
|
||||||
f"?from={from_date}&pageIndex=1&pageSize=100&retrieveOfflineDonations=false"
|
f"?from={from_date}&pageIndex=1&pageSize=100&retrieveOfflineDonations=false"
|
@ -4,7 +4,8 @@
|
|||||||
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
|
||||||
from participation.models import Tournament
|
|
||||||
|
from .models import Tournament
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
@ -3,17 +3,29 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
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 Solution, Tournament
|
|
||||||
|
from .models import Solution, Tournament
|
||||||
|
|
||||||
|
|
||||||
|
PROBLEMS = [
|
||||||
|
"Pliage de polygones",
|
||||||
|
"Mélodie des hirondelles",
|
||||||
|
"Professeur confiné",
|
||||||
|
"Nain sans mémoire",
|
||||||
|
"Bricolage microscopique",
|
||||||
|
"Villes jumelées",
|
||||||
|
"Promenade de chiens",
|
||||||
|
"Persée et la Gorgone",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
activate('fr')
|
activate('fr')
|
||||||
|
|
||||||
base_dir = Path(__file__).parent.parent.parent.parent
|
base_dir = Path(__file__).parent.parent.parent.parent.parent
|
||||||
base_dir /= "output"
|
base_dir /= "output"
|
||||||
if not base_dir.is_dir():
|
if not base_dir.is_dir():
|
||||||
base_dir.mkdir()
|
base_dir.mkdir()
|
||||||
@ -29,7 +41,7 @@ class Command(BaseCommand):
|
|||||||
if not base_dir.is_dir():
|
if not base_dir.is_dir():
|
||||||
base_dir.mkdir()
|
base_dir.mkdir()
|
||||||
|
|
||||||
for problem_id, problem_name in enumerate(settings.PROBLEMS):
|
for problem_id, problem_name in enumerate(PROBLEMS):
|
||||||
dir_name = f"Problème n°{problem_id + 1} : {problem_name}"
|
dir_name = f"Problème n°{problem_id + 1} : {problem_name}"
|
||||||
problem_dir = base_dir / dir_name
|
problem_dir = base_dir / dir_name
|
||||||
if not problem_dir.is_dir():
|
if not problem_dir.is_dir():
|
@ -30,7 +30,7 @@ class Command(BaseCommand):
|
|||||||
else:
|
else:
|
||||||
stat_file = os.stat("tfjm/static/logo.png")
|
stat_file = os.stat("tfjm/static/logo.png")
|
||||||
with open("tfjm/static/logo.png", "rb") as f:
|
with open("tfjm/static/logo.png", "rb") as f:
|
||||||
resp = (await Matrix.upload(f, filename="../../../tfjm/static/logo.png", content_type="image/png",
|
resp = (await Matrix.upload(f, filename="logo.png", content_type="image/png",
|
||||||
filesize=stat_file.st_size))[0][0]
|
filesize=stat_file.st_size))[0][0]
|
||||||
avatar_uri = resp.content_uri
|
avatar_uri = resp.content_uri
|
||||||
with open(".matrix_avatar", "w") as f:
|
with open(".matrix_avatar", "w") as f:
|
||||||
@ -66,7 +66,7 @@ class Command(BaseCommand):
|
|||||||
visibility=RoomVisibility.public,
|
visibility=RoomVisibility.public,
|
||||||
alias="bienvenue",
|
alias="bienvenue",
|
||||||
name="Bienvenue",
|
name="Bienvenue",
|
||||||
topic="Bienvenue au TFJM² 2023 !",
|
topic="Bienvenue au TFJM² 2022 !",
|
||||||
federate=False,
|
federate=False,
|
||||||
preset=RoomPreset.public_chat,
|
preset=RoomPreset.public_chat,
|
||||||
)
|
)
|
@ -3,6 +3,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import participation.models
|
import participation.models
|
||||||
|
|
@ -6,7 +6,7 @@ import os
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Index
|
from django.db.models import Index
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
@ -124,7 +124,6 @@ class Team(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("team")
|
verbose_name = _("team")
|
||||||
verbose_name_plural = _("teams")
|
verbose_name_plural = _("teams")
|
||||||
ordering = ('trigram',)
|
|
||||||
indexes = [
|
indexes = [
|
||||||
Index(fields=("trigram", )),
|
Index(fields=("trigram", )),
|
||||||
]
|
]
|
||||||
@ -279,12 +278,6 @@ class Tournament(models.Model):
|
|||||||
return Synthesis.objects.filter(final_solution=True)
|
return Synthesis.objects.filter(final_solution=True)
|
||||||
return Synthesis.objects.filter(participation__tournament=self)
|
return Synthesis.objects.filter(participation__tournament=self)
|
||||||
|
|
||||||
@property
|
|
||||||
def best_format(self):
|
|
||||||
n = len(self.participations.filter(valid=True).all())
|
|
||||||
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
|
|
||||||
return '+'.join(map(str, sorted(fmt, reverse=True)))
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
||||||
|
|
||||||
@ -322,7 +315,7 @@ class Participation(models.Model):
|
|||||||
valid = models.BooleanField(
|
valid = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
verbose_name=_("valid team"),
|
verbose_name=_("valid"),
|
||||||
help_text=_("The participation got the validation of the organizers."),
|
help_text=_("The participation got the validation of the organizers."),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -341,7 +334,6 @@ class Participation(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("participation")
|
verbose_name = _("participation")
|
||||||
verbose_name_plural = _("participations")
|
verbose_name_plural = _("participations")
|
||||||
ordering = ('valid', 'team__trigram',)
|
|
||||||
|
|
||||||
|
|
||||||
class Pool(models.Model):
|
class Pool(models.Model):
|
||||||
@ -360,16 +352,6 @@ class Pool(models.Model):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
letter = models.PositiveSmallIntegerField(
|
|
||||||
choices=[
|
|
||||||
(1, 'A'),
|
|
||||||
(2, 'B'),
|
|
||||||
(3, 'C'),
|
|
||||||
(4, 'D'),
|
|
||||||
],
|
|
||||||
verbose_name=_('letter'),
|
|
||||||
)
|
|
||||||
|
|
||||||
participations = models.ManyToManyField(
|
participations = models.ManyToManyField(
|
||||||
Participation,
|
Participation,
|
||||||
related_name="pools",
|
related_name="pools",
|
||||||
@ -399,16 +381,12 @@ class Pool(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def solutions(self):
|
def solutions(self):
|
||||||
return [passage.defended_solution for passage in self.passages.all()]
|
return Solution.objects.filter(participation__in=self.participations, final_solution=self.tournament.final)
|
||||||
|
|
||||||
def average(self, participation):
|
def average(self, participation):
|
||||||
return sum(passage.average(participation) for passage in self.passages.all()) \
|
return sum(passage.average(participation) for passage in self.passages.all()) \
|
||||||
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
|
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
|
||||||
|
|
||||||
async def aaverage(self, participation):
|
|
||||||
return sum([passage.average(participation) async for passage in self.passages.all()]) \
|
|
||||||
+ sum([tweak.diff async for tweak in participation.tweaks.filter(pool=self).all()])
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("participation:pool_detail", args=(self.pk,))
|
return reverse_lazy("participation:pool_detail", args=(self.pk,))
|
||||||
|
|
||||||
@ -421,7 +399,6 @@ class Pool(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("pool")
|
verbose_name = _("pool")
|
||||||
verbose_name_plural = _("pools")
|
verbose_name_plural = _("pools")
|
||||||
ordering = ('round', 'letter',)
|
|
||||||
|
|
||||||
|
|
||||||
class Passage(models.Model):
|
class Passage(models.Model):
|
||||||
@ -432,17 +409,10 @@ class Passage(models.Model):
|
|||||||
related_name="passages",
|
related_name="passages",
|
||||||
)
|
)
|
||||||
|
|
||||||
position = models.PositiveSmallIntegerField(
|
|
||||||
verbose_name=_("position"),
|
|
||||||
choices=zip(range(1, 6), range(1, 6)),
|
|
||||||
default=1,
|
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(5)],
|
|
||||||
)
|
|
||||||
|
|
||||||
solution_number = models.PositiveSmallIntegerField(
|
solution_number = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_("defended solution"),
|
verbose_name=_("defended solution"),
|
||||||
choices=[
|
choices=[
|
||||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -467,16 +437,6 @@ class Passage(models.Model):
|
|||||||
related_name="+",
|
related_name="+",
|
||||||
)
|
)
|
||||||
|
|
||||||
observer = models.ForeignKey(
|
|
||||||
Participation,
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
verbose_name=_("observer"),
|
|
||||||
related_name="+",
|
|
||||||
)
|
|
||||||
|
|
||||||
defender_penalties = models.PositiveSmallIntegerField(
|
defender_penalties = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_("penalties"),
|
verbose_name=_("penalties"),
|
||||||
default=0,
|
default=0,
|
||||||
@ -531,25 +491,9 @@ class Passage(models.Model):
|
|||||||
def average_reporter(self) -> float:
|
def average_reporter(self) -> float:
|
||||||
return self.average_reporter_writing + self.average_reporter_oral
|
return self.average_reporter_writing + self.average_reporter_oral
|
||||||
|
|
||||||
@property
|
|
||||||
def average_observer(self) -> float:
|
|
||||||
return self.avg(note.observer_oral for note in self.notes.all())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def averages(self):
|
|
||||||
yield self.average_defender_writing
|
|
||||||
yield self.average_defender_oral
|
|
||||||
yield self.average_opponent_writing
|
|
||||||
yield self.average_opponent_oral
|
|
||||||
yield self.average_reporter_writing
|
|
||||||
yield self.average_reporter_oral
|
|
||||||
if self.observer:
|
|
||||||
yield self.average_observer
|
|
||||||
|
|
||||||
def average(self, participation):
|
def average(self, participation):
|
||||||
return self.average_defender if participation == self.defender else self.average_opponent \
|
return self.average_defender if participation == self.defender else self.average_opponent \
|
||||||
if participation == self.opponent else self.average_reporter if participation == self.reporter \
|
if participation == self.opponent else self.average_reporter if participation == self.reporter else 0
|
||||||
else self.average_observer if participation == self.observer else 0
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("participation:passage_detail", args=(self.pk,))
|
return reverse_lazy("participation:passage_detail", args=(self.pk,))
|
||||||
@ -564,9 +508,6 @@ class Passage(models.Model):
|
|||||||
if self.reporter not in self.pool.participations.all():
|
if self.reporter not in self.pool.participations.all():
|
||||||
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
||||||
.format(trigram=self.reporter.team.trigram))
|
.format(trigram=self.reporter.team.trigram))
|
||||||
if self.observer and self.observer not in self.pool.participations.all():
|
|
||||||
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
|
||||||
.format(trigram=self.observer.team.trigram))
|
|
||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -576,7 +517,6 @@ class Passage(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("passage")
|
verbose_name = _("passage")
|
||||||
verbose_name_plural = _("passages")
|
verbose_name_plural = _("passages")
|
||||||
ordering = ('pool', 'position',)
|
|
||||||
|
|
||||||
|
|
||||||
class Tweak(models.Model):
|
class Tweak(models.Model):
|
||||||
@ -626,7 +566,7 @@ class Solution(models.Model):
|
|||||||
problem = models.PositiveSmallIntegerField(
|
problem = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_("problem"),
|
verbose_name=_("problem"),
|
||||||
choices=[
|
choices=[
|
||||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -746,31 +686,14 @@ class Note(models.Model):
|
|||||||
default=0,
|
default=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
observer_oral = models.SmallIntegerField(
|
|
||||||
verbose_name=_("observer note"),
|
|
||||||
choices=zip(range(-4, 5), range(-4, 5)),
|
|
||||||
default=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_all(self):
|
|
||||||
yield self.defender_writing
|
|
||||||
yield self.defender_oral
|
|
||||||
yield self.opponent_writing
|
|
||||||
yield self.opponent_oral
|
|
||||||
yield self.reporter_writing
|
|
||||||
yield self.reporter_oral
|
|
||||||
if self.passage.observer:
|
|
||||||
yield self.observer_oral
|
|
||||||
|
|
||||||
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
|
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
|
||||||
reporter_writing: int, reporter_oral: int, observer_oral: int = 0):
|
reporter_writing: int, reporter_oral: int):
|
||||||
self.defender_writing = defender_writing
|
self.defender_writing = defender_writing
|
||||||
self.defender_oral = defender_oral
|
self.defender_oral = defender_oral
|
||||||
self.opponent_writing = opponent_writing
|
self.opponent_writing = opponent_writing
|
||||||
self.opponent_oral = opponent_oral
|
self.opponent_oral = opponent_oral
|
||||||
self.reporter_writing = reporter_writing
|
self.reporter_writing = reporter_writing
|
||||||
self.reporter_oral = reporter_oral
|
self.reporter_oral = reporter_oral
|
||||||
self.observer_oral = observer_oral
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
|
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
|
||||||
@ -780,7 +703,7 @@ class Note(models.Model):
|
|||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return any((self.defender_writing, self.defender_oral, self.opponent_writing, self.opponent_oral,
|
return any((self.defender_writing, self.defender_oral, self.opponent_writing, self.opponent_oral,
|
||||||
self.reporter_writing, self.reporter_oral, self.observer_oral))
|
self.reporter_writing, self.reporter_oral))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("note")
|
verbose_name = _("note")
|
@ -6,22 +6,21 @@ from participation.models import Note, Participation, Passage, Pool, Team
|
|||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
|
|
||||||
|
|
||||||
def create_team_participation(instance, created, raw, **_):
|
def create_team_participation(instance, created, **_):
|
||||||
"""
|
"""
|
||||||
When a team got created, create an associated participation.
|
When a team got created, create an associated participation.
|
||||||
"""
|
"""
|
||||||
if not raw:
|
|
||||||
participation = Participation.objects.get_or_create(team=instance)[0]
|
participation = Participation.objects.get_or_create(team=instance)[0]
|
||||||
participation.save()
|
participation.save()
|
||||||
if not created:
|
if not created:
|
||||||
participation.team.create_mailing_list()
|
participation.team.create_mailing_list()
|
||||||
|
|
||||||
|
|
||||||
def update_mailing_list(instance: Team, raw, **_):
|
def update_mailing_list(instance: Team, **_):
|
||||||
"""
|
"""
|
||||||
When a team name or trigram got updated, update mailing lists and Matrix rooms
|
When a team name or trigram got updated, update mailing lists and Matrix rooms
|
||||||
"""
|
"""
|
||||||
if instance.pk and not raw:
|
if instance.pk:
|
||||||
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:
|
||||||
# TODO Rename Matrix room
|
# TODO Rename Matrix room
|
||||||
@ -37,11 +36,10 @@ def update_mailing_list(instance: Team, raw, **_):
|
|||||||
f"{coach.user.first_name} {coach.user.last_name}")
|
f"{coach.user.first_name} {coach.user.last_name}")
|
||||||
|
|
||||||
|
|
||||||
def create_notes(instance: Union[Passage, Pool], raw, **_):
|
def create_notes(instance: Union[Passage, Pool], **_):
|
||||||
if not raw:
|
|
||||||
if isinstance(instance, Pool):
|
if isinstance(instance, Pool):
|
||||||
for passage in instance.passages.all():
|
for passage in instance.passages.all():
|
||||||
create_notes(passage, raw)
|
create_notes(passage)
|
||||||
return
|
return
|
||||||
|
|
||||||
for jury in instance.pool.juries.all():
|
for jury in instance.pool.juries.all():
|
@ -54,7 +54,6 @@ class ParticipationTable(tables.Table):
|
|||||||
}
|
}
|
||||||
model = Team
|
model = Team
|
||||||
fields = ('name', 'trigram', 'valid',)
|
fields = ('name', 'trigram', 'valid',)
|
||||||
order = ('-valid',)
|
|
||||||
|
|
||||||
|
|
||||||
class TournamentTable(tables.Table):
|
class TournamentTable(tables.Table):
|
||||||
@ -77,21 +76,13 @@ class TournamentTable(tables.Table):
|
|||||||
|
|
||||||
|
|
||||||
class PoolTable(tables.Table):
|
class PoolTable(tables.Table):
|
||||||
letter = tables.LinkColumn(
|
teams = tables.LinkColumn(
|
||||||
'participation:pool_detail',
|
'participation:pool_detail',
|
||||||
args=[tables.A('id')],
|
args=[tables.A('id')],
|
||||||
verbose_name=_("pool").capitalize,
|
|
||||||
)
|
|
||||||
|
|
||||||
teams = tables.Column(
|
|
||||||
verbose_name=_("teams").capitalize,
|
verbose_name=_("teams").capitalize,
|
||||||
empty_values=(),
|
empty_values=(),
|
||||||
orderable=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_letter(self, record):
|
|
||||||
return format_lazy(_("Pool {letter}{round}"), letter=record.get_letter_display(), round=record.round)
|
|
||||||
|
|
||||||
def render_teams(self, record):
|
def render_teams(self, record):
|
||||||
return ", ".join(participation.team.trigram for participation in record.participations.all()) \
|
return ", ".join(participation.team.trigram for participation in record.participations.all()) \
|
||||||
or _("No defined team")
|
or _("No defined team")
|
||||||
@ -101,7 +92,7 @@ class PoolTable(tables.Table):
|
|||||||
'class': 'table table-condensed table-striped',
|
'class': 'table table-condensed table-striped',
|
||||||
}
|
}
|
||||||
model = Pool
|
model = Pool
|
||||||
fields = ('letter', 'teams', 'round', 'tournament',)
|
fields = ('teams', 'round', 'tournament',)
|
||||||
|
|
||||||
|
|
||||||
class PassageTable(tables.Table):
|
class PassageTable(tables.Table):
|
||||||
@ -143,4 +134,4 @@ class NoteTable(tables.Table):
|
|||||||
}
|
}
|
||||||
model = Note
|
model = Note
|
||||||
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
||||||
'reporter_writing', 'reporter_oral', 'observer_oral',)
|
'reporter_writing', 'reporter_oral',)
|
@ -13,9 +13,6 @@
|
|||||||
<dt class="col-sm-3">{% trans "Pool:" %}</dt>
|
<dt class="col-sm-3">{% trans "Pool:" %}</dt>
|
||||||
<dd class="col-sm-9"><a href="{{ passage.pool.get_absolute_url }}">{{ passage.pool }}</a></dd>
|
<dd class="col-sm-9"><a href="{{ passage.pool.get_absolute_url }}">{{ passage.pool }}</a></dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Position:" %}</dt>
|
|
||||||
<dd class="col-sm-9">{{ passage.position }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Defender:" %}</dt>
|
<dt class="col-sm-3">{% trans "Defender:" %}</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.defender.get_absolute_url }}">{{ passage.defender.team }}</a></dd>
|
||||||
|
|
||||||
@ -25,11 +22,6 @@
|
|||||||
<dt class="col-sm-3">{% trans "Reporter:" %}</dt>
|
<dt class="col-sm-3">{% trans "Reporter:" %}</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.reporter.get_absolute_url }}">{{ passage.reporter.team }}</a></dd>
|
||||||
|
|
||||||
{% if passage.observer %}
|
|
||||||
<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 "Defended solution:" %}</dt>
|
<dt class="col-sm-3">{% trans "Defended solution:" %}</dt>
|
||||||
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}</a></dd>
|
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}</a></dd>
|
||||||
|
|
||||||
@ -87,11 +79,6 @@
|
|||||||
|
|
||||||
<dt class="col-sm-8">{% trans "Average points for the reporter oral:" %}</dt>
|
<dt class="col-sm-8">{% trans "Average points for the reporter oral:" %}</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd>
|
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd>
|
||||||
|
|
||||||
{% if passage.observer %}
|
|
||||||
<dt class="col-sm-8">{% trans "Average points for the observer oral:" %}</dt>
|
|
||||||
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd>
|
|
||||||
{% endif %}
|
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
@ -105,11 +92,6 @@
|
|||||||
|
|
||||||
<dt class="col-sm-8">{% trans "Reporter points:" %}</dt>
|
<dt class="col-sm-8">{% trans "Reporter points:" %}</dt>
|
||||||
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd>
|
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd>
|
||||||
|
|
||||||
{% if passage.observer %}
|
|
||||||
<dt class="col-sm-8">{% trans "Observer points:" %}</dt>
|
|
||||||
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd>
|
|
||||||
{% endif %}
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -142,7 +124,7 @@
|
|||||||
initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}")
|
initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}")
|
||||||
|
|
||||||
{% if my_note is not None %}
|
{% if my_note is not None %}
|
||||||
initModal("updateNotes", "{% url "participation:update_notes" pk=my_note.pk %}")
|
initModal("updateNotesModal", "{% url "participation:update_notes" pk=my_note.pk %}")
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif user.registration.participates %}
|
{% elif user.registration.participates %}
|
||||||
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
|
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
|
@ -15,9 +15,6 @@
|
|||||||
<dt class="col-sm-3">{% trans "Round:" %}</dt>
|
<dt class="col-sm-3">{% trans "Round:" %}</dt>
|
||||||
<dd class="col-sm-9">{{ pool.get_round_display }}</dd>
|
<dd class="col-sm-9">{{ pool.get_round_display }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Letter:" %}</dt>
|
|
||||||
<dd class="col-sm-9">{{ pool.get_letter_display }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Teams:" %}</dt>
|
<dt class="col-sm-3">{% trans "Teams:" %}</dt>
|
||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">
|
||||||
{% for participation in pool.participations.all %}
|
{% for participation in pool.participations.all %}
|
||||||
@ -26,40 +23,13 @@
|
|||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Juries:" %}</dt>
|
<dt class="col-sm-3">{% trans "Juries:" %}</dt>
|
||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">{{ pool.juries.all|join:", " }}</dd>
|
||||||
{{ pool.juries.all|join:", " }}
|
|
||||||
<a class="badge rounded-pill text-bg-info" href="{% url 'participation:pool_add_jurys' pk=pool.pk %}">
|
|
||||||
<i class="fas fa-plus"></i> {% trans "Add jurys" %}
|
|
||||||
</a>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Defended solutions:" %}</dt>
|
<dt class="col-sm-3">{% trans "Defended 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.defended_solution.file.url }}">{{ passage.defended_solution }}{% if not forloop.last %}, {% endif %}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<a href="{% url 'participation:pool_download_solutions' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary">
|
|
||||||
<i class="fas fa-download"></i> {% trans "Download all" %}
|
|
||||||
</a>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Syntheses:" %}</dt>
|
|
||||||
<dd class="col-sm-9">
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
{% for passage in pool.passages.all %}
|
|
||||||
<li class="list-group-item">
|
|
||||||
{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }} :
|
|
||||||
{% for synthesis in passage.syntheses.all %}
|
|
||||||
<a href="{{ synthesis.file.url }}">{{ synthesis.participation.team.trigram }} ({{ synthesis.get_type_display }})</a>{% if not forloop.last %}, {% endif %}
|
|
||||||
{% empty %}
|
|
||||||
{% trans "No synthesis was uploaded yet." %}
|
|
||||||
{% endfor %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<a href="{% url 'participation:pool_download_syntheses' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary">
|
|
||||||
<i class="fas fa-download"></i> {% trans "Download all" %}
|
|
||||||
</a>
|
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
|
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
|
||||||
@ -77,31 +47,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if user.registration.is_volunteer %}
|
|
||||||
<div class="card-footer text-center">
|
|
||||||
<div class="btn-group">
|
|
||||||
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}">
|
|
||||||
{% trans "Download the scale sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
|
|
||||||
</a>
|
|
||||||
{% if pool.passages.count == 5 %}
|
|
||||||
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}?page=2">
|
|
||||||
{% trans "Room" %} 2
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<a class="btn btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}">
|
|
||||||
{% trans "Download the final notation sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
|
|
||||||
</a>
|
|
||||||
{% if pool.passages.count == 5 %}
|
|
||||||
<a class="btn btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}?page=2">
|
|
||||||
{% trans "Room" %} 2
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal">{% trans "Upload notes from a CSV file" %}</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if user.registration.is_volunteer %}
|
{% if user.registration.is_volunteer %}
|
||||||
@ -109,6 +54,7 @@
|
|||||||
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addPassageModal">{% trans "Add passage" %}</button>
|
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addPassageModal">{% trans "Add passage" %}</button>
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update" %}</button>
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update" %}</button>
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamsModal">{% trans "Update teams" %}</button>
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamsModal">{% trans "Update teams" %}</button>
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal">{% trans "Upload notes from a CSV file" %}</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
@ -115,14 +115,12 @@
|
|||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% if user.registration.is_volunteer %}
|
{% if user.registration.is_volunteer %}
|
||||||
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a class="btn btn-info" href="{% url "participation:team_authorizations" pk=team.pk %}">
|
<a class="btn btn-info" href="{% url "participation:team_authorizations" pk=team.pk %}">
|
||||||
<i class="fas fa-file-archive"></i> {% trans "Download all submitted authorizations" %}
|
<i class="fas fa-file-archive"></i> {% trans "Download all submitted authorizations" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamModal">{% trans "Update" %}</button>
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamModal">{% trans "Update" %}</button>
|
@ -6,11 +6,6 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<div id="form-content">
|
<div id="form-content">
|
||||||
<div class="alert alert-info">
|
|
||||||
<a class="alert-link" href="{% url "participation:pool_notes_template" pk=pool.pk %}">
|
|
||||||
{% trans "Download empty notation sheet" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
</div>
|
</div>
|
@ -7,9 +7,8 @@
|
|||||||
<div id="form-content">
|
<div id="form-content">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
{% trans "Templates:" %}
|
{% trans "Templates:" %}
|
||||||
<a class="alert-link" href="{% static "Fiche_synthèse.pdf" %}"> PDF</a> —
|
<a class="alert-link" href="{% static "Fiche_synthèse.pdf" %}"> PDF</a> -
|
||||||
<a class="alert-link" href="{% static "Fiche_synthèse.tex" %}"> TEX</a> —
|
<a class="alert-link" href="{% static "Fiche_synthèse.tex" %}"> TEX</a>
|
||||||
<a class="alert-link" href="{% static "Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
@ -4,11 +4,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \
|
from .views import CreateTeamView, JoinTeamView, MyParticipationDetailView, MyTeamDetailView, NoteUpdateView, \
|
||||||
MyTeamDetailView, NoteUpdateView, ParticipationDetailView, PassageCreateView, PassageDetailView, \
|
ParticipationDetailView, PassageCreateView, PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, \
|
||||||
PassageUpdateView, PoolAddJurysView, PoolCreateView, PoolDetailView, PoolDownloadView, PoolNotesTemplateView, \
|
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, SolutionUploadView, SynthesisUploadView,\
|
||||||
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, \
|
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
|
||||||
SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
|
|
||||||
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
|
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
|
||||||
TournamentListView, TournamentUpdateView
|
TournamentListView, TournamentUpdateView
|
||||||
|
|
||||||
@ -37,14 +36,8 @@ urlpatterns = [
|
|||||||
path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
|
path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
|
||||||
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:pk>/solutions/", PoolDownloadView.as_view(), name="pool_download_solutions"),
|
|
||||||
path("pools/<int:pk>/syntheses/", PoolDownloadView.as_view(), name="pool_download_syntheses"),
|
|
||||||
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>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
|
path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
|
||||||
path("pools/<int:pk>/add-jurys/", PoolAddJurysView.as_view(), name="pool_add_jurys"),
|
|
||||||
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
|
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
|
||||||
path("pools/<int:pk>/upload-notes/template/", PoolNotesTemplateView.as_view(), name="pool_notes_template"),
|
|
||||||
path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"),
|
path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"),
|
||||||
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"),
|
@ -1,11 +1,8 @@
|
|||||||
# 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
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import os
|
import os
|
||||||
import subprocess
|
|
||||||
from tempfile import mkdtemp
|
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -20,23 +17,18 @@ from django.shortcuts import redirect
|
|||||||
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
|
||||||
from django.utils.crypto import get_random_string
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
|
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
|
||||||
from django.views.generic.edit import FormMixin, ProcessFormView
|
from django.views.generic.edit import FormMixin, ProcessFormView
|
||||||
from django_tables2 import MultiTableMixin, SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from magic import Magic
|
from magic import Magic
|
||||||
from odf.opendocument import OpenDocumentSpreadsheet
|
from registration.models import StudentRegistration
|
||||||
from odf.style import Style, TableCellProperties, TableColumnProperties, TextProperties
|
|
||||||
from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
|
|
||||||
from odf.text import P
|
|
||||||
from registration.models import StudentRegistration, VolunteerRegistration
|
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
from tfjm.matrix import Matrix
|
from tfjm.matrix import Matrix
|
||||||
from tfjm.views import AdminMixin, VolunteerMixin
|
from tfjm.views import AdminMixin, VolunteerMixin
|
||||||
|
|
||||||
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
|
from .forms import JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, PoolForm, \
|
||||||
PoolForm, PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
|
PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
|
||||||
UploadNotesForm, ValidateParticipationForm
|
UploadNotesForm, ValidateParticipationForm
|
||||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
||||||
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
|
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
|
||||||
@ -392,9 +384,9 @@ class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
|
|||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return super().handle_no_permission()
|
return super().handle_no_permission()
|
||||||
if user.registration.is_admin or user.registration.is_volunteer \
|
if user.registration.is_admin or user.registration.is_volunteer \
|
||||||
and (user.registration in self.get_object().participation.tournament.organizers
|
and (self.get_object().participation.tournament in user.registration.interesting_tournaments
|
||||||
or self.get_object().participation.final
|
or self.get_object().participation.final
|
||||||
and user.registration in Tournament.final_tournament().organizers):
|
and Tournament.final_tournament() in user.registration.interesting_tournaments):
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
@ -555,24 +547,16 @@ class TournamentUpdateView(VolunteerMixin, UpdateView):
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TournamentDetailView(MultiTableMixin, DetailView):
|
class TournamentDetailView(DetailView):
|
||||||
"""
|
"""
|
||||||
Display tournament detail.
|
Display tournament detail.
|
||||||
"""
|
"""
|
||||||
model = Tournament
|
model = Tournament
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
self.tables = [
|
|
||||||
ParticipationTable(self.get_object().participations.all()),
|
|
||||||
PoolTable(self.get_object().pools.order_by('id').all()),
|
|
||||||
]
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
tables = context['tables']
|
context["teams"] = ParticipationTable(self.object.participations.all())
|
||||||
context["teams"] = tables[0]
|
context["pools"] = PoolTable(self.object.pools.order_by('id').all())
|
||||||
context["pools"] = tables[1]
|
|
||||||
|
|
||||||
notes = dict()
|
notes = dict()
|
||||||
for participation in self.object.participations.all():
|
for participation in self.object.participations.all():
|
||||||
@ -600,8 +584,7 @@ class TournamentExportCSVView(VolunteerMixin, DetailView):
|
|||||||
content_type='text/csv',
|
content_type='text/csv',
|
||||||
headers={'Content-Disposition': f'attachment; filename="Tournoi de {tournament.name}.csv"'},
|
headers={'Content-Disposition': f'attachment; filename="Tournoi de {tournament.name}.csv"'},
|
||||||
)
|
)
|
||||||
writer = csv.DictWriter(resp, ('Tournoi', 'Équipe', 'Trigramme', 'Nom', 'Prénom', 'Email',
|
writer = csv.DictWriter(resp, ('Tournoi', 'Équipe', 'Trigramme', 'Nom', 'Prénom', 'Genre', 'Date de naissance'))
|
||||||
'Genre', 'Date de naissance'))
|
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
|
|
||||||
for participation in tournament.participations.filter(valid=True).order_by('team__trigram').all():
|
for participation in tournament.participations.filter(valid=True).order_by('team__trigram').all():
|
||||||
@ -613,7 +596,6 @@ class TournamentExportCSVView(VolunteerMixin, DetailView):
|
|||||||
'Trigramme': participation.team.trigram,
|
'Trigramme': participation.team.trigram,
|
||||||
'Nom': registration.user.last_name,
|
'Nom': registration.user.last_name,
|
||||||
'Prénom': registration.user.first_name,
|
'Prénom': registration.user.first_name,
|
||||||
'Email': registration.user.email,
|
|
||||||
'Genre': registration.get_gender_display() if isinstance(registration, StudentRegistration)
|
'Genre': registration.get_gender_display() if isinstance(registration, StudentRegistration)
|
||||||
else 'Encandrant⋅e',
|
else 'Encandrant⋅e',
|
||||||
'Date de naissance': registration.birth_date if isinstance(registration, StudentRegistration)
|
'Date de naissance': registration.birth_date if isinstance(registration, StudentRegistration)
|
||||||
@ -733,106 +715,6 @@ class PoolUpdateTeamsView(VolunteerMixin, UpdateView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
|
|
||||||
class PoolDownloadView(VolunteerMixin, DetailView):
|
|
||||||
"""
|
|
||||||
Download all solutions or syntheses as a ZIP archive.
|
|
||||||
"""
|
|
||||||
model = Pool
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return self.handle_no_permission()
|
|
||||||
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
|
||||||
and (self.get_object().tournament in request.user.registration.organized_tournaments.all()
|
|
||||||
or request.user.registration in self.get_object().juries.all()):
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
return self.handle_no_permission()
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
pool = self.get_object()
|
|
||||||
|
|
||||||
is_solution = 'solutions' in request.path
|
|
||||||
|
|
||||||
output = BytesIO()
|
|
||||||
zf = ZipFile(output, "w")
|
|
||||||
for s in (pool.solutions if is_solution else Synthesis.objects.filter(passage__pool=pool).all()):
|
|
||||||
zf.write("media/" + s.file.name, f"{s}.pdf")
|
|
||||||
|
|
||||||
zf.close()
|
|
||||||
response = HttpResponse(content_type="application/zip")
|
|
||||||
filename = _("Solutions for pool {pool} of tournament {tournament}.zip") \
|
|
||||||
if is_solution else _("Syntheses for pool {pool} of tournament {tournament}.zip")
|
|
||||||
filename = filename.format(pool=pool.get_letter_display() + str(pool.round), tournament=pool.tournament.name)
|
|
||||||
response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \
|
|
||||||
.format(filename=filename)
|
|
||||||
response.write(output.getvalue())
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class PoolAddJurysView(VolunteerMixin, FormView, DetailView):
|
|
||||||
"""
|
|
||||||
This view lets organizers set jurys for a pool, without multiplying clicks.
|
|
||||||
"""
|
|
||||||
model = Pool
|
|
||||||
form_class = AddJuryForm
|
|
||||||
template_name = 'participation/pool_add_jurys.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['title'] = _("Jurys of {pool}").format(pool=self.object)
|
|
||||||
return context
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def form_valid(self, form):
|
|
||||||
self.object = self.get_object()
|
|
||||||
|
|
||||||
# Save the user object first
|
|
||||||
form.save()
|
|
||||||
user = form.instance
|
|
||||||
# Create associated registration object to the new user
|
|
||||||
reg = VolunteerRegistration.objects.create(
|
|
||||||
user=user,
|
|
||||||
professional_activity="Juré⋅e du tournoi " + self.object.tournament.name,
|
|
||||||
)
|
|
||||||
# Add the user in the jury
|
|
||||||
self.object.juries.add(reg)
|
|
||||||
self.object.save()
|
|
||||||
|
|
||||||
reg.send_email_validation_link()
|
|
||||||
|
|
||||||
# Generate new password for the user
|
|
||||||
password = get_random_string(16)
|
|
||||||
user.set_password(password)
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
# Send welcome mail
|
|
||||||
subject = "[TFJM²] " + str(_("New TFJM² jury account"))
|
|
||||||
site = Site.objects.first()
|
|
||||||
message = render_to_string('registration/mails/add_organizer.txt', dict(user=user,
|
|
||||||
inviter=self.request.user,
|
|
||||||
password=password,
|
|
||||||
domain=site.domain))
|
|
||||||
html = render_to_string('registration/mails/add_organizer.html', dict(user=user,
|
|
||||||
inviter=self.request.user,
|
|
||||||
password=password,
|
|
||||||
domain=site.domain))
|
|
||||||
user.email_user(subject, message, html_message=html)
|
|
||||||
|
|
||||||
# Add notification
|
|
||||||
messages.success(self.request, _("The jury {name} has been successfully added!")
|
|
||||||
.format(name=f"{user.first_name} {user.last_name}"))
|
|
||||||
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
def form_invalid(self, form):
|
|
||||||
# This is useful since we have a FormView + a DetailView
|
|
||||||
self.object = self.get_object()
|
|
||||||
return super().form_invalid(form)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse_lazy('participation:pool_add_jurys', args=(self.kwargs['pk'],))
|
|
||||||
|
|
||||||
|
|
||||||
class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
|
class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
|
||||||
model = Pool
|
model = Pool
|
||||||
form_class = UploadNotesForm
|
form_class = UploadNotesForm
|
||||||
@ -841,10 +723,9 @@ class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
|
||||||
if request.user.is_authenticated and \
|
if request.user.registration.is_admin or request.user.registration.is_volunteer \
|
||||||
(request.user.registration.is_admin or request.user.registration.is_volunteer
|
|
||||||
and (self.object.tournament in request.user.registration.organized_tournaments.all()
|
and (self.object.tournament in request.user.registration.organized_tournaments.all()
|
||||||
or request.user.registration in self.object.juries.all())):
|
or request.user.registration in self.object.juries.all()):
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
@ -854,19 +735,13 @@ class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
|
|||||||
pool = self.get_object()
|
pool = self.get_object()
|
||||||
parsed_notes = form.cleaned_data['parsed_notes']
|
parsed_notes = form.cleaned_data['parsed_notes']
|
||||||
|
|
||||||
for vr in parsed_notes.keys():
|
for vr, notes in parsed_notes.items():
|
||||||
if vr not in pool.juries.all():
|
if vr not in pool.juries.all():
|
||||||
form.add_error('file', _("The following user is not registered as a jury:") + " " + str(vr))
|
form.add_error('file', _("The following user is not registered as a jury:") + " " + str(vr))
|
||||||
|
|
||||||
if form.errors:
|
|
||||||
return self.form_invalid(form)
|
|
||||||
|
|
||||||
for vr, notes in parsed_notes.items():
|
|
||||||
# There is an observer note for 4-teams pools
|
|
||||||
notes_count = 7 if pool.passages.count() == 4 else 6
|
|
||||||
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[6 * i:6 * (i + 1)]
|
||||||
note.set_all(*passage_notes)
|
note.set_all(*passage_notes)
|
||||||
note.save()
|
note.save()
|
||||||
|
|
||||||
@ -877,555 +752,6 @@ class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
|
|||||||
return reverse_lazy('participation:pool_detail', args=(self.kwargs['pk'],))
|
return reverse_lazy('participation:pool_detail', args=(self.kwargs['pk'],))
|
||||||
|
|
||||||
|
|
||||||
class PoolNotesTemplateView(VolunteerMixin, DetailView):
|
|
||||||
"""
|
|
||||||
Generate an ODS sheet to fill the notes of the pool.
|
|
||||||
"""
|
|
||||||
model = Pool
|
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs): # noqa: C901
|
|
||||||
pool_size = self.object.passages.count()
|
|
||||||
passage_width = 7 if pool_size == 4 else 6
|
|
||||||
line_length = pool_size * passage_width
|
|
||||||
|
|
||||||
def getcol(number: int) -> str:
|
|
||||||
"""
|
|
||||||
Translates the given number to the nth column name
|
|
||||||
"""
|
|
||||||
if number == 0:
|
|
||||||
return ''
|
|
||||||
return getcol((number - 1) // 26) + chr(65 + (number - 1) % 26)
|
|
||||||
|
|
||||||
doc = OpenDocumentSpreadsheet()
|
|
||||||
|
|
||||||
# Define styles
|
|
||||||
style = Style(name="Contenu", family="table-cell")
|
|
||||||
style.addElement(TableCellProperties(border="0.75pt solid #000000"))
|
|
||||||
doc.styles.addElement(style)
|
|
||||||
|
|
||||||
style_left = Style(name="Contenu gauche", family="table-cell")
|
|
||||||
style_left.addElement(TableCellProperties(border="0.75pt solid #000000", borderleft="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_left)
|
|
||||||
|
|
||||||
style_right = Style(name="Contenu droite", family="table-cell")
|
|
||||||
style_right.addElement(TableCellProperties(border="0.75pt solid #000000", borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_right)
|
|
||||||
|
|
||||||
style_top = Style(name="Contenu haut", family="table-cell")
|
|
||||||
style_top.addElement(TableCellProperties(border="0.75pt solid #000000", bordertop="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_top)
|
|
||||||
|
|
||||||
style_topright = Style(name="Contenu haut droite", family="table-cell")
|
|
||||||
style_topright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderright="2pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_topright)
|
|
||||||
|
|
||||||
style_topleftright = Style(name="Contenu haut gauche droite", family="table-cell")
|
|
||||||
style_topleftright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000",
|
|
||||||
borderright="2pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_topleftright)
|
|
||||||
|
|
||||||
style_leftright = Style(name="Contenu haut gauche droite", family="table-cell")
|
|
||||||
style_leftright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000",
|
|
||||||
borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_leftright)
|
|
||||||
|
|
||||||
style_botleft = Style(name="Contenu bas gauche", family="table-cell")
|
|
||||||
style_botleft.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderbottom="2pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_botleft)
|
|
||||||
|
|
||||||
style_bot = Style(name="Contenu bas", family="table-cell")
|
|
||||||
style_bot.addElement(TableCellProperties(border="0.75pt solid #000000", borderbottom="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_bot)
|
|
||||||
|
|
||||||
style_botright = Style(name="Contenu bas droite", family="table-cell")
|
|
||||||
style_botright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderbottom="2pt solid #000000",
|
|
||||||
borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(style_botright)
|
|
||||||
|
|
||||||
title_style = Style(name="Titre", family="table-cell")
|
|
||||||
title_style.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style.addElement(TableCellProperties(border="0.75pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style)
|
|
||||||
|
|
||||||
title_style_left = Style(name="Titre gauche", family="table-cell")
|
|
||||||
title_style_left.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_left.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_left)
|
|
||||||
|
|
||||||
title_style_right = Style(name="Titre droite", family="table-cell")
|
|
||||||
title_style_right.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_right.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_right)
|
|
||||||
|
|
||||||
title_style_leftright = Style(name="Titre gauche droite", family="table-cell")
|
|
||||||
title_style_leftright.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_leftright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000",
|
|
||||||
borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_leftright)
|
|
||||||
|
|
||||||
title_style_top = Style(name="Titre haut", family="table-cell")
|
|
||||||
title_style_top.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_top.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_top)
|
|
||||||
|
|
||||||
title_style_topbot = Style(name="Titre haut bas", family="table-cell")
|
|
||||||
title_style_topbot.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_topbot.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000",
|
|
||||||
borderbottom="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_topbot)
|
|
||||||
|
|
||||||
title_style_topleft = Style(name="Titre haut gauche", family="table-cell")
|
|
||||||
title_style_topleft.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_topleft.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_topleft)
|
|
||||||
|
|
||||||
title_style_topbotleft = Style(name="Titre haut bas gauche", family="table-cell")
|
|
||||||
title_style_topbotleft.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_topbotleft.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000",
|
|
||||||
borderbottom="2pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_topbotleft)
|
|
||||||
|
|
||||||
title_style_topright = Style(name="Titre haut droite", family="table-cell")
|
|
||||||
title_style_topright.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_topright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000",
|
|
||||||
borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_topright)
|
|
||||||
|
|
||||||
title_style_topbotright = Style(name="Titre haut bas droite", family="table-cell")
|
|
||||||
title_style_topbotright.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_topbotright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000",
|
|
||||||
borderbottom="2pt solid #000000",
|
|
||||||
borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_topbotright)
|
|
||||||
|
|
||||||
title_style_topleftright = Style(name="Titre haut gauche droite", family="table-cell")
|
|
||||||
title_style_topleftright.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_topleftright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
bordertop="2pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000",
|
|
||||||
borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_topleftright)
|
|
||||||
|
|
||||||
title_style_bot = Style(name="Titre bas", family="table-cell")
|
|
||||||
title_style_bot.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_bot.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderbottom="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_bot)
|
|
||||||
|
|
||||||
title_style_botleft = Style(name="Titre bas gauche", family="table-cell")
|
|
||||||
title_style_botleft.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_botleft.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderbottom="2pt solid #000000",
|
|
||||||
borderleft="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_botleft)
|
|
||||||
|
|
||||||
title_style_botright = Style(name="Titre bas droite", family="table-cell")
|
|
||||||
title_style_botright.addElement(TextProperties(fontweight="bold"))
|
|
||||||
title_style_botright.addElement(TableCellProperties(border="0.75pt solid #000000",
|
|
||||||
borderbottom="2pt solid #000000",
|
|
||||||
borderright="2pt solid #000000"))
|
|
||||||
doc.styles.addElement(title_style_botright)
|
|
||||||
|
|
||||||
first_col_style = Style(name="co1", family="table-column")
|
|
||||||
first_col_style.addElement(TableColumnProperties(columnwidth="9cm", breakbefore="auto"))
|
|
||||||
doc.automaticstyles.addElement(first_col_style)
|
|
||||||
|
|
||||||
col_style = Style(name="co2", family="table-column")
|
|
||||||
col_style.addElement(TableColumnProperties(columnwidth="2.6cm", breakbefore="auto"))
|
|
||||||
doc.automaticstyles.addElement(col_style)
|
|
||||||
|
|
||||||
obs_col_style = Style(name="co3", family="table-column")
|
|
||||||
obs_col_style.addElement(TableColumnProperties(columnwidth="5.2cm", breakbefore="auto"))
|
|
||||||
doc.automaticstyles.addElement(obs_col_style)
|
|
||||||
|
|
||||||
table = Table(name=f"Poule {self.object.get_letter_display()}{self.object.round}")
|
|
||||||
doc.spreadsheet.addElement(table)
|
|
||||||
|
|
||||||
table.addElement(TableColumn(stylename=first_col_style))
|
|
||||||
|
|
||||||
for i in range(line_length):
|
|
||||||
table.addElement(TableColumn(stylename=obs_col_style if pool_size == 4
|
|
||||||
and i % passage_width == passage_width - 1 else col_style))
|
|
||||||
|
|
||||||
# Add line for the problems for different passages
|
|
||||||
header_pb = TableRow()
|
|
||||||
table.addElement(header_pb)
|
|
||||||
problems_tc = TableCell(valuetype="string", stylename=title_style_topleft)
|
|
||||||
problems_tc.addElement(P(text="Problème"))
|
|
||||||
header_pb.addElement(problems_tc)
|
|
||||||
for passage in self.object.passages.all():
|
|
||||||
tc = TableCell(valuetype="string", stylename=title_style_topleftright)
|
|
||||||
tc.addElement(P(text=f"Problème {passage.solution_number}"))
|
|
||||||
tc.setAttribute('numbercolumnsspanned', "7" if pool_size == 4 else "6")
|
|
||||||
tc.setAttribute("formula", f"of:=[.B{8 + self.object.juries.count() + passage.position}]")
|
|
||||||
header_pb.addElement(tc)
|
|
||||||
header_pb.addElement(CoveredTableCell(numbercolumnsrepeated=6 if pool_size == 4 else 5))
|
|
||||||
|
|
||||||
# Add roles on the second line of the table
|
|
||||||
header_role = TableRow()
|
|
||||||
table.addElement(header_role)
|
|
||||||
role_tc = TableCell(valuetype="string", stylename=title_style_left)
|
|
||||||
role_tc.addElement(P(text="Rôle"))
|
|
||||||
header_role.addElement(role_tc)
|
|
||||||
for i in range(pool_size):
|
|
||||||
defender_tc = TableCell(valuetype="string", stylename=title_style_left)
|
|
||||||
defender_tc.addElement(P(text="Défenseur⋅se"))
|
|
||||||
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 if pool_size != 4 else title_style)
|
|
||||||
reporter_tc.addElement(P(text="Rapporteur⋅e"))
|
|
||||||
reporter_tc.setAttribute('numbercolumnsspanned', "2")
|
|
||||||
header_role.addElement(reporter_tc)
|
|
||||||
header_role.addElement(CoveredTableCell())
|
|
||||||
|
|
||||||
if pool_size == 4:
|
|
||||||
observer_tc = TableCell(valuetype="string", stylename=title_style_right)
|
|
||||||
observer_tc.addElement(P(text="Intervention exceptionnelle"))
|
|
||||||
header_role.addElement(observer_tc)
|
|
||||||
|
|
||||||
# Add maximum notes on the third line
|
|
||||||
header_notes = TableRow()
|
|
||||||
table.addElement(header_notes)
|
|
||||||
jury_tc = TableCell(valuetype="string", value="Juré⋅e", stylename=title_style_botleft)
|
|
||||||
jury_tc.addElement(P(text="Juré⋅e"))
|
|
||||||
header_notes.addElement(jury_tc)
|
|
||||||
|
|
||||||
for i in range(pool_size):
|
|
||||||
defender_w_tc = TableCell(valuetype="string", stylename=title_style_botleft)
|
|
||||||
defender_w_tc.addElement(P(text="Écrit (/20)"))
|
|
||||||
header_notes.addElement(defender_w_tc)
|
|
||||||
|
|
||||||
defender_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
|
||||||
defender_o_tc.addElement(P(text="Oral (/16)"))
|
|
||||||
header_notes.addElement(defender_o_tc)
|
|
||||||
|
|
||||||
opponent_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
|
||||||
opponent_w_tc.addElement(P(text="Écrit (/9)"))
|
|
||||||
header_notes.addElement(opponent_w_tc)
|
|
||||||
|
|
||||||
opponent_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
|
||||||
opponent_o_tc.addElement(P(text="Oral (/10)"))
|
|
||||||
header_notes.addElement(opponent_o_tc)
|
|
||||||
|
|
||||||
reporter_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
|
|
||||||
reporter_w_tc.addElement(P(text="Écrit (/9)"))
|
|
||||||
header_notes.addElement(reporter_w_tc)
|
|
||||||
|
|
||||||
reporter_o_tc = TableCell(valuetype="string",
|
|
||||||
stylename=title_style_botright if pool_size != 4 else title_style_bot)
|
|
||||||
reporter_o_tc.addElement(P(text="Oral (/10)"))
|
|
||||||
header_notes.addElement(reporter_o_tc)
|
|
||||||
|
|
||||||
if pool_size == 4:
|
|
||||||
observer_tc = TableCell(valuetype="string",
|
|
||||||
stylename=title_style_botright)
|
|
||||||
observer_tc.addElement(P(text="Oral (± 4)"))
|
|
||||||
header_notes.addElement(observer_tc)
|
|
||||||
|
|
||||||
# Add a notation line for each jury
|
|
||||||
for jury in self.object.juries.all():
|
|
||||||
jury_row = TableRow()
|
|
||||||
table.addElement(jury_row)
|
|
||||||
|
|
||||||
name_tc = TableCell(valuetype="string", stylename=style_leftright)
|
|
||||||
name_tc.addElement(P(text=f"{jury.user.first_name} {jury.user.last_name}"))
|
|
||||||
jury_row.addElement(name_tc)
|
|
||||||
|
|
||||||
for passage in self.object.passages.all():
|
|
||||||
notes = Note.objects.get(jury=jury, passage=passage)
|
|
||||||
for j, note in enumerate(notes.get_all()):
|
|
||||||
note_tc = TableCell(valuetype="float", value=note,
|
|
||||||
stylename=style_right if j == passage_width - 1 else style)
|
|
||||||
note_tc.addElement(P(text=str(note)))
|
|
||||||
jury_row.addElement(note_tc)
|
|
||||||
|
|
||||||
jury_size = self.object.juries.count()
|
|
||||||
min_row = 4
|
|
||||||
max_row = 4 + jury_size - 1
|
|
||||||
min_column = 2
|
|
||||||
|
|
||||||
# Add line for averages
|
|
||||||
average_row = TableRow()
|
|
||||||
table.addElement(average_row)
|
|
||||||
average_tc = TableCell(valuetype="string", stylename=title_style_topleftright)
|
|
||||||
average_tc.addElement(P(text="Moyenne"))
|
|
||||||
average_row.addElement(average_tc)
|
|
||||||
for i, passage in enumerate(self.object.passages.all()):
|
|
||||||
for j, note in enumerate(passage.averages):
|
|
||||||
tc = TableCell(valuetype="float", value=note,
|
|
||||||
stylename=style_topright if j == passage_width - 1 else style_top)
|
|
||||||
tc.addElement(P(text=str(note)))
|
|
||||||
column = getcol(min_column + i * passage_width + j)
|
|
||||||
tc.setAttribute("formula", f"of:=AVERAGEIF([.${getcol(min_column + i * passage_width)}${min_row}"
|
|
||||||
f":${getcol(min_column + i * passage_width)}{max_row}]; \">0\"; "
|
|
||||||
f"[.{column}${min_row}:{column}{max_row}])")
|
|
||||||
average_row.addElement(tc)
|
|
||||||
|
|
||||||
# Add coefficients for each note on the next line
|
|
||||||
coeff_row = TableRow()
|
|
||||||
table.addElement(coeff_row)
|
|
||||||
coeff_tc = TableCell(valuetype="string", stylename=title_style_leftright)
|
|
||||||
coeff_tc.addElement(P(text="Coefficient"))
|
|
||||||
coeff_row.addElement(coeff_tc)
|
|
||||||
for passage in self.object.passages.all():
|
|
||||||
defender_w_tc = TableCell(valuetype="float", value=1, stylename=style_left)
|
|
||||||
defender_w_tc.addElement(P(text="1"))
|
|
||||||
coeff_row.addElement(defender_w_tc)
|
|
||||||
|
|
||||||
defender_o_tc = TableCell(valuetype="float", value=2 - 0.5 * passage.defender_penalties, stylename=style)
|
|
||||||
defender_o_tc.addElement(P(text=str(2 - 0.5 * passage.defender_penalties)))
|
|
||||||
coeff_row.addElement(defender_o_tc)
|
|
||||||
|
|
||||||
opponent_w_tc = TableCell(valuetype="float", value=1, 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=1, stylename=style)
|
|
||||||
reporter_w_tc.addElement(P(text="1"))
|
|
||||||
coeff_row.addElement(reporter_w_tc)
|
|
||||||
|
|
||||||
reporter_o_tc = TableCell(valuetype="float", value=1,
|
|
||||||
stylename=style_right if pool_size != 4 else style)
|
|
||||||
reporter_o_tc.addElement(P(text="1"))
|
|
||||||
coeff_row.addElement(reporter_o_tc)
|
|
||||||
|
|
||||||
if pool_size == 4:
|
|
||||||
observer_tc = TableCell(valuetype="float", value=1, stylename=style_right)
|
|
||||||
observer_tc.addElement(P(text="1"))
|
|
||||||
coeff_row.addElement(observer_tc)
|
|
||||||
|
|
||||||
# Add the subtotal on the next line
|
|
||||||
subtotal_row = TableRow()
|
|
||||||
table.addElement(subtotal_row)
|
|
||||||
subtotal_tc = TableCell(valuetype="string", stylename=title_style_botleft)
|
|
||||||
subtotal_tc.addElement(P(text="Sous-total"))
|
|
||||||
subtotal_row.addElement(subtotal_tc)
|
|
||||||
for i, passage in enumerate(self.object.passages.all()):
|
|
||||||
def_w_col = getcol(min_column + passage_width * i)
|
|
||||||
def_o_col = getcol(min_column + passage_width * i + 1)
|
|
||||||
defender_tc = TableCell(valuetype="float", value=passage.average_defender, stylename=style_botleft)
|
|
||||||
defender_tc.addElement(P(text=str(passage.average_defender)))
|
|
||||||
defender_tc.setAttribute('numbercolumnsspanned', "2")
|
|
||||||
defender_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}]")
|
|
||||||
subtotal_row.addElement(defender_tc)
|
|
||||||
subtotal_row.addElement(CoveredTableCell())
|
|
||||||
|
|
||||||
opp_w_col = getcol(min_column + passage_width * i + 2)
|
|
||||||
opp_o_col = getcol(min_column + passage_width * i + 3)
|
|
||||||
opponent_tc = TableCell(valuetype="float", value=passage.average_opponent, stylename=style_bot)
|
|
||||||
opponent_tc.addElement(P(text=str(passage.average_opponent)))
|
|
||||||
opponent_tc.setAttribute('numbercolumnsspanned', "2")
|
|
||||||
opponent_tc.setAttribute("formula", f"of:=[.{opp_w_col}{max_row + 1}] * [.{opp_w_col}{max_row + 2}]"
|
|
||||||
f" + [.{opp_o_col}{max_row + 1}] * [.{opp_o_col}{max_row + 2}]")
|
|
||||||
subtotal_row.addElement(opponent_tc)
|
|
||||||
subtotal_row.addElement(CoveredTableCell())
|
|
||||||
|
|
||||||
rep_w_col = getcol(min_column + passage_width * i + 4)
|
|
||||||
rep_o_col = getcol(min_column + passage_width * i + 5)
|
|
||||||
reporter_tc = TableCell(valuetype="float", value=passage.average_reporter,
|
|
||||||
stylename=style_botright if pool_size != 4 else style_bot)
|
|
||||||
reporter_tc.addElement(P(text=str(passage.average_reporter)))
|
|
||||||
reporter_tc.setAttribute('numbercolumnsspanned', "2")
|
|
||||||
reporter_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}]")
|
|
||||||
subtotal_row.addElement(reporter_tc)
|
|
||||||
subtotal_row.addElement(CoveredTableCell())
|
|
||||||
|
|
||||||
if pool_size == 4:
|
|
||||||
obs_col = getcol(min_column + passage_width * i + 6)
|
|
||||||
observer_tc = TableCell(valuetype="float", value=passage.average_observer,
|
|
||||||
stylename=style_botright)
|
|
||||||
observer_tc.addElement(P(text=str(passage.average_observer)))
|
|
||||||
observer_tc.setAttribute("formula", f"of:=[.{obs_col}{max_row + 1}] * [.{obs_col}{max_row + 2}]")
|
|
||||||
subtotal_row.addElement(observer_tc)
|
|
||||||
|
|
||||||
table.addElement(TableRow())
|
|
||||||
|
|
||||||
# Compute the total scores in a new table
|
|
||||||
scores_header = TableRow()
|
|
||||||
table.addElement(scores_header)
|
|
||||||
team_tc = TableCell(valuetype="string", stylename=title_style_topbotleft)
|
|
||||||
team_tc.addElement(P(text="Équipe"))
|
|
||||||
scores_header.addElement(team_tc)
|
|
||||||
problem_tc = TableCell(valuetype="string", stylename=title_style_topbot)
|
|
||||||
problem_tc.addElement(P(text="Problème"))
|
|
||||||
scores_header.addElement(problem_tc)
|
|
||||||
total_tc = TableCell(valuetype="string", stylename=title_style_topbot)
|
|
||||||
total_tc.addElement(P(text="Total"))
|
|
||||||
scores_header.addElement(total_tc)
|
|
||||||
rank_tc = TableCell(valuetype="string", stylename=title_style_topbotright)
|
|
||||||
rank_tc.addElement(P(text="Rang"))
|
|
||||||
scores_header.addElement(rank_tc)
|
|
||||||
|
|
||||||
# For each line of the matrix P, the ith team is defender on the passage number Pi0,
|
|
||||||
# opponent on the passage number Pi1, reporter on the passage number Pi2
|
|
||||||
# and eventually observer on the passage number Pi3.
|
|
||||||
passage_matrix = []
|
|
||||||
match pool_size:
|
|
||||||
case 3:
|
|
||||||
passage_matrix = [
|
|
||||||
[0, 2, 1],
|
|
||||||
[1, 0, 2],
|
|
||||||
[2, 1, 0],
|
|
||||||
]
|
|
||||||
case 4:
|
|
||||||
passage_matrix = [
|
|
||||||
[0, 3, 2, 1],
|
|
||||||
[1, 0, 3, 2],
|
|
||||||
[2, 1, 0, 3],
|
|
||||||
[3, 2, 1, 0],
|
|
||||||
]
|
|
||||||
case 5:
|
|
||||||
passage_matrix = [
|
|
||||||
[0, 2, 3],
|
|
||||||
[1, 4, 2],
|
|
||||||
[2, 0, 4],
|
|
||||||
[3, 1, 0],
|
|
||||||
[4, 3, 1],
|
|
||||||
]
|
|
||||||
|
|
||||||
sorted_participations = sorted(self.object.participations.all(), key=lambda p: -self.object.average(p))
|
|
||||||
for passage in self.object.passages.all():
|
|
||||||
team_row = TableRow()
|
|
||||||
table.addElement(team_row)
|
|
||||||
|
|
||||||
team_tc = TableCell(valuetype="string",
|
|
||||||
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_row.addElement(team_tc)
|
|
||||||
|
|
||||||
problem_tc = TableCell(valuetype="string",
|
|
||||||
stylename=style_bot if passage.position == pool_size else style)
|
|
||||||
problem_tc.addElement(P(text=f"Problème {passage.solution_number}"))
|
|
||||||
team_row.addElement(problem_tc)
|
|
||||||
|
|
||||||
passage_line = passage_matrix[passage.position - 1]
|
|
||||||
score_tc = TableCell(valuetype="float", value=self.object.average(passage.defender),
|
|
||||||
stylename=style_bot if passage.position == pool_size else style)
|
|
||||||
score_tc.addElement(P(text=self.object.average(passage.defender)))
|
|
||||||
formula = "of:="
|
|
||||||
formula += getcol(min_column + passage_line[0] * passage_width) + str(max_row + 3) # Defender
|
|
||||||
formula += " + " + getcol(min_column + passage_line[1] * passage_width + 2) + str(max_row + 3) # Opponent
|
|
||||||
formula += " + " + getcol(min_column + passage_line[2] * passage_width + 4) + str(max_row + 3) # Reporter
|
|
||||||
if pool_size == 4:
|
|
||||||
# Observer
|
|
||||||
formula += " + " + getcol(min_column + passage_line[3] * passage_width + 6) + str(max_row + 3)
|
|
||||||
score_tc.setAttribute("formula", formula)
|
|
||||||
team_row.addElement(score_tc)
|
|
||||||
|
|
||||||
score_col = 'C'
|
|
||||||
rank_tc = TableCell(valuetype="float", value=sorted_participations.index(passage.defender) + 1,
|
|
||||||
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.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}])")
|
|
||||||
team_row.addElement(rank_tc)
|
|
||||||
|
|
||||||
table.addElement(TableRow())
|
|
||||||
|
|
||||||
# Add small instructions
|
|
||||||
instructions_tr = TableRow()
|
|
||||||
table.addElement(instructions_tr)
|
|
||||||
instructions_tc = TableCell()
|
|
||||||
instructions_tc.addElement(P(text="Merci de ne pas toucher aux noms des juré⋅es.\n"
|
|
||||||
"Si nécessaire, faites les modifications sur le site\n"
|
|
||||||
"et récupérez le nouveau template.\n"
|
|
||||||
"N'entrez que des notes entières.\n"
|
|
||||||
"Ne retirez pas de 0 : toute ligne incomplète sera ignorée.\n"
|
|
||||||
"Dans le cadre de poules à 5, laissez des 0 en face des\n"
|
|
||||||
"juré⋅es qui ne sont pas dans le passage souhaité,\n"
|
|
||||||
"et remplissez uniquement les notes nécessaires dans le tableau.\n"
|
|
||||||
"Les moyennes calculées ignorent les 0, donc pas d'inquiétude."))
|
|
||||||
instructions_tr.addElement(instructions_tc)
|
|
||||||
|
|
||||||
# Save the sheet in a temporary file and send it in the response
|
|
||||||
doc.save('/tmp/notes.ods')
|
|
||||||
|
|
||||||
return FileResponse(streaming_content=open("/tmp/notes.ods", "rb"),
|
|
||||||
content_type="application/vnd.oasis.opendocument.spreadsheet",
|
|
||||||
filename=f"Feuille de notes - {self.object.tournament.name} "
|
|
||||||
f"- Poule {self.object.get_letter_display()}{self.object.round}.ods")
|
|
||||||
|
|
||||||
|
|
||||||
class NotationSheetTemplateView(VolunteerMixin, DetailView):
|
|
||||||
"""
|
|
||||||
Generate a PDF from a LaTeX template for the notation papers.
|
|
||||||
"""
|
|
||||||
model = Pool
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
passages = self.object.passages.all()
|
|
||||||
if passages.count() == 5:
|
|
||||||
page = self.request.GET.get('page', '1')
|
|
||||||
if not page.isnumeric() or page not in ['1', '2']:
|
|
||||||
page = '1'
|
|
||||||
passages = passages.filter(id__in=[passages[0].id, passages[2].id, passages[4].id]
|
|
||||||
if page == '1' else [passages[1].id, passages[3].id])
|
|
||||||
context['page'] = page
|
|
||||||
|
|
||||||
context['passages'] = passages
|
|
||||||
context['esp'] = passages.count() * '&'
|
|
||||||
context['is_jury'] = self.request.user.registration in self.object.juries.all() \
|
|
||||||
and 'blank' not in self.request.GET
|
|
||||||
context['tfjm_number'] = timezone.now().year - 2010
|
|
||||||
return context
|
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
|
||||||
tex = render_to_string(self.template_name, context=context, request=self.request)
|
|
||||||
temp_dir = mkdtemp()
|
|
||||||
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
|
|
||||||
f.write(tex)
|
|
||||||
process = subprocess.Popen(["pdflatex", "-interaction=nonstopmode", f"-output-directory={temp_dir}",
|
|
||||||
os.path.join(temp_dir, "texput.tex"), ])
|
|
||||||
process.wait()
|
|
||||||
return FileResponse(streaming_content=open(os.path.join(temp_dir, "texput.pdf"), "rb"),
|
|
||||||
content_type="application/pdf",
|
|
||||||
filename=self.template_name.split("/")[-1][:-3] + "pdf")
|
|
||||||
|
|
||||||
|
|
||||||
class ScaleNotationSheetTemplateView(NotationSheetTemplateView):
|
|
||||||
template_name = 'participation/tex/bareme.tex'
|
|
||||||
|
|
||||||
|
|
||||||
class FinalNotationSheetTemplateView(NotationSheetTemplateView):
|
|
||||||
template_name = 'participation/tex/finale.tex'
|
|
||||||
|
|
||||||
|
|
||||||
class PassageCreateView(VolunteerMixin, CreateView):
|
class PassageCreateView(VolunteerMixin, CreateView):
|
||||||
model = Passage
|
model = Passage
|
||||||
form_class = PassageForm
|
form_class = PassageForm
|
||||||
@ -1478,9 +804,6 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
|
|||||||
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
|
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
|
||||||
elif self.request.user.registration.is_admin:
|
elif self.request.user.registration.is_admin:
|
||||||
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
|
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
|
||||||
if 'notes' in context and not self.object.observer:
|
|
||||||
# Only display the observer column for 4-teams pools
|
|
||||||
context['notes']._sequence.pop()
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -1564,10 +887,3 @@ class NoteUpdateView(VolunteerMixin, UpdateView):
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
|
||||||
form = super().get_form(form_class)
|
|
||||||
if not self.object.passage.observer:
|
|
||||||
# Set the note of the observer only for 4-teams pools
|
|
||||||
del form.fields['observer_oral']
|
|
||||||
return form
|
|
37
apps/registration/admin.py
Normal file
37
apps/registration/admin.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Copyright (C) 2020 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.admin import ModelAdmin
|
||||||
|
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin
|
||||||
|
|
||||||
|
from .models import CoachRegistration, Payment, Registration, StudentRegistration, VolunteerRegistration
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Registration)
|
||||||
|
class RegistrationAdmin(PolymorphicParentModelAdmin):
|
||||||
|
child_models = (StudentRegistration, CoachRegistration, VolunteerRegistration,)
|
||||||
|
list_display = ("user", "type", "email_confirmed",)
|
||||||
|
polymorphic_list = True
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(StudentRegistration)
|
||||||
|
class StudentRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CoachRegistration)
|
||||||
|
class CoachRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(VolunteerRegistration)
|
||||||
|
class VolunteerRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Payment)
|
||||||
|
class PaymentAdmin(ModelAdmin):
|
||||||
|
list_display = ('registration', 'type', 'valid', )
|
||||||
|
search_fields = ('registration__user__last_name', 'registration__user__first_name', 'registration__user__email',)
|
||||||
|
list_filter = ('type', 'valid',)
|
@ -82,15 +82,6 @@ class UserForm(forms.ModelForm):
|
|||||||
self.fields["last_name"].required = True
|
self.fields["last_name"].required = True
|
||||||
self.fields["email"].required = True
|
self.fields["email"].required = True
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
"""
|
|
||||||
Ensure that the email address is unique.
|
|
||||||
"""
|
|
||||||
email = self.data["email"]
|
|
||||||
if User.objects.filter(email=email).exclude(pk=self.instance.pk).exists():
|
|
||||||
self.add_error("email", _("This email address is already used."))
|
|
||||||
return email
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ('first_name', 'last_name', 'email',)
|
fields = ('first_name', 'last_name', 'email',)
|
@ -191,10 +191,6 @@ class ParticipantRegistration(Registration):
|
|||||||
def form_class(self): # pragma: no cover
|
def form_class(self): # pragma: no cover
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("participant registration")
|
|
||||||
verbose_name_plural = _("participant registrations")
|
|
||||||
|
|
||||||
|
|
||||||
class StudentRegistration(ParticipantRegistration):
|
class StudentRegistration(ParticipantRegistration):
|
||||||
"""
|
"""
|
||||||
@ -321,10 +317,6 @@ class VolunteerRegistration(Registration):
|
|||||||
from registration.forms import VolunteerRegistrationForm
|
from registration.forms import VolunteerRegistrationForm
|
||||||
return VolunteerRegistrationForm
|
return VolunteerRegistrationForm
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("volunteer registration")
|
|
||||||
verbose_name_plural = _("volunteer registrations")
|
|
||||||
|
|
||||||
|
|
||||||
def get_scholarship_filename(instance, filename):
|
def get_scholarship_filename(instance, filename):
|
||||||
return f"authorization/scholarship/scholarship_{instance.registration.pk}"
|
return f"authorization/scholarship/scholarship_{instance.registration.pk}"
|
||||||
@ -369,7 +361,7 @@ class Payment(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
valid = models.BooleanField(
|
valid = models.BooleanField(
|
||||||
verbose_name=_("payment valid"),
|
verbose_name=_("valid"),
|
||||||
null=True,
|
null=True,
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
@ -43,12 +43,12 @@ def create_admin_registration(instance, **_):
|
|||||||
VolunteerRegistration.objects.get_or_create(user=instance, admin=True)
|
VolunteerRegistration.objects.get_or_create(user=instance, admin=True)
|
||||||
|
|
||||||
|
|
||||||
def create_payment(instance: Registration, raw, **_):
|
def create_payment(instance: Registration, **_):
|
||||||
"""
|
"""
|
||||||
When a user is saved, create the associated payment.
|
When a user is saved, create the associated payment.
|
||||||
For a free tournament, the payment is valid.
|
For a free tournament, the payment is valid.
|
||||||
"""
|
"""
|
||||||
if instance.participates and not raw:
|
if instance.participates:
|
||||||
payment = Payment.objects.get_or_create(registration=instance)[0]
|
payment = Payment.objects.get_or_create(registration=instance)[0]
|
||||||
if instance.team and instance.team.participation.valid and instance.team.participation.tournament.price == 0:
|
if instance.team and instance.team.participation.valid and instance.team.participation.tournament.price == 0:
|
||||||
payment.valid = True
|
payment.valid = True
|
@ -19,7 +19,8 @@ class RegistrationTable(tables.Table):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def order_type(self, queryset, desc):
|
def order_type(self, queryset, desc):
|
||||||
return queryset.order_by(('-' if desc else '') + 'polymorphic_ctype'), True
|
types = ["volunteerregistration", "-volunteerregistration__admin", "participantregistration"]
|
||||||
|
return queryset.order_by(*(("-" if desc else "") + t for t in types)), True
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse
|
Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse
|
||||||
<a href="https://{{ domain }}/">https://{{ domain }}/</a>. Vous disposez d'un compte de bénévole.
|
<a href="https://{{ domain }}/">https://{{ domain }}/</a>. Vous disposez d'un compte d'organisateur.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
@ -3,7 +3,7 @@
|
|||||||
Bonjour {{ user.registration }},
|
Bonjour {{ user.registration }},
|
||||||
|
|
||||||
Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse
|
Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse
|
||||||
https://{{ domain }}/. Vous disposez d'un compte de bénévole.
|
https://{{ domain }}/. Vous disposez d'un compte d'organisateur.
|
||||||
|
|
||||||
Un mot de passe aléatoire a été défini : {{ password }}.
|
Un mot de passe aléatoire a été défini : {{ password }}.
|
||||||
Par sécurité, merci de le changer dès votre connexion.
|
Par sécurité, merci de le changer dès votre connexion.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user