1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-05-17 16:52:48 +00:00

Compare commits

..

No commits in common. "1abe463575d7d9bd6574855651e8a6ab9909e9f4" and "f8725cf8a9c7733dd63c91d6ff4771586c0471e5" have entirely different histories.

15 changed files with 38 additions and 266 deletions

View File

@ -1,75 +0,0 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.contrib.auth.models import User
from registration.models import Registration
from .models import Channel
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.
"""
if '_fake_user_id' in self.scope['session']:
self.scope['user'] = await User.objects.aget(pk=self.scope['session']['_fake_user_id'])
# 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.
"""
match content['type']:
case 'fetch_channels':
await self.fetch_channels()
case unknown:
print("Unknown message type:", unknown)
async def fetch_channels(self) -> None:
user = self.scope['user']
read_channels = await Channel.get_accessible_channels(user, 'read')
write_channels = await Channel.get_accessible_channels(user, 'write')
print([channel async for channel in write_channels.all()])
message = {
'type': 'fetch_channels',
'channels': [
{
'id': channel.id,
'name': channel.name,
'read_access': True,
'write_access': await write_channels.acontains(channel),
}
async for channel in read_channels.all()
]
}
await self.send_json(message)

View File

@ -1,4 +1,4 @@
# Generated by Django 5.0.3 on 2024-04-27 07:00 # Generated by Django 5.0.3 on 2024-04-27 06:48
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@ -30,7 +30,7 @@ class Migration(migrations.Migration):
("name", models.CharField(max_length=255, verbose_name="name")), ("name", models.CharField(max_length=255, verbose_name="name")),
( (
"read_access", "read_access",
models.CharField( models.PositiveSmallIntegerField(
choices=[ choices=[
("anonymous", "Everyone, including anonymous users"), ("anonymous", "Everyone, including anonymous users"),
("authenticated", "Authenticated users"), ("authenticated", "Authenticated users"),
@ -53,13 +53,12 @@ class Migration(migrations.Migration):
), ),
("admin", "Admin users"), ("admin", "Admin users"),
], ],
max_length=16,
verbose_name="read permission", verbose_name="read permission",
), ),
), ),
( (
"write_access", "write_access",
models.CharField( models.PositiveSmallIntegerField(
choices=[ choices=[
("anonymous", "Everyone, including anonymous users"), ("anonymous", "Everyone, including anonymous users"),
("authenticated", "Authenticated users"), ("authenticated", "Authenticated users"),
@ -82,7 +81,6 @@ class Migration(migrations.Migration):
), ),
("admin", "Admin users"), ("admin", "Admin users"),
], ],
max_length=16,
verbose_name="write permission", verbose_name="write permission",
), ),
), ),

View File

