mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-11-04 01:32:05 +01:00 
			
		
		
		
	Initialize chat interface
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
		
							
								
								
									
										46
									
								
								chat/consumers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								chat/consumers.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										52
									
								
								chat/static/chat.js
									
									
									
									
									
										Normal 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()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										12
									
								
								chat/templates/chat/chat.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								chat/templates/chat/chat.html
									
									
									
									
									
										Normal 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 %}
 | 
				
			||||||
							
								
								
									
										11
									
								
								chat/urls.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								chat/urls.py
									
									
									
									
									
								
							@@ -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'),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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()),
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 %}
 | 
					 | 
				
			||||||
@@ -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")
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
									
								
							
							
						
						
									
										11
									
								
								tfjm/routing.py
									
									
									
									
									
										Normal 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()),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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')),
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user