1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-08-22 23:27:30 +02:00

Compare commits

...

88 Commits

Author SHA1 Message Date
Emmy D'Anello
7f8934a647 Drop Python 3.8 support, add Python 3.10 and 3.11 support
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2022-11-08 15:55:09 +01:00
Emmy D'Anello
815206a0a5 Linting
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2022-11-08 15:52:54 +01:00
Emmy D'Anello
8350960d5f Fix problems export
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2022-10-22 15:09:26 +02:00
968162f34e Add tweaks to update notes 2022-05-15 16:47:51 +02:00
e848855072 Juries are volunteers 2022-05-15 16:20:43 +02:00
50409931cf Fix error 2022-05-15 16:16:41 +02:00
d18f76cf80 Upload notes from a CSV sheet 2022-05-15 12:24:50 +02:00
5f2cd16071 Files are required for solutions and syntheses
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-04-29 18:53:34 +02:00
c686584e74 Place field is useless
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-04-26 21:42:29 +02:00
3a650a1e89 Fix Hello Asso link
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-04-26 15:10:23 +02:00
51beb47191 Fix scholarship files
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-04-26 14:23:10 +02:00
e3f5541774 Add new "other" payment type
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-04-26 13:47:15 +02:00
14de6cf824 [helloasso] Manage duplicate users + ignore invalid users
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-04-26 13:44:16 +02:00
3e46d06817 Add CSV export for tournaments 2022-04-22 18:05:06 +02:00
0fd9222055 Filter on last name and optionally on first name for Hello Asso
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-03-28 21:36:00 +02:00
b67308065a Update Hello Asso URL 2022-03-28 21:15:44 +02:00
644afc6a0d Le tournoi ça commence le samedi 2022-03-21 19:28:50 +01:00
1ef981571d Parce que les gens ragent 2022-02-05 21:13:18 +01:00
30a8676555 Update 2022 2022-02-04 18:10:07 +01:00
cdf279bb02 Team name don't need to be uppercase 2022-02-04 15:40:45 +01:00
7515c2bec6 Define default auto field for Django 3.2 2022-02-04 15:25:40 +01:00
cce5e7c33c Hello 2022 2022-02-04 15:07:41 +01:00
f9e85dd63e Why was it broken 2022-02-04 15:01:15 +01:00
cb86fd43ac Fix bootstrap-datepicker-plus 2022-02-04 14:54:40 +01:00
be0662420d Upgrade dependencies 2022-02-04 14:45:00 +01:00
da1d7a83fa Remove header 2022-02-04 14:31:01 +01:00
d37354dc24 Don't create rooms for "mise en commun" 2022-02-04 14:14:59 +01:00
d210b2a221 /run/nginx now exists by default, but not /etc/nginx/conf.d
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-01-12 01:11:41 +01:00
e9958faace Add script to export solutions
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-11-21 22:27:09 +01:00
ab1f4c2eba Add script to generate Wordpress results
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-11-18 19:17:54 +01:00
1ba5cfa3f8 Add rooms for problems 2021-05-15 22:21:50 +02:00
e9cfae99da Filter passages per tournament 2021-05-14 13:34:31 +02:00
700df123b7 Fix tournament serializer 2021-05-11 17:19:28 +02:00
582a634da7 Fix participation detail template 2021-05-11 17:10:34 +02:00
837800345b Fix permissions for solutions for the final 2021-05-11 17:06:49 +02:00
384fbfd0b2 Better participation detail page 2021-05-11 17:03:25 +02:00
d8f2e56d45 fix solution str representation 2021-05-11 16:56:44 +02:00
ba6a6338f5 Fix permissions for final tournament 2021-05-11 16:40:18 +02:00
9a1006b341 Fix solution upload 2021-05-09 12:37:53 +02:00
e21c3bb413 Pool number is not day number 2021-04-29 15:47:46 +02:00
afde1d35d5 Indicate if this is a final solution 2021-04-29 15:46:38 +02:00
9e885153c2 We can select teams for the final tournament 2021-04-29 14:10:38 +02:00
ffaa6e8116 Force pool and passage tables to have chronological orders 2021-04-15 23:03:51 +02:00
9797268736 Add default order for solutions and syntheses
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-13 10:05:00 +02:00
fb4edccc40 Use full jquery lib
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-13 09:55:59 +02:00
f8297eebe1 Fix font awesome static files
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-12 22:52:14 +02:00
e41ad64b54 Use local static files 2021-04-12 22:41:50 +02:00
13c4c834d4 Round notes to one decimal 2021-04-10 14:38:15 +02:00
d6aa285bc5 Display notes for authenticated users 2021-04-10 11:43:31 +02:00
bbd8ad43cd Clarify syntheses name 2021-04-10 10:02:49 +02:00
ef8d124ade Display notes iff results are public 2021-04-10 09:59:04 +02:00
bb01e1b0b5 Display notes in django-admin 2021-04-09 16:17:12 +02:00
f9af52ce6a Organizers can manage pools 2021-04-09 14:28:36 +02:00
ef2911ab07 Add synthesis template links 2021-04-07 15:27:48 +02:00
3bd6d2e647 Invite local organizers, not all organizers in pool channels 2021-04-07 09:54:12 +02:00
9d741d76f2 Organizers can see solutions 2021-04-06 19:50:27 +02:00
de504a1706 Fix synthesis upload 2021-04-04 18:13:30 +02:00
30a0e63eb9 Fix solution view 2021-04-04 17:18:50 +02:00
de76abab5f Remove Matrix test 2021-04-04 16:42:09 +02:00
833249191c Missing await 2021-04-04 16:37:02 +02:00
0a99f10899 Create multiple channels in case of five people-pools 2021-04-04 16:28:06 +02:00
5101746d29 Reformat Matrix script 2021-04-04 16:06:16 +02:00
aa69e6eadb Run matrix script into an async loop 2021-04-04 16:02:37 +02:00
7dd85d7402 Update defender penalties 2021-04-04 13:35:45 +02:00
6b2ca1d2e1 Admin can see note details 2021-04-04 13:30:02 +02:00
fbedb941be Better pool display 2021-04-04 13:15:00 +02:00
46e75c7ae8 Passages are read-only 2021-04-04 12:17:54 +02:00
d26dee3bcf Fix tournament serializer 2021-04-04 11:35:00 +02:00
4084f7abb5 Fix solution upload 2021-04-03 22:15:03 +02:00
d4c7b39f46 Fix solution and synthesis forms 2021-04-03 22:02:53 +02:00
0576f3e32b Support penalties 2021-04-03 21:59:06 +02:00
d093414ec7 git is useful 2021-03-29 16:46:44 +02:00
cba4a01117 Upgrade django-cas-server, please ... 2021-03-29 16:45:56 +02:00
fde2fdba63 Remove asgiref dependency, django manages itself 2021-03-29 16:43:34 +02:00
aff1bbda0b Upgrade python-magic in test environment 2021-03-29 16:34:30 +02:00
4f9dfadb71 Add API filters for registration 2021-03-29 16:24:58 +02:00
1df1766753 Upgrade dependencies 2021-03-29 16:18:27 +02:00
9359aa7606 Add API views for participation app 2021-03-29 15:41:20 +02:00
a45d57e51a Team member don't have access to other people authorizations 2021-03-28 20:09:29 +02:00
35863c4bda Matrix cron is buggy 2021-03-28 20:08:00 +02:00
13414ee0c5 Organizers can upload documents for team members 2021-03-18 18:36:37 +01:00
cdacbe2ea1 Matrix is listening on https://tfjm.org/ and https://tfjm.org:8448/ 2021-03-18 18:13:06 +01:00
69325bff9a Fix translations 2021-03-15 10:17:31 +01:00
049234caae Fix hello asso check 2021-03-15 10:07:59 +01:00
f8d38738ea Authenticate to Hello Asso by client id and secret 2021-03-15 09:57:05 +01:00
f7d52aa6da Update HelloAsso link 2021-03-15 09:46:45 +01:00
99a2134a57 Increase cron delay 2021-03-15 09:35:42 +01:00
8fc99803c1 object -> get_object() 2021-03-14 23:46:11 +01:00
64 changed files with 1797 additions and 744 deletions

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@ coverage
secrets.py
*.log
media/
output/
# Virtualenv
env/
venv/

View File

@@ -2,14 +2,6 @@ stages:
- test
- quality-assurance
py38:
stage: test
image: python:3.8-alpine
before_script:
- apk add --no-cache libmagic
- pip install tox --no-cache-dir
script: tox -e py38
py39:
stage: test
image: python:3.9-alpine
@@ -18,6 +10,22 @@ py39:
- pip install tox --no-cache-dir
script: tox -e py39
py310:
stage: test
image: python:3.10-alpine
before_script:
- apk add --no-cache libmagic
- pip install tox --no-cache-dir
script: tox -e py310
py311:
stage: test
image: python:3.11-alpine
before_script:
- apk add --no-cache libmagic
- pip install tox --no-cache-dir
script: tox -e py311
linters:
stage: quality-assurance
image: python:3-alpine

View File

@@ -1,9 +1,9 @@
FROM python:3.8-alpine
FROM python:3.11-alpine
ENV PYTHONUNBUFFERED 1
ENV DJANGO_ALLOW_ASYNC_UNSAFE 1
RUN apk add --no-cache gettext nginx gcc libc-dev libffi-dev libxml2-dev libxslt-dev postgresql-dev libmagic texlive
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
@@ -23,10 +23,8 @@ RUN python manage.py collectstatic --noinput && \
python manage.py compilemessages
# Configure nginx
RUN mkdir /run/nginx
RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log
RUN ln -sf /code/nginx_tfjm.conf /etc/nginx/conf.d/tfjm.conf
RUN rm /etc/nginx/conf.d/default.conf
RUN ln -sf /code/nginx_tfjm.conf /etc/nginx/http.d/tfjm.conf && rm /etc/nginx/http.d/default.conf
RUN crontab /code/tfjm.cron

View File

@@ -16,6 +16,14 @@ if "logs" in settings.INSTALLED_APPS:
from logs.api.urls import register_logs_urls
register_logs_urls(router, "logs")
if "participation" in settings.INSTALLED_APPS:
from participation.api.urls import register_participation_urls
register_participation_urls(router, "participation")
if "registration" in settings.INSTALLED_APPS:
from registration.api.urls import register_registration_urls
register_registration_urls(router, "registration")
app_name = 'api'
# Wire up our API using automatic URL routing.

View File

@@ -4,7 +4,7 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
@admin.register(Team)
@@ -36,6 +36,11 @@ 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',)
@@ -52,3 +57,8 @@ class SynthesisAdmin(admin.ModelAdmin):
class TournamentAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
@admin.register(Tweak)
class TweakAdmin(admin.ModelAdmin):
list_display = ('participation', 'pool', 'diff',)

View File

@@ -0,0 +1,2 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -0,0 +1,63 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = '__all__'
class ParticipationSerializer(serializers.ModelSerializer):
class Meta:
model = Participation
fields = '__all__'
class PassageSerializer(serializers.ModelSerializer):
class Meta:
model = Passage
fields = '__all__'
class PoolSerializer(serializers.ModelSerializer):
passages = serializers.ListSerializer(child=PassageSerializer(), read_only=True)
class Meta:
model = Pool
fields = '__all__'
class SolutionSerializer(serializers.ModelSerializer):
class Meta:
model = Solution
fields = '__all__'
class SynthesisSerializer(serializers.ModelSerializer):
class Meta:
model = Synthesis
fields = '__all__'
class TeamSerializer(serializers.ModelSerializer):
participation = ParticipationSerializer()
class Meta:
model = Team
fields = '__all__'
class TournamentSerializer(serializers.ModelSerializer):
participations = serializers.ListSerializer(child=ParticipationSerializer())
class Meta:
model = Tournament
fields = ('id', 'pk', 'name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
'solutions_available_second_phase', 'syntheses_second_phase_limit',
'description', 'organizers', 'final', 'participations',)

