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 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):
@ -11,8 +26,13 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
tournament_id = self.scope['url_route']['kwargs']['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']
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 \
or not reg.is_volunteer and reg.team.participation.tournament != self.tournament:
# This user may not have access to the drawing session
@ -25,21 +45,45 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
async def disconnect(self, close_code):
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):
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
await self.channel_layer.group_send(
f"tournament-{self.tournament.id}",
{
"type": "draw.message",
"username": self.scope["user"].username,
"message": message,
}
)
@ensure_orga
async def start_draw(self, fmt, **kwargs):
print(fmt, kwargs)
try:
fmt = list(map(int, fmt.split('+')))
except ValueError as e:
return await self.alert(_("Invalid format"), 'danger')
async def draw_message(self, event):
print(event)
await self.send_json({"message": event['message']})
print(fmt, sum(fmt), len(self.participations))
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'),
)
number = models.IntegerField(
number = models.PositiveSmallIntegerField(
choices=[
(1, _('Round 1')),
(2, _('Round 2')),
@ -67,16 +67,19 @@ class Pool(models.Model):
on_delete=models.CASCADE,
)
letter = models.CharField(
max_length=1,
letter = models.PositiveSmallIntegerField(
choices=[
('A', 'A'),
('B', 'B'),
('C', 'C'),
(1, 'A'),
(2, 'B'),
(3, 'C'),
],
verbose_name=_('letter'),
)
size = models.PositiveSmallIntegerField(
verbose_name=_('size'),
)
current_team = models.ForeignKey(
'TeamDraw',
on_delete=models.CASCADE,
@ -104,15 +107,19 @@ class TeamDraw(models.Model):
pool = models.ForeignKey(
Pool,
on_delete=models.CASCADE,
null=True,
default=None,
verbose_name=_('pool'),
)
index = models.PositiveSmallIntegerField(
choices=zip(range(1, 6), range(1, 6)),
null=True,
default=None,
verbose_name=_('index'),
)
accepted = models.IntegerField(
accepted = models.PositiveSmallIntegerField(
choices=[
(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"),
)
last_dice = models.IntegerField(
last_dice = models.PositiveSmallIntegerField(
choices=zip(range(1, 101), range(1, 101)),
null=True,
default=None,
verbose_name=_("last dice"),
)
purposed = models.IntegerField(
purposed = models.PositiveSmallIntegerField(
choices=[
(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 sockets = {}
for (let tournament of tournaments) {
let socket = new WebSocket(
'ws://' + window.location.host + '/ws/draw/' + tournament.id + '/'
)
sockets[tournament.id] = socket
const messages = document.getElementById('messages')
// TODO: For now, we only have a chatbot. Need to implementthe drawing interface
socket.onmessage = function(e) {
console.log(e.data)
const data = JSON.parse(e.data)
console.log(data)
document.querySelector('#chat-log-' + tournament.id).value += (data.message + '\n')
}
document.addEventListener('DOMContentLoaded', () => {
for (let tournament of tournaments) {
let socket = new WebSocket(
'ws://' + window.location.host + '/ws/draw/' + tournament.id + '/'
)
sockets[tournament.id] = socket
socket.onclose = function(e) {
console.error('Chat socket closed unexpectedly')
}
function addMessage(message, type, timeout = 0) {
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)
document.querySelector('#chat-message-' + tournament.id + '-input').focus();
document.querySelector('#chat-message-' + tournament.id + '-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-' + tournament.id + '-submit').click();
if (timeout)
setTimeout(() => wrapper.remove(), timeout)
}
};
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({
'message': message
}));
messageInputDom.value = '';
};
}
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)
console.log(data)
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')
})
socket.addEventListener('open', e => {})
document.getElementById('format-form-' + tournament.id)
.addEventListener('submit', function (e) {
e.preventDefault()
socket.send(JSON.stringify({
'type': 'start_draw',
'fmt': document.getElementById('format-' + tournament.id).value
}))
})
}
})

View File

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

View File

@ -1,6 +1,27 @@
{{ tournament.name }}
{% load i18n %}
<!-- This is only a test -->
<textarea id="chat-log-{{ tournament.id }}"></textarea>
<input id="chat-message-{{ tournament.id }}-input">
<input id="chat-message-{{ tournament.id }}-submit" type="submit">
{% if tournament.draw %}
Tirage lancé !
{% 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 .views import DisplayView
from .views import DisplayContentView, DisplayView
app_name = "draw"
urlpatterns = [
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
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.views.generic import TemplateView
from django.views.generic import TemplateView, DetailView
from participation.models import Tournament
@ -16,12 +15,18 @@ class DisplayView(LoginRequiredMixin, TemplateView):
reg = self.request.user.registration
if reg.is_admin:
tournaments = Tournament.objects.all()
tournaments = Tournament.objects.order_by('id').all()
elif reg.is_volunteer:
tournaments = reg.interesting_tournaments
tournaments = reg.interesting_tournaments.order_by('id').all()
else:
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
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(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):
return reverse_lazy("participation:tournament_detail", args=(self.pk,))