mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-08-22 23:27:30 +02:00
Compare commits
88 Commits
7984ce8e1d
...
7f8934a647
Author | SHA1 | Date | |
---|---|---|---|
|
7f8934a647 | ||
|
815206a0a5 | ||
|
8350960d5f
|
||
968162f34e
|
|||
e848855072
|
|||
50409931cf
|
|||
d18f76cf80
|
|||
5f2cd16071
|
|||
c686584e74
|
|||
3a650a1e89
|
|||
51beb47191
|
|||
e3f5541774
|
|||
14de6cf824
|
|||
3e46d06817
|
|||
0fd9222055
|
|||
b67308065a
|
|||
644afc6a0d
|
|||
1ef981571d
|
|||
30a8676555
|
|||
cdf279bb02
|
|||
7515c2bec6
|
|||
cce5e7c33c
|
|||
f9e85dd63e
|
|||
cb86fd43ac
|
|||
be0662420d
|
|||
da1d7a83fa
|
|||
d37354dc24
|
|||
d210b2a221
|
|||
e9958faace
|
|||
ab1f4c2eba
|
|||
1ba5cfa3f8
|
|||
e9cfae99da
|
|||
700df123b7
|
|||
582a634da7
|
|||
837800345b
|
|||
384fbfd0b2
|
|||
d8f2e56d45
|
|||
ba6a6338f5
|
|||
9a1006b341
|
|||
e21c3bb413
|
|||
afde1d35d5
|
|||
9e885153c2
|
|||
ffaa6e8116
|
|||
9797268736
|
|||
fb4edccc40
|
|||
f8297eebe1
|
|||
e41ad64b54
|
|||
13c4c834d4
|
|||
d6aa285bc5
|
|||
bbd8ad43cd
|
|||
ef8d124ade
|
|||
bb01e1b0b5
|
|||
f9af52ce6a
|
|||
ef2911ab07
|
|||
3bd6d2e647
|
|||
9d741d76f2
|
|||
de504a1706
|
|||
30a0e63eb9
|
|||
de76abab5f
|
|||
833249191c
|
|||
0a99f10899
|
|||
5101746d29
|
|||
aa69e6eadb
|
|||
7dd85d7402
|
|||
6b2ca1d2e1
|
|||
fbedb941be
|
|||
46e75c7ae8
|
|||
d26dee3bcf
|
|||
4084f7abb5
|
|||
d4c7b39f46
|
|||
0576f3e32b
|
|||
d093414ec7
|
|||
cba4a01117
|
|||
fde2fdba63
|
|||
aff1bbda0b
|
|||
4f9dfadb71
|
|||
1df1766753
|
|||
9359aa7606
|
|||
a45d57e51a
|
|||
35863c4bda
|
|||
13414ee0c5
|
|||
cdacbe2ea1
|
|||
69325bff9a
|
|||
049234caae
|
|||
f8d38738ea
|
|||
f7d52aa6da
|
|||
99a2134a57
|
|||
8fc99803c1
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,6 +35,7 @@ coverage
|
||||
secrets.py
|
||||
*.log
|
||||
media/
|
||||
output/
|
||||
# Virtualenv
|
||||
env/
|
||||
venv/
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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',)
|
||||
|
2
apps/participation/api/__init__.py
Normal file
2
apps/participation/api/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
63
apps/participation/api/serializers.py
Normal file
63
apps/participation/api/serializers.py
Normal 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',)
|
19
apps/participation/api/urls.py
Normal file
19
apps/participation/api/urls.py
Normal 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)
|
69
apps/participation/api/views.py
Normal file
69
apps/participation/api/views.py
Normal 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', ]
|
@@ -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):
|
||||
|
@@ -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}, "
|
||||
|
86
apps/participation/management/commands/export_results.py
Normal file
86
apps/participation/management/commands/export_results.py
Normal 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 -->")
|
82
apps/participation/management/commands/export_solutions.py
Normal file
82
apps/participation/management/commands/export_solutions.py
Normal 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')
|
@@ -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())
|
||||
|
@@ -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'),
|
||||
),
|
||||
]
|
18
apps/participation/migrations/0005_pool_results_available.py
Normal file
18
apps/participation/migrations/0005_pool_results_available.py
Normal 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'),
|
||||
),
|
||||
]
|
21
apps/participation/migrations/0006_auto_20220426_1346.py
Normal file
21
apps/participation/migrations/0006_auto_20220426_1346.py
Normal 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'},
|
||||
),
|
||||
]
|
17
apps/participation/migrations/0007_remove_passage_place.py
Normal file
17
apps/participation/migrations/0007_remove_passage_place.py
Normal 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',
|
||||
),
|
||||
]
|
24
apps/participation/migrations/0008_auto_20220429_1853.py
Normal file
24
apps/participation/migrations/0008_auto_20220429_1853.py
Normal 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'),
|
||||
),
|
||||
]
|
27
apps/participation/migrations/0009_tweak.py
Normal file
27
apps/participation/migrations/0009_tweak.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
@@ -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,))
|
||||
|
||||
|
@@ -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'
|
||||
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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");
|
||||
|
@@ -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 %}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
14
apps/participation/templates/participation/upload_notes.html
Normal file
14
apps/participation/templates/participation/upload_notes.html
Normal 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 %}
|
@@ -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>
|
||||
|
@@ -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:
|
||||
|
@@ -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"),
|
||||
|
@@ -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()
|
||||
|
2
apps/registration/api/__init__.py
Normal file
2
apps/registration/api/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
47
apps/registration/api/serializers.py
Normal file
47
apps/registration/api/serializers.py
Normal 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,
|
||||
}
|
11
apps/registration/api/urls.py
Normal file
11
apps/registration/api/urls.py
Normal 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)
|
15
apps/registration/api/views.py
Normal file
15
apps/registration/api/views.py
Normal 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', ]
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
18
apps/registration/migrations/0003_alter_payment_type.py
Normal file
18
apps/registration/migrations/0003_alter_payment_type.py
Normal 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'),
|
||||
),
|
||||
]
|
@@ -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,
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
||||
|
||||
|
@@ -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}.
|
||||
|
||||
|
@@ -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
@@ -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
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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'
|
||||
|
7
tfjm/static/bootstrap/css/bootstrap.min.css
vendored
Normal file
7
tfjm/static/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
tfjm/static/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
7
tfjm/static/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
tfjm/static/fontawasome/css/all.css
Normal file
5
tfjm/static/fontawasome/css/all.css
Normal file
File diff suppressed because one or more lines are too long
5
tfjm/static/fontawasome/css/v4-shims.css
Normal file
5
tfjm/static/fontawasome/css/v4-shims.css
Normal file
File diff suppressed because one or more lines are too long
BIN
tfjm/static/fontawasome/webfonts/fa-brands-400.woff
Normal file
BIN
tfjm/static/fontawasome/webfonts/fa-brands-400.woff
Normal file
Binary file not shown.
BIN
tfjm/static/fontawasome/webfonts/fa-brands-400.woff2
Normal file
BIN
tfjm/static/fontawasome/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
tfjm/static/fontawasome/webfonts/fa-solid-900.woff
Normal file
BIN
tfjm/static/fontawasome/webfonts/fa-solid-900.woff
Normal file
Binary file not shown.
BIN
tfjm/static/fontawasome/webfonts/fa-solid-900.woff2
Normal file
BIN
tfjm/static/fontawasome/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
2
tfjm/static/jquery/jquery-3.6.0.min.js
vendored
Normal file
2
tfjm/static/jquery/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
tfjm/static/turbolinks/turbolinks.js
Normal file
6
tfjm/static/turbolinks/turbolinks.js
Normal file
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
@@ -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 %}
|
||||
|
@@ -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">
|
||||
|
@@ -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
15
tox.ini
@@ -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
|
||||
|
Reference in New Issue
Block a user