View File

@@ -0,0 +1,19 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NoteViewSet, ParticipationViewSet, PassageViewSet, PoolViewSet, \
SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet
def register_participation_urls(router, path):
"""
Configure router for participation REST API.
"""
router.register(path + "/note", NoteViewSet)
router.register(path + "/participation", ParticipationViewSet)
router.register(path + "/passage", PassageViewSet)
router.register(path + "/pool", PoolViewSet)
router.register(path + "/solution", SolutionViewSet)
router.register(path + "/synthesis", SynthesisViewSet)
router.register(path + "/team", TeamViewSet)
router.register(path + "/tournament", TournamentViewSet)

View File

@@ -0,0 +1,69 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet
from .serializers import NoteSerializer, ParticipationSerializer, PassageSerializer, PoolSerializer, \
SolutionSerializer, SynthesisSerializer, TeamSerializer, TournamentSerializer
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
class NoteViewSet(ModelViewSet):
queryset = Note.objects.all()
serializer_class = NoteSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['jury', 'passage', 'defender_writing', 'defender_oral', 'opponent_writing',
'opponent_oral', 'reporter_writing', 'reporter_oral', ]
class ParticipationViewSet(ModelViewSet):
queryset = Participation.objects.all()
serializer_class = ParticipationSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['team', 'team__name', 'team__trigram', 'tournament', 'tournament__name', 'valid', 'final', ]
class PassageViewSet(ModelViewSet):
queryset = Passage.objects.all()
serializer_class = PassageSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['pool', 'solution_number', 'defender', 'opponent', 'reporter', 'pool_tournament', ]
class PoolViewSet(ModelViewSet):
queryset = Pool.objects.all()
serializer_class = PoolSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['tournament', 'tournament__name', 'round', 'participations', 'juries', 'bbb_url', ]
class SolutionViewSet(ModelViewSet):
queryset = Solution.objects.all()
serializer_class = SolutionSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['participation', 'number', 'problem', 'final_solution', ]
class SynthesisViewSet(ModelViewSet):
queryset = Synthesis.objects.all()
serializer_class = SynthesisSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['participation', 'number', 'passage', 'type', ]
class TeamViewSet(ModelViewSet):
queryset = Team.objects.all()
serializer_class = TeamSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'trigram', 'access_code', 'participation__valid', 'participation__tournament',
'participation__tournament__name', 'participation__valid', 'participation__final', ]
class TournamentViewSet(ModelViewSet):
queryset = Tournament.objects.all()
serializer_class = TournamentSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
'solutions_available_second_phase', 'syntheses_second_phase_limit',
'description', 'organizers', 'final', ]

View File

