Initialize chat interface

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2024-04-27 08:57:01 +02:00
parent d9bb0a0860
commit ea8007aa07
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
13 changed files with 150 additions and 32 deletions

46
chat/consumers.py Normal file
View File

@ -0,0 +1,46 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from registration.models import Registration
class ChatConsumer(AsyncJsonWebsocketConsumer):
"""
This consumer manages the websocket of the chat interface.
"""
async def connect(self) -> None:
"""
This function is called when a new websocket is trying to connect to the server.
We accept only if this is a user of a team of the associated tournament, or a volunteer
of the tournament.
"""
# Fetch the registration of the current user
user = self.scope['user']
if user.is_anonymous:
# User is not authenticated
await self.close()
return
reg = await Registration.objects.aget(user_id=user.id)
self.registration = reg
# Accept the connection
await self.accept()
async def disconnect(self, close_code) -> None:
"""
Called when the websocket got disconnected, for any reason.
:param close_code: The error code.
"""
if self.scope['user'].is_anonymous:
# User is not authenticated
return
async def receive_json(self, content, **kwargs):
"""
Called when the client sends us some data, parsed as JSON.
:param content: The sent data, decoded from JSON text. Must content a `type` field.
"""
# TODO Process chat protocol

52
chat/static/chat.js Normal file
View File

@ -0,0 +1,52 @@
(async () => {
// check notification permission
// This is useful to alert people that they should do something
await Notification.requestPermission()
})()
/**
* Display a new notification with the given title and the given body.
* @param title The title of the notification
* @param body The body of the notification
* @param timeout The time (in milliseconds) after that the notification automatically closes. 0 to make indefinite. Default to 5000 ms.
* @return Notification
*/
function showNotification(title, body, timeout = 5000) {
let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
if (timeout)
setTimeout(() => notif.close(), timeout)
return notif
}
document.addEventListener('DOMContentLoaded', () => {
/**
* Process the received data from the server.
* @param data The received message
*/
function processMessage(data) {
// TODO Implement chat protocol
}
function setupSocket(nextDelay = 1000) {
// Open a global websocket
socket = new WebSocket(
(document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws/chat/'
)
// Listen on websockets and process messages from the server
socket.addEventListener('message', e => {
// Parse received data as JSON
const data = JSON.parse(e.data)
processMessage(data)
})
// Manage errors
socket.addEventListener('close', e => {
console.error('Chat socket closed unexpectedly, restarting…')
setTimeout(() => setupSocket(2 * nextDelay), nextDelay)
})
}
setupSocket()
})

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block content %}
{% endblock %}
{% block extrajavascript %}
{# This script contains all data for the chat management #}
<script src="{% static 'chat.js' %}"></script>
{% endblock %}

View File

@ -1,2 +1,13 @@
# Copyright (C) 2024 by Animath # Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from .views import ChatView
app_name = 'chat'
urlpatterns = [
path('', ChatView.as_view(), name='chat'),
]

View File

@ -1,2 +1,13 @@
# Copyright (C) 2024 by Animath # Copyright (C) 2024 by Animath
# 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.views.generic import TemplateView
class ChatView(LoginRequiredMixin, TemplateView):
"""
This view is the main interface of the chat system, which is working
with Javascript and websockets.
"""
template_name = "chat/chat.html"

View File

@ -1,10 +0,0 @@
# Copyright (C) 2023 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from . import consumers
websocket_urlpatterns = [
path("ws/draw/", consumers.DrawConsumer.as_asgi()),
]

View File

@ -14,8 +14,8 @@ from django.contrib.sites.models import Site
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from participation.models import Team, Tournament from participation.models import Team, Tournament
from tfjm import routing as websocket_routing
from . import routing
from .models import Draw, Pool, Round, TeamDraw from .models import Draw, Pool, Round, TeamDraw
@ -55,7 +55,7 @@ class TestDraw(TestCase):
# Connect to Websocket # Connect to Websocket
headers = [(b'cookie', self.async_client.cookies.output(header='', sep='; ').encode())] headers = [(b'cookie', self.async_client.cookies.output(header='', sep='; ').encode())]
communicator = WebsocketCommunicator(AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), communicator = WebsocketCommunicator(AuthMiddlewareStack(URLRouter(websocket_routing.websocket_urlpatterns)),
"/ws/draw/", headers) "/ws/draw/", headers)
connected, subprotocol = await communicator.connect() connected, subprotocol = await communicator.connect()
self.assertTrue(connected) self.assertTrue(connected)

View File

@ -1,13 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
The chat feature is now out of usage. If you feel that having a chat
feature between participants is important, for example to build a
team, please contact us.
{% endblocktrans %}
</div>
{% endblock %}

View File

@ -2,7 +2,6 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path
from django.views.generic import TemplateView
from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotificationsView, JoinTeamView, \ from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotificationsView, JoinTeamView, \
MyParticipationDetailView, MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \ MyParticipationDetailView, MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \
@ -74,5 +73,4 @@ urlpatterns = [
path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"), path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),
path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"), path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"),
path("pools/passages/notes/<int:pk>/", NoteUpdateView.as_view(), name="update_notes"), path("pools/passages/notes/<int:pk>/", NoteUpdateView.as_view(), name="update_notes"),
path("chat/", TemplateView.as_view(template_name="participation/chat.html"), name="chat")
] ]

View File

@ -22,13 +22,13 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfjm.settings')
django_asgi_app = get_asgi_application() django_asgi_app = get_asgi_application()
# useful since the import must be done after the application initialization # useful since the import must be done after the application initialization
import draw.routing # noqa: E402, I202 import tfjm.routing # noqa: E402, I202
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {
"http": django_asgi_app, "http": django_asgi_app,
"websocket": AllowedHostsOriginValidator( "websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(draw.routing.websocket_urlpatterns)) AuthMiddlewareStack(URLRouter(tfjm.routing.websocket_urlpatterns))
), ),
} }
) )

11
tfjm/routing.py Normal file
View File

@ -0,0 +1,11 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import chat.consumers
from django.urls import path
import draw.consumers
websocket_urlpatterns = [
path("ws/chat/", chat.consumers.ChatConsumer.as_asgi()),
path("ws/draw/", draw.consumers.DrawConsumer.as_asgi()),
]

View File

@ -62,8 +62,8 @@
</li> </li>
{% endif %} {% endif %}
{% endif %} {% endif %}
<li class="nav-item active d-none"> <li class="nav-item active">
<a class="nav-link" href="{% url "participation:chat" %}"> <a class="nav-link" href="{% url "chat:chat" %}">
<i class="fas fa-comments"></i> {% trans "Chat" %} <i class="fas fa-comments"></i> {% trans "Chat" %}
</a> </a>
</li> </li>

View File

@ -37,7 +37,7 @@ urlpatterns = [
path('search/', AdminSearchView.as_view(), name="haystack_search"), path('search/', AdminSearchView.as_view(), name="haystack_search"),
path('api/', include('api.urls')), path('api/', include('api.urls')),
# path('chat/', include('chat.urls')), path('chat/', include('chat.urls')),
path('draw/', include('draw.urls')), path('draw/', include('draw.urls')),
path('participation/', include('participation.urls')), path('participation/', include('participation.urls')),
path('registration/', include('registration.urls')), path('registration/', include('registration.urls')),