1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2024-12-25 06:22:22 +00:00

First interface to start draws

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2023-03-22 18:44:49 +01:00
parent 88823b5252
commit bde3758c50
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
8 changed files with 182 additions and 71 deletions

View File

@ -2,8 +2,23 @@ import json
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.utils.translation import gettext_lazy as _
from participation.models import Tournament from draw.models import Draw, Round, Pool, TeamDraw
from participation.models import Tournament, Participation
from registration.models import Registration
def ensure_orga(f):
async def func(self, *args, **kwargs):
reg = self.registration
if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
or not reg.is_volunteer:
return await self.alert(_("You are not an organizer."), 'danger')
return await f(self, *args, **kwargs)
return func
class DrawConsumer(AsyncJsonWebsocketConsumer): class DrawConsumer(AsyncJsonWebsocketConsumer):
@ -11,8 +26,13 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
tournament_id = self.scope['url_route']['kwargs']['tournament_id'] tournament_id = self.scope['url_route']['kwargs']['tournament_id']
self.tournament = await sync_to_async(Tournament.objects.get)(pk=tournament_id) self.tournament = await sync_to_async(Tournament.objects.get)(pk=tournament_id)
self.participations = await sync_to_async(lambda: list(Participation.objects\
.filter(tournament=self.tournament, valid=True)\
.prefetch_related('team').all()))()
user = self.scope['user'] user = self.scope['user']
reg = user.registration reg = await sync_to_async(Registration.objects.get)(user=user)
self.registration = reg
if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \ if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
or not reg.is_volunteer and reg.team.participation.tournament != self.tournament: or not reg.is_volunteer and reg.team.participation.tournament != self.tournament:
# This user may not have access to the drawing session # This user may not have access to the drawing session
@ -25,21 +45,45 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
async def disconnect(self, close_code): async def disconnect(self, close_code):
await self.channel_layer.group_discard(f"tournament-{self.tournament.id}", self.channel_name) await self.channel_layer.group_discard(f"tournament-{self.tournament.id}", self.channel_name)
async def alert(self, message: str, alert_type: str = 'info'):
return await self.send_json({'type': 'alert', 'alert_type': alert_type, 'message': str(message)})
async def receive_json(self, content, **kwargs): async def receive_json(self, content, **kwargs):
message = content["message"] print(content)
print(self.scope) match content['type']:
case 'start_draw':
await self.start_draw(**content)
# TODO: Implement drawing system, instead of making a simple chatbot @ensure_orga
await self.channel_layer.group_send( async def start_draw(self, fmt, **kwargs):
f"tournament-{self.tournament.id}", print(fmt, kwargs)
{ try:
"type": "draw.message", fmt = list(map(int, fmt.split('+')))
"username": self.scope["user"].username, except ValueError as e:
"message": message, return await self.alert(_("Invalid format"), 'danger')
}
)
async def draw_message(self, event): print(fmt, sum(fmt), len(self.participations))
print(event)
await self.send_json({"message": event['message']}) if sum(fmt) != len(self.participations):
return await self.alert(
_("The sum must be equal to the number of teams: expected {len}, got {sum}")\
.format(len=len(self.participations), sum=sum(fmt)), 'danger')
draw = await sync_to_async(Draw.objects.create)(tournament=self.tournament)
r = await sync_to_async(Round.objects.create)(draw=draw, number=1)
for i, f in enumerate(fmt):
sync_to_async(Pool.objects.create)(round=r, letter=i + 1, size=f)
for participation in self.participations:
sync_to_async(TeamDraw.objects.create)(participation=participation)
await self.alert(_("Draw started!"), 'success')
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.start', 'fmt': fmt, 'draw': draw, 'round': r})
async def draw_start(self, content):
await self.alert(_("The draw for the tournament {tournament} will start.")\
.format(tournament=self.tournament.name), 'warning')
await self.send_json({'type': 'draw_start', 'fmt': content['fmt'],
'trigrams': [p.team.trigram for p in self.participations]})

View File