@ -1,13 +1,9 @@
# 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.models import User
from django.db import models from django.db import models
from django.db.models import Q, QuerySet
from django.utils.text import format_lazy from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from participation.models import Tournament
from registration.models import ParticipantRegistration, Registration, VolunteerRegistration
from tfjm.permissions import PermissionType from tfjm.permissions import PermissionType
@ -17,14 +13,12 @@ class Channel(models.Model):
verbose_name=_("name"), verbose_name=_("name"),
) )
read_access = models.CharField( read_access = models.PositiveSmallIntegerField(
max_length=16,
verbose_name=_("read permission"), verbose_name=_("read permission"),
choices=PermissionType, choices=PermissionType,
) )
write_access = models.CharField( write_access = models.PositiveSmallIntegerField(
max_length=16,
verbose_name=_("write permission"), verbose_name=_("write permission"),
choices=PermissionType, choices=PermissionType,
) )
@ -78,73 +72,7 @@ class Channel(models.Model):
) )
def __str__(self): def __str__(self):
return str(format_lazy(_("Channel {name}"), name=self.name)) return format_lazy(_("Channel {name}"), name=self.name)
@staticmethod
async def get_accessible_channels(user: User, permission_type: str = 'read') -> QuerySet["Channel"]:
permission_type = 'write_access' if 'write' in permission_type.lower() else 'read_access'
qs = Channel.objects.none()
if user.is_anonymous:
return Channel.objects.filter(**{permission_type: PermissionType.ANONYMOUS})
qs |= Channel.objects.filter(**{permission_type: PermissionType.AUTHENTICATED})
registration = await Registration.objects.aget(user_id=user.id)
if registration.is_admin:
return Channel.objects.all()
if registration.is_volunteer:
registration = await VolunteerRegistration.objects \
.prefetch_related('jury_in__tournament', 'organized_tournaments').aget(user_id=user.id)
qs |= Channel.objects.filter(**{permission_type: PermissionType.VOLUNTEER})
qs |= Channel.objects.filter(Q(tournament__in=registration.interesting_tournaments),
**{permission_type: PermissionType.TOURNAMENT_MEMBER})
qs |= Channel.objects.filter(Q(tournament__in=registration.organized_tournaments.all()),
**{permission_type: PermissionType.TOURNAMENT_ORGANIZER})
qs |= Channel.objects.filter(Q(tournament__pools__in=registration.pools_presided.all())
| Q(tournament__in=registration.organized_tournaments.all()),
**{permission_type: PermissionType.TOURNAMENT_JURY_PRESIDENT})
qs |= Channel.objects.filter(Q(pool__in=registration.jury_in.all())
| Q(pool__tournament__in=registration.organized_tournaments.all())
| Q(pool__tournament__pools__in=registration.pools_presided.all()),
**{permission_type: PermissionType.JURY_MEMBER})
qs |= Channel.objects.filter(Q(pool__in=registration.jury_in.all())
| Q(pool__tournament__in=registration.organized_tournaments.all())
| Q(pool__tournament__pools__in=registration.pools_presided.all()),
**{permission_type: PermissionType.POOL_MEMBER})
else:
registration = await ParticipantRegistration.objects \
.prefetch_related('team__participation__pools', 'team__participation__tournament').aget(user_id=user.id)
team = registration.team
tournaments = []
if team.participation.valid:
tournaments.append(team.participation.tournament)
if team.participation.final:
tournaments.append(await Tournament.objects.aget(final=True))
qs |= Channel.objects.filter(Q(tournament__in=tournaments),
**{permission_type: PermissionType.TOURNAMENT_MEMBER})
qs |= Channel.objects.filter(Q(pool__in=team.participation.pools.all()),
**{permission_type: PermissionType.POOL_MEMBER})
qs |= Channel.objects.filter(Q(team=team),
**{permission_type: PermissionType.TEAM_MEMBER})
qs |= Channel.objects.filter(invited=user)
print(user)
print(qs.query)
return qs
class Meta: class Meta:
verbose_name = _("channel") verbose_name = _("channel")

View File

@ -1,59 +0,0 @@
(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
console.log(data)
}
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)
})
socket.addEventListener('open', e => {
socket.send(JSON.stringify({
'type': 'fetch_channels',
}))
})
}
setupSocket()
})

View File

@ -1,12 +0,0 @@
{% 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,13 +1,2 @@
# 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,13 +1,2 @@
# 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"

10
draw/routing.py Normal file
View File

@ -0,0 +1,10 @@
# 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(websocket_routing.websocket_urlpatterns)), communicator = WebsocketCommunicator(AuthMiddlewareStack(URLRouter(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

@ -0,0 +1,13 @@
{% 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,6 +2,7 @@
# 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, \
@ -73,4 +74,5 @@ 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 tfjm.routing # noqa: E402, I202 import draw.routing # noqa: E402, I202
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {
"http": django_asgi_app, "http": django_asgi_app,
"websocket": AllowedHostsOriginValidator( "websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(tfjm.routing.websocket_urlpatterns)) AuthMiddlewareStack(URLRouter(draw.routing.websocket_urlpatterns))
), ),
} }
) )

View File

@ -1,11 +0,0 @@
# 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"> <li class="nav-item active d-none">
<a class="nav-link" href="{% url "chat:chat" %}"> <a class="nav-link" href="{% url "participation: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')),