@@ -1,14 +1,20 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import csv
from io import StringIO
import re
from typing import Iterable
from bootstrap_datepicker_plus import DatePickerInput, DateTimePickerInput
from bootstrap_datepicker_plus.widgets import DatePickerInput, DateTimePickerInput
from django import forms
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.utils import formats
from django.utils.translation import gettext_lazy as _
from PyPDF3 import PdfFileReader
from registration.models import VolunteerRegistration
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
@@ -19,7 +25,7 @@ class TeamForm(forms.ModelForm):
"""
def clean_name(self):
if "name" in self.cleaned_data:
name = self.cleaned_data["name"].upper()
name = self.cleaned_data["name"]
if not self.instance.pk and Team.objects.filter(name=name).exists():
raise ValidationError(_("This name is already used."))
return name
@@ -67,7 +73,7 @@ class ParticipationForm(forms.ModelForm):
"""
class Meta:
model = Participation
fields = ('tournament',)
fields = ('tournament', 'final',)
class MotivationLetterForm(forms.ModelForm):
@@ -136,6 +142,7 @@ class TournamentForm(forms.ModelForm):
self.fields["syntheses_second_phase_limit"].widget = DateTimePickerInput(
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
self.fields["organizers"].widget = forms.CheckboxSelectMultiple()
self.fields["organizers"].queryset = VolunteerRegistration.objects.all()
class Meta:
model = Tournament
@@ -154,7 +161,7 @@ class SolutionForm(forms.ModelForm):
pages = len(pdf_reader.pages)
if pages > 30:
raise ValidationError(_("The PDF file must not have more than 30 pages."))
return self.cleaned_data["photo_authorization"]
return self.cleaned_data["file"]
def save(self, commit=True):
"""
@@ -169,7 +176,7 @@ class SolutionForm(forms.ModelForm):
class PoolForm(forms.ModelForm):
class Meta:
model = Pool
fields = ('tournament', 'round', 'bbb_url', 'juries',)
fields = ('tournament', 'round', 'bbb_url', 'results_available', 'juries',)
widgets = {
"juries": forms.CheckboxSelectMultiple,
}
@@ -188,6 +195,70 @@ class PoolTeamsForm(forms.ModelForm):
}
class UploadNotesForm(forms.Form):
file = forms.FileField(
label=_("CSV file:"),
validators=[FileExtensionValidator(allowed_extensions=["csv"])],
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['file'].widget.attrs['accept'] = 'text/csv'
def clean(self):
cleaned_data = super().clean()
if 'file' in cleaned_data:
file = cleaned_data['file']
with file:
try:
csvfile = csv.reader(StringIO(file.read().decode()))
except UnicodeDecodeError:
self.add_error('file', _("This file contains non-UTF-8 content. "
"Please send your sheet as a CSV file."))
self.process(csvfile, cleaned_data)
return cleaned_data
def process(self, csvfile: Iterable[str], cleaned_data: dict):
parsed_notes = {}
for line in csvfile:
line = [s for s in line if s]
if len(line) < 19:
continue
name = line[0]
notes = line[1:19]
if not all(s.isnumeric() for s in notes):
continue
notes = list(map(int, notes))
if max(notes) < 3 or min(notes) < 0:
continue
max_notes = 3 * [20, 16, 9, 10, 9, 10]
for n, max_n in zip(notes, max_notes):
if n > max_n:
self.add_error('file',
_("The following note is higher of the maximum expected value:")
+ str(n) + " > " + str(max_n))
first_name, last_name = tuple(name.split(' ', 1))
jury = User.objects.filter(first_name=first_name, last_name=last_name,
registration__volunteerregistration__isnull=False)
if jury.count() != 1:
self.add_error('file', _("The following user was not found:") + " " + name)
continue
jury = jury.get()
vr = jury.registration
parsed_notes[vr] = notes
cleaned_data['parsed_notes'] = parsed_notes
return cleaned_data
class PassageForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
@@ -202,7 +273,7 @@ class PassageForm(forms.ModelForm):
class Meta:
model = Passage
fields = ('solution_number', 'place', 'defender', 'opponent', 'reporter',)
fields = ('solution_number', 'defender', 'opponent', 'reporter', 'defender_penalties',)
class SynthesisForm(forms.ModelForm):
@@ -213,7 +284,7 @@ class SynthesisForm(forms.ModelForm):
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
if file.content_type != "application/pdf":
raise ValidationError(_("The uploaded file must be a PDF file."))
return self.cleaned_data["photo_authorization"]
return self.cleaned_data["file"]
def save(self, commit=True):
"""
@@ -222,7 +293,7 @@ class SynthesisForm(forms.ModelForm):
class Meta:
model = Synthesis
fields = ('type', 'file',)
fields = ('file',)
class NoteForm(forms.ModelForm):

View File

@@ -5,19 +5,31 @@ import os
from django.contrib.auth.models import User
from django.core.management import BaseCommand
from django.db.models import Q
import requests
class Command(BaseCommand):
def handle(self, *args, **options): # noqa: C901
# Get access token
response = requests.post('https://api.helloasso.com/oauth2/token', headers={
'Content-Type': 'application/x-www-form-urlencoded',
}, data={
'client_id': os.getenv('HELLOASSO_CLIENT_ID', ''),
'client_secret': os.getenv('HELLOASSO_CLIENT_SECRET', ''),
'grant_type': 'client_credentials',
}).json()
token = response['access_token']
organization = "animath"
form_slug = "tfjmm-2018"
form_slug = "tfjm-2022-tournois-regionaux"
from_date = "2000-01-01"
url = f"https://api.helloasso.com/v5/organizations/{organization}/forms/Event/{form_slug}/payments" \
f"?from={from_date}&pageIndex=1&pageSize=10000&retrieveOfflineDonations=false"
f"?from={from_date}&pageIndex=1&pageSize=100&retrieveOfflineDonations=false"
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {os.getenv('HELLO_ASSO_TOKEN', '')}",
"Authorization": f"Bearer {token}",
}
http_response = requests.get(url, headers=headers)
response = http_response.json()
@@ -27,17 +39,38 @@ class Command(BaseCommand):
self.stderr.write(f"Error while querying Hello Asso: {message}")
return
for payment in response:
for payment in response["data"]:
if payment["state"] != "Authorized":
continue
payer = payment["payer"]
email = payer["email"]
qs = User.objects.filter(email=email)
last_name = payer["lastName"]
first_name = payer["firstName"]
base_filter = Q(
registration__participantregistration__isnull=False,
registration__participantregistration__team__isnull=False,
registration__participantregistration__team__participation__valid=True,
)
qs = User.objects.filter(
base_filter,
email=email,
)
if not qs.exists():
self.stderr.write(f"Warning: a payment was found by the email address {email}, "
qs = User.objects.filter(
base_filter,
last_name__icontains=last_name,
)
if qs.count() >= 2:
qs = qs.filter(first_name__icontains=first_name)
if not qs.exists():
self.stderr.write(f"Warning: a payment was found by {first_name} {last_name} ({email}), "
"but this user is unknown.")
continue
if qs.count() > 1:
self.stderr.write(f"Warning: a payment was found by {first_name} {last_name} ({email}), "
f"but there are {qs.count()} matching users.")
continue
user = qs.get()
if not user.registration.participates:
self.stderr.write(f"Warning: a payment was found by the email address {email}, "

View File

@@ -0,0 +1,86 @@
# Copyright (C) 2021 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.management import BaseCommand
from django.utils.formats import date_format
from django.utils.translation import activate
from .models import Tournament
class Command(BaseCommand):
def handle(self, *args, **kwargs):
activate('fr')
tournaments = Tournament.objects.order_by('-date_start', 'name')
for tournament in tournaments:
self.handle_tournament(tournament)
self.w("")
self.w("")
def w(self, msg):
self.stdout.write(msg)
def handle_tournament(self, tournament):
name = tournament.name
date_start = date_format(tournament.date_start, "DATE_FORMAT")
date_end = date_format(tournament.date_end, "DATE_FORMAT")
notes = dict()
for participation in tournament.participations.filter(valid=True).all():
note = sum(pool.average(participation)
for pool in tournament.pools.filter(participations=participation).all())
notes[participation] = note
notes = sorted(notes.items(), key=lambda x: x[1], reverse=True)
self.w("<!-- wp:heading {\"level\":3} -->")
self.w(f"<h3><strong>{name}</strong></h3>")
self.w("<!-- /wp:heading -->")
self.w("")
self.w("<!-- wp:paragraph -->")
if tournament.final:
self.w(f"<p>La finale a eu lieu le weekend du {date_start} au {date_end} et a été remporté par l'équipe "
f"<em>{notes[0][0].team.name}</em> suivie de l'équipe <em>{notes[1][0].team.name}</em>. "
f"Les deux premières équipes sont sélectionnées pour représenter la France lors de l'ITYM.</p>")
else:
self.w(f"<p>Le tournoi de {name} a eu lieu le weekend du {date_start} au {date_end} et a été remporté par "
f"l'équipe <em>{notes[0][0].team.name}</em>.</p>")
self.w("<!-- /wp:paragraph -->")
self.w("")
self.w("")
self.w("<!-- wp:table -->")
self.w("<figure class=\"wp-block-table\">")
self.w("<table>")
self.w("<thead>")
self.w("<tr>")
self.w("\t<th>Équipe</th>")
self.w("\t<th>Score Tour 1</th>")
self.w("\t<th>Score Tour 2</th>")
self.w("\t<th>Total</th>")
self.w("\t<th class=\"has-text-align-center\">Prix</th>")
self.w("</tr>")
self.w("</thead>")
self.w("<tbody>")
for i, (participation, note) in enumerate(notes):
self.w("<tr>")
if i < (2 if len(notes) >= 7 else 1):
self.w(f"\t<th>{participation.team.name} ({participation.team.trigram})</td>")
else:
self.w(f"\t<td>{participation.team.name} ({participation.team.trigram})</td>")
for pool in tournament.pools.filter(participations=participation).all():
pool_note = pool.average(participation)
self.w(f"\t<td>{pool_note:.01f}</td>")
self.w(f"\t<td>{note:.01f}</td>")
if i == 0:
self.w("\t<td class=\"has-text-align-center\">1<sup>er</sup> prix</td>")
elif i < (5 if tournament.final else 3):
self.w(f"\t<td class=\"has-text-align-center\">{i + 1}<sup>ème</sup> prix</td>")
elif i < 2 * len(notes) / 3:
self.w("\t<td class=\"has-text-align-center\">Mention très honorable</td>")
else:
self.w("\t<td class=\"has-text-align-center\">Mention honorable</td>")
self.w("</tr>")
self.w("</tbody>")
self.w("</table>")
self.w("</figure>")
self.w("<!-- /wp:table -->")

View File

@@ -0,0 +1,82 @@
# Copyright (C) 2021 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
from django.core.management import BaseCommand
from django.utils.translation import activate
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):
def handle(self, *args, **kwargs):
activate('fr')
base_dir = Path(__file__).parent.parent.parent.parent.parent
base_dir /= "output"
if not base_dir.is_dir():
base_dir.mkdir()
base_dir /= "Par équipe"
if not base_dir.is_dir():
base_dir.mkdir()
tournaments = Tournament.objects.all()
for tournament in tournaments:
self.handle_tournament(tournament, base_dir)
base_dir = base_dir.parent / "Par problème"
if not base_dir.is_dir():
base_dir.mkdir()
for problem_id, problem_name in enumerate(PROBLEMS):
dir_name = f"Problème n°{problem_id + 1} : {problem_name}"
problem_dir = base_dir / dir_name
if not problem_dir.is_dir():
problem_dir.mkdir()
self.handle_problem(problem_id + 1, problem_dir)
def handle_tournament(self, tournament, base_dir):
name = tournament.name
tournament_dir = base_dir / name
if not tournament_dir.is_dir():
tournament_dir.mkdir()
for participation in tournament.participations.filter(valid=True).all():
self.handle_participation(participation, tournament_dir)
def handle_participation(self, participation, tournament_dir):
name = participation.team.name
trigram = participation.team.trigram
team_dir = tournament_dir / f"{name} ({trigram})"
if not team_dir.is_dir():
team_dir.mkdir()
for solution in participation.solutions.all():
filename = f"{solution}.pdf"
with solution.file as file_input:
with open(team_dir / filename, 'wb') as file_output:
file_output.write(file_input.read())
def handle_problem(self, problem_id, directory):
solutions = Solution.objects.filter(problem=problem_id).all()
for solution in solutions:
team = solution.participation.team
tournament_name = team.participation.tournament.name
output_file = directory / f'{solution}.pdf'
if output_file.is_file():
output_file.unlink()
output_file.symlink_to(f'../../Par équipe/{tournament_name}/{team.name} ({team.trigram})/{solution}.pdf')

View File

@@ -1,9 +1,9 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import asyncio
import os
from asgiref.sync import async_to_sync
from django.core.management import BaseCommand
from django.utils.http import urlencode
from django.utils.translation import activate
@@ -16,384 +16,462 @@ class Command(BaseCommand):
def handle(self, *args, **options): # noqa: C901
activate("fr")
Matrix.set_display_name("Bot du TFJM²")
async def main():
await Matrix.set_display_name("Bot du TFJM²")
if not os.getenv("SYNAPSE_PASSWORD"):
avatar_uri = "plop"
else: # pragma: no cover
if not os.path.isfile(".matrix_avatar"):
avatar_uri = Matrix.get_avatar()
if isinstance(avatar_uri, str):
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
else:
stat_file = os.stat("tfjm/static/logo.png")
with open("tfjm/static/logo.png", "rb") as f:
resp = Matrix.upload(f, filename="logo.png", content_type="image/png",
filesize=stat_file.st_size)[0][0]
avatar_uri = resp.content_uri
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
Matrix.set_avatar(avatar_uri)
if not os.getenv("SYNAPSE_PASSWORD"):
avatar_uri = "plop"
else: # pragma: no cover
if not os.path.isfile(".matrix_avatar"):
avatar_uri = await Matrix.get_avatar()
if isinstance(avatar_uri, str):
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
else:
stat_file = os.stat("tfjm/static/logo.png")
with open("tfjm/static/logo.png", "rb") as f:
resp = (await Matrix.upload(f, filename="logo.png", content_type="image/png",
filesize=stat_file.st_size))[0][0]
avatar_uri = resp.content_uri
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
await Matrix.set_avatar(avatar_uri)
with open(".matrix_avatar", "r") as f:
avatar_uri = f.read().rstrip(" \t\r\n")
with open(".matrix_avatar", "r") as f:
avatar_uri = f.read().rstrip(" \t\r\n")
# Create basic channels
if not async_to_sync(Matrix.resolve_room_alias)("#aide-jurys-orgas:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="aide-jurys-orgas",
name="Aide jurys & orgas",
topic="Pour discuter de propblèmes d'organisation",
federate=False,
preset=RoomPreset.private_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)("#annonces:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="annonces",
name="Annonces",
topic="Informations importantes du TFJM²",
federate=False,
preset=RoomPreset.public_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)("#bienvenue:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="bienvenue",
name="Bienvenue",
topic="Bienvenue au TFJM² 2021 !",
federate=False,
preset=RoomPreset.public_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)("#bot:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="bot",
name="Bot",
topic="Vive les r0b0ts",
federate=False,
preset=RoomPreset.public_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)("#cno:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="cno",
name="CNO",
topic="Channel des dieux",
federate=False,
preset=RoomPreset.private_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)("#dev-bot:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="dev-bot",
name="Bot - développement",
topic="Vive le bot",
federate=False,
preset=RoomPreset.private_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)("#faq:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="faq",
name="FAQ",
topic="Posez toutes vos questions ici !",
federate=False,
preset=RoomPreset.public_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)("#flood:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="flood",
name="Flood",
topic="Discutez de tout et de rien !",
federate=False,
preset=RoomPreset.public_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)("#je-cherche-une-equipe:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias="je-cherche-une-equipe",
name="Je cherche une équipe",
topic="Le Tinder du TFJM²",
federate=False,
preset=RoomPreset.public_chat,
)
# Setup avatars
Matrix.set_room_avatar("#aide-jurys-orgas:tfjm.org", avatar_uri)
Matrix.set_room_avatar("#annonces:tfjm.org", avatar_uri)
Matrix.set_room_avatar("#bienvenue:tfjm.org", avatar_uri)
Matrix.set_room_avatar("#bot:tfjm.org", avatar_uri)
Matrix.set_room_avatar("#cno:tfjm.org", avatar_uri)
Matrix.set_room_avatar("#dev-bot:tfjm.org", avatar_uri)
Matrix.set_room_avatar("#faq:tfjm.org", avatar_uri)
Matrix.set_room_avatar("#flood:tfjm.org", avatar_uri)
Matrix.set_room_avatar("#je-cherche-une-equipe:tfjm.org", avatar_uri)
# Read-only channels
Matrix.set_room_power_level_event("#annonces:tfjm.org", "events_default", 50)
Matrix.set_room_power_level_event("#bienvenue:tfjm.org", "events_default", 50)
# Invite everyone to public channels
for r in Registration.objects.all():
Matrix.invite("#annonces:tfjm.org", f"@{r.matrix_username}:tfjm.org")
Matrix.invite("#bienvenue:tfjm.org", f"@{r.matrix_username}:tfjm.org")
Matrix.invite("#bot:tfjm.org", f"@{r.matrix_username}:tfjm.org")
Matrix.invite("#faq:tfjm.org", f"@{r.matrix_username}:tfjm.org")
Matrix.invite("#flood:tfjm.org", f"@{r.matrix_username}:tfjm.org")
Matrix.invite("#je-cherche-une-equipe:tfjm.org",
f"@{r.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {r} in most common channels...")
# Volunteers have access to the help channel
for volunteer in VolunteerRegistration.objects.all():
Matrix.invite("#aide-jurys-orgas:tfjm.org", f"@{volunteer.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {volunteer} in #aide-jury-orgas...")
# Admins are admins
for admin in AdminRegistration.objects.all():
self.stdout.write(f"Invite {admin} in #cno and #dev-bot...")
Matrix.invite("#cno:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
Matrix.invite("#dev-bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Give admin permissions for {admin}...")
Matrix.set_room_power_level("#aide-jurys-orgas:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level("#annonces:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level("#bienvenue:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level("#bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level("#cno:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level("#dev-bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level("#faq:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level("#flood:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level("#je-cherche-une-equipe:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
# Create tournament-specific channels
for tournament in Tournament.objects.all():
self.stdout.write(f"Managing tournament of {tournament.name}.")
name = tournament.name
slug = name.lower().replace(" ", "-")
if not async_to_sync(Matrix.resolve_room_alias)(f"#annonces-{slug}:tfjm.org"):
Matrix.create_room(
# Create basic channels
if not await Matrix.resolve_room_alias("#aide-jurys-orgas:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"annonces-{slug}",
name=f"{name} - Annonces",
topic=f"Annonces du tournoi de {name}",
alias="aide-jurys-orgas",
name="Aide jurys & orgas",
topic="Pour discuter de propblèmes d'organisation",
federate=False,
preset=RoomPreset.private_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)(f"#general-{slug}:tfjm.org"):
Matrix.create_room(
if not await Matrix.resolve_room_alias("#annonces:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"general-{slug}",
name=f"{name} - Général",
topic=f"Accueil du tournoi de {name}",
alias="annonces",
name="Annonces",
topic="Informations importantes du TFJM²",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#bienvenue:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="bienvenue",
name="Bienvenue",
topic="Bienvenue au TFJM² 2022 !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#bot:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="bot",
name="Bot",
topic="Vive les r0b0ts",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#cno:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="cno",
name="CNO",
topic="Channel des dieux",
federate=False,
preset=RoomPreset.private_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)(f"#flood-{slug}:tfjm.org"):
Matrix.create_room(
if not await Matrix.resolve_room_alias("#dev-bot:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"flood-{slug}",
name=f"{name} - Flood",
topic=f"Discussion libre du tournoi de {name}",
alias="dev-bot",
name="Bot - développement",
topic="Vive le bot",
federate=False,
preset=RoomPreset.private_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)(f"#jury-{slug}:tfjm.org"):
Matrix.create_room(
if not await Matrix.resolve_room_alias("#faq:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"jury-{slug}",
name=f"{name} - Jury",
topic=f"Discussion entre les orgas et jurys du tournoi de {name}",
alias="faq",
name="FAQ",
topic="Posez toutes vos questions ici !",
federate=False,
preset=RoomPreset.private_chat,
preset=RoomPreset.public_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)(f"#orga-{slug}:tfjm.org"):
Matrix.create_room(
if not await Matrix.resolve_room_alias("#flood:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"orga-{slug}",
name=f"{name} - Organisateurs",
topic=f"Discussion entre les orgas du tournoi de {name}",
alias="flood",
name="Flood",
topic="Discutez de tout et de rien !",
federate=False,
preset=RoomPreset.private_chat,
preset=RoomPreset.public_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)(f"#tirage-au-sort-{slug}:tfjm.org"):
Matrix.create_room(
if not await Matrix.resolve_room_alias("#je-cherche-une-equipe:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"tirage-au-sort-{slug}",
name=f"{name} - Tirage au sort",
topic=f"Tirage au sort du tournoi de {name}",
alias="je-cherche-une-equipe",
name="Je cherche une équipe",
topic="Le Tinder du TFJM²",
federate=False,
preset=RoomPreset.private_chat,
preset=RoomPreset.public_chat,
)
# Setup avatars
Matrix.set_room_avatar(f"#annonces-{slug}:tfjm.org", avatar_uri)
Matrix.set_room_avatar(f"#flood-{slug}:tfjm.org", avatar_uri)
Matrix.set_room_avatar(f"#general-{slug}:tfjm.org", avatar_uri)
Matrix.set_room_avatar(f"#jury-{slug}:tfjm.org", avatar_uri)
Matrix.set_room_avatar(f"#orga-{slug}:tfjm.org", avatar_uri)
Matrix.set_room_avatar(f"#tirage-au-sort-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#aide-jurys-orgas:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#annonces:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#bienvenue:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#bot:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#cno:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#dev-bot:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#faq:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#flood:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#je-cherche-une-equipe:tfjm.org", avatar_uri)
# Invite admins and give permissions
# Read-only channels
await Matrix.set_room_power_level_event("#annonces:tfjm.org", "events_default", 50)
await Matrix.set_room_power_level_event("#bienvenue:tfjm.org", "events_default", 50)
# Invite everyone to public channels
for r in Registration.objects.all():
await Matrix.invite("#annonces:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#bienvenue:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#bot:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#faq:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#flood:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#je-cherche-une-equipe:tfjm.org",
f"@{r.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {r} in most common channels...")
# Volunteers have access to the help channel
for volunteer in VolunteerRegistration.objects.all():
await Matrix.invite("#aide-jurys-orgas:tfjm.org", f"@{volunteer.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {volunteer} in #aide-jury-orgas...")
# Admins are admins
for admin in AdminRegistration.objects.all():
self.stdout.write(f"Invite {admin} in all channels of the tournament {name}...")
Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
Matrix.invite(f"#general-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {admin} in #cno and #dev-bot...")
await Matrix.invite("#cno:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite("#dev-bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Give permissions to {admin} in all channels of the tournament {name}...")
Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level(f"#general-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
self.stdout.write(f"Give admin permissions for {admin}...")
await Matrix.set_room_power_level("#aide-jurys-orgas:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#annonces:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#bienvenue:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#bot:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#cno:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#dev-bot:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#faq:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#flood:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#je-cherche-une-equipe:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in tournament.organizers.all():
self.stdout.write(f"Invite organizer {orga} in all channels of the tournament {name}...")
Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
Matrix.invite(f"#general-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
# Create tournament-specific channels
for tournament in Tournament.objects.all():
self.stdout.write(f"Managing tournament of {tournament.name}.")
if not orga.is_admin:
Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
Matrix.set_room_power_level(f"#general-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
name = tournament.name
slug = name.lower().replace(" ", "-")
# Invite participants
for participation in tournament.participations.filter(valid=True).all():
for participant in participation.team.participants.all():
self.stdout.write(f"Invite {participant} in public channels of the tournament {name}...")
Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
Matrix.invite(f"#general-{slug}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
# Create pool-specific channels
for pool in tournament.pools.all():
self.stdout.write(f"Managing {pool}...")
if not async_to_sync(Matrix.resolve_room_alias)(f"#poule-{slug}-{pool.id}:tfjm.org"):
Matrix.create_room(
if not await Matrix.resolve_room_alias(f"#annonces-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}",
name=f"{name} - Jour {pool.round} - Poule "
f"{', '.join(participation.team.trigram for participation in pool.participations.all())}",
topic=f"Discussion avec les équipes - {pool}",
federate=False,
preset=RoomPreset.private_chat,
)
if not async_to_sync(Matrix.resolve_room_alias)(f"#poule-{slug}-{pool.id}-jurys:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}-jurys",
name=f"{name} - Jour {pool.round} - Jurys poule "
f"{', '.join(participation.team.trigram for participation in pool.participations.all())}",
topic=f"Discussion avec les jurys - {pool}",
alias=f"annonces-{slug}",
name=f"{name} - Annonces",
topic=f"Annonces du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}:tfjm.org", avatar_uri)
Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}-jurys:tfjm.org", avatar_uri)
if not await Matrix.resolve_room_alias(f"#general-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"general-{slug}",
name=f"{name} - Général",
topic=f"Accueil du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
url_params = urlencode(dict(url=pool.bbb_url,
isAudioConf='false', displayName='$matrix_display_name',
avatarUrl='$matrix_avatar_url', userId='$matrix_user_id')) \
.replace("%24", "$")
Matrix.add_integration(f"#poule-{slug}-{pool.id}:tfjm.org",
f"https://scalar.vector.im/api/widgets/bigbluebutton.html?{url_params}",
f"bbb-{slug}-{pool.id}", "bigbluebutton", "BigBlueButton", str(pool))
Matrix.add_integration(f"#poule-{slug}-{pool.id}:tfjm.org",
f"https://board.tfjm.org/boards/{slug}-{pool.id}", f"board-{slug}-{pool.id}",
"customwidget", "Tableau", str(pool))
if not await Matrix.resolve_room_alias(f"#flood-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"flood-{slug}",
name=f"{name} - Flood",
topic=f"Discussion libre du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#jury-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"jury-{slug}",
name=f"{name} - Jury",
topic=f"Discussion entre les orgas et jurys du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#orga-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"orga-{slug}",
name=f"{name} - Organisateurs",
topic=f"Discussion entre les orgas du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#tirage-au-sort-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"tirage-au-sort-{slug}",
name=f"{name} - Tirage au sort",
topic=f"Tirage au sort du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
# Setup avatars
await Matrix.set_room_avatar(f"#annonces-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#flood-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#general-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#jury-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#orga-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#tirage-au-sort-{slug}:tfjm.org", avatar_uri)
# Invite admins and give permissions
for admin in AdminRegistration.objects.all():
Matrix.invite(f"#poule-{slug}-{pool.id}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
Matrix.invite(f"#poule-{slug}-{pool.id}-jurys:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {admin} in all channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}-jurys:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
self.stdout.write(f"Give permissions to {admin} in all channels of the tournament {name}...")
await Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#general-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in VolunteerRegistration.objects.all():
Matrix.invite(f"#poule-{slug}-{pool.id}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
Matrix.invite(f"#poule-{slug}-{pool.id}-jurys:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
for orga in tournament.organizers.all():
self.stdout.write(f"Invite organizer {orga} in all channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
if not orga.is_admin:
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#general-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
# Invite the jury, give good permissions
for jury in pool.juries.all():
Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
Matrix.invite(f"#general-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
Matrix.invite(f"#poule-{slug}-{pool.id}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
Matrix.invite(f"#poule-{slug}-{pool.id}-jurys:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
if not jury.is_admin:
Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org", 50)
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
# Invite participants to the right pool
for participation in pool.participations.all():
# Invite participants
for participation in tournament.participations.filter(valid=True).all():
for participant in participation.team.participants.all():
Matrix.invite(f"#poule-{slug}-{pool.id}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {participant} in public channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
# Create private channels for teams
for team in Team.objects.all():
self.stdout.write(f"Create private channel for {team}...")
if not async_to_sync(Matrix.resolve_room_alias)(f"#equipe-{team.trigram.lower()}:tfjm.org"):
Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"equipe-{team.trigram.lower()}",
name=f"Équipe {team.trigram}",
topic=f"Discussion interne de l'équipe {team.name}",
federate=False,
preset=RoomPreset.private_chat,
)
for participant in team.participants.all():
Matrix.invite(f"#equipe-{team.trigram.lower}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
Matrix.set_room_power_level(f"#equipe-{team.trigram.lower()}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org", 50)
# Create pool-specific channels
for pool in tournament.pools.all():
self.stdout.write(f"Managing {pool}...")
five = pool.participations.count() >= 5
for i in range(2 if five else 1):
# Fix for five teams-pools
suffix = f"-{chr(ord('A') + i)}" if five else ""
if not await Matrix.resolve_room_alias(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}{suffix}",
name=f"{name} - Jour {pool.round} - Poule " +
', '.join(participation.team.trigram
for participation in pool.participations.all()) + suffix,
topic=f"Discussion avec les équipes - {pool}{suffix}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}{suffix}-jurys",
name=f"{name} - Jour {pool.round}{suffix} - Jurys poule " +
', '.join(participation.team.trigram
for participation in pool.participations.all()) + suffix,
topic=f"Discussion avec les jurys - {pool}{suffix}",
federate=False,
preset=RoomPreset.private_chat,
)
await Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org", avatar_uri)
bbb_url = pool.bbb_url.strip()
if five and ';' in bbb_url:
bbb_url = bbb_url.split(";")[i].strip()
url_params = urlencode(dict(url=bbb_url,
isAudioConf='false', displayName='$matrix_display_name',
avatarUrl='$matrix_avatar_url', userId='$matrix_user_id')) \
.replace("%24", "$")
await Matrix.add_integration(
f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"https://scalar.vector.im/api/widgets/bigbluebutton.html?{url_params}",
f"bbb-{slug}-{pool.id}{suffix}", "bigbluebutton", "BigBlueButton", str(pool))
await Matrix.add_integration(
f"#poule-{slug}-{pool.id}:tfjm.org",
f"https://board.tfjm.org/boards/{slug}-{pool.id}", f"board-{slug}-{pool.id}",
"customwidget", "Tableau", str(pool))
# Invite admins and give permissions
for admin in AdminRegistration.objects.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{admin.matrix_username}:tfjm.org")
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in tournament.organizers.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org")
if not orga.is_admin:
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
# Invite the jury, give good permissions
for jury in pool.juries.all():
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
if not jury.is_admin:
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
# Invite participants to the right pool
for participation in pool.participations.all():
for participant in participation.team.participants.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
# Create private channels for teams
for team in Team.objects.all():
self.stdout.write(f"Create private channel for {team}...")
if not await Matrix.resolve_room_alias(f"#equipe-{team.trigram.lower()}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"equipe-{team.trigram.lower()}",
name=f"Équipe {team.trigram}",
topic=f"Discussion interne de l'équipe {team.name}",
federate=False,
preset=RoomPreset.private_chat,
)
for participant in team.participants.all():
await Matrix.invite(f"#equipe-{team.trigram.lower}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.set_room_power_level(f"#equipe-{team.trigram.lower()}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org", 50)
"""
# Manage channels to discuss about problems
for i in range(9):
self.stdout.write(f"Create channel for problem {i}...")
if not await Matrix.resolve_room_alias(f"#mec-{i}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"mec-{i}",
name=f"Mise en commun - {'Général' if i == 0 else f'Problème {i}'}",
topic=f"Discussion autour des problèmes",
federate=False,
preset=RoomPreset.public_chat,
invite=[f"@{registration.matrix_username}:tfjm.org"
for registration in Registration.objects.all()],
power_level_override={
f"@{registration.matrix_username}:tfjm.org": (95 if registration.is_admin else 50)
for registration in VolunteerRegistration.objects.all()
},
)
await Matrix.set_room_avatar(f"#mec-{i}:tfjm.org", avatar_uri)
for registration in Registration.objects.all():
await Matrix.invite(f"#mec-{i}:tfjm.org", registration.matrix_username)
for registration in VolunteerRegistration.objects.all():
await Matrix.set_room_power_level(f"#mec-{i}:tfjm.org",
f"@{registration.matrix_username}:tfjm.org",
95 if registration.is_admin else 50)
"""
asyncio.get_event_loop().run_until_complete(main())

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-03 19:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('participation', '0003_tournament_remote'),
]
operations = [
migrations.AddField(
model_name='passage',
name='defender_penalties',
field=models.PositiveSmallIntegerField(default=0, help_text='Number of penalties for the defender. The defender will loose a 0.5 coefficient per penalty.', verbose_name='penalties'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-10 07:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('participation', '0004_passage_defender_penalties'),
]
operations = [
migrations.AddField(
model_name='pool',
name='results_available',
field=models.BooleanField(default=False, help_text='Check this case when results become accessible to teams. They stay accessible to you. Only averages are given.', verbose_name='results available'),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.2.13 on 2022-04-26 11:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('participation', '0005_pool_results_available'),
]
operations = [
migrations.AlterModelOptions(
name='solution',
options={'ordering': ('participation__team__trigram', 'final_solution', 'problem'), 'verbose_name': 'solution', 'verbose_name_plural': 'solutions'},
),
migrations.AlterModelOptions(
name='synthesis',
options={'ordering': ('passage__pool__round', 'type'), 'verbose_name': 'synthesis', 'verbose_name_plural': 'syntheses'},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.13 on 2022-04-26 19:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('participation', '0006_auto_20220426_1346'),
]
operations = [
migrations.RemoveField(
model_name='passage',
name='place',
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.13 on 2022-04-29 16:53
from django.db import migrations, models
import participation.models
class Migration(migrations.Migration):
dependencies = [
('participation', '0007_remove_passage_place'),
]
operations = [
migrations.AlterField(
model_name='solution',
name='file',
field=models.FileField(unique=True, upload_to=participation.models.get_solution_filename, verbose_name='file'),
),
migrations.AlterField(
model_name='synthesis',
name='file',
field=models.FileField(unique=True, upload_to=participation.models.get_synthesis_filename, verbose_name='file'),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.2.13 on 2022-05-15 14:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('participation', '0008_auto_20220429_1853'),
]
operations = [
migrations.CreateModel(
name='Tweak',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('diff', models.IntegerField(help_text='Score to add/remove on the final score', verbose_name='difference')),
('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tweaks', to='participation.participation', verbose_name='participation')),
('pool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.pool', verbose_name='passage')),
],
options={
'verbose_name': 'tweak',
'verbose_name_plural': 'tweaks',
},
),
]

View File

@@ -368,18 +368,26 @@ class Pool(models.Model):
help_text=_("The link of the BBB visio for this pool."),
)
results_available = models.BooleanField(
default=False,
verbose_name=_("results available"),
help_text=_("Check this case when results become accessible to teams. "
"They stay accessible to you. Only averages are given."),
)
@property
def solutions(self):
return Solution.objects.filter(participation__in=self.participations, final_solution=self.tournament.final)
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())
def get_absolute_url(self):
return reverse_lazy("participation:pool_detail", args=(self.pk,))
def __str__(self):
return _("Pool {round} for tournament {tournament} with teams {teams}")\
return _("Pool of day {round} for tournament {tournament} with teams {teams}")\
.format(round=self.round,
tournament=str(self.tournament),
teams=", ".join(participation.team.trigram for participation in self.participations.all()))
@@ -397,13 +405,6 @@ class Passage(models.Model):
related_name="passages",
)
place = models.CharField(
verbose_name=_("place"),
max_length=255,
help_text=_("Where the solution is presented?"),
default="Non indiqué",
)
solution_number = models.PositiveSmallIntegerField(
verbose_name=_("defended solution"),
choices=[
@@ -432,6 +433,13 @@ class Passage(models.Model):
related_name="+",
)
defender_penalties = models.PositiveSmallIntegerField(
verbose_name=_("penalties"),
default=0,
help_text=_("Number of penalties for the defender. "
"The defender will loose a 0.5 coefficient per penalty."),
)
@property
def defended_solution(self) -> "Solution":
return Solution.objects.get(
@@ -439,44 +447,44 @@ class Passage(models.Model):
problem=self.solution_number,
final_solution=self.pool.tournament.final)
def avg(self, iterator) -> int:
def avg(self, iterator) -> float:
items = [i for i in iterator if i]
return sum(items) / len(items) if items else 0
@property
def average_defender_writing(self) -> int:
def average_defender_writing(self) -> float:
return self.avg(note.defender_writing for note in self.notes.all())
@property
def average_defender_oral(self) -> int:
def average_defender_oral(self) -> float:
return self.avg(note.defender_oral for note in self.notes.all())
@property
def average_defender(self) -> int:
return self.average_defender_writing + 2 * self.average_defender_oral
def average_defender(self) -> float:
return self.average_defender_writing + (2 - 0.5 * self.defender_penalties) * self.average_defender_oral
@property
def average_opponent_writing(self) -> int:
def average_opponent_writing(self) -> float:
return self.avg(note.opponent_writing for note in self.notes.all())
@property
def average_opponent_oral(self) -> int:
def average_opponent_oral(self) -> float:
return self.avg(note.opponent_oral for note in self.notes.all())
@property
def average_opponent(self) -> int:
def average_opponent(self) -> float:
return self.average_opponent_writing + 2 * self.average_opponent_oral
@property
def average_reporter_writing(self) -> int:
def average_reporter_writing(self) -> float:
return self.avg(note.reporter_writing for note in self.notes.all())
@property
def average_reporter_oral(self) -> int:
def average_reporter_oral(self) -> float:
return self.avg(note.reporter_oral for note in self.notes.all())
@property
def average_reporter(self) -> int:
def average_reporter(self) -> float:
return self.average_reporter_writing + self.average_reporter_oral
def average(self, participation):
@@ -507,6 +515,33 @@ class Passage(models.Model):
verbose_name_plural = _("passages")
class Tweak(models.Model):
pool = models.ForeignKey(
Pool,
on_delete=models.CASCADE,
verbose_name=_("passage"),
)
participation = models.ForeignKey(
Participation,
on_delete=models.CASCADE,
verbose_name=_("participation"),
related_name='tweaks',
)
diff = models.IntegerField(
verbose_name=_("difference"),
help_text=_("Score to add/remove on the final score"),
)
def __str__(self):
return f"Tweak for {self.participation.team} of {self.diff} points"
class Meta:
verbose_name = _("tweak")
verbose_name_plural = _("tweaks")
def get_solution_filename(instance, filename):
return f"solutions/{instance.participation.team.trigram}_{instance.problem}" \
+ ("final" if instance.final_solution else "")
@@ -540,18 +575,18 @@ class Solution(models.Model):
verbose_name=_("file"),
upload_to=get_solution_filename,
unique=True,
blank=True,
default="",
)
def __str__(self):
return _("Solution of team {team} for problem {problem}")\
.format(team=self.participation.team.name, problem=self.problem)
.format(team=self.participation.team.name, problem=self.problem)\
+ (" " + str(_("for final")) if self.final_solution else "")
class Meta:
verbose_name = _("solution")
verbose_name_plural = _("solutions")
unique_together = (('participation', 'problem', 'final_solution', ), )
ordering = ('participation__team__trigram', 'final_solution', 'problem',)
class Synthesis(models.Model):
@@ -579,17 +614,21 @@ class Synthesis(models.Model):
verbose_name=_("file"),
upload_to=get_synthesis_filename,
unique=True,
blank=True,
default="",
)
def __str__(self):
return _("Synthesis for the {type} of the {passage}").format(type=self.get_type_display(), passage=self.passage)
return _("Synthesis of {team} as {type} for problem {problem} of {defender}").format(
team=self.participation.team.trigram,
type=self.get_type_display(),
problem=self.passage.solution_number,
defender=self.passage.defender.team.trigram,
)
class Meta:
verbose_name = _("synthesis")
verbose_name_plural = _("syntheses")
unique_together = (('participation', 'passage', 'type', ), )
ordering = ('passage__pool__round', 'type',)
class Note(models.Model):
@@ -643,6 +682,15 @@ class Note(models.Model):
default=0,
)
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
reporter_writing: int, reporter_oral: int):
self.defender_writing = defender_writing
self.defender_oral = defender_oral
self.opponent_writing = opponent_writing
self.opponent_oral = opponent_oral
self.reporter_writing = reporter_writing
self.reporter_oral = reporter_oral
def get_absolute_url(self):
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))

View File

@@ -107,20 +107,20 @@ class PassageTable(tables.Table):
)
def render_defender(self, value):
return value.team
return value.team.trigram
def render_opponent(self, value):
return value.team
return value.team.trigram
def render_reporter(self, value):
return value.team
return value.team.trigram
class Meta:
attrs = {
'class': 'table table-condensed table-striped text-center',
}
model = Passage
fields = ('defender', 'opponent', 'reporter', 'place',)
fields = ('defender', 'opponent', 'reporter', 'solution_number', )
template_name = 'django_tables2/bootstrap4.html'

View File

@@ -24,24 +24,33 @@
<dt class="col-sm-2">{% trans "Solutions:" %}</dt>
<dd class="col-sm-10">
{% for solution in participation.solutions.all %}
<a href="{{ solution.file.url }}" data-turbolinks="false">{{ solution }}{% if not forloop.last %}, {% endif %}</a>
{% empty %}
{% trans "No solution was uploaded yet." %}
{% endfor %}
<ul>
{% for solution in participation.solutions.all %}
<li><a href="{{ solution.file.url }}" data-turbolinks="false">{{ solution }}</a></li>
{% empty %}
<li>{% trans "No solution was uploaded yet." %}</li>
{% endfor %}
</ul>
</dd>
{% if participation.pools.all %}
<dt class="col-sm-2">{% trans "Pools:" %}</dt>
<dd class="col-sm-10">
{% for pool in participation.pools.all %}
<a href="{{ pool.get_absolute_url }}" data-turbolinks="false">{{ pool }}{% if not forloop.last %}, {% endif %}</a>
{% endfor %}
<ul>
{% for pool in participation.pools.all %}
<li><a href="{{ pool.get_absolute_url }}" data-turbolinks="false">{{ pool }}{% if not forloop.last %}, {% endif %}</a></li>
{% endfor %}
</ul>
</dd>
{% endif %}
</dl>
</div>
<div class="card-footer text-center">
{% if participation.final %}
<div class="alert alert-info">
{% trans "If you upload a solution, this will replace the version for the final tournament." %}
</div>
{% endif %}
<button class="btn btn-primary" data-toggle="modal" data-target="#uploadSolutionModal">{% trans "Upload solution" %}</button>
</div>
</div>

View File

@@ -25,8 +25,8 @@
<dt class="col-sm-3">{% trans "Defended solution:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}" data-turbolinks="false">{{ passage.defended_solution }}</a></dd>
<dt class="col-sm-3">{% trans "Place:" %}</dt>
<dd class="col-sm-9">{{ passage.place }}</dd>
<dt class="col-sm-3">{% trans "Defender penalties count:" %}</dt>
<dd class="col-sm-9">{{ passage.defender_penalties }}</dd>
<dt class="col-sm-3">{% trans "Syntheses:" %}</dt>
<dd class="col-sm-9">
@@ -38,9 +38,11 @@
</dd>
</dl>
</div>
{% if user.registration.is_admin %}
{% if notes is not None %}
<div class="card-footer text-center">
<button class="btn btn-info" data-toggle="modal" data-target="#updateNotesModal">{% trans "Update notes" %}</button>
{% if my_note is not None %}
<button class="btn btn-info" data-toggle="modal" data-target="#updateNotesModal">{% trans "Update notes" %}</button>
{% endif %}
<button class="btn btn-primary" data-toggle="modal" data-target="#updatePassageModal">{% trans "Update" %}</button>
</div>
{% elif user.registration.participates %}
@@ -61,50 +63,52 @@
<div class="card-body">
<dl class="row">
<dt class="col-sm-8">{% trans "Average points for the defender writing:" %}</dt>
<dd class="col-sm-4">{{ passage.average_defender_writing }}/20</dd>
<dd class="col-sm-4">{{ passage.average_defender_writing|floatformat }}/20</dd>
<dt class="col-sm-8">{% trans "Average points for the defender oral:" %}</dt>
<dd class="col-sm-4">{{ passage.average_defender_oral }}/16</dd>
<dd class="col-sm-4">{{ passage.average_defender_oral|floatformat }}/16</dd>
<dt class="col-sm-8">{% trans "Average points for the opponent writing:" %}</dt>
<dd class="col-sm-4">{{ passage.average_opponent_writing }}/9</dd>
<dd class="col-sm-4">{{ passage.average_opponent_writing|floatformat }}/9</dd>
<dt class="col-sm-8">{% trans "Average points for the opponent oral:" %}</dt>
<dd class="col-sm-4">{{ passage.average_opponent_oral }}/10</dd>
<dd class="col-sm-4">{{ passage.average_opponent_oral|floatformat }}/10</dd>
<dt class="col-sm-8">{% trans "Average points for the reporter writing:" %}</dt>
<dd class="col-sm-4">{{ passage.average_reporter_writing }}/9</dd>
<dd class="col-sm-4">{{ passage.average_reporter_writing|floatformat }}/9</dd>
<dt class="col-sm-8">{% trans "Average points for the reporter oral:" %}</dt>
<dd class="col-sm-4">{{ passage.average_reporter_oral }}/10</dd>
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd>
</dl>
<hr>
<dl class="row">
<dt class="col-sm-8">{% trans "Defender points:" %}</dt>
<dd class="col-sm-4">{{ passage.average_defender }}/52</dd>
<dd class="col-sm-4">{{ passage.average_defender|floatformat }}/52</dd>
<dt class="col-sm-8">{% trans "Opponent points:" %}</dt>
<dd class="col-sm-4">{{ passage.average_opponent }}/29</dd>
<dd class="col-sm-4">{{ passage.average_opponent|floatformat }}/29</dd>
<dt class="col-sm-8">{% trans "Reporter points:" %}</dt>
<dd class="col-sm-4">{{ passage.average_reporter }}/19</dd>
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd>
</dl>
</div>
</div>
{% endif %}
{% if user.registration.is_admin %}
{% if notes is not None %}
{% trans "Update passage" as modal_title %}
{% trans "Update" as modal_button %}
{% url "participation:passage_update" pk=passage.pk as modal_action %}
{% include "base_modal.html" with modal_id="updatePassage" %}
{% trans "Update notes" as modal_title %}
{% trans "Update" as modal_button %}
{% url "participation:update_notes" pk=my_note.pk as modal_action %}
{% include "base_modal.html" with modal_id="updateNotes" %}
{% if my_note is not None %}
{% trans "Update notes" as modal_title %}
{% trans "Update" as modal_button %}
{% url "participation:update_notes" pk=my_note.pk as modal_action %}
{% include "base_modal.html" with modal_id="updateNotes" %}
{% endif %}
{% elif user.registration.participates %}
{% trans "Upload synthesis" as modal_title %}
{% trans "Upload" as modal_button %}
@@ -116,18 +120,20 @@
{% block extrajavascript %}
<script>
$(document).ready(function () {
{% if user.registration.is_admin %}
{% if notes is not None %}
$('button[data-target="#updatePassageModal"]').click(function() {
let modalBody = $("#updatePassageModal div.modal-body");
if (!modalBody.html().trim())
modalBody.load("{% url "participation:passage_update" pk=passage.pk %} #form-content")
});
$('button[data-target="#updateNotesModal"]').click(function() {
let modalBody = $("#updateNotesModal div.modal-body");
if (!modalBody.html().trim())
modalBody.load("{% url "participation:update_notes" pk=my_note.pk %} #form-content")
});
{% if my_note is not None %}
$('button[data-target="#updateNotesModal"]').click(function() {
let modalBody = $("#updateNotesModal div.modal-body");
if (!modalBody.html().trim())
modalBody.load("{% url "participation:update_notes" pk=my_note.pk %} #form-content")
});
{% endif %}
{% elif user.registration.participates %}
$('button[data-target="#uploadSynthesisModal"]').click(function() {
let modalBody = $("#uploadSynthesisModal div.modal-body");

View File

@@ -33,7 +33,7 @@
</dd>
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
<dd class="col-sm-9"><a href="{{ pool.bbb_url }}">{{ pool.bbb_url }}</a></dd>
<dd class="col-sm-9">{{ pool.bbb_url|urlize }}</dd>
</dl>
<div class="card bg-light shadow">
@@ -43,17 +43,18 @@
<div class="card-body">
<ul>
{% for participation, note in notes %}
<li><strong>{{ participation.team }} :</strong> {{ note }}</li>
<li><strong>{{ participation.team }} :</strong> {{ note|floatformat }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% if user.registration.is_admin %}
{% if user.registration.is_volunteer %}
<div class="card-footer text-center">
<button class="btn btn-success" data-toggle="modal" data-target="#addPassageModal">{% trans "Add passage" %}</button>
<button class="btn btn-primary" data-toggle="modal" data-target="#updatePoolModal">{% trans "Update" %}</button>
<button class="btn btn-primary" data-toggle="modal" data-target="#updateTeamsModal">{% trans "Update teams" %}</button>
<button class="btn btn-primary" data-toggle="modal" data-target="#uploadNotesModal">{% trans "Upload notes from a CSV file" %}</button>
</div>
{% endif %}
</div>
@@ -78,6 +79,11 @@
{% trans "Update" as modal_button %}
{% url "participation:pool_update_teams" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="updateTeams" %}
{% trans "Upload notes" as modal_title %}
{% trans "Upload" as modal_button %}
{% url "participation:pool_upload_notes" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadNotes" modal_button_type="success" modal_enctype="multipart/form-data" %}
{% endblock %}
{% block extrajavascript %}
@@ -100,6 +106,12 @@
if (!modalBody.html().trim())
modalBody.load("{% url "participation:passage_create" pk=pool.pk %} #form-content")
});
$('button[data-target="#uploadNotesModal"]').click(function() {
let modalBody = $("#uploadNotesModal div.modal-body");
if (!modalBody.html().trim())
modalBody.load("{% url "participation:pool_upload_notes" pk=pool.pk %} #form-content")
});
});
</script>
{% endblock %}

View File

@@ -101,11 +101,13 @@
</dd>
</dl>
{% if user.registration.is_volunteer %}
<div class="text-center">
<a class="btn btn-info" href="{% url "participation:team_authorizations" pk=team.pk %}" data-turbolinks="false">
<i class="fas fa-file-archive"></i> {% trans "Download all submitted authorizations" %}
</a>
</div>
{% endif %}
</div>
<div class="card-footer text-center">
<button class="btn btn-primary" data-toggle="modal" data-target="#updateTeamModal">{% trans "Update" %}</button>

View File

@@ -62,6 +62,7 @@
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
<div class="card-footer text-center">
<a href="{% url "participation:tournament_update" pk=tournament.pk %}"><button class="btn btn-secondary">{% trans "Edit tournament" %}</button></a>
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}"><button class="btn btn-success">{% trans "Export as CSV" %}</button></a>
</div>
{% endif %}
</div>
@@ -96,7 +97,7 @@
<div class="card-body">
<ul>
{% for participation, note in notes %}
<li><strong>{{ participation.team }} :</strong> {{ note }}</li>
<li><strong>{{ participation.team }} :</strong> {{ note|floatformat }}</li>
{% endfor %}
</ul>
</div>

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock %}

View File

@@ -1,10 +1,15 @@
{% extends "base.html" %}
{% load crispy_forms_filters i18n %}
{% load crispy_forms_filters i18n static %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
<div class="alert alert-info">
{% trans "Templates:" %}
<a class="alert-link" href="{% static "Fiche_synthèse.pdf" %}"> PDF</a> -
<a class="alert-link" href="{% static "Fiche_synthèse.tex" %}"> TEX</a>
</div>
{% csrf_token %}
{{ form|crispy }}
</div>

View File

@@ -487,20 +487,6 @@ class TestStudentParticipation(TestCase):
resp = self.client.get(reverse("participation:participation_detail", args=(self.second_team.pk,)))
self.assertEqual(resp.status_code, 403)
def test_cover_matrix(self):
"""
Load matrix scripts, to cover them and ensure that they can run.
"""
self.user.registration.team = self.team
self.user.registration.save()
self.second_user.registration.team = self.second_team
self.second_user.registration.save()
self.team.participation.valid = True
self.team.participation.received_participation = self.second_team.participation
self.team.participation.save()
call_command('fix_matrix_channels')
class TestAdmin(TestCase):
def setUp(self) -> None:

View File

@@ -6,9 +6,10 @@ from django.views.generic import TemplateView
from .views import CreateTeamView, JoinTeamView, MyParticipationDetailView, MyTeamDetailView, NoteUpdateView, \
ParticipationDetailView, PassageCreateView, PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, \
PoolUpdateTeamsView, PoolUpdateView, SolutionUploadView, SynthesisUploadView, TeamAuthorizationsView, \
TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, TeamUploadMotivationLetterView, TournamentCreateView, \
TournamentDetailView, TournamentListView, TournamentUpdateView
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, SolutionUploadView, SynthesisUploadView,\
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentListView, TournamentUpdateView
app_name = "participation"
@@ -31,10 +32,12 @@ urlpatterns = [
path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"),
path("tournament/<int:pk>/", TournamentDetailView.as_view(), name="tournament_detail"),
path("tournament/<int:pk>/update/", TournamentUpdateView.as_view(), name="tournament_update"),
path("tournament/<int:pk>/csv/", TournamentExportCSVView.as_view(), name="tournament_csv"),
path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
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-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
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>/update/", PassageUpdateView.as_view(), name="passage_update"),

View File

@@ -1,11 +1,12 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import csv
from io import BytesIO
import os
from zipfile import ZipFile
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied
@@ -28,7 +29,7 @@ from tfjm.views import AdminMixin, VolunteerMixin
from .forms import JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, PoolForm, \
PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
ValidateParticipationForm
UploadNotesForm, ValidateParticipationForm
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
@@ -164,7 +165,9 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
if user.registration.is_admin or user.registration.participates and \
user.registration.team and user.registration.team.pk == kwargs["pk"] \
or user.registration.is_volunteer \
and self.object.participation.tournament in user.registration.interesting_tournaments:
and (self.object.participation.tournament in user.registration.interesting_tournaments
or self.object.participation.final
and Tournament.final_tournament() in user.registration.interesting_tournaments):
return super().get(request, *args, **kwargs)
raise PermissionDenied
@@ -292,7 +295,9 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
if user.registration.is_admin or user.registration.participates and \
user.registration.team and user.registration.team.pk == kwargs["pk"] \
or user.registration.is_volunteer \
and self.get_object().participation.tournament in user.registration.interesting_tournaments:
and (self.get_object().participation.tournament in user.registration.interesting_tournaments
or self.get_object().participation.final
and Tournament.final_tournament() in user.registration.interesting_tournaments):
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
@@ -342,6 +347,7 @@ class MotivationLetterView(LoginRequiredMixin, View):
"""
Display the sent motivation letter.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/authorization/motivation_letters/{filename}"
@@ -372,9 +378,10 @@ class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
if user.registration.is_admin or user.registration.participates and user.registration.team.pk == kwargs["pk"] \
or user.registration.is_volunteer \
and self.get_object().participation.tournament in user.registration.interesting_tournaments:
if user.registration.is_admin or user.registration.is_volunteer \
and (self.get_object().participation.tournament in user.registration.interesting_tournaments
or self.get_object().participation.final
and Tournament.final_tournament() in user.registration.interesting_tournaments):
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
@@ -454,6 +461,7 @@ class MyParticipationDetailView(LoginRequiredMixin, RedirectView):
"""
Redirects to the detail view of the participation of the team.
"""
def get_redirect_url(self, *args, **kwargs):
user = self.request.user
registration = user.registration
@@ -480,7 +488,9 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView):
and user.registration.team.participation \
and user.registration.team.participation.pk == kwargs["pk"] \
or user.registration.is_volunteer \
and self.object.tournament in user.registration.interesting_tournaments:
and (self.get_object().tournament in user.registration.interesting_tournaments
or self.get_object().final
and Tournament.final_tournament() in user.registration.interesting_tournaments):
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
@@ -535,12 +545,14 @@ class TournamentDetailView(DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["teams"] = ParticipationTable(self.object.participations.all())
context["pools"] = PoolTable(self.object.pools.all())
context["pools"] = PoolTable(self.object.pools.order_by('id').all())
notes = dict()
for participation in self.object.participations.all():
note = sum(pool.average(participation)
for pool in self.object.pools.filter(participations=participation).all())
for pool in self.object.pools.filter(participations=participation).all()
if pool.results_available
or (self.request.user.is_authenticated and self.request.user.registration.is_volunteer))
if note:
notes[participation] = note
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
@@ -548,6 +560,40 @@ class TournamentDetailView(DetailView):
return context
class TournamentExportCSVView(VolunteerMixin, DetailView):
"""
Export team information in a CSV file.
"""
model = Tournament
def get(self, request, *args, **kwargs):
tournament = self.get_object()
resp = HttpResponse(
content_type='text/csv',
headers={'Content-Disposition': f'attachment; filename="Tournoi de {tournament.name}.csv"'},
)
writer = csv.DictWriter(resp, ('Tournoi', 'Équipe', 'Trigramme', 'Nom', 'Prénom', 'Genre', 'Date de naissance'))
writer.writeheader()
for participation in tournament.participations.filter(valid=True).order_by('team__trigram').all():
for registration in participation.team.participants\
.order_by('coachregistration', 'user__last_name').all():
writer.writerow({
'Tournoi': tournament.name,
'Équipe': participation.team.name,
'Trigramme': participation.team.trigram,
'Nom': registration.user.last_name,
'Prénom': registration.user.first_name,
'Genre': registration.get_gender_display() if isinstance(registration, StudentRegistration)
else 'Encandrant⋅e',
'Date de naissance': registration.birth_date if isinstance(registration, StudentRegistration)
else 'Encandrant⋅e',
})
return resp
class SolutionUploadView(LoginRequiredMixin, FormView):
template_name = "participation/upload_solution.html"
form_class = SolutionForm
@@ -563,6 +609,7 @@ class SolutionUploadView(LoginRequiredMixin, FormView):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
@transaction.atomic
def form_valid(self, form):
"""
When a solution is submitted, it replaces a previous solution if existing,
@@ -574,7 +621,7 @@ class SolutionUploadView(LoginRequiredMixin, FormView):
problem=form_sol.problem,
final_solution=self.participation.final)
tournament = Tournament.final_tournament() if self.participation.final else self.participation.final
tournament = Tournament.final_tournament() if self.participation.final else self.participation.tournament
if timezone.now() > tournament.solution_limit and sol_qs.exists():
form.add_error(None, _("You can't upload a solution after the deadline."))
return self.form_invalid(form)
@@ -585,7 +632,7 @@ class SolutionUploadView(LoginRequiredMixin, FormView):
sol.save()
sol.delete()
form_sol.participation = self.participation
form_sol.final = self.participation.final
form_sol.final_solution = self.participation.final
form_sol.save()
return super().form_valid(form)
@@ -615,14 +662,16 @@ class PoolDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["passages"] = PassageTable(self.object.passages.all())
context["passages"] = PassageTable(self.object.passages.order_by('id').all())
notes = dict()
for participation in self.object.participations.all():
note = self.object.average(participation)
if note:
notes[participation] = note
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
if self.object.results_available or self.request.user.registration.is_volunteer:
# Hide notes before the end of the turn
notes = dict()
for participation in self.object.participations.all():
note = self.object.average(participation)
if note:
notes[participation] = note
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
return context
@@ -655,6 +704,43 @@ class PoolUpdateTeamsView(VolunteerMixin, UpdateView):
return self.handle_no_permission()
class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
model = Pool
form_class = UploadNotesForm
template_name = 'participation/upload_notes.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and (self.object.tournament in request.user.registration.organized_tournaments.all()
or request.user.registration in self.object.juries.all()):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
@transaction.atomic
def form_valid(self, form):
pool = self.get_object()
parsed_notes = form.cleaned_data['parsed_notes']
for vr, notes in parsed_notes.items():
if vr not in pool.juries.all():
form.add_error('file', _("The following user is not registered as a jury:") + " " + str(vr))
for i, passage in enumerate(pool.passages.all()):
note = Note.objects.get_or_create(jury=vr, passage=passage)[0]
passage_notes = notes[6 * i:6 * (i + 1)]
note.set_all(*passage_notes)
note.save()
messages.success(self.request, _("Notes were successfully uploaded."))
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('participation:pool_detail', args=(self.kwargs['pk'],))
class PassageCreateView(VolunteerMixin, CreateView):
model = Passage
form_class = PassageForm
@@ -703,7 +789,9 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.registration in self.object.pool.juries.all():
context["my_note"] = Note.objects.get(passage=self.object, jury=self.request.user.registration)
context["my_note"] = Note.objects.get_or_create(passage=self.object, jury=self.request.user.registration)[0]
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
elif self.request.user.registration.is_admin:
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
return context
@@ -738,7 +826,7 @@ class SynthesisUploadView(LoginRequiredMixin, FormView):
self.participation = self.request.user.registration.team.participation
self.passage = qs.get()
if self.participation not in [self.passage.defender, self.passage.opponent, self.passage.reporter]:
if self.participation not in [self.passage.opponent, self.passage.reporter]:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
@@ -750,6 +838,7 @@ class SynthesisUploadView(LoginRequiredMixin, FormView):
It is discriminating whenever the team is selected for the final tournament or not.
"""
form_syn = form.instance
form_syn.type = 1 if self.participation == self.passage.opponent else 2
syn_qs = Synthesis.objects.filter(participation=self.participation,
passage=self.passage,
type=form_syn.type).all()

View File

@@ -0,0 +1,2 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -0,0 +1,47 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from ..models import AdminRegistration, CoachRegistration, ParticipantRegistration, \
StudentRegistration, VolunteerRegistration
class AdminSerializer(serializers.ModelSerializer):
class Meta:
model = AdminRegistration
fields = '__all__'
class CoachSerializer(serializers.ModelSerializer):
class Meta:
model = CoachRegistration
fields = '__all__'
class ParticipantSerializer(serializers.ModelSerializer):
class Meta:
model = ParticipantRegistration
fields = '__all__'
class StudentSerializer(serializers.ModelSerializer):
class Meta:
model = StudentRegistration
fields = '__all__'
class VolunteerSerializer(serializers.ModelSerializer):
class Meta:
model = VolunteerRegistration
fields = '__all__'
class RegistrationSerializer(PolymorphicSerializer):
model_serializer_mapping = {
AdminRegistration: AdminSerializer,
CoachRegistration: CoachSerializer,
StudentRegistration: StudentSerializer,
VolunteerRegistration: VolunteerSerializer,
}

View File

@@ -0,0 +1,11 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import RegistrationViewSet
def register_registration_urls(router, path):
"""
Configure router for registration REST API.
"""
router.register(path + "/registration", RegistrationViewSet)

View File

@@ -0,0 +1,15 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet
from .serializers import RegistrationSerializer
from ..models import Registration
class RegistrationViewSet(ModelViewSet):
queryset = Registration.objects.all()
serializer_class = RegistrationSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['user', 'participantregistration__team', ]

View File

@@ -5,7 +5,7 @@
"fields": {
"pos": 100,
"name": "Plateforme du TFJM²",
"pattern": "^https://tfjm.org:8448/.*$",
"pattern": "^https://tfjm.org(:8448)?/.*$",
"user_field": "matrix_username",
"restrict_users": false,
"proxy": true,

View File

@@ -221,7 +221,7 @@ class PaymentForm(forms.ModelForm):
cleaned_data = super().clean()
if "type" in cleaned_data and cleaned_data["type"] == "scholarship" \
and "scholarship" not in cleaned_data and not self.instance.scholarship_file:
and "scholarship_file" not in cleaned_data and not self.instance.scholarship_file:
self.add_error("scholarship_file", _("You must upload your scholarship attestation."))
return cleaned_data

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.13 on 2022-04-26 11:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registration', '0002_participantregistration_health_issues'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='type',
field=models.CharField(blank=True, choices=[('', 'No payment'), ('helloasso', 'Hello Asso'), ('scholarship', 'Scholarship'), ('bank_transfer', 'Bank transfer'), ('other', 'Other (please indicate)'), ('free', 'The tournament is free')], default='', max_length=16, verbose_name='type'),
),
]

View File

@@ -333,6 +333,7 @@ class Payment(models.Model):
('helloasso', "Hello Asso"),
('scholarship', _("Scholarship")),
('bank_transfer', _("Bank transfer")),
('other', _("Other (please indicate)")),
('free', _("The tournament is free")),
],
blank=True,

View File

@@ -16,7 +16,7 @@
<p>
{% blocktrans trimmed %}
You can pay with a credit card through
<a class="alert-link" href="https://www.helloasso.com/associations/animath/evenements/tfjmm-2018">our Hello Asso page</a>.
<a class="alert-link" href="https://www.helloasso.com/associations/animath/evenements/tfjm-2022-tournois-regionaux">our Hello Asso page</a>.
To make the validation of the payment easier, <span class="text-danger">please use the same e-mail
address that you use on this platform.</span> The payment verification will be checked automatically
under 10 minutes, you don't necessary need to fill this form.

View File

@@ -53,7 +53,7 @@ né(e) le {{ registration.birth_date }},
à participer au Tournoi Français des Jeunes Mathématiciennes et Mathématiciens ($\mathbb{TFJM}^2$) organisé \`a :
{{ tournament.place }}, du {{ tournament.date_start }} au {{ tournament.date_end }}.
Iel se rendra au lieu indiqu\'e ci-dessus le vendredi matin et quittera les lieux l'après-midi du dimanche par
Iel se rendra au lieu indiqu\'e ci-dessus le samedi matin et quittera les lieux l'après-midi du dimanche par
ses propres moyens et sous la responsabilité du représentant légal.

View File

@@ -73,7 +73,7 @@ Si le paiement de plusieurs élèves est fait en une seule opération, merci de
\href{mailto: contact@tfjm.org}{contact@tfjm.org} \textbf{avant le paiement} pour garantir l'identification de ce dernier.
\subsubsection*{Carte bancaire (uniquement les cartes françaises)}
Le paiement s'effectue en ligne via la plateforme à l'adresse : \url{https://www.helloasso.com/associations/animath/evenements/tfjmm-2021}
Le paiement s'effectue en ligne via la plateforme à l'adresse : \url{https://www.helloasso.com/associations/animath/evenements/tfjm-2022-tournois-regionaux}
Vous devez impérativement indiquer dans le champ "Référence" la mention "TFJMpu" suivie des noms et prénoms \textbf{de l'élève}.

View File

@@ -242,7 +242,9 @@ class UserDetailView(LoginRequiredMixin, DetailView):
user = self.get_object()
if user == me or me.registration.is_admin or me.registration.is_volunteer \
and user.registration.participates and user.registration.team \
and user.registration.team.participation.tournament in me.registration.organized_tournaments.all() \
and (user.registration.team.participation.tournament in me.registration.organized_tournaments.all()
or user.registration.team.participation.final
and Tournament.final_tournament() in me.registration.organized_tournaments.all()) \
or user.registration.is_volunteer and me.registration.is_volunteer \
and me.registration.interesting_tournaments.intersection(user.registration.interesting_tournaments):
return super().dispatch(request, *args, **kwargs)
@@ -541,12 +543,19 @@ class SolutionView(LoginRequiredMixin, View):
raise Http404
solution = Solution.objects.get(file__endswith=filename)
user = request.user
passage_participant_qs = Passage.objects.filter(Q(defender=user.registration.team.participation)
| Q(opponent=user.registration.team.participation)
| Q(reporter=user.registration.team.participation),
defender=solution.participation,
solution_number=solution.problem)
if not (user.registration.is_admin or user.registration.is_volunteer
if user.registration.participates:
passage_participant_qs = Passage.objects.filter(Q(defender=user.registration.team.participation)
| Q(opponent=user.registration.team.participation)
| Q(reporter=user.registration.team.participation),
defender=solution.participation,
solution_number=solution.problem)
else:
passage_participant_qs = Passage.objects.none()
if not (user.registration.is_admin
or user.registration.is_volunteer and user.registration
in (solution.participation.tournament
if not solution.final_solution else Tournament.final_tournament()).organizers.all()
or user.registration.is_volunteer
and Passage.objects.filter(Q(pool__juries=user.registration)
| Q(pool__tournament__in=user.registration.organized_tournaments.all()),
defender=solution.participation,

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,24 @@
asgiref~=3.3.1
Django~=3.0
Django>=3.2,<4.0
django-address~=0.2
django-bootstrap-datepicker-plus~=3.0
django-cas-server~=1.2
django-bootstrap-datepicker-plus~=4.0
django-cas-server~=1.3
django-crispy-forms~=1.9
django-extensions~=3.0
django-filter~=2.3
django-filter~=2.4
django-haystack~=3.0
django-mailer~=2.0
django-mailer~=2.1
django-phonenumber-field~=5.0.0
django-polymorphic~=3.0
django-tables2~=2.3
django-tables2~=2.4
djangorestframework~=3.12
django-rest-polymorphic~=0.1
gunicorn~=20.0
matrix-nio~=0.15
gunicorn~=20.1
matrix-nio~=0.16
phonenumbers~=8.9.10
psycopg2-binary~=2.8
PyPDF3~=1.0.2
ipython~=7.19.0
python-magic==0.4.18
requests~=2.25.0
python-magic>=0.4.22
requests~=2.25.1
sympasoap~=1.0
whoosh~=2.7

View File

@@ -8,10 +8,10 @@
0 * * * * cd /code && python manage.py update_index -v 0
# Recreate sympa lists
*/6 * * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
*/2 * * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
# Update matrix channels
*/6 * * * * cd /code && python manage.py fix_matrix_channels &> /dev/null
03 */6 * * * cd /code && python manage.py fix_matrix_channels &> /dev/null
# Check payments from Hello Asso
*/6 * * * * cd /code && python manage.py check_hello_asso &> /dev/null

View File

@@ -4,8 +4,6 @@
from enum import Enum
import os
from asgiref.sync import async_to_sync
class Matrix:
"""
@@ -51,7 +49,6 @@ class Matrix:
return client
@classmethod
@async_to_sync
async def set_display_name(cls, name: str):
"""
Set the display name of the bot account.
@@ -60,7 +57,6 @@ class Matrix:
return await client.set_displayname(name)
@classmethod
@async_to_sync
async def set_avatar(cls, avatar_url: str): # pragma: no cover
"""
Set the display avatar of the bot account.
@@ -69,7 +65,6 @@ class Matrix:
return await client.set_avatar(avatar_url)
@classmethod
@async_to_sync
async def get_avatar(cls): # pragma: no cover
"""
Set the display avatar of the bot account.
@@ -79,7 +74,6 @@ class Matrix:
return resp.avatar_url if hasattr(resp, "avatar_url") else resp
@classmethod
@async_to_sync
async def upload(
cls,
data_provider,
@@ -146,7 +140,6 @@ class Matrix:
if not isinstance(client, FakeMatrixClient) else None, None
@classmethod
@async_to_sync
async def create_room(
cls,
visibility=None,
@@ -232,7 +225,6 @@ class Matrix:
return resp.room_id if resp and hasattr(resp, "room_id") else None
@classmethod
@async_to_sync
async def invite(cls, room_id: str, user_id: str):
"""
Invite a user to a room.
@@ -251,7 +243,6 @@ class Matrix:
return await client.room_invite(room_id, user_id)
@classmethod
@async_to_sync
async def send_message(cls, room_id: str, body: str, formatted_body: str = None,
msgtype: str = "m.text", html: bool = True):
"""
@@ -274,7 +265,6 @@ class Matrix:
)
@classmethod
@async_to_sync
async def add_integration(cls, room_id: str, widget_url: str, state_key: str,
widget_type: str = "customwidget", widget_name: str = "Custom widget",
widget_title: str = ""):
@@ -301,7 +291,6 @@ class Matrix:
)
@classmethod
@async_to_sync
async def remove_integration(cls, room_id: str, state_key: str):
client = await cls._get_client()
if room_id.startswith("#"):
@@ -314,7 +303,6 @@ class Matrix:
)
@classmethod
@async_to_sync
async def kick(cls, room_id: str, user_id: str, reason: str = None):
"""
Kick a user from a room, or withdraw their invitation.
@@ -337,7 +325,6 @@ class Matrix:
return await client.room_kick(room_id, user_id, reason)
@classmethod
@async_to_sync
async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int): # pragma: no cover
"""
Put a given power level to a user in a certain room.
@@ -364,7 +351,6 @@ class Matrix:
return await client.room_put_state(room_id, "m.room.power_levels", content=content, state_key=resp.state_key)
@classmethod
@async_to_sync
async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int): # pragma: no cover
"""
Define the minimal power level to have to send a certain event type
@@ -395,7 +381,6 @@ class Matrix:
return await client.room_put_state(room_id, "m.room.power_levels", content=content, state_key=resp.state_key)
@classmethod
@async_to_sync
async def set_room_avatar(cls, room_id: str, avatar_uri: str):
"""
Define the avatar of a room.

View File

@@ -190,6 +190,8 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
CRISPY_TEMPLATE_PACK = 'bootstrap4'
DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap4.html'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@
{% block content %}
<div class="text-justify">
<p>
La plateforme d'inscription du TFJM² a été développée entre 2019 et 2021
La plateforme d'inscription du TFJM² a été développée entre 2019 et 2022
par Yohann D'ANELLO, bénévole pour l'association Animath. Elle est vouée à être utilisée par les participants
pour intéragir avec les organisateurs et les autres participants.
</p>

View File

@@ -19,17 +19,14 @@
{% endif %}
{# Bootstrap CSS #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/all.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/v4-shims.css">
<link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'fontawasome/css/all.css' %}">
<link rel="stylesheet" href="{% static 'fontawasome/css/v4-shims.css' %}">
{# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
crossorigin="anonymous"></script>
<script src="{% static 'jquery/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
<script src="{% static 'turbolinks/turbolinks.js' %}"></script>
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
{% if form.media %}

View File

@@ -3,18 +3,10 @@
{% block content %}
<div>
<div class="alert alert-warning">
<div class="alert alert-success">
<p>
Certains d'entre vous rencontrent des difficultés lors de l'inscription. Quelques points :
<ul>
<li>Tout d'abord, il est inutile de créer plusieurs comptes.</li>
<li>Si vous ne recevez pas le mail de validation de l'adresse mail, vous pouvez quand même vous connecter.
Vous pourrez via un lien redemander un mail. Merci de ne pas spammer ce bouton toutefois. Pensez à vérifier
vos spams. Contactez-nous si vous ne recevez toujours rien.</li>
<li>Les différentes autorisations sont à soumettre sur la page de votre compte.</li>
<li>La date limite d'inscription est fixée en général à un mois avant le début du tournoi, soit début mars.
Le site <a href="https://tfjm.org/">https://tfjm.org/</a> sera mis à jour dans les jours qui suivent.</li>
</ul>
Les inscriptions sont à présent ouvertes, vous pouvez créer votre compte. Prenez garde toutefois
aux dates indiquées qui sont pour l'instant provisoires.
</p>
<p>
Une documentation plus complète sera disponible dans les jours à venir et régulièrement mise à jour pour apprendre à utiliser la plateforme.
@@ -45,11 +37,17 @@
</div>
<div class="alert alert-success">
<h4><strong><i class="fas fa-newspaper"></i> Grande nouveauté :</strong></h4>
Cette année, après avoir appris de l'édition complètement à distance de 2020, la communication se simplifie :
retrouvez-nous sur Element, disponible sur tous vos appareils, ou sur votre navigateur à l'adresse
<a class="alert-link" href="https://element.tfjm.org">element.tfjm.org</a>
(connectez-vous avec les identifiants de cette plateforme une fois inscrits)
<h4><strong><i class="fas fa-newspaper"></i> Informations 2022 :</strong></h4>
<p>
Après 2 ans de pandémie, le tournoi devrait (enfin !) revenir en présentiel. Selon les mesures
gouvernementales en vigueur au moment du tournoi et peut-être même selon le lieu d'accueil du tournoi,
le pass sanitaire ou vaccinal pourra être requis.
</p>
<p>
La plateforme <a class="alert-link" href="https://element.tfjm.org">element.tfjm.org</a> reste ouverte
cette année pour faciliter d'éventuelles communications. C'est ici notamment que se déroulera le tirage
au sort. Nous vous invitons à tenter de vous connecter sur la plateforme une fois votre compte créé.
</p>
</div>
<div class="jumbotron">

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from haystack.generic_views import SearchView
@@ -30,7 +31,11 @@ class UserMixin(LoginRequiredMixin):
class UserRegistrationMixin(LoginRequiredMixin):
def dispatch(self, request, *args, **kwargs):
user = request.user
if user.is_authenticated and not user.registration.is_admin and user.registration.pk != kwargs["pk"]:
user_object = User.objects.get(registration__pk=kwargs["pk"])
if user.is_authenticated and not user.registration.is_admin and user.registration.pk != kwargs["pk"] and \
not (user.registration.is_volunteer and user_object.registration.team is not None
and user_object.registration.team.participation.tournament
in user.registration.organized_tournaments.all()):
self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)

15
tox.ini
View File

@@ -1,7 +1,8 @@
[tox]
envlist =
py38
py39
py310
py311
linters
skipsdist = True
@@ -10,20 +11,20 @@ skipsdist = True
sitepackages = False
deps =
coverage
Django~=3.1
Django>=3.2,<4.0
django-address~=0.2
django-bootstrap-datepicker-plus~=3.0
django-bootstrap-datepicker-plus~=4.0
django-crispy-forms~=1.9
django-filter~=2.3
django-filter~=2.4
django-haystack~=3.0
django-phonenumber-field~=5.0.0
django-polymorphic~=3.0
django-tables2~=2.3
django-tables2~=2.4
djangorestframework~=3.12
django-rest-polymorphic~=0.1
phonenumbers~=8.9.10
PyPDF3~=1.0.2
python-magic==0.4.18
python-magic==0.4.22
whoosh~=2.7
commands =
coverage run --source=apps,tfjm ./manage.py test apps/ tfjm/
@@ -57,4 +58,4 @@ max-complexity = 10
max-line-length = 160
import-order-style = google
application-import-names = flake8
format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
#format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s