@ -36,7 +36,7 @@ class Round(models.Model):
verbose_name=_('draw'), verbose_name=_('draw'),
) )
number = models.IntegerField( number = models.PositiveSmallIntegerField(
choices=[ choices=[
(1, _('Round 1')), (1, _('Round 1')),
(2, _('Round 2')), (2, _('Round 2')),
@ -67,16 +67,19 @@ class Pool(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
letter = models.CharField( letter = models.PositiveSmallIntegerField(
max_length=1,
choices=[ choices=[
('A', 'A'), (1, 'A'),
('B', 'B'), (2, 'B'),
('C', 'C'), (3, 'C'),
], ],
verbose_name=_('letter'), verbose_name=_('letter'),
) )
size = models.PositiveSmallIntegerField(
verbose_name=_('size'),
)
current_team = models.ForeignKey( current_team = models.ForeignKey(
'TeamDraw', 'TeamDraw',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -104,15 +107,19 @@ class TeamDraw(models.Model):
pool = models.ForeignKey( pool = models.ForeignKey(
Pool, Pool,
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=True,
default=None,
verbose_name=_('pool'), verbose_name=_('pool'),
) )
index = models.PositiveSmallIntegerField( index = models.PositiveSmallIntegerField(
choices=zip(range(1, 6), range(1, 6)), choices=zip(range(1, 6), range(1, 6)),
null=True,
default=None,
verbose_name=_('index'), verbose_name=_('index'),
) )
accepted = models.IntegerField( accepted = models.PositiveSmallIntegerField(
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, settings.PROBLEM_COUNT + 1)
], ],
@ -121,12 +128,14 @@ class TeamDraw(models.Model):
verbose_name=_("accepted problem"), verbose_name=_("accepted problem"),
) )
last_dice = models.IntegerField( last_dice = models.PositiveSmallIntegerField(
choices=zip(range(1, 101), range(1, 101)), choices=zip(range(1, 101), range(1, 101)),
null=True,
default=None,
verbose_name=_("last dice"), verbose_name=_("last dice"),
) )
purposed = models.IntegerField( purposed = models.PositiveSmallIntegerField(
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, settings.PROBLEM_COUNT + 1)
], ],

View File

@ -1,37 +1,61 @@
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent) const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
const sockets = {} const sockets = {}
const messages = document.getElementById('messages')
document.addEventListener('DOMContentLoaded', () => {
for (let tournament of tournaments) { for (let tournament of tournaments) {
let socket = new WebSocket( let socket = new WebSocket(
'ws://' + window.location.host + '/ws/draw/' + tournament.id + '/' 'ws://' + window.location.host + '/ws/draw/' + tournament.id + '/'
) )
sockets[tournament.id] = socket sockets[tournament.id] = socket
// TODO: For now, we only have a chatbot. Need to implementthe drawing interface function addMessage(message, type, timeout = 0) {
socket.onmessage = function(e) { const wrapper = document.createElement('div')
console.log(e.data) 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)
}
function draw_start(data) {
fetch(`/draw/content/${tournament.id}/`).then(resp => resp.text()).then(text => {
document.getElementById(`tab-${tournament.id}-pane`).innerHTML = text
})
}
socket.addEventListener('message', e => {
const data = JSON.parse(e.data) const data = JSON.parse(e.data)
console.log(data) console.log(data)
document.querySelector('#chat-log-' + tournament.id).value += (data.message + '\n')
}
socket.onclose = function(e) { switch (data.type) {
case 'alert':
addMessage(data.message, data.alert_type)
break
case 'draw_start':
draw_start(data)
}
})
socket.addEventListener('close', e => {
console.error('Chat socket closed unexpectedly') console.error('Chat socket closed unexpectedly')
} })
document.querySelector('#chat-message-' + tournament.id + '-input').focus(); socket.addEventListener('open', e => {})
document.querySelector('#chat-message-' + tournament.id + '-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return document.getElementById('format-form-' + tournament.id)
document.querySelector('#chat-message-' + tournament.id + '-submit').click(); .addEventListener('submit', function (e) {
} e.preventDefault()
};
document.querySelector('#chat-message-' + tournament.id + '-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-' + tournament.id + '-input');
const message = messageInputDom.value;
socket.send(JSON.stringify({ socket.send(JSON.stringify({
'message': message 'type': 'start_draw',
})); 'fmt': document.getElementById('format-' + tournament.id).value
messageInputDom.value = ''; }))
}; })
} }
})

