mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-02-26 18:26:28 +00:00
Compare commits
54 Commits
2a545dae10
...
ca7cf5987c
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
@ -2,14 +2,6 @@ stages:
|
|||||||
- test
|
- test
|
||||||
- quality-assurance
|
- quality-assurance
|
||||||
|
|
||||||
py39:
|
|
||||||
stage: test
|
|
||||||
image: python:3.9-alpine
|
|
||||||
before_script:
|
|
||||||
- apk add --no-cache libmagic
|
|
||||||
- pip install tox --no-cache-dir
|
|
||||||
script: tox -e py39
|
|
||||||
|
|
||||||
py310:
|
py310:
|
||||||
stage: test
|
stage: test
|
||||||
image: python:3.10-alpine
|
image: python:3.10-alpine
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.urls import include, path
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from .viewsets import UserViewSet
|
from .viewsets import UserViewSet
|
||||||
@ -29,6 +29,6 @@ app_name = 'api'
|
|||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
# Additionally, we include login URLs for the browsable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url('^', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
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,14 +0,0 @@
|
|||||||
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
|
||||||
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<form method="post" enctype="multipart/form-data">
|
|
||||||
<div id="form-content">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
@ -1,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, Round, Pool, 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")
|
1008
draw/consumers.py
Normal file
1008
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
512
draw/models.py
Normal file
512
draw/models.py
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
# 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 MinValueValidator, MaxValueValidator
|
||||||
|
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 Passage, Participation, 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."),
|
||||||
|
)
|
||||||
|
|
||||||
|
@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."),
|
||||||
|
)
|
||||||
|
|
||||||
|
@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
|
||||||
|
if self.size == 3:
|
||||||
|
table = [
|
||||||
|
[0, 1, 2],
|
||||||
|
[1, 2, 0],
|
||||||
|
[2, 0, 1],
|
||||||
|
]
|
||||||
|
elif self.size == 4:
|
||||||
|
table = [
|
||||||
|
[0, 1, 2],
|
||||||
|
[1, 2, 3],
|
||||||
|
[2, 3, 0],
|
||||||
|
[3, 0, 1],
|
||||||
|
]
|
||||||
|
elif self.size == 5:
|
||||||
|
table = [
|
||||||
|
[0, 2, 3],
|
||||||
|
[1, 3, 4],
|
||||||
|
[2, 0, 1],
|
||||||
|
[3, 4, 0],
|
||||||
|
[4, 1, 2],
|
||||||
|
]
|
||||||
|
|
||||||
|
for line in table:
|
||||||
|
# Create the passage
|
||||||
|
await Passage.objects.acreate(
|
||||||
|
pool=self.associated_pool,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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=_("accepted problem"),
|
||||||
|
)
|
||||||
|
|
||||||
|
rejected = models.JSONField(
|
||||||
|
default=list,
|
||||||
|
verbose_name=_('rejected problems'),
|
||||||
|
)
|
||||||
|
|
||||||
|
@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',)
|
7
draw/routing.py
Normal file
7
draw/routing.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import consumers
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
path("ws/draw/<int:tournament_id>/", consumers.DrawConsumer.as_asgi()),
|
||||||
|
]
|
742
draw/static/draw.js
Normal file
742
draw/static/draw.js
Normal file
@ -0,0 +1,742 @@
|
|||||||
|
(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)
|
||||||
|
const sockets = {}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
sockets[tid].send(JSON.stringify({'type': 'abort'}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
*/
|
||||||
|
function drawDice(tid, trigram = null) {
|
||||||
|
sockets[tid].send(JSON.stringify({'type': 'dice', 'trigram': trigram}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to draw a new problem.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
|
function drawProblem(tid) {
|
||||||
|
sockets[tid].send(JSON.stringify({'type': 'draw_problem'}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept the current proposed problem.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
|
function acceptProblem(tid) {
|
||||||
|
sockets[tid].send(JSON.stringify({'type': 'accept'}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject the current proposed problem.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
|
function rejectProblem(tid) {
|
||||||
|
sockets[tid].send(JSON.stringify({'type': 'reject'}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volunteers can export the draw to make it available for notation.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
|
function exportDraw(tid) {
|
||||||
|
sockets[tid].send(JSON.stringify({'type': 'export'}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volunteers can make the draw continue for the second round of the final.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
|
function continueFinal(tid) {
|
||||||
|
sockets[tid].send(JSON.stringify({'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()))
|
||||||
|
|
||||||
|
for (let tournament of tournaments) {
|
||||||
|
// Open a websocket per tournament
|
||||||
|
let socket = new WebSocket(
|
||||||
|
(document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host
|
||||||
|
+ '/ws/draw/' + tournament.id + '/'
|
||||||
|
)
|
||||||
|
sockets[tournament.id] = socket
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 info The content to updated
|
||||||
|
*/
|
||||||
|
function setInfo(info) {
|
||||||
|
document.getElementById(`messages-${tournament.id}`).innerHTML = info
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the draw interface, given the list of teams.
|
||||||
|
* @param teams The list of teams (represented by their trigrams) that are present on this draw.
|
||||||
|
*/
|
||||||
|
function drawStart(teams) {
|
||||||
|
// Hide the not-started-banner
|
||||||
|
document.getElementById(`banner-not-started-${tournament.id}`).classList.add('d-none')
|
||||||
|
// Display the full draw interface
|
||||||
|
document.getElementById(`draw-content-${tournament.id}`).classList.remove('d-none')
|
||||||
|
|
||||||
|
let dicesDiv = document.getElementById(`dices-${tournament.id}`)
|
||||||
|
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-${tournament.id}-${team}`
|
||||||
|
diceDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
|
||||||
|
if (document.getElementById(`abort-${tournament.id}`) !== null) {
|
||||||
|
// Check if this is a volunteer, who can launch a dice for a specific team
|
||||||
|
diceDiv.onclick = (e) => drawDice(tournament.id, team)
|
||||||
|
}
|
||||||
|
diceDiv.textContent = `${team} 🎲 ??`
|
||||||
|
col.append(diceDiv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort the current draw, and make all invisible, except the not-started-banner.
|
||||||
|
*/
|
||||||
|
function drawAbort() {
|
||||||
|
document.getElementById(`banner-not-started-${tournament.id}`).classList.remove('d-none')
|
||||||
|
document.getElementById(`draw-content-${tournament.id}`).classList.add('d-none')
|
||||||
|
document.getElementById(`dices-${tournament.id}`).innerHTML = ""
|
||||||
|
document.getElementById(`recap-${tournament.id}-round-list`).innerHTML = ""
|
||||||
|
document.getElementById(`tables-${tournament.id}`).innerHTML = ""
|
||||||
|
updateDiceVisibility(false)
|
||||||
|
updateBoxVisibility(false)
|
||||||
|
updateButtonsVisibility(false)
|
||||||
|
updateExportVisibility(false)
|
||||||
|
updateContinueVisibility(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 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(trigram, result) {
|
||||||
|
let elem = document.getElementById(`dice-${tournament.id}-${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 visible The visibility status
|
||||||
|
*/
|
||||||
|
function updateDiceVisibility(visible) {
|
||||||
|
let div = document.getElementById(`launch-dice-${tournament.id}`)
|
||||||
|
if (visible)
|
||||||
|
div.classList.remove('d-none')
|
||||||
|
else
|
||||||
|
div.classList.add('d-none')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the box button.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
|
function updateBoxVisibility(visible) {
|
||||||
|
let div = document.getElementById(`draw-problem-${tournament.id}`)
|
||||||
|
if (visible)
|
||||||
|
div.classList.remove('d-none')
|
||||||
|
else
|
||||||
|
div.classList.add('d-none')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the accept and reject buttons.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
|
function updateButtonsVisibility(visible) {
|
||||||
|
let div = document.getElementById(`buttons-${tournament.id}`)
|
||||||
|
if (visible)
|
||||||
|
div.classList.remove('d-none')
|
||||||
|
else
|
||||||
|
div.classList.add('d-none')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the export button.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
|
function updateExportVisibility(visible) {
|
||||||
|
let div = document.getElementById(`export-${tournament.id}`)
|
||||||
|
if (visible)
|
||||||
|
div.classList.remove('d-none')
|
||||||
|
else
|
||||||
|
div.classList.add('d-none')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the continuation button.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
|
function updateContinueVisibility(visible) {
|
||||||
|
let div = document.getElementById(`continue-${tournament.id}`)
|
||||||
|
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 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(round, poules) {
|
||||||
|
let roundList = document.getElementById(`recap-${tournament.id}-round-list`)
|
||||||
|
let poolListId = `recap-${tournament.id}-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-${tournament.id}-round-${round}`
|
||||||
|
div.classList.add('col-md-6', 'px-3', 'py-3')
|
||||||
|
div.setAttribute('data-tournament', tournament.id)
|
||||||
|
|
||||||
|
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-${tournament.id}-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-${tournament.id}-round-${round}-pool-${poule.letter}`
|
||||||
|
li.classList.add('list-group-item', 'px-3', 'py-3')
|
||||||
|
li.setAttribute('data-tournament', tournament.id)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poule.teams.length > 0) {
|
||||||
|
// The pool is initialized
|
||||||
|
for (let team of poule.teams) {
|
||||||
|
// Reorder dices
|
||||||
|
let diceDiv = document.getElementById(`dice-${tournament.id}-${team}`)
|
||||||
|
diceDiv.parentElement.style.order = c.toString()
|
||||||
|
c += 1
|
||||||
|
|
||||||
|
let teamLiId = `recap-${tournament.id}-round-${round}-team-${team}`
|
||||||
|
let teamLi = document.getElementById(teamLiId)
|
||||||
|
|
||||||
|
if (teamLi === null) {
|
||||||
|
// Add a line for the team in the recap
|
||||||
|
teamLi = document.createElement('li')
|
||||||
|
teamLi.id = teamLiId
|
||||||
|
teamLi.classList.add('list-group-item')
|
||||||
|
teamLi.setAttribute('data-tournament', tournament.id)
|
||||||
|
|
||||||
|
teamList.append(teamLi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the accepted problem div (empty for now)
|
||||||
|
let acceptedDivId = `recap-${tournament.id}-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-${tournament.id}-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-${tournament.id}`)
|
||||||
|
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-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-${tournament.id}-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(round, poule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the table for the given round and the given pool, where there will be the chosen problems.
|
||||||
|
* @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(round, poule) {
|
||||||
|
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
|
||||||
|
let pouleTable = document.getElementById(`table-${tournament.id}-${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-${tournament.id}-${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-${tournament.id}-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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highligh the team that is currently choosing its problem.
|
||||||
|
* @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(round, pool, team) {
|
||||||
|
// Remove the previous highlights
|
||||||
|
document.querySelectorAll(`div.text-bg-secondary[data-tournament="${tournament.id}"]`)
|
||||||
|
.forEach(elem => elem.classList.remove('text-bg-secondary'))
|
||||||
|
document.querySelectorAll(`li.list-group-item-success[data-tournament="${tournament.id}"]`)
|
||||||
|
.forEach(elem => elem.classList.remove('list-group-item-success'))
|
||||||
|
document.querySelectorAll(`li.list-group-item-info[data-tournament="${tournament.id}"]`)
|
||||||
|
.forEach(elem => elem.classList.remove('list-group-item-info'))
|
||||||
|
|
||||||
|
// Highlight current round, if existing
|
||||||
|
let roundDiv = document.getElementById(`recap-${tournament.id}-round-${round}`)
|
||||||
|
if (roundDiv !== null)
|
||||||
|
roundDiv.classList.add('text-bg-secondary')
|
||||||
|
|
||||||
|
// Highlight current pool, if existing
|
||||||
|
let poolLi = document.getElementById(`recap-${tournament.id}-round-${round}-pool-${pool}`)
|
||||||
|
if (poolLi !== null)
|
||||||
|
poolLi.classList.add('list-group-item-success')
|
||||||
|
|
||||||
|
// Highlight current team, if existing
|
||||||
|
let teamLi = document.getElementById(`recap-${tournament.id}-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 round The current round, as integer (1 or 2)
|
||||||
|
* @param team The current team trigram
|
||||||
|
* @param problem The accepted problem, as integer
|
||||||
|
*/
|
||||||
|
function setProblemAccepted(round, team, problem) {
|
||||||
|
// Update recap
|
||||||
|
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-accepted`)
|
||||||
|
recapDiv.classList.remove('text-bg-warning')
|
||||||
|
recapDiv.classList.add('text-bg-success')
|
||||||
|
recapDiv.textContent = `${team} 📃 ${problem}`
|
||||||
|
|
||||||
|
// Update table
|
||||||
|
let tableSpan = document.getElementById(`table-${tournament.id}-round-${round}-problem-${team}`)
|
||||||
|
tableSpan.textContent = problem
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the recap when a team rejects a problem.
|
||||||
|
* @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(round, team, rejected) {
|
||||||
|
// Update recap
|
||||||
|
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-rejected`)
|
||||||
|
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
|
||||||
|
|
||||||
|
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
|
||||||
|
let penaltyDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-penalty`)
|
||||||
|
if (penaltyDiv === null) {
|
||||||
|
penaltyDiv = document.createElement('div')
|
||||||
|
penaltyDiv.id = `recap-${tournament.id}-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))}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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(round, poule, teams, problems) {
|
||||||
|
// Redraw the pool table
|
||||||
|
let table = document.getElementById(`table-${tournament.id}-${round}-${poule}`)
|
||||||
|
table.parentElement.parentElement.remove()
|
||||||
|
|
||||||
|
updatePouleTable(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(round, team, problem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen on websockets and process messages from the server
|
||||||
|
socket.addEventListener('message', e => {
|
||||||
|
// Parse received data as JSON
|
||||||
|
const data = JSON.parse(e.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(data.information)
|
||||||
|
break
|
||||||
|
case 'draw_start':
|
||||||
|
// Start the draw and update the interface
|
||||||
|
drawStart(data.trigrams)
|
||||||
|
break
|
||||||
|
case 'abort':
|
||||||
|
// Abort the current draw
|
||||||
|
drawAbort()
|
||||||
|
break
|
||||||
|
case 'dice':
|
||||||
|
// Update the interface after a dice launch
|
||||||
|
updateDiceInfo(data.team, data.result)
|
||||||
|
break
|
||||||
|
case 'dice_visibility':
|
||||||
|
// Update the dice button visibility
|
||||||
|
updateDiceVisibility(data.visible)
|
||||||
|
break
|
||||||
|
case 'box_visibility':
|
||||||
|
// Update the box button visibility
|
||||||
|
updateBoxVisibility(data.visible)
|
||||||
|
break
|
||||||
|
case 'buttons_visibility':
|
||||||
|
// Update the accept/reject buttons visibility
|
||||||
|
updateButtonsVisibility(data.visible)
|
||||||
|
break
|
||||||
|
case 'export_visibility':
|
||||||
|
// Update the export button visibility
|
||||||
|
updateExportVisibility(data.visible)
|
||||||
|
break
|
||||||
|
case 'continue_visibility':
|
||||||
|
// Update the continue button visibility for the final tournament
|
||||||
|
updateContinueVisibility(data.visible)
|
||||||
|
break
|
||||||
|
case 'set_poules':
|
||||||
|
// Set teams order and pools and update the interface
|
||||||
|
updatePoules(data.round, data.poules)
|
||||||
|
break
|
||||||
|
case 'set_active':
|
||||||
|
// Highlight the team that is selecting a problem
|
||||||
|
updateActiveRecap(data.round, data.poule, data.team)
|
||||||
|
break
|
||||||
|
case 'set_problem':
|
||||||
|
// Mark a problem as accepted and update the interface
|
||||||
|
setProblemAccepted(data.round, data.team, data.problem)
|
||||||
|
break
|
||||||
|
case 'reject_problem':
|
||||||
|
// Mark a problem as rejected and update the interface
|
||||||
|
setProblemRejected(data.round, data.team, data.rejected)
|
||||||
|
break
|
||||||
|
case 'reorder_poule':
|
||||||
|
// Reorder a pool and redraw the associated table
|
||||||
|
reorderPoule(data.round, data.poule, data.teams, data.problems)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Manage errors
|
||||||
|
socket.addEventListener('close', e => {
|
||||||
|
console.error('Chat socket closed unexpectedly')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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({
|
||||||
|
'type': 'set_language',
|
||||||
|
'language': document.getElementsByName('language')[0].value,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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({
|
||||||
|
'type': 'start_draw',
|
||||||
|
'fmt': document.getElementById('format-' + tournament.id).value
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
39
draw/templates/draw/index.html
Normal file
39
draw/templates/draw/index.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
{% 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 %}
|
329
draw/templates/draw/tournament_content.html
Normal file
329
draw/templates/draw/tournament_content.html
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
{% 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 %}
|
||||||
|
{# Volunteers can click on this button to abort the draw #}
|
||||||
|
<button id="abort-{{ tournament.id }}" class="badge rounded-pill text-bg-danger" onclick="abortDraw({{ tournament.id }})">
|
||||||
|
{% trans "Abort" %}
|
||||||
|
</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>
|
||||||
|
</div>
|
2
draw/tests.py
Normal file
2
draw/tests.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Copyright (C) 2023 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
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'),
|
||||||
|
]
|
40
draw/views.py
Normal file
40
draw/views.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# 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.views.generic import TemplateView, DetailView
|
||||||
|
|
||||||
|
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:
|
||||||
|
# A participant can see its own tournament, or the final if necessary
|
||||||
|
tournaments = [reg.team.participation.tournament]
|
||||||
|
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
|
nginx
|
||||||
|
|
||||||
if [ "$TFJM_STAGE" = "prod" ]; then
|
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
|
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
|
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
|
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
|
Each time a model is saved, an entry in the table `Changelog` is added in the database
|
||||||
in order to store each modification made
|
in order to store each modification made
|
||||||
"""
|
"""
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal") or raw:
|
||||||
return
|
return
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
@ -10,9 +10,15 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://tfjm;
|
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_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 {
|
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',)
|
||||||
|
|
||||||
|
@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',)
|
@ -170,7 +170,7 @@ class SolutionForm(forms.ModelForm):
|
|||||||
class PoolForm(forms.ModelForm):
|
class PoolForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Pool
|
model = Pool
|
||||||
fields = ('tournament', 'round', 'bbb_url', 'results_available', 'juries',)
|
fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'juries',)
|
||||||
widgets = {
|
widgets = {
|
||||||
"juries": forms.SelectMultiple(attrs={
|
"juries": forms.SelectMultiple(attrs={
|
||||||
'class': 'selectpicker',
|
'class': 'selectpicker',
|
||||||
@ -231,6 +231,8 @@ class UploadNotesForm(forms.Form):
|
|||||||
if len(line) < 19:
|
if len(line) < 19:
|
||||||
continue
|
continue
|
||||||
name = line[0]
|
name = line[0]
|
||||||
|
if name in ["moyenne", "coefficient", "sous-total"]:
|
||||||
|
continue
|
||||||
notes = line[1:19]
|
notes = line[1:19]
|
||||||
if not all(s.isnumeric() for s in notes):
|
if not all(s.isnumeric() for s in notes):
|
||||||
continue
|
continue
|
0
participation/management/__init__.py
Normal file
0
participation/management/__init__.py
Normal file
@ -30,7 +30,7 @@ class Command(BaseCommand):
|
|||||||
else:
|
else:
|
||||||
stat_file = os.stat("tfjm/static/logo.png")
|
stat_file = os.stat("tfjm/static/logo.png")
|
||||||
with open("tfjm/static/logo.png", "rb") as f:
|
with open("tfjm/static/logo.png", "rb") as f:
|
||||||
resp = (await Matrix.upload(f, filename="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]
|
filesize=stat_file.st_size))[0][0]
|
||||||
avatar_uri = resp.content_uri
|
avatar_uri = resp.content_uri
|
||||||
with open(".matrix_avatar", "w") as f:
|
with open(".matrix_avatar", "w") as f:
|
@ -3,7 +3,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import participation.models
|
import participation.models
|
||||||
|
|
@ -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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -124,6 +124,7 @@ class Team(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("team")
|
verbose_name = _("team")
|
||||||
verbose_name_plural = _("teams")
|
verbose_name_plural = _("teams")
|
||||||
|
ordering = ('trigram',)
|
||||||
indexes = [
|
indexes = [
|
||||||
Index(fields=("trigram", )),
|
Index(fields=("trigram", )),
|
||||||
]
|
]
|
||||||
@ -278,6 +279,13 @@ class Tournament(models.Model):
|
|||||||
return Synthesis.objects.filter(final_solution=True)
|
return Synthesis.objects.filter(final_solution=True)
|
||||||
return Synthesis.objects.filter(participation__tournament=self)
|
return Synthesis.objects.filter(participation__tournament=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def best_format(self):
|
||||||
|
n = len(self.participations.filter(valid=True).all())
|
||||||
|
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
|
||||||
|
return '+'.join(map(str, sorted(fmt, reverse=True)))
|
||||||
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
||||||
|
|
||||||
@ -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(
|
participations = models.ManyToManyField(
|
||||||
Participation,
|
Participation,
|
||||||
related_name="pools",
|
related_name="pools",
|
||||||
@ -387,6 +405,10 @@ class Pool(models.Model):
|
|||||||
return sum(passage.average(participation) for passage in self.passages.all()) \
|
return sum(passage.average(participation) for passage in self.passages.all()) \
|
||||||
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
|
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
|
||||||
|
|
||||||
|
async def aaverage(self, participation):
|
||||||
|
return sum([passage.average(participation) async for passage in self.passages.all()]) \
|
||||||
|
+ sum([tweak.diff async for tweak in participation.tweaks.filter(pool=self).all()])
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("participation:pool_detail", args=(self.pk,))
|
return reverse_lazy("participation:pool_detail", args=(self.pk,))
|
||||||
|
|
||||||
@ -399,6 +421,7 @@ class Pool(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("pool")
|
verbose_name = _("pool")
|
||||||
verbose_name_plural = _("pools")
|
verbose_name_plural = _("pools")
|
||||||
|
ordering = ('round', 'letter',)
|
||||||
|
|
||||||
|
|
||||||
class Passage(models.Model):
|
class Passage(models.Model):
|
||||||
@ -412,7 +435,7 @@ class Passage(models.Model):
|
|||||||
solution_number = models.PositiveSmallIntegerField(
|
solution_number = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_("defended solution"),
|
verbose_name=_("defended solution"),
|
||||||
choices=[
|
choices=[
|
||||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -566,7 +589,7 @@ class Solution(models.Model):
|
|||||||
problem = models.PositiveSmallIntegerField(
|
problem = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_("problem"),
|
verbose_name=_("problem"),
|
||||||
choices=[
|
choices=[
|
||||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -6,21 +6,22 @@ from participation.models import Note, Participation, Passage, Pool, Team
|
|||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
|
|
||||||
|
|
||||||
def create_team_participation(instance, created, **_):
|
def create_team_participation(instance, created, raw, **_):
|
||||||
"""
|
"""
|
||||||
When a team got created, create an associated participation.
|
When a team got created, create an associated participation.
|
||||||
"""
|
"""
|
||||||
|
if not raw:
|
||||||
participation = Participation.objects.get_or_create(team=instance)[0]
|
participation = Participation.objects.get_or_create(team=instance)[0]
|
||||||
participation.save()
|
participation.save()
|
||||||
if not created:
|
if not created:
|
||||||
participation.team.create_mailing_list()
|
participation.team.create_mailing_list()
|
||||||
|
|
||||||
|
|
||||||
def update_mailing_list(instance: Team, **_):
|
def update_mailing_list(instance: Team, raw, **_):
|
||||||
"""
|
"""
|
||||||
When a team name or trigram got updated, update mailing lists and Matrix rooms
|
When a team name or trigram got updated, update mailing lists and Matrix rooms
|
||||||
"""
|
"""
|
||||||
if instance.pk:
|
if instance.pk and not raw:
|
||||||
old_team = Team.objects.get(pk=instance.pk)
|
old_team = Team.objects.get(pk=instance.pk)
|
||||||
if old_team.trigram != instance.trigram:
|
if old_team.trigram != instance.trigram:
|
||||||
# TODO Rename Matrix room
|
# TODO Rename Matrix room
|
||||||
@ -36,10 +37,11 @@ def update_mailing_list(instance: Team, **_):
|
|||||||
f"{coach.user.first_name} {coach.user.last_name}")
|
f"{coach.user.first_name} {coach.user.last_name}")
|
||||||
|
|
||||||
|
|
||||||
def create_notes(instance: Union[Passage, Pool], **_):
|
def create_notes(instance: Union[Passage, Pool], raw, **_):
|
||||||
|
if not raw:
|
||||||
if isinstance(instance, Pool):
|
if isinstance(instance, Pool):
|
||||||
for passage in instance.passages.all():
|
for passage in instance.passages.all():
|
||||||
create_notes(passage)
|
create_notes(passage, raw)
|
||||||
return
|
return
|
||||||
|
|
||||||
for jury in instance.pool.juries.all():
|
for jury in instance.pool.juries.all():
|
BIN
participation/static/Fiche notations - 3 équipes.ods
Normal file
BIN
participation/static/Fiche notations - 3 équipes.ods
Normal file
Binary file not shown.
BIN
participation/static/Fiche notations - 4 équipes.ods
Normal file
BIN
participation/static/Fiche notations - 4 équipes.ods
Normal file
Binary file not shown.
BIN
participation/static/Fiche notations - 5 équipes.ods
Normal file
BIN
participation/static/Fiche notations - 5 équipes.ods
Normal file
Binary file not shown.
@ -76,13 +76,20 @@ class TournamentTable(tables.Table):
|
|||||||
|
|
||||||
|
|
||||||
class PoolTable(tables.Table):
|
class PoolTable(tables.Table):
|
||||||
teams = tables.LinkColumn(
|
letter = tables.LinkColumn(
|
||||||
'participation:pool_detail',
|
'participation:pool_detail',
|
||||||
args=[tables.A('id')],
|
args=[tables.A('id')],
|
||||||
|
verbose_name=_("pool").capitalize,
|
||||||
|
)
|
||||||
|
|
||||||
|
teams = tables.Column(
|
||||||
verbose_name=_("teams").capitalize,
|
verbose_name=_("teams").capitalize,
|
||||||
empty_values=(),
|
empty_values=(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def render_letter(self, record):
|
||||||
|
return format_lazy(_("Pool {letter}{round}"), letter=record.get_letter_display(), round=record.round)
|
||||||
|
|
||||||
def render_teams(self, record):
|
def render_teams(self, record):
|
||||||
return ", ".join(participation.team.trigram for participation in record.participations.all()) \
|
return ", ".join(participation.team.trigram for participation in record.participations.all()) \
|
||||||
or _("No defined team")
|
or _("No defined team")
|
||||||
@ -92,7 +99,7 @@ class PoolTable(tables.Table):
|
|||||||
'class': 'table table-condensed table-striped',
|
'class': 'table table-condensed table-striped',
|
||||||
}
|
}
|
||||||
model = Pool
|
model = Pool
|
||||||
fields = ('teams', 'round', 'tournament',)
|
fields = ('letter', 'teams', 'round', 'tournament',)
|
||||||
|
|
||||||
|
|
||||||
class PassageTable(tables.Table):
|
class PassageTable(tables.Table):
|
@ -124,7 +124,7 @@
|
|||||||
initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}")
|
initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}")
|
||||||
|
|
||||||
{% if my_note is not None %}
|
{% if my_note is not None %}
|
||||||
initModal("updateNotesModal", "{% url "participation:update_notes" pk=my_note.pk %}")
|
initModal("updateNotes", "{% url "participation:update_notes" pk=my_note.pk %}")
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif user.registration.participates %}
|
{% elif user.registration.participates %}
|
||||||
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
|
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
|
@ -15,6 +15,9 @@
|
|||||||
<dt class="col-sm-3">{% trans "Round:" %}</dt>
|
<dt class="col-sm-3">{% trans "Round:" %}</dt>
|
||||||
<dd class="col-sm-9">{{ pool.get_round_display }}</dd>
|
<dd class="col-sm-9">{{ pool.get_round_display }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">{% trans "Letter:" %}</dt>
|
||||||
|
<dd class="col-sm-9">{{ pool.get_letter_display }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">{% trans "Teams:" %}</dt>
|
<dt class="col-sm-3">{% trans "Teams:" %}</dt>
|
||||||
<dd class="col-sm-9">
|
<dd class="col-sm-9">
|
||||||
{% for participation in pool.participations.all %}
|
{% for participation in pool.participations.all %}
|
26
participation/templates/participation/upload_notes.html
Normal file
26
participation/templates/participation/upload_notes.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
||||||
|
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<div id="form-content">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% if object.participations.count == 3 %}
|
||||||
|
<a class="alert-link" href="{% static "Fiche notations - 3 équipes.ods" %}">
|
||||||
|
{% elif object.participations.count == 4 %}
|
||||||
|
<a class="alert-link" href="{% static "Fiche notations - 4 équipes.ods" %}">
|
||||||
|
{% elif object.participations.count == 5 %}
|
||||||
|
<a class="alert-link" href="{% static "Fiche notations - 5 équipes.ods" %}">
|
||||||
|
{% endif %}
|
||||||
|
{% trans "Download empty notation sheet" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
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