mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-02-26 10:26:32 +00:00
Compare commits
110 Commits
ff414ea046
...
2f4755ffc7
Author | SHA1 | Date | |
---|---|---|---|
|
2f4755ffc7 | ||
|
230dc545f4 | ||
|
20daecf619 | ||
|
3333add7e0 | ||
|
777ae059f9 | ||
|
310ac70a74 | ||
|
29074c4bfd | ||
|
9bc0e99d6d | ||
|
b38302449c | ||
|
feee5069b1 | ||
|
6b962a74b3 | ||
|
0c80385958 | ||
|
8c41684993 | ||
|
8245ba0063 | ||
|
0e7a275a28 | ||
|
59268f2d1e | ||
|
2ad7799b38 | ||
|
3b7f2130f3 | ||
|
d75c800275 | ||
|
41e69992c0 | ||
|
43af14ad77 | ||
|
acf906b284 | ||
|
80f0baac1e | ||
|
3d7a39a593 | ||
|
a240d7cad5 | ||
|
b40dce27df | ||
|
9734b51f53 | ||
|
80cfe874f5 | ||
|
bcf4e294e0 | ||
|
a27a115d66 | ||
|
6ac36fdb69 | ||
|
505a94e3aa | ||
|
b921ca045e | ||
|
a382e089ae | ||
|
9eed5ca2a0 | ||
|
cbf34fe90e | ||
|
7dc812984b | ||
|
1ed4e9c17a | ||
|
5f09c35dee | ||
|
ae62e3daf7 | ||
|
8778f58fe4 | ||
|
751e35ac62 | ||
|
f41b2e16ab | ||
|
1f6ce072bf | ||
|
746aae464a | ||
|
7e212d011e | ||
|
2840a15fd5 | ||
|
c1482d4802 | ||
|
16c4376941 | ||
|
dfc45dbc93 | ||
|
31f5373652 | ||
|
ca7cf5987c | ||
|
34390a541a | ||
|
b8b4891e9b | ||
|
9cfab53bd2 | ||
|
82cda0b279 | ||
|
4357d51b9a | ||
|
90bfc45858 | ||
|
bb9f0dab22 | ||
|
b0a248e81a | ||
|
b3c26b8c1c | ||
|
073d761a03 | ||
|
bd31375bf3 | ||
|
7605b9cc00 | ||
|
0fa76d6f25 | ||
|
14505260ff | ||
|
cf8892ee1a | ||
|
7f7d921c53 | ||
|
8668430760 | ||
|
45818eae24 | ||
|
b154c4985d | ||
|
ac039c1073 | ||
|
3717cd8b3f | ||
|
7855ec2225 | ||
|
fbaca32615 | ||
|
5b1374bf1b | ||
|
18bd2c7c18 | ||
|
a4c7951475 | ||
|
c299ff6634 | ||
|
7d8975339e | ||
|
1bd9cea458 | ||
|
b838f1b3f0 | ||
|
e95d511017 | ||
|
942c96dbfa | ||
|
3cd40ee192 | ||
|
cebe977d49 | ||
|
e90005b192 | ||
|
6b5c630048 | ||
|
c9fcfcf498 | ||
|
dec9f9be11 | ||
|
f85a563cf3 | ||
|
5399a875c6 | ||
|
eb8ad4e771 | ||
|
93a71fb561 | ||
|
bde3758c50 | ||
|
88823b5252 | ||
|
9aa19ad3ca | ||
|
ad4593a2f6 | ||
|
849194414d | ||
|
b9ce4c737c | ||
|
30efff0d9d | ||
|
7364d27b4b | ||
|
19f41152ee | ||
|
f3d611913e | ||
|
1d81213773 | ||
|
2a545dae10 | ||
|
fc6e2593b4 | ||
|
ce25341496 | ||
|
57bddc5628 | ||
|
d7b293dc87 |
@ -2,14 +2,6 @@ stages:
|
||||
- test
|
||||
- quality-assurance
|
||||
|
||||
py39:
|
||||
stage: test
|
||||
image: python:3.9-alpine
|
||||
before_script:
|
||||
- apk add --no-cache libmagic
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e py39
|
||||
|
||||
py310:
|
||||
stage: test
|
||||
image: python:3.10-alpine
|
||||
|
@ -3,7 +3,7 @@ FROM python:3.11-alpine
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV DJANGO_ALLOW_ASYNC_UNSAFE 1
|
||||
|
||||
RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libxml2-dev libxslt-dev postgresql-dev libmagic texlive
|
||||
RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libxml2-dev libxslt-dev postgresql-dev libmagic texlive texmf-dist-latexextra
|
||||
|
||||
RUN apk add --no-cache bash
|
||||
|
||||
@ -13,6 +13,8 @@ COPY requirements.txt /code/requirements.txt
|
||||
COPY docs/requirements.txt /code/docs/requirements.txt
|
||||
RUN pip install -r requirements.txt --no-cache-dir
|
||||
RUN pip install -r docs/requirements.txt --no-cache-dir
|
||||
# FIXME Remove this line when all dependencies will be ready
|
||||
RUN pip install "Django>=4.2,<5.0"
|
||||
|
||||
COPY . /code/
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
from rest_framework import routers
|
||||
|
||||
from .viewsets import UserViewSet
|
||||
@ -29,6 +29,6 @@ app_name = 'api'
|
||||
# Wire up our API using automatic URL routing.
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url('^', include(router.urls)),
|
||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
path('', include(router.urls)),
|
||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
]
|
@ -1,64 +0,0 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'trigram', 'valid',)
|
||||
search_fields = ('name', 'trigram',)
|
||||
list_filter = ('participation__valid',)
|
||||
|
||||
def valid(self, team):
|
||||
return team.participation.valid
|
||||
|
||||
valid.short_description = _('valid')
|
||||
|
||||
|
||||
@admin.register(Participation)
|
||||
class ParticipationAdmin(admin.ModelAdmin):
|
||||
list_display = ('team', 'valid',)
|
||||
search_fields = ('team__name', 'team__trigram',)
|
||||
list_filter = ('valid',)
|
||||
|
||||
|
||||
@admin.register(Pool)
|
||||
class PoolAdmin(admin.ModelAdmin):
|
||||
search_fields = ('participations__team__name', 'participations__team__trigram',)
|
||||
|
||||
|
||||
@admin.register(Passage)
|
||||
class PassageAdmin(admin.ModelAdmin):
|
||||
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
|
||||
|
||||
|
||||
@admin.register(Note)
|
||||
class NoteAdmin(admin.ModelAdmin):
|
||||
search_fields = ('jury',)
|
||||
|
||||
|
||||
@admin.register(Solution)
|
||||
class SolutionAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||
|
||||
|
||||
@admin.register(Synthesis)
|
||||
class SynthesisAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||
|
||||
|
||||
@admin.register(Tournament)
|
||||
class TournamentAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
@admin.register(Tweak)
|
||||
class TweakAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'pool', 'diff',)
|
@ -1,37 +0,0 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import ModelAdmin
|
||||
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin
|
||||
|
||||
from .models import CoachRegistration, Payment, Registration, StudentRegistration, VolunteerRegistration
|
||||
|
||||
|
||||
@admin.register(Registration)
|
||||
class RegistrationAdmin(PolymorphicParentModelAdmin):
|
||||
child_models = (StudentRegistration, CoachRegistration, VolunteerRegistration,)
|
||||
list_display = ("user", "type", "email_confirmed",)
|
||||
polymorphic_list = True
|
||||
|
||||
|
||||
@admin.register(StudentRegistration)
|
||||
class StudentRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(CoachRegistration)
|
||||
class CoachRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(VolunteerRegistration)
|
||||
class VolunteerRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(Payment)
|
||||
class PaymentAdmin(ModelAdmin):
|
||||
list_display = ('registration', 'type', 'valid', )
|
||||
search_fields = ('registration__user__last_name', 'registration__user__first_name', 'registration__user__email',)
|
||||
list_filter = ('type', 'valid',)
|
4
draw/__init__.py
Normal file
4
draw/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'draw.apps.DrawConfig'
|
62
draw/admin.py
Normal file
62
draw/admin.py
Normal file
@ -0,0 +1,62 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Draw, Pool, Round, TeamDraw
|
||||
|
||||
|
||||
@admin.register(Draw)
|
||||
class DrawAdmin(admin.ModelAdmin):
|
||||
list_display = ('tournament', 'teams', 'current_round', 'get_state',)
|
||||
list_filter = ('tournament', 'current_round',)
|
||||
search_fields = ('tournament__name', 'tournament__participation__team__trigram',)
|
||||
|
||||
@admin.display(description=_("teams"))
|
||||
def teams(self, record: Draw):
|
||||
return ', '.join(p.team.trigram for p in record.tournament.participations.filter(valid=True).all())
|
||||
|
||||
|
||||
@admin.register(Round)
|
||||
class RoundAdmin(admin.ModelAdmin):
|
||||
list_display = ('draw', 'number', 'teams',)
|
||||
list_filter = ('draw__tournament', 'number',)
|
||||
search_fields = ('draw__tournament__name', 'pool__teamdraw__participation__team__trigram')
|
||||
ordering = ('draw__tournament__name', 'number')
|
||||
|
||||
@admin.display(description=_("teams"))
|
||||
def teams(self, record: Round):
|
||||
return ', '.join(td.participation.team.trigram for td in record.team_draws)
|
||||
|
||||
|
||||
@admin.register(Pool)
|
||||
class PoolAdmin(admin.ModelAdmin):
|
||||
list_display = ('tournament', 'round', 'letter', 'teams')
|
||||
list_filter = ('round__draw__tournament', 'round__number', 'letter')
|
||||
ordering = ('round__draw__tournament__name', 'round', 'letter')
|
||||
search_fields = ('round__draw__tournament__name', 'teamdraw__participation__team__trigram',)
|
||||
|
||||
@admin.display(ordering='round__draw__tournament__name', description=_("tournament"))
|
||||
def tournament(self, record):
|
||||
return record.round.draw.tournament
|
||||
|
||||
@admin.display(description=_("teams"))
|
||||
def teams(self, record: Round):
|
||||
return ', '.join(td.participation.team.trigram for td in record.team_draws)
|
||||
|
||||
|
||||
@admin.register(TeamDraw)
|
||||
class TeamDrawAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'tournament', 'view_round', 'pool', 'accepted', 'rejected',
|
||||
'passage_index', 'choose_index', 'passage_dice', 'choice_dice',)
|
||||
list_filter = ('round__draw__tournament', 'round__number', 'pool__letter',)
|
||||
search_fields = ('round__draw__tournament__name', 'participation__team__trigram',)
|
||||
|
||||
@admin.display(ordering='round__draw__tournament__name', description=_("tournament"))
|
||||
def tournament(self, record):
|
||||
return record.round.draw.tournament
|
||||
|
||||
@admin.display(ordering='round__number', description=_('round'))
|
||||
def view_round(self, record):
|
||||
return record.round.get_number_display()
|
10
draw/apps.py
Normal file
10
draw/apps.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class DrawConfig(AppConfig):
|
||||
name = 'draw'
|
||||
verbose_name = _("Draw")
|
1594
draw/consumers.py
Normal file
1594
draw/consumers.py
Normal file
File diff suppressed because it is too large
Load Diff
529
draw/migrations/0001_initial.py
Normal file
529
draw/migrations/0001_initial.py
Normal file
@ -0,0 +1,529 @@
|
||||
# Generated by Django 4.2 on 2023-04-04 17:54
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("participation", "0005_alter_team_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Draw",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_message",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="The last message that is displayed on the drawing interface.",
|
||||
verbose_name="last message",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "draw",
|
||||
"verbose_name_plural": "draws",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Pool",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"letter",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[(1, "A"), (2, "B"), (3, "C"), (4, "D")],
|
||||
help_text="The letter of the pool: A, B, C or D.",
|
||||
verbose_name="letter",
|
||||
),
|
||||
),
|
||||
(
|
||||
"size",
|
||||
models.PositiveSmallIntegerField(
|
||||
help_text="The number of teams in this pool, between 3 and 5.",
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(3),
|
||||
django.core.validators.MaxValueValidator(5),
|
||||
],
|
||||
verbose_name="size",
|
||||
),
|
||||
),
|
||||
(
|
||||
"associated_pool",
|
||||
models.OneToOneField(
|
||||
default=None,
|
||||
help_text="The full pool instance.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="draw_pool",
|
||||
to="participation.pool",
|
||||
verbose_name="associated pool",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "pool",
|
||||
"verbose_name_plural": "pools",
|
||||
"ordering": (
|
||||
"round__draw__tournament__name",
|
||||
"round__number",
|
||||
"letter",
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Round",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"number",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[(1, "Round 1"), (2, "Round 2")],
|
||||
help_text="The number of the round, 1 or 2",
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(2),
|
||||
],
|
||||
verbose_name="number",
|
||||
),
|
||||
),
|
||||
(
|
||||
"current_pool",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
help_text="The current pool where teams select their problems.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="draw.pool",
|
||||
verbose_name="current pool",
|
||||
),
|
||||
),
|
||||
(
|
||||
"draw",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="draw.draw",
|
||||
verbose_name="draw",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "round",
|
||||
"verbose_name_plural": "rounds",
|
||||
"ordering": ("draw__tournament__name", "number"),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="TeamDraw",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"passage_index",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)],
|
||||
default=None,
|
||||
help_text="The passage order in the pool, between 0 and the size of the pool minus 1.",
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(0),
|
||||
django.core.validators.MaxValueValidator(4),
|
||||
],
|
||||
verbose_name="passage index",
|
||||
),
|
||||
),
|
||||
(
|
||||
"choose_index",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)],
|
||||
default=None,
|
||||
help_text="The choice order in the pool, between 0 and the size of the pool minus 1.",
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(0),
|
||||
django.core.validators.MaxValueValidator(4),
|
||||
],
|
||||
verbose_name="choose index",
|
||||
),
|
||||
),
|
||||
(
|
||||
"accepted",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, "Problem #1"),
|
||||
(2, "Problem #2"),
|
||||
(3, "Problem #3"),
|
||||
(4, "Problem #4"),
|
||||
(5, "Problem #5"),
|
||||
(6, "Problem #6"),
|
||||
(7, "Problem #7"),
|
||||
(8, "Problem #8"),
|
||||
],
|
||||
default=None,
|
||||
null=True,
|
||||
verbose_name="accepted problem",
|
||||
),
|
||||
),
|
||||
(
|
||||
"passage_dice",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
(4, 4),
|
||||
(5, 5),
|
||||
(6, 6),
|
||||
(7, 7),
|
||||
(8, 8),
|
||||
(9, 9),
|
||||
(10, 10),
|
||||
(11, 11),
|
||||
(12, 12),
|
||||
(13, 13),
|
||||
(14, 14),
|
||||
(15, 15),
|
||||
(16, 16),
|
||||
(17, 17),
|
||||
(18, 18),
|
||||
(19, 19),
|
||||
(20, 20),
|
||||
(21, 21),
|
||||
(22, 22),
|
||||
(23, 23),
|
||||
(24, 24),
|
||||
(25, 25),
|
||||
(26, 26),
|
||||
(27, 27),
|
||||
(28, 28),
|
||||
(29, 29),
|
||||
(30, 30),
|
||||
(31, 31),
|
||||
(32, 32),
|
||||
(33, 33),
|
||||
(34, 34),
|
||||
(35, 35),
|
||||
(36, 36),
|
||||
(37, 37),
|
||||
(38, 38),
|
||||
(39, 39),
|
||||
(40, 40),
|
||||
(41, 41),
|
||||
(42, 42),
|
||||
(43, 43),
|
||||
(44, 44),
|
||||
(45, 45),
|
||||
(46, 46),
|
||||
(47, 47),
|
||||
(48, 48),
|
||||
(49, 49),
|
||||
(50, 50),
|
||||
(51, 51),
|
||||
(52, 52),
|
||||
(53, 53),
|
||||
(54, 54),
|
||||
(55, 55),
|
||||
(56, 56),
|
||||
(57, 57),
|
||||
(58, 58),
|
||||
(59, 59),
|
||||
(60, 60),
|
||||
(61, 61),
|
||||
(62, 62),
|
||||
(63, 63),
|
||||
(64, 64),
|
||||
(65, 65),
|
||||
(66, 66),
|
||||
(67, 67),
|
||||
(68, 68),
|
||||
(69, 69),
|
||||
(70, 70),
|
||||
(71, 71),
|
||||
(72, 72),
|
||||
(73, 73),
|
||||
(74, 74),
|
||||
(75, 75),
|
||||
(76, 76),
|
||||
(77, 77),
|
||||
(78, 78),
|
||||
(79, 79),
|
||||
(80, 80),
|
||||
(81, 81),
|
||||
(82, 82),
|
||||
(83, 83),
|
||||
(84, 84),
|
||||
(85, 85),
|
||||
(86, 86),
|
||||
(87, 87),
|
||||
(88, 88),
|
||||
(89, 89),
|
||||
(90, 90),
|
||||
(91, 91),
|
||||
(92, 92),
|
||||
(93, 93),
|
||||
(94, 94),
|
||||
(95, 95),
|
||||
(96, 96),
|
||||
(97, 97),
|
||||
(98, 98),
|
||||
(99, 99),
|
||||
(100, 100),
|
||||
],
|
||||
default=None,
|
||||
null=True,
|
||||
verbose_name="passage dice",
|
||||
),
|
||||
),
|
||||
(
|
||||
"choice_dice",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
(4, 4),
|
||||
(5, 5),
|
||||
(6, 6),
|
||||
(7, 7),
|
||||
(8, 8),
|
||||
(9, 9),
|
||||
(10, 10),
|
||||
(11, 11),
|
||||
(12, 12),
|
||||
(13, 13),
|
||||
(14, 14),
|
||||
(15, 15),
|
||||
(16, 16),
|
||||
(17, 17),
|
||||
(18, 18),
|
||||
(19, 19),
|
||||
(20, 20),
|
||||
(21, 21),
|
||||
(22, 22),
|
||||
(23, 23),
|
||||
(24, 24),
|
||||
(25, 25),
|
||||
(26, 26),
|
||||
(27, 27),
|
||||
(28, 28),
|
||||
(29, 29),
|
||||
(30, 30),
|
||||
(31, 31),
|
||||
(32, 32),
|
||||
(33, 33),
|
||||
(34, 34),
|
||||
(35, 35),
|
||||
(36, 36),
|
||||
(37, 37),
|
||||
(38, 38),
|
||||
(39, 39),
|
||||
(40, 40),
|
||||
(41, 41),
|
||||
(42, 42),
|
||||
(43, 43),
|
||||
(44, 44),
|
||||
(45, 45),
|
||||
(46, 46),
|
||||
(47, 47),
|
||||
(48, 48),
|
||||
(49, 49),
|
||||
(50, 50),
|
||||
(51, 51),
|
||||
(52, 52),
|
||||
(53, 53),
|
||||
(54, 54),
|
||||
(55, 55),
|
||||
(56, 56),
|
||||
(57, 57),
|
||||
(58, 58),
|
||||
(59, 59),
|
||||
(60, 60),
|
||||
(61, 61),
|
||||
(62, 62),
|
||||
(63, 63),
|
||||
(64, 64),
|
||||
(65, 65),
|
||||
(66, 66),
|
||||
(67, 67),
|
||||
(68, 68),
|
||||
(69, 69),
|
||||
(70, 70),
|
||||
(71, 71),
|
||||
(72, 72),
|
||||
(73, 73),
|
||||
(74, 74),
|
||||
(75, 75),
|
||||
(76, 76),
|
||||
(77, 77),
|
||||
(78, 78),
|
||||
(79, 79),
|
||||
(80, 80),
|
||||
(81, 81),
|
||||
(82, 82),
|
||||
(83, 83),
|
||||
(84, 84),
|
||||
(85, 85),
|
||||
(86, 86),
|
||||
(87, 87),
|
||||
(88, 88),
|
||||
(89, 89),
|
||||
(90, 90),
|
||||
(91, 91),
|
||||
(92, 92),
|
||||
(93, 93),
|
||||
(94, 94),
|
||||
(95, 95),
|
||||
(96, 96),
|
||||
(97, 97),
|
||||
(98, 98),
|
||||
(99, 99),
|
||||
(100, 100),
|
||||
],
|
||||
default=None,
|
||||
null=True,
|
||||
verbose_name="choice dice",
|
||||
),
|
||||
),
|
||||
(
|
||||
"purposed",
|
||||
models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, "Problem #1"),
|
||||
(2, "Problem #2"),
|
||||
(3, "Problem #3"),
|
||||
(4, "Problem #4"),
|
||||
(5, "Problem #5"),
|
||||
(6, "Problem #6"),
|
||||
(7, "Problem #7"),
|
||||
(8, "Problem #8"),
|
||||
],
|
||||
default=None,
|
||||
null=True,
|
||||
verbose_name="accepted problem",
|
||||
),
|
||||
),
|
||||
(
|
||||
"rejected",
|
||||
models.JSONField(default=list, verbose_name="rejected problems"),
|
||||
),
|
||||
(
|
||||
"participation",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="participation.participation",
|
||||
verbose_name="participation",
|
||||
),
|
||||
),
|
||||
(
|
||||
"pool",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="draw.pool",
|
||||
verbose_name="pool",
|
||||
),
|
||||
),
|
||||
(
|
||||
"round",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="draw.round",
|
||||
verbose_name="round",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "team draw",
|
||||
"verbose_name_plural": "team draws",
|
||||
"ordering": (
|
||||
"round__draw__tournament__name",
|
||||
"round__number",
|
||||
"pool__letter",
|
||||
"passage_index",
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="pool",
|
||||
name="current_team",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="The current team that is selecting its problem.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="draw.teamdraw",
|
||||
verbose_name="current team",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="pool",
|
||||
name="round",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="draw.round"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="draw",
|
||||
name="current_round",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="The current round where teams select their problems.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="draw.round",
|
||||
verbose_name="current round",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="draw",
|
||||
name="tournament",
|
||||
field=models.OneToOneField(
|
||||
help_text="The associated tournament.",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="participation.tournament",
|
||||
verbose_name="tournament",
|
||||
),
|
||||
),
|
||||
]
|
0
draw/migrations/__init__.py
Normal file
0
draw/migrations/__init__.py
Normal file
526
draw/models.py
Normal file
526
draw/models.py
Normal file
@ -0,0 +1,526 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.text import format_lazy, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from participation.models import Participation, Passage, Pool as PPool, Tournament
|
||||
|
||||
|
||||
class Draw(models.Model):
|
||||
"""
|
||||
A draw instance is linked to a :model:`participation.Tournament` and contains all information
|
||||
about a draw.
|
||||
"""
|
||||
|
||||
tournament = models.OneToOneField(
|
||||
Tournament,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('tournament'),
|
||||
help_text=_("The associated tournament.")
|
||||
)
|
||||
|
||||
current_round = models.ForeignKey(
|
||||
'Round',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current round'),
|
||||
help_text=_("The current round where teams select their problems."),
|
||||
)
|
||||
|
||||
last_message = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("last message"),
|
||||
help_text=_("The last message that is displayed on the drawing interface.")
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('draw:index') + f'#{slugify(self.tournament.name)}'
|
||||
|
||||
@property
|
||||
def exportable(self) -> bool:
|
||||
"""
|
||||
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
||||
This operation is synchronous.
|
||||
"""
|
||||
return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all())
|
||||
|
||||
async def is_exportable(self) -> bool:
|
||||
"""
|
||||
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
||||
This operation is asynchronous.
|
||||
"""
|
||||
return any([await pool.is_exportable() async for r in self.round_set.all() async for pool in r.pool_set.all()])
|
||||
|
||||
def get_state(self) -> str:
|
||||
"""
|
||||
The current state of the draw.
|
||||
Can be:
|
||||
|
||||
* **DICE_SELECT_POULES** if we are waiting for teams to launch their dice to determine pools and passage order ;
|
||||
* **DICE_ORDER_POULE** if we are waiting for teams to launch their dice to determine the problem draw order ;
|
||||
* **WAITING_DRAW_PROBLEM** if we are waiting for a team to draw a problem ;
|
||||
* **WAITING_CHOOSE_PROBLEM** if we are waiting for a team to accept or reject a problem ;
|
||||
* **WAITING_FINAL** if this is the final tournament and we are between the two rounds ;
|
||||
* **DRAW_ENDED** if the draw is ended.
|
||||
|
||||
Warning: the current round and the current team must be prefetched in an async context.
|
||||
"""
|
||||
if self.current_round.current_pool is None:
|
||||
return 'DICE_SELECT_POULES'
|
||||
elif self.current_round.current_pool.current_team is None:
|
||||
return 'DICE_ORDER_POULE'
|
||||
elif self.current_round.current_pool.current_team.accepted is not None:
|
||||
if self.current_round.number == 1:
|
||||
# The last step can be the last problem acceptation after the first round
|
||||
# only for the final between the two rounds
|
||||
return 'WAITING_FINAL'
|
||||
else:
|
||||
return 'DRAW_ENDED'
|
||||
elif self.current_round.current_pool.current_team.purposed is None:
|
||||
return 'WAITING_DRAW_PROBLEM'
|
||||
else:
|
||||
return 'WAITING_CHOOSE_PROBLEM'
|
||||
|
||||
@property
|
||||
def information(self):
|
||||
"""
|
||||
The information header on the draw interface, which is defined according to the
|
||||
current state.
|
||||
|
||||
Warning: this property is synchronous.
|
||||
"""
|
||||
s = ""
|
||||
if self.last_message:
|
||||
s += self.last_message + "<br><br>"
|
||||
|
||||
match self.get_state():
|
||||
case 'DICE_SELECT_POULES':
|
||||
# Waiting for dices to determine pools and passage order
|
||||
if self.current_round.number == 1:
|
||||
# Specific information for the first round
|
||||
s += """Nous allons commencer le tirage des problèmes.<br>
|
||||
Vous pouvez à tout moment poser toute question si quelque chose
|
||||
n'est pas clair ou ne va pas.<br><br>
|
||||
Nous allons d'abord tirer les poules et l'ordre de passage
|
||||
pour le premier tour avec toutes les équipes puis pour chaque poule,
|
||||
nous tirerons l'ordre de tirage pour le tour et les problèmes.<br><br>"""
|
||||
s += """
|
||||
Les capitaines, vous pouvez désormais toustes lancer un dé 100,
|
||||
en cliquant sur le gros bouton. Les poules et l'ordre de passage
|
||||
lors du premier tour sera l'ordre croissant des dés, c'est-à-dire
|
||||
que le plus petit lancer sera le premier à passer dans la poule A."""
|
||||
case 'DICE_ORDER_POULE':
|
||||
# Waiting for dices to determine the choice order
|
||||
s += f"""Nous passons au tirage des problèmes pour la poule
|
||||
<strong>{self.current_round.current_pool}</strong>, entre les équipes
|
||||
<strong>{', '.join(td.participation.team.trigram
|
||||
for td in self.current_round.current_pool.teamdraw_set.all())}</strong>.
|
||||
Les capitaines peuvent lancer un dé 100 en cliquant sur le gros bouton
|
||||
pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra
|
||||
tirer en premier."""
|
||||
case 'WAITING_DRAW_PROBLEM':
|
||||
# Waiting for a problem draw
|
||||
td = self.current_round.current_pool.current_team
|
||||
s += f"""C'est au tour de l'équipe <strong>{td.participation.team.trigram}</strong>
|
||||
de choisir son problème. Cliquez sur l'urne au milieu pour tirer un problème au sort."""
|
||||
case 'WAITING_CHOOSE_PROBLEM':
|
||||
# Waiting for the team that can accept or reject the problem
|
||||
td = self.current_round.current_pool.current_team
|
||||
s += f"""L'équipe <strong>{td.participation.team.trigram}</strong> a tiré le problème
|
||||
<strong>{td.purposed} : {settings.PROBLEMS[td.purposed - 1]}</strong>. """
|
||||
if td.purposed in td.rejected:
|
||||
# The problem was previously rejected
|
||||
s += """Elle a déjà refusé ce problème auparavant, elle peut donc le refuser sans pénalité et
|
||||
tirer un nouveau problème immédiatement, ou bien revenir sur son choix."""
|
||||
else:
|
||||
# The problem can be rejected
|
||||
s += "Elle peut décider d'accepter ou de refuser ce problème. "
|
||||
if len(td.rejected) >= len(settings.PROBLEMS) - 5:
|
||||
s += "Refuser ce problème ajoutera une nouvelle pénalité de 0.5 sur le coefficient de l'oral de læ défenseur⋅se."
|
||||
else:
|
||||
s += f"Il reste {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité."
|
||||
case 'WAITING_FINAL':
|
||||
# We are between the two rounds of the final tournament
|
||||
s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !"
|
||||
case 'DRAW_ENDED':
|
||||
# The draw is ended
|
||||
s += "Le tirage au sort est terminé. Les solutions des autres équipes peuvent être trouvées dans l'onglet « Ma participation »."
|
||||
|
||||
s += "<br><br>" if s else ""
|
||||
s += """Pour plus de détails sur le déroulement du tirage au sort,
|
||||
le règlement est accessible sur
|
||||
<a class="alert-link" href="https://tfjm.org/reglement">https://tfjm.org/reglement</a>."""
|
||||
return s
|
||||
|
||||
async def ainformation(self) -> str:
|
||||
"""
|
||||
Asynchronous version to get the information header content.
|
||||
"""
|
||||
return await sync_to_async(lambda: self.information)()
|
||||
|
||||
def __str__(self):
|
||||
return str(format_lazy(_("Draw of tournament {tournament}"), tournament=self.tournament.name))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('draw')
|
||||
verbose_name_plural = _('draws')
|
||||
|
||||
|
||||
class Round(models.Model):
|
||||
"""
|
||||
This model is attached to a :model:`draw.Draw` and represents the draw
|
||||
for one round of the :model:`participation.Tournament`.
|
||||
"""
|
||||
draw = models.ForeignKey(
|
||||
Draw,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('draw'),
|
||||
)
|
||||
|
||||
number = models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, _('Round 1')),
|
||||
(2, _('Round 2')),
|
||||
],
|
||||
verbose_name=_('number'),
|
||||
help_text=_("The number of the round, 1 or 2"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(2)],
|
||||
)
|
||||
|
||||
current_pool = models.ForeignKey(
|
||||
'Pool',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current pool'),
|
||||
help_text=_("The current pool where teams select their problems."),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('draw:index') + f'#{slugify(self.draw.tournament.name)}'
|
||||
|
||||
@property
|
||||
def team_draws(self) -> QuerySet["TeamDraw"]:
|
||||
"""
|
||||
Returns a query set ordered by pool and by passage index of all team draws.
|
||||
"""
|
||||
return self.teamdraw_set.order_by('pool__letter', 'passage_index').all()
|
||||
|
||||
async def next_pool(self):
|
||||
"""
|
||||
Returns the next pool of the round.
|
||||
For example, after the pool A, we have the pool B.
|
||||
"""
|
||||
pool = self.current_pool
|
||||
return await self.pool_set.aget(letter=pool.letter + 1)
|
||||
|
||||
def __str__(self):
|
||||
return self.get_number_display()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('round')
|
||||
verbose_name_plural = _('rounds')
|
||||
ordering = ('draw__tournament__name', 'number',)
|
||||
|
||||
|
||||
class Pool(models.Model):
|
||||
"""
|
||||
A Pool is a collection of teams in a :model:`draw.Round` of a `draw.Draw`.
|
||||
It has a letter (eg. A, B, C or D) and a size, between 3 and 5.
|
||||
After the draw, the pool can be exported in a `participation.Pool` instance.
|
||||
"""
|
||||
round = models.ForeignKey(
|
||||
Round,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
letter = models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, 'A'),
|
||||
(2, 'B'),
|
||||
(3, 'C'),
|
||||
(4, 'D'),
|
||||
],
|
||||
verbose_name=_('letter'),
|
||||
help_text=_("The letter of the pool: A, B, C or D."),
|
||||
)
|
||||
|
||||
size = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('size'),
|
||||
validators=[MinValueValidator(3), MaxValueValidator(5)],
|
||||
help_text=_("The number of teams in this pool, between 3 and 5."),
|
||||
)
|
||||
|
||||
current_team = models.ForeignKey(
|
||||
'TeamDraw',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current team'),
|
||||
help_text=_("The current team that is selecting its problem."),
|
||||
)
|
||||
|
||||
associated_pool = models.OneToOneField(
|
||||
'participation.Pool',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
related_name='draw_pool',
|
||||
verbose_name=_("associated pool"),
|
||||
help_text=_("The full pool instance."),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('draw:index') + f'#{slugify(self.round.draw.tournament.name)}'
|
||||
|
||||
@property
|
||||
def team_draws(self) -> QuerySet["TeamDraw"]:
|
||||
"""
|
||||
Returns a query set ordered by passage index of all team draws in this pool.
|
||||
"""
|
||||
return self.teamdraw_set.order_by('passage_index').all()
|
||||
|
||||
@property
|
||||
def trigrams(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of trigrams of the teams in this pool ordered by passage index.
|
||||
This property is synchronous.
|
||||
"""
|
||||
return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')
|
||||
.prefetch_related('participation__team').all()]
|
||||
|
||||
async def atrigrams(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of trigrams of the teams in this pool ordered by passage index.
|
||||
This property is asynchronous.
|
||||
"""
|
||||
return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')
|
||||
.prefetch_related('participation__team').all()]
|
||||
|
||||
async def next_td(self) -> "TeamDraw":
|
||||
"""
|
||||
Returns the next team draw after the current one, to know who should draw a new problem.
|
||||
"""
|
||||
td = self.current_team
|
||||
current_index = (td.choose_index + 1) % self.size
|
||||
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
||||
while td.accepted:
|
||||
# Ignore if the next team already accepted its problem
|
||||
current_index += 1
|
||||
current_index %= self.size
|
||||
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
||||
return td
|
||||
|
||||
@property
|
||||
def exportable(self) -> bool:
|
||||
"""
|
||||
True if this pool is exportable, ie. can be exported to the tournament interface. That means that
|
||||
each team selected its problem.
|
||||
This operation is synchronous.
|
||||
"""
|
||||
return self.associated_pool_id is None and self.teamdraw_set.exists() \
|
||||
and all(td.accepted is not None for td in self.teamdraw_set.all())
|
||||
|
||||
async def is_exportable(self) -> bool:
|
||||
"""
|
||||
True if this pool is exportable, ie. can be exported to the tournament interface. That means that
|
||||
each team selected its problem.
|
||||
This operation is asynchronous.
|
||||
"""
|
||||
return self.associated_pool_id is None and await self.teamdraw_set.aexists() \
|
||||
and all([td.accepted is not None async for td in self.teamdraw_set.all()])
|
||||
|
||||
async def export(self) -> PPool:
|
||||
"""
|
||||
Translates this Pool instance in a :model:`participation.Pool` instance, with the passage orders.
|
||||
"""
|
||||
# Create the pool
|
||||
self.associated_pool = await PPool.objects.acreate(
|
||||
tournament=self.round.draw.tournament,
|
||||
round=self.round.number,
|
||||
letter=self.letter,
|
||||
)
|
||||
|
||||
# Define the participations of the pool
|
||||
tds = [td async for td in self.team_draws.prefetch_related('participation')]
|
||||
await self.associated_pool.participations.aset([td.participation async for td in self.team_draws
|
||||
.prefetch_related('participation')])
|
||||
await self.asave()
|
||||
|
||||
# Define the passage matrix according to the number of teams
|
||||
table = []
|
||||
if self.size == 3:
|
||||
table = [
|
||||
[0, 1, 2],
|
||||
[1, 2, 0],
|
||||
[2, 0, 1],
|
||||
]
|
||||
elif self.size == 4:
|
||||
table = [
|
||||
[0, 1, 2, 3],
|
||||
[1, 2, 3, 0],
|
||||
[2, 3, 0, 1],
|
||||
[3, 0, 1, 2],
|
||||
]
|
||||
elif self.size == 5:
|
||||
table = [
|
||||
[0, 2, 3],
|
||||
[1, 3, 4],
|
||||
[2, 0, 1],
|
||||
[3, 4, 0],
|
||||
[4, 1, 2],
|
||||
]
|
||||
|
||||
for i, line in enumerate(table):
|
||||
# Create the passage
|
||||
passage = await Passage.objects.acreate(
|
||||
pool=self.associated_pool,
|
||||
position=i + 1,
|
||||
solution_number=tds[line[0]].accepted,
|
||||
defender=tds[line[0]].participation,
|
||||
opponent=tds[line[1]].participation,
|
||||
reporter=tds[line[2]].participation,
|
||||
defender_penalties=tds[line[0]].penalty_int,
|
||||
)
|
||||
if self.size == 4:
|
||||
# Add observer for 4-teams pools
|
||||
passage.observer = tds[line[3]].participation
|
||||
await passage.asave()
|
||||
|
||||
return self.associated_pool
|
||||
|
||||
def __str__(self):
|
||||
return str(format_lazy(_("Pool {letter}{number}"), letter=self.get_letter_display(), number=self.round.number))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('pool')
|
||||
verbose_name_plural = _('pools')
|
||||
ordering = ('round__draw__tournament__name', 'round__number', 'letter',)
|
||||
|
||||
|
||||
class TeamDraw(models.Model):
|
||||
"""
|
||||
This model represents the state of the draw for a given team, including
|
||||
its accepted problem or their rejected ones.
|
||||
"""
|
||||
participation = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('participation'),
|
||||
)
|
||||
|
||||
round = models.ForeignKey(
|
||||
Round,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('round'),
|
||||
)
|
||||
|
||||
pool = models.ForeignKey(
|
||||
Pool,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('pool'),
|
||||
)
|
||||
|
||||
passage_index = models.PositiveSmallIntegerField(
|
||||
choices=zip(range(0, 5), range(0, 5)),
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('passage index'),
|
||||
help_text=_("The passage order in the pool, between 0 and the size of the pool minus 1."),
|
||||
validators=[MinValueValidator(0), MaxValueValidator(4)],
|
||||
)
|
||||
|
||||
choose_index = models.PositiveSmallIntegerField(
|
||||
choices=zip(range(0, 5), range(0, 5)),
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('choose index'),
|
||||
help_text=_("The choice order in the pool, between 0 and the size of the pool minus 1."),
|
||||
validators=[MinValueValidator(0), MaxValueValidator(4)],
|
||||
)
|
||||
|
||||
accepted = models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
||||
],
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("accepted problem"),
|
||||
)
|
||||
|
||||
passage_dice = models.PositiveSmallIntegerField(
|
||||
choices=zip(range(1, 101), range(1, 101)),
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("passage dice"),
|
||||
)
|
||||
|
||||
choice_dice = models.PositiveSmallIntegerField(
|
||||
choices=zip(range(1, 101), range(1, 101)),
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("choice dice"),
|
||||
)
|
||||
|
||||
purposed = models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
||||
],
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("purposed problem"),
|
||||
)
|
||||
|
||||
rejected = models.JSONField(
|
||||
default=list,
|
||||
verbose_name=_('rejected problems'),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('draw:index') + f'#{slugify(self.round.draw.tournament.name)}'
|
||||
|
||||
@property
|
||||
def last_dice(self):
|
||||
"""
|
||||
The last dice that was thrown.
|
||||
"""
|
||||
return self.passage_dice if self.round.draw.get_state() == 'DICE_SELECT_POULES' else self.choice_dice
|
||||
|
||||
@property
|
||||
def penalty_int(self):
|
||||
"""
|
||||
The number of penalties, which is the number of rejected problems after the P - 5 free rejects,
|
||||
where P is the number of problems.
|
||||
"""
|
||||
return max(0, len(self.rejected) - (len(settings.PROBLEMS) - 5))
|
||||
|
||||
@property
|
||||
def penalty(self):
|
||||
"""
|
||||
The penalty multiplier on the defender oral, which is a malus of 0.5 for each penalty.
|
||||
"""
|
||||
return 0.5 * self.penalty_int
|
||||
|
||||
def __str__(self):
|
||||
return str(format_lazy(_("Draw of the team {trigram} for the pool {letter}{number}"),
|
||||
trigram=self.participation.team.trigram,
|
||||
letter=self.pool.get_letter_display() if self.pool else "",
|
||||
number=self.round.number))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('team draw')
|
||||
verbose_name_plural = _('team draws')
|
||||
ordering = ('round__draw__tournament__name', 'round__number', 'pool__letter', 'passage_index',)
|
10
draw/routing.py
Normal file
10
draw/routing.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path("ws/draw/", consumers.DrawConsumer.as_asgi()),
|
||||
]
|
811
draw/static/draw.js
Normal file
811
draw/static/draw.js
Normal file
@ -0,0 +1,811 @@
|
||||
(async () => {
|
||||
// check notification permission
|
||||
// This is useful to alert people that they should do something
|
||||
await Notification.requestPermission()
|
||||
})()
|
||||
|
||||
const problems_count = JSON.parse(document.getElementById('problems_count').textContent)
|
||||
|
||||
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
|
||||
let socket = null
|
||||
|
||||
const messages = document.getElementById('messages')
|
||||
|
||||
/**
|
||||
* Request to abort the draw of the given tournament.
|
||||
* Only volunteers are allowed to do this.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function abortDraw(tid) {
|
||||
socket.send(JSON.stringify({'tid': tid, 'type': 'abort'}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to cancel the last step.
|
||||
* Only volunteers are allowed to do this.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function cancelLastStep(tid) {
|
||||
socket.send(JSON.stringify({'tid': tid, 'type': 'cancel'}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to launch a dice between 1 and 100, for the two first steps.
|
||||
* The parameter `trigram` can be specified (by volunteers) to launch a dice for a specific team.
|
||||
* @param tid The tournament id
|
||||
* @param trigram The trigram of the team that a volunteer wants to force the dice launch (default: null)
|
||||
* @param result The forced value. Null if unused (for regular people)
|
||||
*/
|
||||
function drawDice(tid, trigram = null, result = null) {
|
||||
socket.send(JSON.stringify({'tid': tid, 'type': 'dice', 'trigram': trigram, 'result': result}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to draw a new problem.
|
||||
* @param tid The tournament id
|
||||
* @param problem The forced problem. Null if unused (for regular people)
|
||||
*/
|
||||
function drawProblem(tid, problem = null) {
|
||||
socket.send(JSON.stringify({'tid': tid, 'type': 'draw_problem', 'problem': problem}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the current proposed problem.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function acceptProblem(tid) {
|
||||
socket.send(JSON.stringify({'tid': tid, 'type': 'accept'}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject the current proposed problem.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function rejectProblem(tid) {
|
||||
socket.send(JSON.stringify({'tid': tid, 'type': 'reject'}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Volunteers can export the draw to make it available for notation.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function exportDraw(tid) {
|
||||
socket.send(JSON.stringify({'tid': tid, 'type': 'export'}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Volunteers can make the draw continue for the second round of the final.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function continueFinal(tid) {
|
||||
socket.send(JSON.stringify({'tid': tid, 'type': 'continue_final'}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a new notification with the given title and the given body.
|
||||
* @param title The title of the notification
|
||||
* @param body The body of the notification
|
||||
* @param timeout The time (in milliseconds) after that the notification automatically closes. 0 to make indefinite. Default to 5000 ms.
|
||||
* @return Notification
|
||||
*/
|
||||
function showNotification(title, body, timeout = 5000) {
|
||||
let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
|
||||
if (timeout)
|
||||
setTimeout(() => notif.close(), timeout)
|
||||
return notif
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.location.hash) {
|
||||
// Open the tab of the tournament that is present in the hash
|
||||
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(elem => {
|
||||
if ('#' + elem.innerText.toLowerCase() === document.location.hash.toLowerCase()) {
|
||||
elem.click()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// When a tab is opened, add the tournament name in the hash
|
||||
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(
|
||||
elem => elem.addEventListener(
|
||||
'click', () => document.location.hash = '#' + elem.innerText.toLowerCase()))
|
||||
|
||||
/**
|
||||
* Add alert message on the top on the interface.
|
||||
* @param message The content of the alert.
|
||||
* @param type The alert type, which is a bootstrap color (success, info, warning, danger,…).
|
||||
* @param timeout The time (in milliseconds) before the alert is auto-closing. 0 to infinitely, default to 5000 ms.
|
||||
*/
|
||||
function addMessage(message, type, timeout = 5000) {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.innerHTML = [
|
||||
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
|
||||
`<div>${message}</div>`,
|
||||
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
||||
].join('\n')
|
||||
messages.append(wrapper)
|
||||
|
||||
if (timeout)
|
||||
setTimeout(() => wrapper.remove(), timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the information banner.
|
||||
* @param tid The tournament id
|
||||
* @param info The content to updated
|
||||
*/
|
||||
function setInfo(tid, info) {
|
||||
document.getElementById(`messages-${tid}`).innerHTML = info
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the draw interface, given the list of teams.
|
||||
* @param tid The tournament id
|
||||
* @param teams The list of teams (represented by their trigrams) that are present on this draw.
|
||||
*/
|
||||
function drawStart(tid, teams) {
|
||||
// Hide the not-started-banner
|
||||
document.getElementById(`banner-not-started-${tid}`).classList.add('d-none')
|
||||
// Display the full draw interface
|
||||
document.getElementById(`draw-content-${tid}`).classList.remove('d-none')
|
||||
|
||||
let dicesDiv = document.getElementById(`dices-${tid}`)
|
||||
for (let team of teams) {
|
||||
// Add empty dice score badge for each team
|
||||
let col = document.createElement('div')
|
||||
col.classList.add('col-md-1')
|
||||
dicesDiv.append(col)
|
||||
|
||||
let diceDiv = document.createElement('div')
|
||||
diceDiv.id = `dice-${tid}-${team}`
|
||||
diceDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
|
||||
if (document.getElementById(`abort-${tid}`) !== null) {
|
||||
// Check if this is a volunteer, who can launch a die for a specific team
|
||||
diceDiv.onclick = (_) => drawDice(tid, team)
|
||||
}
|
||||
diceDiv.textContent = `${team} 🎲 ??`
|
||||
col.append(diceDiv)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the current draw, and make all invisible, except the not-started-banner.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function drawAbort(tid) {
|
||||
document.getElementById(`banner-not-started-${tid}`).classList.remove('d-none')
|
||||
document.getElementById(`draw-content-${tid}`).classList.add('d-none')
|
||||
document.getElementById(`dices-${tid}`).innerHTML = ""
|
||||
document.getElementById(`recap-${tid}-round-list`).innerHTML = ""
|
||||
document.getElementById(`tables-${tid}`).innerHTML = ""
|
||||
updateDiceVisibility(tid, false)
|
||||
updateBoxVisibility(tid, false)
|
||||
updateButtonsVisibility(tid, false)
|
||||
updateExportVisibility(tid, false)
|
||||
updateContinueVisibility(tid, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is triggered after a new dice result. We update the score of the team.
|
||||
* Can be resetted to empty values if the result is null.
|
||||
* @param tid The tournament id
|
||||
* @param trigram The trigram of the team that launched its dice
|
||||
* @param result The result of the dice. null if it is a reset.
|
||||
*/
|
||||
function updateDiceInfo(tid, trigram, result) {
|
||||
let elem = document.getElementById(`dice-${tid}-${trigram}`)
|
||||
if (result === null) {
|
||||
elem.classList.remove('text-bg-success')
|
||||
elem.classList.add('text-bg-warning')
|
||||
elem.innerText = `${trigram} 🎲 ??`
|
||||
} else {
|
||||
elem.classList.remove('text-bg-warning')
|
||||
elem.classList.add('text-bg-success')
|
||||
elem.innerText = `${trigram} 🎲 ${result}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the dice button.
|
||||
* @param tid The tournament id
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateDiceVisibility(tid, visible) {
|
||||
let div = document.getElementById(`launch-dice-${tid}`)
|
||||
if (visible)
|
||||
div.classList.remove('d-none')
|
||||
else
|
||||
div.classList.add('d-none')
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the box button.
|
||||
* @param tid The tournament id
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateBoxVisibility(tid, visible) {
|
||||
let div = document.getElementById(`draw-problem-${tid}`)
|
||||
if (visible)
|
||||
div.classList.remove('d-none')
|
||||
else
|
||||
div.classList.add('d-none')
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the accept and reject buttons.
|
||||
* @param tid The tournament id
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateButtonsVisibility(tid, visible) {
|
||||
let div = document.getElementById(`buttons-${tid}`)
|
||||
if (visible)
|
||||
div.classList.remove('d-none')
|
||||
else
|
||||
div.classList.add('d-none')
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the export button.
|
||||
* @param tid The tournament id
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateExportVisibility(tid, visible) {
|
||||
let div = document.getElementById(`export-${tid}`)
|
||||
if (visible)
|
||||
div.classList.remove('d-none')
|
||||
else
|
||||
div.classList.add('d-none')
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the continuation button.
|
||||
* @param tid The tournament id
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateContinueVisibility(tid, visible) {
|
||||
let div = document.getElementById(`continue-${tid}`)
|
||||
if (div !== null) {
|
||||
// Only present during the final
|
||||
if (visible)
|
||||
div.classList.remove('d-none')
|
||||
else
|
||||
div.classList.add('d-none')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the different pools for the given round, and update the interface.
|
||||
* @param tid The tournament id
|
||||
* @param round The round number, as integer (1 or 2)
|
||||
* @param poules The list of poules, which are represented with their letters and trigrams,
|
||||
* [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}]
|
||||
*/
|
||||
function updatePoules(tid, round, poules) {
|
||||
let roundList = document.getElementById(`recap-${tid}-round-list`)
|
||||
let poolListId = `recap-${tid}-round-${round}-pool-list`
|
||||
let poolList = document.getElementById(poolListId)
|
||||
if (poolList === null) {
|
||||
// Add a div for the round in the recap div
|
||||
let div = document.createElement('div')
|
||||
div.id = `recap-${tid}-round-${round}`
|
||||
div.classList.add('col-md-6', 'px-3', 'py-3')
|
||||
div.setAttribute('data-tournament', tid)
|
||||
|
||||
let title = document.createElement('strong')
|
||||
title.textContent = 'Tour ' + round
|
||||
|
||||
poolList = document.createElement('ul')
|
||||
poolList.id = poolListId
|
||||
poolList.classList.add('list-group', 'list-group-flush')
|
||||
|
||||
div.append(title, poolList)
|
||||
roundList.append(div)
|
||||
}
|
||||
|
||||
let c = 1
|
||||
|
||||
for (let poule of poules) {
|
||||
let teamListId = `recap-${tid}-round-${round}-pool-${poule.letter}-team-list`
|
||||
let teamList = document.getElementById(teamListId)
|
||||
if (teamList === null) {
|
||||
// Add a div for the pool in the recap div
|
||||
let li = document.createElement('li')
|
||||
li.id = `recap-${tid}-round-${round}-pool-${poule.letter}`
|
||||
li.classList.add('list-group-item', 'px-3', 'py-3')
|
||||
li.setAttribute('data-tournament', tid)
|
||||
|
||||
let title = document.createElement('strong')
|
||||
title.textContent = 'Poule ' + poule.letter + round
|
||||
|
||||
teamList = document.createElement('ul')
|
||||
teamList.id = teamListId
|
||||
teamList.classList.add('list-group', 'list-group-flush')
|
||||
|
||||
li.append(title, teamList)
|
||||
poolList.append(li)
|
||||
}
|
||||
teamList.innerHTML = ""
|
||||
|
||||
for (let team of poule.teams) {
|
||||
// Reorder dices
|
||||
let diceDiv = document.getElementById(`dice-${tid}-${team}`)
|
||||
diceDiv.parentElement.style.order = c.toString()
|
||||
c += 1
|
||||
|
||||
let teamLiId = `recap-${tid}-round-${round}-team-${team}`
|
||||
|
||||
// Add a line for the team in the recap
|
||||
let teamLi = document.createElement('li')
|
||||
teamLi.id = teamLiId
|
||||
teamLi.classList.add('list-group-item')
|
||||
teamLi.setAttribute('data-tournament', tid)
|
||||
|
||||
teamList.append(teamLi)
|
||||
|
||||
// Add the accepted problem div (empty for now)
|
||||
let acceptedDivId = `recap-${tid}-round-${round}-team-${team}-accepted`
|
||||
let acceptedDiv = document.getElementById(acceptedDivId)
|
||||
if (acceptedDiv === null) {
|
||||
acceptedDiv = document.createElement('div')
|
||||
acceptedDiv.id = acceptedDivId
|
||||
acceptedDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
|
||||
acceptedDiv.textContent = `${team} 📃 ?`
|
||||
teamLi.append(acceptedDiv)
|
||||
}
|
||||
|
||||
// Add the rejected problems div (empty for now)
|
||||
let rejectedDivId = `recap-${tid}-round-${round}-team-${team}-rejected`
|
||||
let rejectedDiv = document.getElementById(rejectedDivId)
|
||||
if (rejectedDiv === null) {
|
||||
rejectedDiv = document.createElement('div')
|
||||
rejectedDiv.id = rejectedDivId
|
||||
rejectedDiv.classList.add('badge', 'rounded-pill', 'text-bg-danger')
|
||||
rejectedDiv.textContent = '🗑️'
|
||||
teamLi.append(rejectedDiv)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw tables
|
||||
let tablesDiv = document.getElementById(`tables-${tid}`)
|
||||
let tablesRoundDiv = document.getElementById(`tables-${tid}-round-${round}`)
|
||||
if (tablesRoundDiv === null) {
|
||||
// Add the tables div for the current round if necessary
|
||||
let card = document.createElement('div')
|
||||
card.classList.add('card', 'col-md-6')
|
||||
tablesDiv.append(card)
|
||||
|
||||
let cardHeader = document.createElement('div')
|
||||
cardHeader.classList.add('card-header')
|
||||
cardHeader.innerHTML = `<h2>Tour ${round}</h2>`
|
||||
card.append(cardHeader)
|
||||
|
||||
tablesRoundDiv = document.createElement('div')
|
||||
tablesRoundDiv.id = `tables-${tid}-round-${round}`
|
||||
tablesRoundDiv.classList.add('card-body', 'd-flex', 'flex-wrap')
|
||||
card.append(tablesRoundDiv)
|
||||
}
|
||||
|
||||
for (let poule of poules) {
|
||||
if (poule.teams.length === 0)
|
||||
continue
|
||||
|
||||
// Display the table for the pool
|
||||
updatePouleTable(tid, round, poule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the table for the given round and the given pool, where there will be the chosen problems.
|
||||
* @param tid The tournament id
|
||||
* @param round The round number, as integer (1 or 2)
|
||||
* @param poule The current pool, which id represented with its letter and trigrams,
|
||||
* {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}
|
||||
*/
|
||||
function updatePouleTable(tid, round, poule) {
|
||||
let tablesRoundDiv = document.getElementById(`tables-${tid}-round-${round}`)
|
||||
let pouleTable = document.getElementById(`table-${tid}-${round}-${poule.letter}`)
|
||||
if (pouleTable === null) {
|
||||
// Create table
|
||||
let card = document.createElement('div')
|
||||
card.classList.add('card', 'w-100', 'my-3', `order-${poule.letter.charCodeAt(0) - 64}`)
|
||||
tablesRoundDiv.append(card)
|
||||
|
||||
let cardHeader = document.createElement('div')
|
||||
cardHeader.classList.add('card-header')
|
||||
cardHeader.innerHTML = `<h2>Poule ${poule.letter}${round}</h2>`
|
||||
card.append(cardHeader)
|
||||
|
||||
let cardBody = document.createElement('div')
|
||||
cardBody.classList.add('card-body')
|
||||
card.append(cardBody)
|
||||
|
||||
pouleTable = document.createElement('table')
|
||||
pouleTable.id = `table-${tid}-${round}-${poule.letter}`
|
||||
pouleTable.classList.add('table', 'table-stripped')
|
||||
cardBody.append(pouleTable)
|
||||
|
||||
let thead = document.createElement('thead')
|
||||
pouleTable.append(thead)
|
||||
|
||||
let phaseTr = document.createElement('tr')
|
||||
thead.append(phaseTr)
|
||||
|
||||
let teamTh = document.createElement('th')
|
||||
teamTh.classList.add('text-center')
|
||||
teamTh.rowSpan = poule.teams.length === 5 ? 3 : 2
|
||||
teamTh.textContent = "Équipe"
|
||||
phaseTr.append(teamTh)
|
||||
|
||||
// Add columns
|
||||
for (let i = 1; i <= (poule.teams.length === 4 ? 4 : 3); ++i) {
|
||||
let phaseTh = document.createElement('th')
|
||||
phaseTh.classList.add('text-center')
|
||||
if (poule.teams.length === 5 && i < 3)
|
||||
phaseTh.colSpan = 2
|
||||
phaseTh.textContent = `Phase ${i}`
|
||||
phaseTr.append(phaseTh)
|
||||
}
|
||||
|
||||
if (poule.teams.length === 5) {
|
||||
let roomTr = document.createElement('tr')
|
||||
thead.append(roomTr)
|
||||
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
let roomTh = document.createElement('th')
|
||||
roomTh.classList.add('text-center')
|
||||
roomTh.textContent = `Salle ${1 + (i % 2)}`
|
||||
roomTr.append(roomTh)
|
||||
}
|
||||
}
|
||||
|
||||
let problemTr = document.createElement('tr')
|
||||
thead.append(problemTr)
|
||||
|
||||
for (let team of poule.teams) {
|
||||
let problemTh = document.createElement('th')
|
||||
problemTh.classList.add('text-center')
|
||||
// Problem is unknown for now
|
||||
problemTh.innerHTML = `Pb. <span id="table-${tid}-round-${round}-problem-${team}">?</span>`
|
||||
problemTr.append(problemTh)
|
||||
}
|
||||
|
||||
// Add body
|
||||
let tbody = document.createElement('tbody')
|
||||
pouleTable.append(tbody)
|
||||
|
||||
for (let i = 0; i < poule.teams.length; ++i) {
|
||||
let team = poule.teams[i]
|
||||
|
||||
let teamTr = document.createElement('tr')
|
||||
tbody.append(teamTr)
|
||||
|
||||
// First create cells, then we will add them in the table
|
||||
let teamTd = document.createElement('td')
|
||||
teamTd.classList.add('text-center')
|
||||
teamTd.innerText = team
|
||||
teamTr.append(teamTd)
|
||||
|
||||
let defenderTd = document.createElement('td')
|
||||
defenderTd.classList.add('text-center')
|
||||
defenderTd.innerText = 'Déf'
|
||||
|
||||
let opponentTd = document.createElement('td')
|
||||
opponentTd.classList.add('text-center')
|
||||
opponentTd.innerText = 'Opp'
|
||||
|
||||
let reporterTd = document.createElement('td')
|
||||
reporterTd.classList.add('text-center')
|
||||
reporterTd.innerText = 'Rap'
|
||||
|
||||
// Put the cells in their right places, according to the pool size and the row number.
|
||||
if (poule.teams.length === 3) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
teamTr.append(defenderTd, reporterTd, opponentTd)
|
||||
break
|
||||
case 1:
|
||||
teamTr.append(opponentTd, defenderTd, reporterTd)
|
||||
break
|
||||
case 2:
|
||||
teamTr.append(reporterTd, opponentTd, defenderTd)
|
||||
break
|
||||
}
|
||||
} else if (poule.teams.length === 4) {
|
||||
let emptyTd = document.createElement('td')
|
||||
switch (i) {
|
||||
case 0:
|
||||
teamTr.append(defenderTd, emptyTd, reporterTd, opponentTd)
|
||||
break
|
||||
case 1:
|
||||
teamTr.append(opponentTd, defenderTd, emptyTd, reporterTd)
|
||||
break
|
||||
case 2:
|
||||
teamTr.append(reporterTd, opponentTd, defenderTd, emptyTd)
|
||||
break
|
||||
case 3:
|
||||
teamTr.append(emptyTd, reporterTd, opponentTd, defenderTd)
|
||||
break
|
||||
}
|
||||
} else if (poule.teams.length === 5) {
|
||||
let emptyTd = document.createElement('td')
|
||||
let emptyTd2 = document.createElement('td')
|
||||
switch (i) {
|
||||
case 0:
|
||||
teamTr.append(defenderTd, emptyTd, opponentTd, reporterTd, emptyTd2)
|
||||
break
|
||||
case 1:
|
||||
teamTr.append(emptyTd, defenderTd, reporterTd, emptyTd2, opponentTd)
|
||||
break
|
||||
case 2:
|
||||
teamTr.append(opponentTd, emptyTd, defenderTd, emptyTd2, reporterTd)
|
||||
break
|
||||
case 3:
|
||||
teamTr.append(reporterTd, opponentTd, emptyTd, defenderTd, emptyTd2)
|
||||
break
|
||||
case 4:
|
||||
teamTr.append(emptyTd, reporterTd, emptyTd2, opponentTd, defenderTd)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the team that is currently choosing its problem.
|
||||
* @param tid The tournament id
|
||||
* @param round The current round number, as integer (1 or 2)
|
||||
* @param pool The current pool letter (A, B, C or D) (null if non-relevant)
|
||||
* @param team The current team trigram (null if non-relevant)
|
||||
*/
|
||||
function updateActiveRecap(tid, round, pool, team) {
|
||||
// Remove the previous highlights
|
||||
document.querySelectorAll(`div.text-bg-secondary[data-tournament="${tid}"]`)
|
||||
.forEach(elem => elem.classList.remove('text-bg-secondary'))
|
||||
document.querySelectorAll(`li.list-group-item-success[data-tournament="${tid}"]`)
|
||||
.forEach(elem => elem.classList.remove('list-group-item-success'))
|
||||
document.querySelectorAll(`li.list-group-item-info[data-tournament="${tid}"]`)
|
||||
.forEach(elem => elem.classList.remove('list-group-item-info'))
|
||||
|
||||
// Highlight current round, if existing
|
||||
let roundDiv = document.getElementById(`recap-${tid}-round-${round}`)
|
||||
if (roundDiv !== null)
|
||||
roundDiv.classList.add('text-bg-secondary')
|
||||
|
||||
// Highlight current pool, if existing
|
||||
let poolLi = document.getElementById(`recap-${tid}-round-${round}-pool-${pool}`)
|
||||
if (poolLi !== null)
|
||||
poolLi.classList.add('list-group-item-success')
|
||||
|
||||
// Highlight current team, if existing
|
||||
let teamLi = document.getElementById(`recap-${tid}-round-${round}-team-${team}`)
|
||||
if (teamLi !== null)
|
||||
teamLi.classList.add('list-group-item-info')
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the recap and the table when a team accepts a problem.
|
||||
* @param tid The tournament id
|
||||
* @param round The current round, as integer (1 or 2)
|
||||
* @param team The current team trigram
|
||||
* @param problem The accepted problem, as integer
|
||||
*/
|
||||
function setProblemAccepted(tid, round, team, problem) {
|
||||
// Update recap
|
||||
let recapDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-accepted`)
|
||||
if (problem !== null) {
|
||||
recapDiv.classList.remove('text-bg-warning')
|
||||
recapDiv.classList.add('text-bg-success')
|
||||
} else {
|
||||
recapDiv.classList.add('text-bg-warning')
|
||||
recapDiv.classList.remove('text-bg-success')
|
||||
}
|
||||
recapDiv.textContent = `${team} 📃 ${problem ? problem : '?'}`
|
||||
|
||||
// Update table
|
||||
let tableSpan = document.getElementById(`table-${tid}-round-${round}-problem-${team}`)
|
||||
tableSpan.textContent = problem ? problem : '?'
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the recap when a team rejects a problem.
|
||||
* @param tid The tournament id
|
||||
* @param round The current round, as integer (1 or 2)
|
||||
* @param team The current team trigram
|
||||
* @param rejected The full list of rejected problems
|
||||
*/
|
||||
function setProblemRejected(tid, round, team, rejected) {
|
||||
// Update recap
|
||||
let recapDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-rejected`)
|
||||
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
|
||||
|
||||
let penaltyDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-penalty`)
|
||||
if (rejected.length > problems_count - 5) {
|
||||
// If more than P - 5 problems were rejected, add a penalty of 0.5 of the coefficient of the oral defender
|
||||
if (penaltyDiv === null) {
|
||||
penaltyDiv = document.createElement('div')
|
||||
penaltyDiv.id = `recap-${tid}-round-${round}-team-${team}-penalty`
|
||||
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
|
||||
recapDiv.parentNode.append(penaltyDiv)
|
||||
}
|
||||
penaltyDiv.textContent = `❌ ${0.5 * (rejected.length - (problems_count - 5))}`
|
||||
} else {
|
||||
// Eventually remove this div
|
||||
if (penaltyDiv !== null)
|
||||
penaltyDiv.remove()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For a 5-teams pool, we may reorder the pool if two teams select the same problem.
|
||||
* Then, we redraw the table and set the accepted problems.
|
||||
* @param tid The tournament id
|
||||
* @param round The current round, as integer (1 or 2)
|
||||
* @param poule The pool represented by its letter
|
||||
* @param teams The teams list represented by their trigrams, ["ABC", "DEF", "GHI", "JKL", "MNO"]
|
||||
* @param problems The accepted problems in the same order than the teams, [1, 1, 2, 2, 3]
|
||||
*/
|
||||
function reorderPoule(tid, round, poule, teams, problems) {
|
||||
// Redraw the pool table
|
||||
let table = document.getElementById(`table-${tid}-${round}-${poule}`)
|
||||
table.parentElement.parentElement.remove()
|
||||
|
||||
updatePouleTable(tid, round, {'letter': poule, 'teams': teams})
|
||||
|
||||
// Put the problems in the table
|
||||
for (let i = 0; i < teams.length; ++i) {
|
||||
let team = teams[i]
|
||||
let problem = problems[i]
|
||||
|
||||
setProblemAccepted(tid, round, team, problem)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the received data from the server.
|
||||
* @param tid The tournament id
|
||||
* @param data The received message
|
||||
*/
|
||||
function processMessage(tid, data) {
|
||||
switch (data.type) {
|
||||
case 'alert':
|
||||
// Add alert message
|
||||
addMessage(data.message, data.alert_type)
|
||||
break
|
||||
case 'notification':
|
||||
// Add notification
|
||||
showNotification(data.title, data.body)
|
||||
break
|
||||
case 'set_info':
|
||||
// Update information banner
|
||||
setInfo(tid, data.information)
|
||||
break
|
||||
case 'draw_start':
|
||||
// Start the draw and update the interface
|
||||
drawStart(tid, data.trigrams)
|
||||
break
|
||||
case 'abort':
|
||||
// Abort the current draw
|
||||
drawAbort(tid)
|
||||
break
|
||||
case 'dice':
|
||||
// Update the interface after a dice launch
|
||||
updateDiceInfo(tid, data.team, data.result)
|
||||
break
|
||||
case 'dice_visibility':
|
||||
// Update the dice button visibility
|
||||
updateDiceVisibility(tid, data.visible)
|
||||
break
|
||||
case 'box_visibility':
|
||||
// Update the box button visibility
|
||||
updateBoxVisibility(tid, data.visible)
|
||||
break
|
||||
case 'buttons_visibility':
|
||||
// Update the accept/reject buttons visibility
|
||||
updateButtonsVisibility(tid, data.visible)
|
||||
break
|
||||
case 'export_visibility':
|
||||
// Update the export button visibility
|
||||
updateExportVisibility(tid, data.visible)
|
||||
break
|
||||
case 'continue_visibility':
|
||||
// Update the continue button visibility for the final tournament
|
||||
updateContinueVisibility(tid, data.visible)
|
||||
break
|
||||
case 'set_poules':
|
||||
// Set teams order and pools and update the interface
|
||||
updatePoules(tid, data.round, data.poules)
|
||||
break
|
||||
case 'set_active':
|
||||
// Highlight the team that is selecting a problem
|
||||
updateActiveRecap(tid, data.round, data.poule, data.team)
|
||||
break
|
||||
case 'set_problem':
|
||||
// Mark a problem as accepted and update the interface
|
||||
setProblemAccepted(tid, data.round, data.team, data.problem)
|
||||
break
|
||||
case 'reject_problem':
|
||||
// Mark a problem as rejected and update the interface
|
||||
setProblemRejected(tid, data.round, data.team, data.rejected)
|
||||
break
|
||||
case 'reorder_poule':
|
||||
// Reorder a pool and redraw the associated table
|
||||
reorderPoule(tid, data.round, data.poule, data.teams, data.problems)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function setupSocket() {
|
||||
// Open a global websocket
|
||||
socket = new WebSocket(
|
||||
(document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws/draw/'
|
||||
)
|
||||
|
||||
// Listen on websockets and process messages from the server
|
||||
socket.addEventListener('message', e => {
|
||||
// Parse received data as JSON
|
||||
const data = JSON.parse(e.data)
|
||||
|
||||
processMessage(data['tid'], data)
|
||||
})
|
||||
|
||||
// Manage errors
|
||||
socket.addEventListener('close', e => {
|
||||
console.error('Chat socket closed unexpectedly, restarting…')
|
||||
setupSocket()
|
||||
})
|
||||
|
||||
// When the socket is opened, set the language in order to receive alerts in the good language
|
||||
socket.addEventListener('open', e => {
|
||||
socket.send(JSON.stringify({
|
||||
'tid': tournaments[0].id,
|
||||
'type': 'set_language',
|
||||
'language': document.getElementsByName('language')[0].value,
|
||||
}))
|
||||
})
|
||||
|
||||
for (let tournament of tournaments) {
|
||||
// Manage the start form
|
||||
let format_form = document.getElementById('format-form-' + tournament.id)
|
||||
if (format_form !== null) {
|
||||
format_form.addEventListener('submit', function (e) {
|
||||
e.preventDefault()
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
'tid': tournament.id,
|
||||
'type': 'start_draw',
|
||||
'fmt': document.getElementById('format-' + tournament.id).value
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupSocket()
|
||||
|
||||
if (document.querySelector('a[href="/admin/"]')) {
|
||||
// Administrators can fake the draw
|
||||
// This is useful for debug purposes, or
|
||||
document.getElementsByTagName('body')[0].addEventListener('keyup', event => {
|
||||
if (event.key === 'f') {
|
||||
let activeTab = document.querySelector('#tournaments-tab button.active')
|
||||
let tid = activeTab.id.substring(4)
|
||||
|
||||
let dice = document.getElementById(`launch-dice-${tid}`)
|
||||
let box = document.getElementById(`draw-problem-${tid}`)
|
||||
let value = NaN
|
||||
if (!dice.classList.contains('d-none')) {
|
||||
value = parseInt(prompt("Entrez la valeur du dé (laissez vide pour annuler) :"))
|
||||
if (!isNaN(value) && 1 <= value && value <= 100)
|
||||
drawDice(tid, null, value)
|
||||
|
||||
} else if (!box.classList.contains('d-none')) {
|
||||
value = parseInt(prompt("Entrez le numéro du problème à choisir (laissez vide pour annuler) :"))
|
||||
if (!isNaN(value) && 1 <= value && value <= 8)
|
||||
drawProblem(tid, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
44
draw/templates/draw/index.html
Normal file
44
draw/templates/draw/index.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{# The navbar to select the tournament #}
|
||||
<ul class="nav nav-tabs" id="tournaments-tab" role="tablist">
|
||||
{% for tournament in tournaments %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link{% if forloop.first %} active{% endif %}"
|
||||
id="tab-{{ tournament.id }}" data-bs-toggle="tab"
|
||||
data-bs-target="#tab-{{ tournament.id }}-pane" type="button" role="tab"
|
||||
aria-controls="tab-{{ tournament.id }}-pane" aria-selected="true">
|
||||
{{ tournament.name }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="tab-content">
|
||||
{# For each tournament, we draw a div #}
|
||||
{% for tournament in tournaments %}
|
||||
<div class="tab-pane fade{% if forloop.first %} show active{% endif %}"
|
||||
id="tab-{{ tournament.id }}-pane" role="tabpanel"
|
||||
aria-labelledby="tab-{{ tournament.id }}" tabindex="0">
|
||||
{% include "draw/tournament_content.html" with tournament=tournament %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "You don't participate to any tournament." %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
{# Import the list of tournaments and give it to JavaScript #}
|
||||
{{ tournaments_simplified|json_script:'tournaments_list' }}
|
||||
{{ problems|length|json_script:'problems_count' }}
|
||||
|
||||
{# This script contains all data for the draw management #}
|
||||
<script src="{% static 'draw.js' %}"></script>
|
||||
{% endblock %}
|
359
draw/templates/draw/tournament_content.html
Normal file
359
draw/templates/draw/tournament_content.html
Normal file
@ -0,0 +1,359 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div id="banner-not-started-{{ tournament.id }}" class="alert alert-warning{% if tournament.draw %} d-none{% endif %}">
|
||||
{# This div is visible iff the draw is not started. #}
|
||||
{% trans "The draw has not started yet." %}
|
||||
|
||||
{% if user.registration.is_volunteer %}
|
||||
{# Volunteers have a form to start the draw #}
|
||||
<form id="format-form-{{ tournament.id }}">
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="format-{{ tournament.id }}">
|
||||
{% trans "Configuration:" %}
|
||||
</label>
|
||||
{# The configuration is the size of pools per pool, for example 3+3+3 #}
|
||||
<input type="text" class="form-control" id="format-{{ tournament.id }}"
|
||||
pattern="^[345](\+[345])*$"
|
||||
placeholder="{{ tournament.best_format }}"
|
||||
value="{{ tournament.best_format }}">
|
||||
<button class="btn btn-success input-group-btn">{% trans "Start!" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="draw-content-{{ tournament.id }}" class="{% if not tournament.draw %}d-none{% endif %}">
|
||||
{# Displayed only if the tournament has started #}
|
||||
<div class="container">
|
||||
<div class="card col-md-12 my-3">
|
||||
<div class="card-header">
|
||||
<h2>{% trans "Last dices" %}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dices-{{ tournament.id }}" class="row">
|
||||
{# Display last dices of all teams #}
|
||||
{% for td in tournament.draw.current_round.team_draws %}
|
||||
<div class="col-md-1" style="order: {{ forloop.counter }};">
|
||||
<div id="dice-{{ tournament.id }}-{{ td.participation.team.trigram }}"
|
||||
class="badge rounded-pill text-bg-{% if td.last_dice %}success{% else %}warning{% endif %}"
|
||||
{% if request.user.registration.is_volunteer %}
|
||||
{# Volunteers can click on dices to launch the dice of a team #}
|
||||
onclick="drawDice({{ tournament.id }}, '{{ td.participation.team.trigram }}')"
|
||||
{% endif %}>
|
||||
{{ td.participation.team.trigram }} 🎲 {{ td.last_dice|default:'??' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-5 my-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Recap
|
||||
{% if user.registration.is_volunteer %}
|
||||
<button id="cancel-last-step-{{ tournament.id }}"
|
||||
class="badge rounded-pill text-bg-warning"
|
||||
onclick="cancelLastStep({{ tournament.id }})">
|
||||
🔙 {% trans "Cancel last step" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="recap-{{ tournament.id }}-round-list" class="row">
|
||||
{% for round in tournament.draw.round_set.all %}
|
||||
{# For each round, add a recap of drawn problems #}
|
||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}"
|
||||
class="col-md-6 px-3 py-3 {% if tournament.draw.current_round == round %} text-bg-secondary{% endif %}"
|
||||
data-tournament="{{ tournament.id }}">
|
||||
<strong>{{ round }}</strong>
|
||||
<ul id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-list"
|
||||
class="list-group list-group-flush">
|
||||
{% for pool in round.pool_set.all %}
|
||||
{# Add one item per pool #}
|
||||
<li id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-{{ pool.get_letter_display }}"
|
||||
class="list-group-item px-3 py-3 {% if tournament.draw.current_round.current_pool == pool %} list-group-item-success{% endif %}"
|
||||
data-tournament="{{ tournament.id }}">
|
||||
<strong>{{ pool }}</strong>
|
||||
<ul id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-{{ pool.get_letter_display }}-team-list"
|
||||
class="list-group list-group-flush">
|
||||
{% for td in pool.team_draws.all %}
|
||||
{# Add teams of the pool #}
|
||||
<li id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}"
|
||||
class="list-group-item{% if tournament.draw.current_round.current_pool.current_team == td %} list-group-item-info{% endif %}"
|
||||
data-tournament="{{ tournament.id }}">
|
||||
{# Add the accepted problem, if existing #}
|
||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-accepted"
|
||||
class="badge rounded-pill text-bg-{% if td.accepted %}success{% else %}warning{% endif %}">
|
||||
{{ td.participation.team.trigram }} 📃 {{ td.accepted|default:'?' }}
|
||||
</div>
|
||||
{# Add the rejected problems #}
|
||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-rejected"
|
||||
class="badge rounded-pill text-bg-danger">
|
||||
🗑️ {{ td.rejected|join:', ' }}
|
||||
</div>
|
||||
{% if td.penalty %}
|
||||
{# If needed, add the penalty of the team #}
|
||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-penalty"
|
||||
class="badge rounded-pill text-bg-info">
|
||||
❌ {{ td.penalty }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7 my-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="messages-{{ tournament.id }}" class="alert alert-info">
|
||||
{# Display the insctructions of the draw to the teams #}
|
||||
{{ tournament.draw.information|safe }}
|
||||
</div>
|
||||
|
||||
<div id="launch-dice-{{ tournament.id }}"
|
||||
{% if tournament.draw.get_state != 'DICE_SELECT_POULES' and tournament.draw.get_state != 'DICE_ORDER_POULE' %}class="d-none"
|
||||
{% else %}{% if not user.registration.is_volunteer and user.registration.team.trigram not in tournament.draw.current_round.current_pool.trigrams %}class="d-none"{% endif %}{% endif %}>
|
||||
{# Display the dice interface if this is the time for it #}
|
||||
{# ie. if we are in the state where teams must launch a dice to choose the passage order or the choice order and we are in a team in the good pool, or a volunteer #}
|
||||
<div class="text-center">
|
||||
<button class="btn btn-lg" style="font-size: 100pt" onclick="drawDice({{ tournament.id }})">
|
||||
🎲
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="text-center">
|
||||
{% trans "Launch dice" %}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="draw-problem-{{ tournament.id }}"
|
||||
{% if tournament.draw.get_state != 'WAITING_DRAW_PROBLEM' %}class="d-none"
|
||||
{% else %}{% if user.registration.team.participation != tournament.draw.current_round.current_pool.current_team.participation and not user.registration.is_volunteer %}class="d-none"{% endif %}{% endif %}>
|
||||
{# Display the box only if needed #}
|
||||
<div class="text-center">
|
||||
<button class="btn btn-lg" style="font-size: 100pt" onclick="drawProblem({{ tournament.id }})">
|
||||
🗳️
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="text-center">
|
||||
{% trans "Draw a problem" %}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="buttons-{{ tournament.id }}"
|
||||
{% if tournament.draw.get_state != 'WAITING_CHOOSE_PROBLEM' %}class="d-none"
|
||||
{% else %}{% if user.registration.team.participation != tournament.draw.current_round.current_pool.current_team.participation and not user.registration.is_volunteer %}class="d-none"{% endif %}{% endif %}>
|
||||
{# Display buttons if a problem has been drawn and we are waiting for its acceptation or reject #}
|
||||
<div class="d-grid">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success" onclick="acceptProblem({{ tournament.id }})">
|
||||
{% trans "Accept" %}
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="rejectProblem({{ tournament.id }})">
|
||||
{% trans "Decline" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.registration.is_volunteer %}
|
||||
{# Volunteers can export the draw if possible #}
|
||||
<div id="export-{{ tournament.id }}"
|
||||
class="card-footer text-center{% if not tournament.draw.exportable %} d-none{% endif %}">
|
||||
<button class="btn btn-info text-center" onclick="exportDraw({{ tournament.id }})">
|
||||
📁 {% trans "Export" %}
|
||||
</button>
|
||||
</div>
|
||||
{% if tournament.final %}
|
||||
{# Volunteers can continue the second round for the final tournament #}
|
||||
<div id="continue-{{ tournament.id }}"
|
||||
class="card-footer text-center{% if tournament.draw.get_state != 'WAITING_FINAL' %} d-none{% endif %}">
|
||||
<button class="btn btn-success text-center" onclick="continueFinal({{ tournament.id }})">
|
||||
➡️ {% trans "Continue draw" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tables-{{ tournament.id }}" class="row">
|
||||
{# Display tables with the advancement of the draw below #}
|
||||
{% for round in tournament.draw.round_set.all %}
|
||||
<div class="card col-md-6">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
{{ round }}
|
||||
</h2>
|
||||
</div>
|
||||
<div id="tables-{{ tournament.id }}-round-{{ round.number }}" class="card-body d-flex flex-wrap">
|
||||
{% for pool in round.pool_set.all %}
|
||||
{# Draw one table per pool #}
|
||||
{% if pool.teamdraw_set.count %}
|
||||
<div class="card w-100 my-3 order-{{ pool.letter }}">
|
||||
<div class="card-header">
|
||||
<h3>
|
||||
{{ pool }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="table-{{ tournament.id }}-{{ round.number }}-{{ pool.get_letter_display }}" class="table table-striped">
|
||||
<thead>
|
||||
{# One column per phase #}
|
||||
<tr>
|
||||
<th class="text-center" rowspan="{% if pool.size == 5 %}3{% else %}2{% endif %}">{% trans "team"|capfirst %}</th>
|
||||
<th class="text-center"{% if pool.size == 5 %} colspan="2"{% endif %}>Phase 1</th>
|
||||
<th class="text-center"{% if pool.size == 5 %} colspan="2"{% endif %}>Phase 2</th>
|
||||
<th class="text-center">Phase 3</th>
|
||||
{% if pool.size == 4 %}
|
||||
<th class="text-center">Phase 4</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if pool.size == 5 %}
|
||||
<tr>
|
||||
<th class="text-center">{% trans "Room" %} 1</th>
|
||||
<th class="text-center">{% trans "Room" %} 2</th>
|
||||
<th class="text-center">{% trans "Room" %} 1</th>
|
||||
<th class="text-center">{% trans "Room" %} 2</th>
|
||||
<th class="text-center">{% trans "Room" %} 1</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
{% for td in pool.team_draws.all %}
|
||||
<th class="text-center">
|
||||
Pb.
|
||||
<span id="table-{{ tournament.id }}-round-{{ round.number }}-problem-{{ td.participation.team.trigram }}">{{ td.accepted|default:"?" }}</span>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{# Draw the order regarding the pool size #}
|
||||
{% for td in pool.team_draws %}
|
||||
<tr>
|
||||
<td class="text-center">{{ td.participation.team.trigram }}</td>
|
||||
{% if pool.size == 3 %}
|
||||
{% if forloop.counter == 1 %}
|
||||
<td class="text-center">Déf</td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td class="text-center">Opp</td>
|
||||
{% elif forloop.counter == 2 %}
|
||||
<td class="text-center">Opp</td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td class="text-center">Rap</td>
|
||||
{% elif forloop.counter == 3 %}
|
||||
<td class="text-center">Rap</td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td class="text-center">Déf</td>
|
||||
{% endif %}
|
||||
{% elif pool.size == 4 %}
|
||||
{% if forloop.counter == 1 %}
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td class="text-center">Opp</td>
|
||||
{% elif forloop.counter == 2 %}
|
||||
<td class="text-center">Opp</td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
<td class="text-center">Rap</td>
|
||||
{% elif forloop.counter == 3 %}
|
||||
<td class="text-center">Rap</td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
{% elif forloop.counter == 4 %}
|
||||
<td></td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td class="text-center">Déf</td>
|
||||
{% endif %}
|
||||
{% elif pool.size == 5 %}
|
||||
{% if forloop.counter == 1 %}
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td></td>
|
||||
{% elif forloop.counter == 2 %}
|
||||
<td></td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td></td>
|
||||
<td class="text-center">Opp</td>
|
||||
{% elif forloop.counter == 3 %}
|
||||
<td class="text-center">Opp</td>
|
||||
<td></td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
<td class="text-center">Rap</td>
|
||||
{% elif forloop.counter == 4 %}
|
||||
<td class="text-center">Rap</td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td></td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
{% elif forloop.counter == 5 %}
|
||||
<td></td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td></td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user.registration.is_volunteer %}
|
||||
{# Volunteers can click on this button to abort the draw #}
|
||||
<div class="text-center mt-3">
|
||||
<button id="abort-{{ tournament.id }}" class="badge rounded-pill text-bg-danger" data-bs-toggle="modal" data-bs-target="#abort{{ tournament.id }}Modal">
|
||||
{% trans "Abort" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="abort{{ tournament.id }}Modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Are you sure?" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% trans "This will reset the draw from the beginning." %}
|
||||
{% trans "This operation is irreversible." %}
|
||||
{% trans "Are you sure you want to abort this draw?" %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal" onclick="abortDraw({{ tournament.id }})">{% trans "Abort" %}</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
812
draw/tests.py
Normal file
812
draw/tests.py
Normal file
@ -0,0 +1,812 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import asyncio
|
||||
from random import shuffle
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.routing import URLRouter
|
||||
from channels.testing import WebsocketCommunicator
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.sites.models import Site
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from participation.models import Team, Tournament
|
||||
|
||||
from . import routing
|
||||
from .models import Draw, Pool, Round, TeamDraw
|
||||
|
||||
|
||||
class TestDraw(TestCase):
|
||||
def setUp(self):
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
password="toto1234",
|
||||
)
|
||||
|
||||
self.tournament = Tournament.objects.create(
|
||||
name="Test",
|
||||
)
|
||||
self.teams = []
|
||||
for i in range(12):
|
||||
t = Team.objects.create(
|
||||
name=f"Team {i + 1}",
|
||||
trigram=3 * chr(65 + i),
|
||||
)
|
||||
t.participation.tournament = self.tournament
|
||||
t.participation.valid = True
|
||||
t.participation.save()
|
||||
self.teams.append(t)
|
||||
shuffle(self.teams)
|
||||
|
||||
async def test_draw(self): # noqa: C901
|
||||
"""
|
||||
Simulate a full draw operation.
|
||||
"""
|
||||
await sync_to_async(self.async_client.force_login)(self.superuser)
|
||||
|
||||
tid = self.tournament.id
|
||||
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Connect to Websocket
|
||||
headers = [(b'cookie', self.async_client.cookies.output(header='', sep='; ').encode())]
|
||||
communicator = WebsocketCommunicator(AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
|
||||
"/ws/draw/", headers)
|
||||
connected, subprotocol = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
|
||||
# Define language
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'set_language', 'language': 'en'})
|
||||
|
||||
# Ensure that Draw has not started
|
||||
self.assertFalse(await Draw.objects.filter(tournament=self.tournament).aexists())
|
||||
|
||||
# Must be an error since 1+1+1 != 12
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'start_draw', 'fmt': '1+1+1'})
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'alert')
|
||||
self.assertEqual(resp['alert_type'], 'danger')
|
||||
self.assertEqual(resp['message'], "The sum must be equal to the number of teams: expected 12, got 3")
|
||||
self.assertFalse(await Draw.objects.filter(tournament=self.tournament).aexists())
|
||||
|
||||
# Now start the draw
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'start_draw', 'fmt': '3+4+5'})
|
||||
|
||||
# Receive data after the start
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'alert')
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_poules', 'round': 1,
|
||||
'poules': [{'letter': 'A', 'teams': []},
|
||||
{'letter': 'B', 'teams': []},
|
||||
{'letter': 'C', 'teams': []}]})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_poules', 'round': 2,
|
||||
'poules': [{'letter': 'A', 'teams': []},
|
||||
{'letter': 'B', 'teams': []},
|
||||
{'letter': 'C', 'teams': []}]})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'alert')
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'draw_start', 'fmt': [5, 4, 3],
|
||||
'trigrams': ['AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF',
|
||||
'GGG', 'HHH', 'III', 'JJJ', 'KKK', 'LLL']})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': None, 'team': None})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'notification')
|
||||
|
||||
# Ensure that now tournament has started
|
||||
self.assertTrue(await Draw.objects.filter(tournament=self.tournament).aexists())
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Try to relaunch the draw
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'start_draw', 'fmt': '3+4+5'})
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'alert')
|
||||
self.assertEqual(resp['alert_type'], 'danger')
|
||||
self.assertEqual(resp['message'], "The draw is already started.")
|
||||
|
||||
draw: Draw = await Draw.objects.prefetch_related(
|
||||
'current_round__current_pool__current_team__participation__team').aget(tournament=self.tournament)
|
||||
r: Round = draw.current_round
|
||||
|
||||
for i, team in enumerate(self.teams):
|
||||
# Launch a new dice
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': team.trigram})
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], "dice")
|
||||
self.assertEqual(resp['team'], team.trigram)
|
||||
self.assertGreaterEqual(resp['result'], 1)
|
||||
self.assertLessEqual(resp['result'], 100)
|
||||
td: TeamDraw = await r.teamdraw_set.aget(participation=team.participation)
|
||||
if i != len(self.teams) - 1:
|
||||
self.assertEqual(resp['result'], td.passage_dice)
|
||||
|
||||
# Try to relaunch the dice
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': team.trigram})
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'alert')
|
||||
self.assertEqual(resp['message'], "You've already launched the dice.")
|
||||
|
||||
# Force exactly one duplicate
|
||||
await td.arefresh_from_db()
|
||||
td.passage_dice = 101 + i if i != 2 else 101
|
||||
await td.asave()
|
||||
|
||||
# Manage duplicates
|
||||
while dup_count := await r.teamdraw_set.filter(passage_dice__isnull=True).acount():
|
||||
for i in range(dup_count):
|
||||
# Dice
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'dice')
|
||||
self.assertIsNone(resp['result'])
|
||||
# Alert
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'alert')
|
||||
|
||||
for i in range(dup_count):
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': None})
|
||||
await communicator.receive_json_from()
|
||||
|
||||
# Reset dices
|
||||
for _i in range(12):
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'dice')
|
||||
self.assertIsNone(resp['result'])
|
||||
|
||||
# Hide and re-display the dice
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': True})
|
||||
|
||||
# Set pools for the two rounds
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_poules')
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_poules')
|
||||
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
# Manage the first pool
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'A', 'team': None})
|
||||
r: Round = await Round.objects.prefetch_related('current_pool__current_team__participation__team')\
|
||||
.aget(number=1, draw=draw)
|
||||
p = r.current_pool
|
||||
self.assertEqual(p.letter, 1)
|
||||
self.assertEqual(p.size, 5)
|
||||
self.assertEqual(await p.teamdraw_set.acount(), 5)
|
||||
self.assertEqual(p.current_team, None)
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
i = 0
|
||||
async for td in p.teamdraw_set.prefetch_related('participation__team').all():
|
||||
# Launch a new dice
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': td.participation.team.trigram})
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], "dice")
|
||||
trigram = td.participation.team.trigram
|
||||
self.assertEqual(resp['team'], trigram)
|
||||
self.assertGreaterEqual(resp['result'], 1)
|
||||
self.assertLessEqual(resp['result'], 100)
|
||||
if i != p.size - 1:
|
||||
await td.arefresh_from_db()
|
||||
self.assertEqual(resp['result'], td.choice_dice)
|
||||
|
||||
# Try to relaunch the dice
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': trigram})
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'alert')
|
||||
self.assertEqual(resp['message'], "You've already launched the dice.")
|
||||
|
||||
# Force exactly one duplicate
|
||||
await td.arefresh_from_db()
|
||||
td.passage_dice = 101 + i if i != 1 else 101
|
||||
await td.asave()
|
||||
i += 1
|
||||
|
||||
# Manage duplicates
|
||||
while dup_count := await p.teamdraw_set.filter(choice_dice__isnull=True).acount():
|
||||
for i in range(dup_count):
|
||||
# Dice
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'dice')
|
||||
self.assertIsNone(resp['result'])
|
||||
# Alert
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'alert')
|
||||
|
||||
for i in range(dup_count):
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': None})
|
||||
await communicator.receive_json_from()
|
||||
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
# Check current pool
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=1)
|
||||
td = p.current_team
|
||||
trigram = td.participation.team.trigram
|
||||
self.assertEqual(td.choose_index, 0)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1,
|
||||
'poule': 'A', 'team': td.participation.team.trigram})
|
||||
# Dice is hidden for everyone first
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': False})
|
||||
# The draw box is displayed for the current team and for volunteers
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Try to launch a dice while it is not the time
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'dice', 'trigram': None})
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'alert')
|
||||
self.assertEqual(resp['message'], "This is not the time for this.")
|
||||
|
||||
# Draw a problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
purposed = td.purposed
|
||||
self.assertIsNotNone(td.purposed)
|
||||
self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1))
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Try to redraw a problem while it is not the time
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'alert')
|
||||
self.assertEqual(resp['message'], "This is not the time for this.")
|
||||
|
||||
# Reject the first problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'reject'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'reject_problem', 'round': 1, 'team': trigram, 'rejected': [purposed]})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
self.assertEqual(td.rejected, [purposed])
|
||||
|
||||
for i in range(4):
|
||||
# Next team
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=1)
|
||||
td = p.current_team
|
||||
trigram = td.participation.team.trigram
|
||||
self.assertEqual(td.choose_index, i + 1)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'A', 'team': trigram})
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Draw a problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNotNone(td.purposed)
|
||||
self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1))
|
||||
|
||||
# Assume that this is the problem 1 for teams 2 et 4 and the problem 2 for teams 3 and 5
|
||||
td.purposed = 1 + (i % 2)
|
||||
await td.asave()
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Accept the problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'accept'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': 1 + (i % 2)})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
self.assertEqual(td.accepted, 1 + (i % 2))
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Go back to the first team of the pool
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=1)
|
||||
td = p.current_team
|
||||
trigram = td.participation.team.trigram
|
||||
self.assertEqual(td.choose_index, 0)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'A', 'team': trigram})
|
||||
|
||||
# Draw and reject 100 times a problem
|
||||
for _i in range(100):
|
||||
# Draw a problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNotNone(td.purposed)
|
||||
# Problems 1 and 2 are not available
|
||||
self.assertIn(td.purposed, range(3, len(settings.PROBLEMS) + 1))
|
||||
|
||||
# Reject
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'reject'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': False})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'reject_problem')
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
self.assertIn(purposed, td.rejected)
|
||||
|
||||
# Ensures that this is still the first team
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=1)
|
||||
self.assertEqual(p.current_team, td)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'A', 'team': trigram})
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Ensures that there is a penalty
|
||||
self.assertGreaterEqual(td.penalty, 1)
|
||||
|
||||
# Draw a last problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNotNone(td.purposed)
|
||||
self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1))
|
||||
|
||||
# Accept the problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'accept'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': td.purposed})
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
|
||||
# Reorder the pool since there are 5 teams
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'reorder_poule')
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'B', 'team': None})
|
||||
|
||||
# Start pool 2
|
||||
r: Round = await Round.objects.prefetch_related('current_pool__current_team__participation__team')\
|
||||
.aget(number=1, draw=draw)
|
||||
p = r.current_pool
|
||||
self.assertEqual(p.letter, 2)
|
||||
self.assertEqual(p.size, 4)
|
||||
self.assertEqual(await p.teamdraw_set.acount(), 4)
|
||||
self.assertEqual(p.current_team, None)
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
i = 0
|
||||
async for td in p.teamdraw_set.prefetch_related('participation__team').all():
|
||||
# Launch a new dice
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': td.participation.team.trigram})
|
||||
await communicator.receive_json_from()
|
||||
await td.arefresh_from_db()
|
||||
td.choice_dice = 101 + i # Avoid duplicates
|
||||
await td.asave()
|
||||
i += 1
|
||||
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
for i in range(4):
|
||||
# Next team
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=2)
|
||||
td = p.current_team
|
||||
trigram = td.participation.team.trigram
|
||||
self.assertEqual(td.choose_index, i)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'B', 'team': trigram})
|
||||
if i == 0:
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Draw a problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNotNone(td.purposed)
|
||||
self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1))
|
||||
# Lower problems are already accepted
|
||||
self.assertGreaterEqual(td.purposed, i + 1)
|
||||
|
||||
# Assume that this is the problem is i for the team i
|
||||
td.purposed = i + 1
|
||||
await td.asave()
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Accept the problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'accept'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': i + 1})
|
||||
if i < 3:
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
else:
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
self.assertEqual(td.accepted, i + 1)
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Start pool 3
|
||||
r: Round = await Round.objects.prefetch_related('current_pool__current_team__participation__team')\
|
||||
.aget(number=1, draw=draw)
|
||||
p = r.current_pool
|
||||
self.assertEqual(p.letter, 3)
|
||||
self.assertEqual(p.size, 3)
|
||||
self.assertEqual(await p.teamdraw_set.acount(), 3)
|
||||
self.assertEqual(p.current_team, None)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'C', 'team': None})
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
i = 0
|
||||
async for td in p.teamdraw_set.prefetch_related('participation__team').all():
|
||||
# Launch a new dice
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': td.participation.team.trigram})
|
||||
await communicator.receive_json_from()
|
||||
await td.arefresh_from_db()
|
||||
td.choice_dice = 101 + i # Avoid duplicates
|
||||
await td.asave()
|
||||
i += 1
|
||||
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
for i in range(3):
|
||||
# Next team
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=3)
|
||||
td = p.current_team
|
||||
trigram = td.participation.team.trigram
|
||||
self.assertEqual(td.choose_index, i)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'C', 'team': trigram})
|
||||
if i == 0:
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Draw a problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNotNone(td.purposed)
|
||||
self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1))
|
||||
# Lower problems are already accepted
|
||||
self.assertGreaterEqual(td.purposed, i + 1)
|
||||
|
||||
# Assume that this is the problem is i for the team i
|
||||
td.purposed = i + 1
|
||||
await td.asave()
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Accept the problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'accept'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': i + 1})
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
self.assertEqual(td.accepted, i + 1)
|
||||
if i == 2:
|
||||
break
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Start round 2
|
||||
draw: Draw = await Draw.objects.prefetch_related(
|
||||
'current_round__current_pool__current_team__participation__team').aget(tournament=self.tournament)
|
||||
r = draw.current_round
|
||||
p = r.current_pool
|
||||
self.assertEqual(r.number, 2)
|
||||
self.assertEqual(p.letter, 1)
|
||||
|
||||
for j in range(12):
|
||||
# Reset dices
|
||||
self.assertIsNone((await communicator.receive_json_from())['result'])
|
||||
|
||||
# Get pools
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'set_poules')
|
||||
self.assertEqual(resp['round'], 2)
|
||||
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': True})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'export_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
for i in range(3):
|
||||
# Iterate on each pool
|
||||
r: Round = await Round.objects.prefetch_related('current_pool__current_team__participation__team') \
|
||||
.aget(draw=draw, number=2)
|
||||
p = r.current_pool
|
||||
self.assertEqual(p.letter, i + 1)
|
||||
self.assertEqual(p.size, 5 - i)
|
||||
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 2, 'poule': chr(65 + i), 'team': None})
|
||||
|
||||
j = 0
|
||||
async for td in p.teamdraw_set.prefetch_related('participation__team').all():
|
||||
# Launch a new dice
|
||||
await communicator.send_json_to({'tid': tid, 'type': "dice", 'trigram': td.participation.team.trigram})
|
||||
await communicator.receive_json_from()
|
||||
await td.arefresh_from_db()
|
||||
td.choice_dice = 101 + j # Avoid duplicates
|
||||
await td.asave()
|
||||
j += 1
|
||||
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'set_info')
|
||||
|
||||
for j in range(5 - i):
|
||||
# Next team
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r,
|
||||
letter=i + 1)
|
||||
td = p.current_team
|
||||
trigram = td.participation.team.trigram
|
||||
self.assertEqual(td.choose_index, j)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 2, 'poule': chr(65 + i),
|
||||
'team': trigram})
|
||||
if j == 0:
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
|
||||
# Render page
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Draw a problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'draw_problem'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNotNone(td.purposed)
|
||||
self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1))
|
||||
# Check that the problem is different from the previous day
|
||||
old_td = await TeamDraw.objects.aget(round__number=1, round__draw=draw,
|
||||
participation_id=td.participation_id)
|
||||
self.assertNotEqual(td.purposed, old_td.accepted)
|
||||
|
||||
# Accept the problem
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'accept'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': False})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_problem')
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
if j == 4 - i:
|
||||
break
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
if i == 0:
|
||||
# Reorder the pool since there are 5 teams
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'reorder_poule')
|
||||
if i < 2:
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': True})
|
||||
else:
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'export_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_active')
|
||||
|
||||
# Export the draw
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'export'})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'export_visibility', 'visible': False})
|
||||
|
||||
# Cancel all steps and reset all
|
||||
for i in range(1000):
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'cancel'})
|
||||
|
||||
# Purge receive queue
|
||||
while True:
|
||||
try:
|
||||
await communicator.receive_json_from()
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
|
||||
if await Draw.objects.filter(tournament_id=tid).aexists():
|
||||
print((await Draw.objects.filter(tournament_id=tid).aexists()))
|
||||
current_state = (await Draw.objects.filter(tournament_id=tid).prefetch_related(
|
||||
'current_round__current_pool__current_team__participation__team').aget()).get_state()
|
||||
raise AssertionError("Draw wasn't aborted after 1000 steps, current state: " + current_state)
|
||||
|
||||
# Abort while the tournament is already aborted
|
||||
await communicator.send_json_to({'tid': tid, 'type': "abort"})
|
||||
|
||||
def test_admin_pages(self):
|
||||
"""
|
||||
Check that admin pages are rendering successfully.
|
||||
"""
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
draw = Draw.objects.create(tournament=self.tournament)
|
||||
r1 = Round.objects.create(draw=draw, number=1)
|
||||
r2 = Round.objects.create(draw=draw, number=2)
|
||||
p11 = Pool.objects.create(round=r1, letter=1, size=5)
|
||||
p12 = Pool.objects.create(round=r1, letter=2, size=4)
|
||||
p13 = Pool.objects.create(round=r1, letter=3, size=3)
|
||||
p21 = Pool.objects.create(round=r2, letter=1, size=5)
|
||||
p22 = Pool.objects.create(round=r2, letter=2, size=4)
|
||||
p23 = Pool.objects.create(round=r2, letter=3, size=3)
|
||||
tds = []
|
||||
for i, team in enumerate(self.teams):
|
||||
tds.append(TeamDraw.objects.create(participation=team.participation,
|
||||
round=r1,
|
||||
pool=p11 if i < 5 else p12 if i < 9 else p13))
|
||||
tds.append(TeamDraw.objects.create(participation=team.participation,
|
||||
round=r2,
|
||||
pool=p21) if i < 5 else p22 if i < 9 else p23)
|
||||
|
||||
p11.current_team = tds[0]
|
||||
p11.save()
|
||||
r1.current_pool = p11
|
||||
r1.save()
|
||||
draw.current_round = r1
|
||||
draw.save()
|
||||
|
||||
response = self.client.get(reverse("admin:index") + "draw/draw/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"draw/draw/{draw.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Draw).id}/"
|
||||
f"{draw.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(draw.get_absolute_url()), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index") + "draw/round/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"draw/round/{r1.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"draw/round/{r2.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Round).id}/"
|
||||
f"{r1.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(r1.get_absolute_url()), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index") + "draw/pool/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"draw/pool/{p11.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Pool).id}/"
|
||||
f"{p11.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(p11.get_absolute_url()), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index") + "draw/teamdraw/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"draw/teamdraw/{tds[0].pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(TeamDraw).id}/"
|
||||
f"{tds[0].pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(tds[0].get_absolute_url()), 302, 200)
|
13
draw/urls.py
Normal file
13
draw/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from .views import DisplayView
|
||||
|
||||
|
||||
app_name = "draw"
|
||||
|
||||
urlpatterns = [
|
||||
path('', DisplayView.as_view(), name='index'),
|
||||
]
|
43
draw/views.py
Normal file
43
draw/views.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
from participation.models import Tournament
|
||||
|
||||
|
||||
class DisplayView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
This view is the main interface of the drawing system, which is working
|
||||
with Javascript and websockets.
|
||||
"""
|
||||
template_name = 'draw/index.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
reg = self.request.user.registration
|
||||
if reg.is_admin:
|
||||
# Administrators can manage all tournaments
|
||||
tournaments = Tournament.objects.order_by('id').all()
|
||||
elif reg.is_volunteer:
|
||||
# A volunteer can see their tournaments
|
||||
tournaments = reg.interesting_tournaments
|
||||
else:
|
||||
if not reg.team:
|
||||
raise PermissionDenied(_("You are not in a team."))
|
||||
|
||||
# A participant can see its own tournament, or the final if necessary
|
||||
tournaments = [reg.team.participation.tournament] if reg.team.participation.valid else []
|
||||
if reg.team.participation.final:
|
||||
tournaments.append(Tournament.final_tournament())
|
||||
|
||||
context['tournaments'] = tournaments
|
||||
# This will be useful for JavaScript data
|
||||
context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments]
|
||||
context['problems'] = settings.PROBLEMS
|
||||
|
||||
return context
|
@ -8,7 +8,20 @@ python manage.py loaddata initial
|
||||
nginx
|
||||
|
||||
if [ "$TFJM_STAGE" = "prod" ]; then
|
||||
gunicorn -b 0.0.0.0:8000 --workers=2 --threads=4 --worker-class=gthread tfjm.wsgi --access-logfile '-' --error-logfile '-';
|
||||
gunicorn -b 0.0.0.0:8000 \
|
||||
--workers=2 \
|
||||
--threads=4 \
|
||||
--worker-class=uvicorn.workers.UvicornWorker \
|
||||
tfjm.asgi \
|
||||
--access-logfile '-' \
|
||||
--error-logfile '-'
|
||||
else
|
||||
python manage.py runserver 0.0.0.0:8000;
|
||||
gunicorn -b 0.0.0.0:8000 \
|
||||
--workers=2 \
|
||||
--threads=4 \
|
||||
--worker-class=uvicorn.workers.UvicornWorker \
|
||||
tfjm.asgi \
|
||||
--access-logfile '-' \
|
||||
--error-logfile '-' \
|
||||
--reload
|
||||
fi
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -34,13 +34,13 @@ def pre_save_object(sender, instance, **kwargs):
|
||||
instance._previous = None
|
||||
|
||||
|
||||
def save_object(sender, instance, **kwargs):
|
||||
def save_object(sender, instance, raw, **kwargs):
|
||||
"""
|
||||
Each time a model is saved, an entry in the table `Changelog` is added in the database
|
||||
in order to store each modification made
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal") or raw:
|
||||
return
|
||||
|
||||
# noinspection PyProtectedMember
|
@ -10,9 +10,15 @@ server {
|
||||
|
||||
location / {
|
||||
proxy_pass http://tfjm;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
}
|
||||
|
||||
location /static {
|
||||
|
135
participation/admin.py
Normal file
135
participation/admin.py
Normal file
@ -0,0 +1,135 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'trigram', 'tournament', 'valid', 'final',)
|
||||
search_fields = ('name', 'trigram',)
|
||||
list_filter = ('participation__valid', 'participation__tournament', 'participation__final',)
|
||||
|
||||
@admin.display(description=_("tournament"))
|
||||
def tournament(self, record):
|
||||
return record.participation.tournament
|
||||
|
||||
@admin.display(description=_("valid"), boolean=True)
|
||||
def valid(self, team):
|
||||
return team.participation.valid
|
||||
|
||||
@admin.display(description=_("selected for final"), boolean=True)
|
||||
def final(self, team):
|
||||
return team.participation.final
|
||||
|
||||
|
||||
@admin.register(Participation)
|
||||
class ParticipationAdmin(admin.ModelAdmin):
|
||||
list_display = ('team', 'tournament', 'valid', 'final',)
|
||||
search_fields = ('team__name', 'team__trigram',)
|
||||
list_filter = ('valid',)
|
||||
autocomplete_fields = ('team', 'tournament',)
|
||||
|
||||
|
||||
@admin.register(Pool)
|
||||
class PoolAdmin(admin.ModelAdmin):
|
||||
list_display = ('__str__', 'tournament', 'round', 'letter', 'teams',)
|
||||
list_filter = ('tournament', 'round', 'letter',)
|
||||
search_fields = ('participations__team__name', 'participations__team__trigram',)
|
||||
autocomplete_fields = ('tournament', 'participations', 'juries',)
|
||||
|
||||
@admin.display(description=_("teams"))
|
||||
def teams(self, record: Pool):
|
||||
return ', '.join(p.team.trigram for p in record.participations.all())
|
||||
|
||||
|
||||
@admin.register(Passage)
|
||||
class PassageAdmin(admin.ModelAdmin):
|
||||
list_display = ('__str__', 'defender_trigram', 'solution_number', 'opponent_trigram', 'reporter_trigram',
|
||||
'pool_abbr', 'tournament')
|
||||
list_filter = ('pool__tournament', 'pool__round', 'pool__letter', 'solution_number',)
|
||||
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
|
||||
autocomplete_fields = ('pool', 'defender', 'opponent', 'reporter', 'observer',)
|
||||
|
||||
@admin.display(description=_("defender"))
|
||||
def defender_trigram(self, record: Passage):
|
||||
return record.defender.team.trigram
|
||||
|
||||
@admin.display(description=_("opponent"))
|
||||
def opponent_trigram(self, record: Passage):
|
||||
return record.opponent.team.trigram
|
||||
|
||||
@admin.display(description=_("reporter"))
|
||||
def reporter_trigram(self, record: Passage):
|
||||
return record.reporter.team.trigram
|
||||
|
||||
@admin.display(description=_("pool"))
|
||||
def pool_abbr(self, record):
|
||||
return f"{record.pool.get_letter_display()}{record.pool.round}"
|
||||
|
||||
@admin.display(description=_("tournament"))
|
||||
def tournament(self, record: Passage):
|
||||
return record.pool.tournament
|
||||
|
||||
|
||||
@admin.register(Note)
|
||||
class NoteAdmin(admin.ModelAdmin):
|
||||
list_display = ('passage', 'pool', 'jury', 'defender_writing', 'defender_oral',
|
||||
'opponent_writing', 'opponent_oral', 'reporter_writing', 'reporter_oral',)
|
||||
list_filter = ('passage__pool__letter', 'passage__solution_number', 'jury',
|
||||
'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
||||
'reporter_writing', 'reporter_oral')
|
||||
search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__defender__team__trigram',)
|
||||
autocomplete_fields = ('jury', 'passage',)
|
||||
|
||||
@admin.display(description=_("pool"))
|
||||
def pool(self, record):
|
||||
return record.passage.pool.get_letter_display()
|
||||
|
||||
|
||||
@admin.register(Solution)
|
||||
class SolutionAdmin(admin.ModelAdmin):
|
||||
list_display = ('team', 'tournament', 'problem', 'final_solution',)
|
||||
list_filter = ('problem', 'participation__tournament', 'final_solution',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||
autocomplete_fields = ('participation',)
|
||||
|
||||
@admin.display(ordering='participation__team', description=_("team"))
|
||||
def team(self, record):
|
||||
return record.participation.team
|
||||
|
||||
@admin.display(ordering='participation__tournament__name', description=_("tournament"))
|
||||
def tournament(self, record):
|
||||
return Tournament.final_tournament() if record.final_solution else record.participation.tournament
|
||||
|
||||
|
||||
@admin.register(Synthesis)
|
||||
class SynthesisAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'type', 'defender', 'passage',)
|
||||
list_filter = ('participation__tournament', 'type', 'passage__solution_number',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||
autocomplete_fields = ('participation', 'passage',)
|
||||
|
||||
@admin.display(description=_("defender"))
|
||||
def defender(self, record: Synthesis):
|
||||
return record.passage.defender
|
||||
|
||||
@admin.display(description=_("problem"))
|
||||
def problem(self, record: Synthesis):
|
||||
return record.passage.solution_number
|
||||
|
||||
|
||||
@admin.register(Tournament)
|
||||
class TournamentAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
||||
autocomplete_fields = ('organizers',)
|
||||
|
||||
|
||||
@admin.register(Tweak)
|
||||
class TweakAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'pool', 'diff',)
|
||||
autocomplete_fields = ('participation', 'pool',)
|
@ -6,12 +6,17 @@ from io import StringIO
|
||||
import re
|
||||
from typing import Iterable
|
||||
|
||||
from crispy_forms.bootstrap import InlineField
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Div, Fieldset, Submit
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from django.db.models import CharField, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from pypdf import PdfFileReader
|
||||
from pypdf import PdfReader
|
||||
|
||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
||||
|
||||
@ -23,7 +28,7 @@ class TeamForm(forms.ModelForm):
|
||||
def clean_name(self):
|
||||
if "name" in self.cleaned_data:
|
||||
name = self.cleaned_data["name"]
|
||||
if not self.instance.pk and Team.objects.filter(name=name).exists():
|
||||
if Team.objects.filter(name=name).exclude(pk=self.instance.pk).exists():
|
||||
raise ValidationError(_("This name is already used."))
|
||||
return name
|
||||
|
||||
@ -33,7 +38,7 @@ class TeamForm(forms.ModelForm):
|
||||
if not re.match("[A-Z]{3}", trigram):
|
||||
raise ValidationError(_("The trigram must be composed of three uppercase letters."))
|
||||
|
||||
if not self.instance.pk and Team.objects.filter(trigram=trigram).exists():
|
||||
if Team.objects.filter(trigram=trigram).exclude(pk=self.instance.pk).exists():
|
||||
raise ValidationError(_("This trigram is already used."))
|
||||
return trigram
|
||||
|
||||
@ -151,7 +156,7 @@ class SolutionForm(forms.ModelForm):
|
||||
raise ValidationError(_("The uploaded file size must be under 5 Mo."))
|
||||
if file.content_type != "application/pdf":
|
||||
raise ValidationError(_("The uploaded file must be a PDF file."))
|
||||
pdf_reader = PdfFileReader(file)
|
||||
pdf_reader = PdfReader(file)
|
||||
pages = len(pdf_reader.pages)
|
||||
if pages > 30:
|
||||
raise ValidationError(_("The PDF file must not have more than 30 pages."))
|
||||
@ -170,7 +175,7 @@ class SolutionForm(forms.ModelForm):
|
||||
class PoolForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Pool
|
||||
fields = ('tournament', 'round', 'bbb_url', 'results_available', 'juries',)
|
||||
fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'juries',)
|
||||
widgets = {
|
||||
"juries": forms.SelectMultiple(attrs={
|
||||
'class': 'selectpicker',
|
||||
@ -198,6 +203,48 @@ class PoolTeamsForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class AddJuryForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_class = 'form-inline'
|
||||
self.helper.layout = Fieldset(
|
||||
_("Add new jury"),
|
||||
Div(
|
||||
Div(
|
||||
InlineField('first_name', autofocus="autofocus"),
|
||||
css_class='col-xl-3',
|
||||
),
|
||||
Div(
|
||||
InlineField('last_name'),
|
||||
css_class='col-xl-3',
|
||||
),
|
||||
Div(
|
||||
InlineField('email'),
|
||||
css_class='col-xl-5',
|
||||
),
|
||||
Div(
|
||||
Submit('submit', _("Add")),
|
||||
css_class='col-xl-1',
|
||||
),
|
||||
css_class='row',
|
||||
)
|
||||
)
|
||||
|
||||
def clean_email(self):
|
||||
"""
|
||||
Ensure that the email address is unique.
|
||||
"""
|
||||
email = self.data["email"]
|
||||
if User.objects.filter(email=email).exists():
|
||||
self.add_error("email", _("This email address is already used."))
|
||||
return email
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('first_name', 'last_name', 'email',)
|
||||
|
||||
|
||||
class UploadNotesForm(forms.Form):
|
||||
file = forms.FileField(
|
||||
label=_("CSV file:"),
|
||||
@ -215,40 +262,57 @@ class UploadNotesForm(forms.Form):
|
||||
file = cleaned_data['file']
|
||||
with file:
|
||||
try:
|
||||
csvfile = csv.reader(StringIO(file.read().decode()))
|
||||
data: bytes = file.read()
|
||||
try:
|
||||
content = data.decode()
|
||||
except UnicodeDecodeError:
|
||||
self.add_error('file', _("This file contains non-UTF-8 content. "
|
||||
"Please send your sheet as a CSV file."))
|
||||
|
||||
# This is not UTF-8, grrrr
|
||||
content = data.decode('latin1')
|
||||
csvfile = csv.reader(StringIO(content))
|
||||
self.process(csvfile, cleaned_data)
|
||||
except UnicodeDecodeError:
|
||||
self.add_error('file', _("This file contains non-UTF-8 and non-ISO-8859-1 content. "
|
||||
"Please send your sheet as a CSV file."))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def process(self, csvfile: Iterable[str], cleaned_data: dict):
|
||||
parsed_notes = {}
|
||||
valid_lengths = [1 + 6 * 3, 1 + 7 * 4, 1 + 6 * 5] # Per pool sizes
|
||||
pool_size = 0
|
||||
line_length = 0
|
||||
for line in csvfile:
|
||||
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:
|
||||
line = [s.strip() for s in line if s]
|
||||
if line and line[0] == 'Problème':
|
||||
pool_size = len(line) - 1
|
||||
if pool_size < 3 or pool_size > 5:
|
||||
self.add_error('file', _("Can't determine the pool size. Are you sure your file is correct?"))
|
||||
return
|
||||
line_length = valid_lengths[pool_size - 3]
|
||||
continue
|
||||
|
||||
max_notes = 3 * [20, 16, 9, 10, 9, 10]
|
||||
if pool_size == 0 or len(line) < line_length:
|
||||
continue
|
||||
|
||||
name = line[0]
|
||||
if name.lower() in ["rôle", "juré", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]:
|
||||
continue
|
||||
notes = line[1:line_length]
|
||||
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
|
||||
continue
|
||||
notes = list(map(int, notes))
|
||||
|
||||
max_notes = pool_size * ([20, 16, 9, 10, 9, 10] + ([4] if pool_size == 4 else []))
|
||||
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)
|
||||
# Search by "{first_name} {last_name}"
|
||||
jury = User.objects.annotate(full_name=Concat('first_name', Value(' '), 'last_name',
|
||||
output_field=CharField())) \
|
||||
.filter(full_name=name.replace('’', '\''), registration__volunteerregistration__isnull=False)
|
||||
if jury.count() != 1:
|
||||
self.add_error('file', _("The following user was not found:") + " " + name)
|
||||
continue
|
||||
@ -276,7 +340,7 @@ class PassageForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Passage
|
||||
fields = ('solution_number', 'defender', 'opponent', 'reporter', 'defender_penalties',)
|
||||
fields = ('position', 'solution_number', 'defender', 'opponent', 'reporter', 'observer', 'defender_penalties',)
|
||||
|
||||
|
||||
class SynthesisForm(forms.ModelForm):
|
||||
@ -287,6 +351,10 @@ 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."))
|
||||
pdf_reader = PdfReader(file)
|
||||
pages = len(pdf_reader.pages)
|
||||
if pages > 2:
|
||||
raise ValidationError(_("The PDF file must not have more than 2 pages."))
|
||||
return self.cleaned_data["file"]
|
||||
|
||||
def save(self, commit=True):
|
||||
@ -303,4 +371,4 @@ class NoteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = ('defender_writing', 'defender_oral', 'opponent_writing',
|
||||
'opponent_oral', 'reporter_writing', 'reporter_oral', )
|
||||
'opponent_oral', 'reporter_writing', 'reporter_oral', 'observer_oral', )
|
0
participation/management/__init__.py
Normal file
0
participation/management/__init__.py
Normal file
@ -23,7 +23,7 @@ class Command(BaseCommand):
|
||||
token = response['access_token']
|
||||
|
||||
organization = "animath"
|
||||
form_slug = "tfjm-2022-tournois-regionaux"
|
||||
form_slug = "tfjm-2023-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=100&retrieveOfflineDonations=false"
|
@ -4,8 +4,7 @@
|
||||
from django.core.management import BaseCommand
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import activate
|
||||
|
||||
from .models import Tournament
|
||||
from participation.models import Tournament
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
@ -3,29 +3,17 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
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",
|
||||
]
|
||||
from participation.models import Solution, Tournament
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **kwargs):
|
||||
activate('fr')
|
||||
|
||||
base_dir = Path(__file__).parent.parent.parent.parent.parent
|
||||
base_dir = Path(__file__).parent.parent.parent.parent
|
||||
base_dir /= "output"
|
||||
if not base_dir.is_dir():
|
||||
base_dir.mkdir()
|
||||
@ -41,7 +29,7 @@ class Command(BaseCommand):
|
||||
if not base_dir.is_dir():
|
||||
base_dir.mkdir()
|
||||
|
||||
for problem_id, problem_name in enumerate(PROBLEMS):
|
||||
for problem_id, problem_name in enumerate(settings.PROBLEMS):
|
||||
dir_name = f"Problème n°{problem_id + 1} : {problem_name}"
|
||||
problem_dir = base_dir / dir_name
|
||||
if not problem_dir.is_dir():
|
@ -30,7 +30,7 @@ class Command(BaseCommand):
|
||||
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",
|
||||
resp = (await Matrix.upload(f, filename="../../../tfjm/static/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:
|
||||
@ -66,7 +66,7 @@ class Command(BaseCommand):
|
||||
visibility=RoomVisibility.public,
|
||||
alias="bienvenue",
|
||||
name="Bienvenue",
|
||||
topic="Bienvenue au TFJM² 2022 !",
|
||||
topic="Bienvenue au TFJM² 2023 !",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
@ -3,7 +3,6 @@
|
||||
import datetime
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import participation.models
|
||||
|
@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-31 15:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("participation", "0003_alter_team_trigram"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="pool",
|
||||
options={
|
||||
"ordering": ("round", "letter"),
|
||||
"verbose_name": "pool",
|
||||
"verbose_name_plural": "pools",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="pool",
|
||||
name="letter",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[(1, "A"), (2, "B"), (3, "C"), (4, "D")],
|
||||
default=1,
|
||||
verbose_name="letter",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
20
participation/migrations/0005_alter_team_options.py
Normal file
20
participation/migrations/0005_alter_team_options.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.1.7 on 2023-04-03 17:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("participation", "0004_alter_pool_options_pool_letter"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="team",
|
||||
options={
|
||||
"ordering": ("trigram",),
|
||||
"verbose_name": "team",
|
||||
"verbose_name_plural": "teams",
|
||||
},
|
||||
),
|
||||
]
|
@ -0,0 +1,44 @@
|
||||
# Generated by Django 4.2 on 2023-04-06 22:05
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("participation", "0005_alter_team_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="passage",
|
||||
options={
|
||||
"ordering": ("pool", "position"),
|
||||
"verbose_name": "passage",
|
||||
"verbose_name_plural": "passages",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="passage",
|
||||
name="position",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
|
||||
default=1,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(5),
|
||||
],
|
||||
verbose_name="position",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="participation",
|
||||
name="valid",
|
||||
field=models.BooleanField(
|
||||
default=None,
|
||||
help_text="The participation got the validation of the organizers.",
|
||||
null=True,
|
||||
verbose_name="valid team",
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,45 @@
|
||||
# Generated by Django 4.2 on 2023-04-07 10:07
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("participation", "0006_alter_passage_options_passage_position_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="note",
|
||||
name="observer_oral",
|
||||
field=models.SmallIntegerField(
|
||||
choices=[
|
||||
(-4, -4),
|
||||
(-3, -3),
|
||||
(-2, -2),
|
||||
(-1, -1),
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
(4, 4),
|
||||
],
|
||||
default=0,
|
||||
verbose_name="observer note",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="passage",
|
||||
name="observer",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="+",
|
||||
to="participation.participation",
|
||||
verbose_name="observer",
|
||||
),
|
||||
),
|
||||
]
|
20
participation/migrations/0008_alter_participation_options.py
Normal file
20
participation/migrations/0008_alter_participation_options.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2 on 2023-04-11 20:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("participation", "0007_note_observer_oral_passage_observer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="participation",
|
||||
options={
|
||||
"ordering": ("valid", "team__trigram"),
|
||||
"verbose_name": "participation",
|
||||
"verbose_name_plural": "participations",
|
||||
},
|
||||
),
|
||||
]
|
@ -6,7 +6,7 @@ import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Index
|
||||
from django.urls import reverse_lazy
|
||||
@ -124,6 +124,7 @@ class Team(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("team")
|
||||
verbose_name_plural = _("teams")
|
||||
ordering = ('trigram',)
|
||||
indexes = [
|
||||
Index(fields=("trigram", )),
|
||||
]
|
||||
@ -278,6 +279,12 @@ class Tournament(models.Model):
|
||||
return Synthesis.objects.filter(final_solution=True)
|
||||
return Synthesis.objects.filter(participation__tournament=self)
|
||||
|
||||
@property
|
||||
def best_format(self):
|
||||
n = len(self.participations.filter(valid=True).all())
|
||||
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
|
||||
return '+'.join(map(str, sorted(fmt, reverse=True)))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
||||
|
||||
@ -315,7 +322,7 @@ class Participation(models.Model):
|
||||
valid = models.BooleanField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("valid"),
|
||||
verbose_name=_("valid team"),
|
||||
help_text=_("The participation got the validation of the organizers."),
|
||||
)
|
||||
|
||||
@ -334,6 +341,7 @@ class Participation(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("participation")
|
||||
verbose_name_plural = _("participations")
|
||||
ordering = ('valid', 'team__trigram',)
|
||||
|
||||
|
||||
class Pool(models.Model):
|
||||
@ -352,6 +360,16 @@ class Pool(models.Model):
|
||||
]
|
||||
)
|
||||
|
||||
letter = models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, 'A'),
|
||||
(2, 'B'),
|
||||
(3, 'C'),
|
||||
(4, 'D'),
|
||||
],
|
||||
verbose_name=_('letter'),
|
||||
)
|
||||
|
||||
participations = models.ManyToManyField(
|
||||
Participation,
|
||||
related_name="pools",
|
||||
@ -381,12 +399,16 @@ class Pool(models.Model):
|
||||
|
||||
@property
|
||||
def solutions(self):
|
||||
return Solution.objects.filter(participation__in=self.participations, final_solution=self.tournament.final)
|
||||
return [passage.defended_solution for passage in self.passages.all()]
|
||||
|
||||
def average(self, participation):
|
||||
return sum(passage.average(participation) for passage in self.passages.all()) \
|
||||
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
|
||||
|
||||
async def aaverage(self, participation):
|
||||
return sum([passage.average(participation) async for passage in self.passages.all()]) \
|
||||
+ sum([tweak.diff async for tweak in participation.tweaks.filter(pool=self).all()])
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:pool_detail", args=(self.pk,))
|
||||
|
||||
@ -399,6 +421,7 @@ class Pool(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("pool")
|
||||
verbose_name_plural = _("pools")
|
||||
ordering = ('round', 'letter',)
|
||||
|
||||
|
||||
class Passage(models.Model):
|
||||
@ -409,10 +432,17 @@ class Passage(models.Model):
|
||||
related_name="passages",
|
||||
)
|
||||
|
||||
position = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("position"),
|
||||
choices=zip(range(1, 6), range(1, 6)),
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(5)],
|
||||
)
|
||||
|
||||
solution_number = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("defended solution"),
|
||||
choices=[
|
||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
||||
],
|
||||
)
|
||||
|
||||
@ -437,6 +467,16 @@ class Passage(models.Model):
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
observer = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name=_("observer"),
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
defender_penalties = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("penalties"),
|
||||
default=0,
|
||||
@ -491,9 +531,25 @@ class Passage(models.Model):
|
||||
def average_reporter(self) -> float:
|
||||
return self.average_reporter_writing + self.average_reporter_oral
|
||||
|
||||
@property
|
||||
def average_observer(self) -> float:
|
||||
return self.avg(note.observer_oral for note in self.notes.all())
|
||||
|
||||
@property
|
||||
def averages(self):
|
||||
yield self.average_defender_writing
|
||||
yield self.average_defender_oral
|
||||
yield self.average_opponent_writing
|
||||
yield self.average_opponent_oral
|
||||
yield self.average_reporter_writing
|
||||
yield self.average_reporter_oral
|
||||
if self.observer:
|
||||
yield self.average_observer
|
||||
|
||||
def average(self, participation):
|
||||
return self.average_defender if participation == self.defender else self.average_opponent \
|
||||
if participation == self.opponent else self.average_reporter if participation == self.reporter else 0
|
||||
if participation == self.opponent else self.average_reporter if participation == self.reporter \
|
||||
else self.average_observer if participation == self.observer else 0
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:passage_detail", args=(self.pk,))
|
||||
@ -508,6 +564,9 @@ class Passage(models.Model):
|
||||
if self.reporter not in self.pool.participations.all():
|
||||
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
||||
.format(trigram=self.reporter.team.trigram))
|
||||
if self.observer and self.observer not in self.pool.participations.all():
|
||||
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
||||
.format(trigram=self.observer.team.trigram))
|
||||
return super().clean()
|
||||
|
||||
def __str__(self):
|
||||
@ -517,6 +576,7 @@ class Passage(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("passage")
|
||||
verbose_name_plural = _("passages")
|
||||
ordering = ('pool', 'position',)
|
||||
|
||||
|
||||
class Tweak(models.Model):
|
||||
@ -566,7 +626,7 @@ class Solution(models.Model):
|
||||
problem = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("problem"),
|
||||
choices=[
|
||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
||||
],
|
||||
)
|
||||
|
||||
@ -686,14 +746,31 @@ class Note(models.Model):
|
||||
default=0,
|
||||
)
|
||||
|
||||
observer_oral = models.SmallIntegerField(
|
||||
verbose_name=_("observer note"),
|
||||
choices=zip(range(-4, 5), range(-4, 5)),
|
||||
default=0,
|
||||
)
|
||||
|
||||
def get_all(self):
|
||||
yield self.defender_writing
|
||||
yield self.defender_oral
|
||||
yield self.opponent_writing
|
||||
yield self.opponent_oral
|
||||
yield self.reporter_writing
|
||||
yield self.reporter_oral
|
||||
if self.passage.observer:
|
||||
yield self.observer_oral
|
||||
|
||||
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
|
||||
reporter_writing: int, reporter_oral: int):
|
||||
reporter_writing: int, reporter_oral: int, observer_oral: int = 0):
|
||||
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
|
||||
self.observer_oral = observer_oral
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
|
||||
@ -703,7 +780,7 @@ class Note(models.Model):
|
||||
|
||||
def __bool__(self):
|
||||
return any((self.defender_writing, self.defender_oral, self.opponent_writing, self.opponent_oral,
|
||||
self.reporter_writing, self.reporter_oral))
|
||||
self.reporter_writing, self.reporter_oral, self.observer_oral))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("note")
|
@ -6,21 +6,22 @@ from participation.models import Note, Participation, Passage, Pool, Team
|
||||
from tfjm.lists import get_sympa_client
|
||||
|
||||
|
||||
def create_team_participation(instance, created, **_):
|
||||
def create_team_participation(instance, created, raw, **_):
|
||||
"""
|
||||
When a team got created, create an associated participation.
|
||||
"""
|
||||
if not raw:
|
||||
participation = Participation.objects.get_or_create(team=instance)[0]
|
||||
participation.save()
|
||||
if not created:
|
||||
participation.team.create_mailing_list()
|
||||
|
||||
|
||||
def update_mailing_list(instance: Team, **_):
|
||||
def update_mailing_list(instance: Team, raw, **_):
|
||||
"""
|
||||
When a team name or trigram got updated, update mailing lists and Matrix rooms
|
||||
"""
|
||||
if instance.pk:
|
||||
if instance.pk and not raw:
|
||||
old_team = Team.objects.get(pk=instance.pk)
|
||||
if old_team.trigram != instance.trigram:
|
||||
# TODO Rename Matrix room
|
||||
@ -36,10 +37,11 @@ def update_mailing_list(instance: Team, **_):
|
||||
f"{coach.user.first_name} {coach.user.last_name}")
|
||||
|
||||
|
||||
def create_notes(instance: Union[Passage, Pool], **_):
|
||||
def create_notes(instance: Union[Passage, Pool], raw, **_):
|
||||
if not raw:
|
||||
if isinstance(instance, Pool):
|
||||
for passage in instance.passages.all():
|
||||
create_notes(passage)
|
||||
create_notes(passage, raw)
|
||||
return
|
||||
|
||||
for jury in instance.pool.juries.all():
|
@ -54,6 +54,7 @@ class ParticipationTable(tables.Table):
|
||||
}
|
||||
model = Team
|
||||
fields = ('name', 'trigram', 'valid',)
|
||||
order = ('-valid',)
|
||||
|
||||
|
||||
class TournamentTable(tables.Table):
|
||||
@ -76,13 +77,21 @@ class TournamentTable(tables.Table):
|
||||
|
||||
|
||||
class PoolTable(tables.Table):
|
||||
teams = tables.LinkColumn(
|
||||
letter = tables.LinkColumn(
|
||||
'participation:pool_detail',
|
||||
args=[tables.A('id')],
|
||||
verbose_name=_("pool").capitalize,
|
||||
)
|
||||
|
||||
teams = tables.Column(
|
||||
verbose_name=_("teams").capitalize,
|
||||
empty_values=(),
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
def render_letter(self, record):
|
||||
return format_lazy(_("Pool {letter}{round}"), letter=record.get_letter_display(), round=record.round)
|
||||
|
||||
def render_teams(self, record):
|
||||
return ", ".join(participation.team.trigram for participation in record.participations.all()) \
|
||||
or _("No defined team")
|
||||
@ -92,7 +101,7 @@ class PoolTable(tables.Table):
|
||||
'class': 'table table-condensed table-striped',
|
||||
}
|
||||
model = Pool
|
||||
fields = ('teams', 'round', 'tournament',)
|
||||
fields = ('letter', 'teams', 'round', 'tournament',)
|
||||
|
||||
|
||||
class PassageTable(tables.Table):
|
||||
@ -134,4 +143,4 @@ class NoteTable(tables.Table):
|
||||
}
|
||||
model = Note
|
||||
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
||||
'reporter_writing', 'reporter_oral',)
|
||||
'reporter_writing', 'reporter_oral', 'observer_oral',)
|
@ -13,6 +13,9 @@
|
||||
<dt class="col-sm-3">{% trans "Pool:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.pool.get_absolute_url }}">{{ passage.pool }}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Position:" %}</dt>
|
||||
<dd class="col-sm-9">{{ passage.position }}</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Defender:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.defender.get_absolute_url }}">{{ passage.defender.team }}</a></dd>
|
||||
|
||||
@ -22,6 +25,11 @@
|
||||
<dt class="col-sm-3">{% trans "Reporter:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.reporter.get_absolute_url }}">{{ passage.reporter.team }}</a></dd>
|
||||
|
||||
{% if passage.observer %}
|
||||
<dt class="col-sm-3">{% trans "Observer:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.observer.get_absolute_url }}">{{ passage.observer.team }}</a></dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-3">{% trans "Defended solution:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}</a></dd>
|
||||
|
||||
@ -79,6 +87,11 @@
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the reporter oral:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd>
|
||||
|
||||
{% if passage.observer %}
|
||||
<dt class="col-sm-8">{% trans "Average points for the observer oral:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
<hr>
|
||||
@ -92,6 +105,11 @@
|
||||
|
||||
<dt class="col-sm-8">{% trans "Reporter points:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd>
|
||||
|
||||
{% if passage.observer %}
|
||||
<dt class="col-sm-8">{% trans "Observer points:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
@ -124,7 +142,7 @@
|
||||
initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}")
|
||||
|
||||
{% if my_note is not None %}
|
||||
initModal("updateNotesModal", "{% url "participation:update_notes" pk=my_note.pk %}")
|
||||
initModal("updateNotes", "{% url "participation:update_notes" pk=my_note.pk %}")
|
||||
{% endif %}
|
||||
{% elif user.registration.participates %}
|
||||
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
|
47
participation/templates/participation/pool_add_jurys.html
Normal file
47
participation/templates/participation/pool_add_jurys.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
{% trans "You can here register juries for the pool." %}
|
||||
{% trans "Be careful: this form register new users. To add existing users into the jury, please use this form:" %}
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update pool" %}</button>
|
||||
</p>
|
||||
<p>
|
||||
{% trans "For now, the registered juries for the tournament are:" %}
|
||||
<ul>
|
||||
{% for jury in pool.juries.all %}
|
||||
<li>{{ jury.user.first_name }} {{ jury.user.last_name }} (<a class="alert-link" href="mailto:{{ jury.user.email }}">{{ jury.user.email }}</a>)</li>
|
||||
{% empty %}
|
||||
<li><i>{% trans "There is no jury yet." %}</i></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
{% crispy form %}
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row text-center">
|
||||
<a href="{% url 'participation:pool_detail' pk=pool.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to pool detail" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% trans "Update pool" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:pool_update" pk=pool.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updatePool" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initModal("updatePool", "{% url "participation:pool_update" pk=pool.pk %}")
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -15,6 +15,9 @@
|
||||
<dt class="col-sm-3">{% trans "Round:" %}</dt>
|
||||
<dd class="col-sm-9">{{ pool.get_round_display }}</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Letter:" %}</dt>
|
||||
<dd class="col-sm-9">{{ pool.get_letter_display }}</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Teams:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% for participation in pool.participations.all %}
|
||||
@ -23,13 +26,40 @@
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Juries:" %}</dt>
|
||||
<dd class="col-sm-9">{{ pool.juries.all|join:", " }}</dd>
|
||||
<dd class="col-sm-9">
|
||||
{{ pool.juries.all|join:", " }}
|
||||
<a class="badge rounded-pill text-bg-info" href="{% url 'participation:pool_add_jurys' pk=pool.pk %}">
|
||||
<i class="fas fa-plus"></i> {% trans "Add jurys" %}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Defended solutions:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% for passage in pool.passages.all %}
|
||||
<a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}{% if not forloop.last %}, {% endif %}</a>
|
||||
<a href="{{ passage.defended_solution.file.url }}">{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
<a href="{% url 'participation:pool_download_solutions' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary">
|
||||
<i class="fas fa-download"></i> {% trans "Download all" %}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Syntheses:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for passage in pool.passages.all %}
|
||||
<li class="list-group-item">
|
||||
{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }} :
|
||||
{% for synthesis in passage.syntheses.all %}
|
||||
<a href="{{ synthesis.file.url }}">{{ synthesis.participation.team.trigram }} ({{ synthesis.get_type_display }})</a>{% if not forloop.last %}, {% endif %}
|
||||
{% empty %}
|
||||
{% trans "No synthesis was uploaded yet." %}
|
||||
{% endfor %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url 'participation:pool_download_syntheses' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary">
|
||||
<i class="fas fa-download"></i> {% trans "Download all" %}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
|
||||
@ -47,6 +77,31 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if user.registration.is_volunteer %}
|
||||
<div class="card-footer text-center">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}">
|
||||
{% trans "Download the scale sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
|
||||
</a>
|
||||
{% if pool.passages.count == 5 %}
|
||||
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}?page=2">
|
||||
{% trans "Room" %} 2
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}">
|
||||
{% trans "Download the final notation sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
|
||||
</a>
|
||||
{% if pool.passages.count == 5 %}
|
||||
<a class="btn btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}?page=2">
|
||||
{% trans "Room" %} 2
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal">{% trans "Upload notes from a CSV file" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if user.registration.is_volunteer %}
|
||||
@ -54,7 +109,6 @@
|
||||
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addPassageModal">{% trans "Add passage" %}</button>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update" %}</button>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamsModal">{% trans "Update teams" %}</button>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal">{% trans "Upload notes from a CSV file" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -115,12 +115,14 @@
|
||||
</dl>
|
||||
|
||||
{% if user.registration.is_volunteer %}
|
||||
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
|
||||
<div class="text-center">
|
||||
<a class="btn btn-info" href="{% url "participation:team_authorizations" pk=team.pk %}">
|
||||
<i class="fas fa-file-archive"></i> {% trans "Download all submitted authorizations" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamModal">{% trans "Update" %}</button>
|
126
participation/templates/participation/tex/bareme.tex
Normal file
126
participation/templates/participation/tex/bareme.tex
Normal file
@ -0,0 +1,126 @@
|
||||
\documentclass[12pt,a4paper,landscape]{article}
|
||||
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[utf8x]{inputenc}
|
||||
\usepackage[french]{babel}
|
||||
|
||||
\usepackage[a4paper]{geometry}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amsfonts}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{amsthm}
|
||||
\usepackage{hyperref}
|
||||
\usepackage{color}
|
||||
\usepackage{mathtools}
|
||||
\usepackage{comment}
|
||||
\usepackage{array}
|
||||
\usepackage{multirow}
|
||||
\usepackage{footnote}
|
||||
\usepackage{xintexpr}
|
||||
|
||||
\addtolength{\textwidth}{4cm}
|
||||
\setlength{\parindent}{0mm}
|
||||
|
||||
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=2cm}
|
||||
|
||||
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
|
||||
\pagestyle{empty}
|
||||
\renewcommand{\leq}{\leqslant}
|
||||
\def\tfjmedition{~{{ tfjm_number }}}
|
||||
|
||||
\begin{document}
|
||||
\thispagestyle{empty}
|
||||
|
||||
|
||||
\begin{center}
|
||||
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}
|
||||
\end{center}
|
||||
\vspace{3mm}
|
||||
|
||||
\begin{center}
|
||||
\begin{itemize}
|
||||
{% for passage in passages.all %}
|
||||
\item D\'efenseur\textperiodcentered{}se au passage {{ forloop.counter }} : \underline{\texttt{~{{ passage.defender.team.trigram }}~}} $\qquad$ probl\`eme \underline{~{{ passage.solution_number }}~}
|
||||
{% endfor %}
|
||||
\end{itemize}
|
||||
\end{center}
|
||||
|
||||
\vspace{6mm}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR
|
||||
\begin{tabular}{|c|p{20mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||
\multicolumn{4}{|l|}{Læ {\bf D\'efenseur\textperiodcentered{}se} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %}\\ \hline \hline
|
||||
%ECRIT
|
||||
\multirow{6}{3mm}{\centering \bf\'E\\ C\\ R\\ I\\ T} & \multirow{3}{20mm}{Partie scientifique} & Profondeur des r\'esultats d\'emontr\'es & [0,5] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Originalit\'e et pertinence des preuves& [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Exactitude et justesse des d\'emonstrations, algorithmes, etc. & [0,7] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multirow{2}{20mm}{Forme} & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Clart\'e du raisonnement : facile \`a comprendre ou compl\`etement obscur ? & [0,3]{{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/20)} {{ esp|safe }} \\ \hline \hline
|
||||
%ORAL
|
||||
\multirow{8}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{4}{20mm}{Partie scientifique} & Compr\'ehension du mat\'eriel, connaissance des sujets math\'ematiques correspondants \emph{lors de la pr\'esentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& P\'edagogie, notamment clart\'e, exactitude et justesse des d\'emonstrations \emph{lors de la pr\'esentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Capacit\'e \`a r\'eagir aux questions et remarques de l'Opposant\textperiodcentered{}e et de læ Rapporteur\textperiodcentered{}e & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Capacit\'e \`a r\'eagir aux questions et remarques du jury & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multirow{3}{20mm}{Forme} & Bri\`evet\'e et propret\'e de la pr\'esentation & [0,2] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Capacit\'e de faire avancer le d\'ebat & [0,2] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& \emph{Conformit\'e} entre la pr\'esentation et le mat\'eriel \'ecrit & [--5,0] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/16)} {{ esp|safe }} \\ \hline
|
||||
|
||||
\end{tabular}
|
||||
|
||||
\newpage
|
||||
|
||||
%%%%%%%%%%%%%%%%%OPPOSANT
|
||||
\begin{tabular}{|c|p{20mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||
\multicolumn{4}{|l|}{L' {\bf Opposant\textperiodcentered{}e} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.}
|
||||
{% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %} \\ \hline \hline
|
||||
%ECRIT
|
||||
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{2}{20mm}{Partie scientifique} & Compr\'ehension du probl\`eme, savoir \'evaluer la qualit\'e g\'en\'erale de la solution & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Rep\'erer les erreurs et leur importance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
& Forme & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/9)} {{ esp|safe }} \\ \hline \hline
|
||||
%ORAL
|
||||
\multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{3}{20mm}{Partie scientifique} & Compr\'ehension du probl\`eme, savoir \'evaluer la qualit\'e g\'en\'erale de la pr\'esentation de læ D\'efenseur\textperiodcentered{}se
|
||||
& [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Rep\'erer les erreurs et leur importance & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Pertinence des questions & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
& Forme & M\`ene un d\'ebat de fa\c con comp\'etente et propre. & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
|
||||
\end{tabular}
|
||||
|
||||
\vfill
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR
|
||||
\begin{tabular}{|c|p{20mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||
\multicolumn{4}{|l|}{Læ {\bf Rapporteur\textperiodcentered{}e} \normalsize \'evalue le d\'ebat entre læ D\'efenseur\textperiodcentered{}se et l'Opposant\textperiodcentered{}e.} {% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %}\\ \hline \hline
|
||||
%ECRIT
|
||||
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{2}{20mm}{Partie scientifique} & Compr\'ehension du probl\`eme, savoir \'evaluer la qualit\'e g\'en\'erale de la solution & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
& & Rep\'erer les erreurs et leur importance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
& Forme & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/9)} {{ esp|safe }}\\ \hline \hline
|
||||
%ORAL
|
||||
\multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} &\multirow{4}{20mm}{Partie scientifique} & Compr\'ehension du probl\`eme, savoir \'evaluer la qualit\'e g\'en\'erale de la pr\'esentation de læ D\'efenseur\textperiodcentered{}se & [0,1] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Savoir \'evaluer la qualit\'e g\'en\'erale du d\'ebat & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Rep\'erer les points importants non abord\'es & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Pertinence des questions & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
& Forme & M\`ene un d\'ebat de fa\c con comp\'etente et propre. & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
& \multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
|
||||
\end{tabular}
|
||||
|
||||
\vfill
|
||||
|
||||
{% if passages.count == 4 %}
|
||||
%%%%%%% INTERVENTION EXCEPTIONNELLE
|
||||
\begin{tabular}{|c|p{11cm}|c|p{2cm}|p{2cm}|p{2cm}|p{2cm}|}\hline
|
||||
\multicolumn{3}{|l|}{L'{\bf Intervention exceptionnelle} \normalsize permet de signaler une erreur grave omise par tous.} {% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %}\\ \hline \hline
|
||||
%ORAL
|
||||
\multirow{1}{3mm}{\centering\bf O\\ R\\ A\\ L}
|
||||
& Toute intervention exceptionnelle non pertinente est sanctionn\'ee par une note n\'egative, l'absence d'intervention re\c coit un z\'ero forfaitaire. \phantom{pour avoir oral en entier dans la} \phantom{colonne il} \phantom{faut blablater un peu}& [-4,4] {{ esp|safe }}\\ \hline
|
||||
\end{tabular}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
\end{document}
|
90
participation/templates/participation/tex/finale.tex
Normal file
90
participation/templates/participation/tex/finale.tex
Normal file
@ -0,0 +1,90 @@
|
||||
\documentclass[10pt,a4paper,landscape]{article}
|
||||
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[utf8x]{inputenc}
|
||||
\usepackage[french]{babel}
|
||||
|
||||
\usepackage[a4paper]{geometry}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amsfonts}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{amsthm}
|
||||
\usepackage{hyperref}
|
||||
\usepackage{color}
|
||||
\usepackage{mathtools}
|
||||
\usepackage{comment}
|
||||
\usepackage{array}
|
||||
\usepackage{multirow}
|
||||
\usepackage{footnote}
|
||||
\usepackage{tabularx}
|
||||
\usepackage{xintexpr}
|
||||
|
||||
\addtolength{\textwidth}{6cm}
|
||||
\addtolength{\oddsidemargin}{-3cm}
|
||||
\addtolength{\textheight}{2cm}
|
||||
\addtolength{\topmargin}{-0.5cm}
|
||||
\setlength{\parindent}{0mm}
|
||||
|
||||
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
|
||||
\renewcommand{\leq}{\leqslant}
|
||||
\def\tfjmedition{~{{ tfjm_number }}}
|
||||
|
||||
\begin{document}
|
||||
\pagenumbering{gobble}
|
||||
|
||||
\centering
|
||||
|
||||
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\
|
||||
\vspace{3mm}
|
||||
Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{{ page }} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_start }}{% else %}{{ pool.tournament.date_end }}{% endif %}
|
||||
|
||||
|
||||
\vspace{15mm}
|
||||
|
||||
|
||||
\begin{tabular}{|p{35mm}{% for passage in passages.all %}{% if passages.count == 3 %}|p{3cm}|p{3cm}{% else %}|p{2.5cm}|p{2.5cm}{% endif %}{% endfor %}|}\hline
|
||||
\multirow{2}{35mm}{\LARGE R\^ole} {% for passage in passages.all %}& \multicolumn{2}{c|}{ \Large Probl\`eme {{ passage.solution_number }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
|
||||
{% for passage in passages.all %}& \hspace{4mm} {\Large \'ECRIT} & \hspace{4mm} {\Large ORAL}{% endfor %} \\ \hline
|
||||
\multirow{2}{35mm}{\LARGE D\'efenseur\textperiodcentered{}se} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.defender.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
|
||||
{% for passage in passages.all %}
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 20$
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 16$
|
||||
{% endfor %} & \hline
|
||||
\multirow{2}{35mm}{\LARGE Opposant\textperiodcentered{}e} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.opponent.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
|
||||
{% for passage in passages.all %}
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 9$
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||
{% endfor %} & \hline
|
||||
\multirow{2}{35mm}{\LARGE Rapporteur\textperiodcentered{}e} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.reporter.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
|
||||
{% for passage in passages.all %}
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 9$
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||
{% endfor %} & \hline
|
||||
{% if passages.count == 4 %}
|
||||
\multirow{4}{35mm}{\Large Intervention exceptionnelle}{% for passage in passages.all %} & \multicolumn{2}{c|}{\Large {{ passage.observer.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
|
||||
& \multicolumn{2}{c|}{\phantom{asd asd} \phantom{asd asd}}
|
||||
& \multicolumn{2}{c|}{\phantom{asd asd} \phantom{asd asd}}
|
||||
& \multicolumn{2}{c|}{\phantom{asd asd} \phantom{asd asd}}
|
||||
& \multicolumn{2}{c|}{\phantom{asd asd} \phantom{asd asd}}\\
|
||||
& \multicolumn{2}{c|}{\phantom{asd asd} \phantom{asd asd}}
|
||||
& \multicolumn{2}{c|}{\phantom{asd asd} \phantom{asd asd}}
|
||||
& \multicolumn{2}{c|}{\phantom{asd asd} \phantom{asd asd}}
|
||||
& \multicolumn{2}{c|}{\phantom{asd asd} \phantom{asd asd}}\\
|
||||
& \multicolumn{2}{c|}{\centering \normalsize$-4\leq x\leq 4$}
|
||||
& \multicolumn{2}{c|}{\centering \normalsize$-4\leq x\leq 4$}
|
||||
& \multicolumn{2}{c|}{\centering \normalsize$-4\leq x\leq 4$}
|
||||
& \multicolumn{2}{c|}{\centering \normalsize$-4\leq x\leq 4$} & \hline
|
||||
{% endif %}
|
||||
|
||||
\end{tabular}
|
||||
|
||||
\vspace{15mm}
|
||||
|
||||
\LARGE Nom de læ jur\'e\textperiodcentered{}e :
|
||||
{% if is_jury %}\underline{ {{ user.first_name|safe }} {{ user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %}
|
||||
$\qquad$ Signature : \underline{\phantom{Phrase moins longue}}
|
||||
|
||||
\newpage
|
||||
%}
|
||||
\end{document}
|
@ -6,6 +6,11 @@
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div id="form-content">
|
||||
<div class="alert alert-info">
|
||||
<a class="alert-link" href="{% url "participation:pool_notes_template" pk=pool.pk %}">
|
||||
{% trans "Download empty notation sheet" %}
|
||||
</a>
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
@ -7,8 +7,9 @@
|
||||
<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>
|
||||
<a class="alert-link" href="{% static "Fiche_synthèse.pdf" %}"> PDF</a> —
|
||||
<a class="alert-link" href="{% static "Fiche_synthèse.tex" %}"> TEX</a> —
|
||||
<a class="alert-link" href="{% static "Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a>
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user