View File

@ -7,9 +7,9 @@
{% for tournament in tournaments %} {% for tournament in tournaments %}
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link{% if forloop.first %} active{% endif %}" <button class="nav-link{% if forloop.first %} active{% endif %}"
id="tab-{{ tournament.name|slugify }}" data-bs-toggle="tab" id="tab-{{ tournament.id }}" data-bs-toggle="tab"
data-bs-target="#tab-{{ tournament.name|slugify }}-pane" type="button" role="tab" data-bs-target="#tab-{{ tournament.id }}-pane" type="button" role="tab"
aria-controls="tab-{{ tournament.name|slugify }}-pane" aria-selected="true"> aria-controls="tab-{{ tournament.id }}-pane" aria-selected="true">
{{ tournament.name }} {{ tournament.name }}
</button> </button>
</li> </li>
@ -19,8 +19,8 @@
<div class="tab-content" id="tab-content"> <div class="tab-content" id="tab-content">
{% for tournament in tournaments %} {% for tournament in tournaments %}
<div class="tab-pane fade{% if forloop.first %} show active{% endif %}" <div class="tab-pane fade{% if forloop.first %} show active{% endif %}"
id="tab-{{ tournament.name|slugify }}-pane" role="tabpanel" id="tab-{{ tournament.id }}-pane" role="tabpanel"
aria-labelledby="tab-{{ tournament.name|slugify }}" tabindex="0"> aria-labelledby="tab-{{ tournament.id }}" tabindex="0">
{% include "draw/tournament_content.html" with tournament=tournament %} {% include "draw/tournament_content.html" with tournament=tournament %}
</div> </div>
{% endfor %} {% endfor %}
@ -28,7 +28,7 @@
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
{{ tournaments|json_script:'tournaments_list' }} {{ tournaments_simplified|json_script:'tournaments_list' }}
<script src="{% static 'draw.js' %}"></script> <script src="{% static 'draw.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,27 @@
{{ tournament.name }} {% load i18n %}
<!-- This is only a test -->
<textarea id="chat-log-{{ tournament.id }}"></textarea> {% if tournament.draw %}
<input id="chat-message-{{ tournament.id }}-input"> Tirage lancé !
<input id="chat-message-{{ tournament.id }}-submit" type="submit"> {% else %}
<div class="alert alert-warning">
{% trans "The draw has not started yet." %}
{% if user.registration.is_volunteer %}
<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>
<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>
{% endif %}

View File

@ -3,11 +3,12 @@
from django.urls import path from django.urls import path
from .views import DisplayView from .views import DisplayContentView, DisplayView
app_name = "draw" app_name = "draw"
urlpatterns = [ urlpatterns = [
path('', DisplayView.as_view()), path('', DisplayView.as_view()),
path('content/<int:pk>/', DisplayContentView.as_view()),
] ]

View File

@ -2,8 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.views.generic import TemplateView, DetailView
from django.views.generic import TemplateView
from participation.models import Tournament from participation.models import Tournament
@ -16,12 +15,18 @@ class DisplayView(LoginRequiredMixin, TemplateView):
reg = self.request.user.registration reg = self.request.user.registration
if reg.is_admin: if reg.is_admin:
tournaments = Tournament.objects.all() tournaments = Tournament.objects.order_by('id').all()
elif reg.is_volunteer: elif reg.is_volunteer:
tournaments = reg.interesting_tournaments tournaments = reg.interesting_tournaments.order_by('id').all()
else: else:
tournaments = [reg.team.participation.tournament] tournaments = [reg.team.participation.tournament]
context['tournaments'] = [{'id': t.id, 'name': t.name} for t in tournaments] context['tournaments'] = tournaments
context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments]
return context return context
class DisplayContentView(LoginRequiredMixin, DetailView):
model = Tournament
template_name = 'draw/tournament_content.html'

View File

@ -278,6 +278,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, fmt))
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,))