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:
parent
88823b5252
commit
bde3758c50
@ -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]})
|
||||||
|
@ -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)
|
||||||
],
|
],
|
||||||
|
@ -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 = '';
|
}))
|
||||||
};
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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()),
|
||||||
]
|
]
|
||||||
|
@ -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'
|
||||||
|
@ -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,))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user