First interface to start draws
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
parent
88823b5252
commit
bde3758c50
|
@ -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]})
|
||||
|
|
|
@ -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)
|
||||
],
|
||||
|
|
|
@ -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
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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()),
|
||||
]
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,))
|
||||
|
||||
|
|
Loading…
Reference in New Issue