First play with websockets

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2023-03-22 15:24:15 +01:00
parent 30efff0d9d
commit b9ce4c737c
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
10 changed files with 171 additions and 3 deletions

45
draw/consumers.py Normal file
View File

@ -0,0 +1,45 @@
import json
from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from participation.models import Tournament
class DrawConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
tournament_id = self.scope['url_route']['kwargs']['tournament_id']
self.tournament = await sync_to_async(Tournament.objects.get)(pk=tournament_id)
user = self.scope['user']
reg = user.registration
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
await self.close()
return
await self.accept()
await self.channel_layer.group_add(f"tournament-{self.tournament.id}", self.channel_name)
async def disconnect(self, close_code):
await self.channel_layer.group_discard(f"tournament-{self.tournament.id}", self.channel_name)
async def receive_json(self, content, **kwargs):
message = content["message"]
print(self.scope)
# 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,
}
)
async def draw_message(self, event):
print(event)
await self.send_json({"message": event['message']})

7
draw/routing.py Normal file
View 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()),
]

37
draw/static/draw.js Normal file
View File

@ -0,0 +1,37 @@
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
// 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')
}
socket.onclose = function(e) {
console.error('Chat socket closed unexpectedly')
}
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();
}
};
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 = '';
};
}

View File

@ -1,5 +1,34 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
Hello world!
<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.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">
{{ tournament.name }}
</button>
</li>
{% endfor %}
</ul>
<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">
{% include "draw/tournament_content.html" with tournament=tournament %}
</div>
{% endfor %}
</div>
{% endblock %}
{% block extrajavascript %}
{{ tournaments|json_script:'tournaments_list' }}
<script src="{% static 'draw.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,6 @@
{{ tournament.name }}
<!-- 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">

View File

@ -1,8 +1,27 @@
# Copyright (C) 2023 by Animath
# 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 participation.models import Tournament
class DisplayView(TemplateView):
class DisplayView(LoginRequiredMixin, TemplateView):
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:
tournaments = Tournament.objects.all()
elif reg.is_volunteer:
tournaments = reg.interesting_tournaments
else:
tournaments = [reg.team.participation.tournament]
context['tournaments'] = [{'id': t.id, 'name': t.name} for t in tournaments]
return context

View File

@ -1,3 +1,4 @@
channels[daphne]~=4.0.0
crispy-bootstrap5~=0.7
Django>=4.1,<5.0
django-cas-server~=2.0

View File

@ -12,8 +12,20 @@ https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
import draw.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfjm.settings')
application = get_asgi_application()
application = ProtocolTypeRouter(
{
"http": get_asgi_application(),
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(draw.routing.websocket_urlpatterns))
),
}
)

View File

@ -42,6 +42,8 @@ ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'daphne',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -51,6 +53,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.forms',
'channels',
'crispy_forms',
'crispy_bootstrap5',
'django_filters',
@ -111,6 +114,7 @@ TEMPLATES = [
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
ASGI_APPLICATION = 'tfjm.asgi.application'
WSGI_APPLICATION = 'tfjm.wsgi.application'
@ -249,6 +253,13 @@ FORBIDDEN_TRIGRAMS = [
"SEX",
]
# TODO: Use a redis server in production
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
if os.getenv("TFJM_STAGE", "dev") == "prod": # pragma: no cover
from .settings_prod import * # noqa: F401,F403
else:

View File

@ -11,6 +11,7 @@ skipsdist = True
sitepackages = False
deps =
coverage
channels[daphne]~=4.0.0
crispy-bootstrap5~=0.7
Django>=4.1,<5.0
django-crispy-forms~=1.14