diff --git a/gestioasso/asgi.py b/gestioasso/asgi.py index 773acaa0..728a3433 100644 --- a/gestioasso/asgi.py +++ b/gestioasso/asgi.py @@ -1,8 +1,15 @@ +""" +ASGI entrypoint. Configures Django and then runs the application +defined in the ASGI_APPLICATION setting. +""" + import os -from channels.asgi import get_channel_layer +import django +from channels.routing import get_default_application if "DJANGO_SETTINGS_MODULE" not in os.environ: - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.local") -channel_layer = get_channel_layer() +django.setup() +application = get_default_application() diff --git a/gestioasso/routing.py b/gestioasso/routing.py index 3c2e5718..c293a9cb 100644 --- a/gestioasso/routing.py +++ b/gestioasso/routing.py @@ -1,3 +1,18 @@ -from channels.routing import include +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from django.urls import path -routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")] +from kfet.routing import KFRouter + +application = ProtocolTypeRouter( + { + # WebSocket chat handler + "websocket": AuthMiddlewareStack( + URLRouter( + [ + path("ws/k-fet", KFRouter), + ] + ) + ), + } +) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index 9e3f9f70..1044133d 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -109,6 +109,8 @@ MEDIA_URL = "/gestion/media/" CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr") +ASGI_APPLICATION = "gestioasso.routing.application" + # --- # Auth-related stuff # --- @@ -147,7 +149,7 @@ CACHES = { CHANNEL_LAYERS = { "default": { - "BACKEND": "asgi_redis.RedisChannelLayer", + "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [ ( @@ -160,11 +162,9 @@ CHANNEL_LAYERS = { ) ] }, - "ROUTING": "gestioasso.routing.routing", } } - # --- # reCAPTCHA settings # https://github.com/praekelt/django-recaptcha diff --git a/gestioasso/settings/local.py b/gestioasso/settings/local.py index 5c8c2734..2cba6a10 100644 --- a/gestioasso/settings/local.py +++ b/gestioasso/settings/local.py @@ -47,8 +47,7 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache" # Use the default in memory asgi backend for local development CHANNEL_LAYERS = { "default": { - "BACKEND": "asgiref.inmemory.ChannelLayer", - "ROUTING": "gestioasso.routing.routing", + "BACKEND": "channels.layers.InMemoryChannelLayer", } } diff --git a/kfet/consumers.py b/kfet/consumers.py index 2655c86b..e8334421 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -4,3 +4,6 @@ from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer): groups = ["kfet.kpsul"] perms_connect = ["kfet.is_team"] + + async def kpsul(self, event): + await self.send_json(event) diff --git a/kfet/open/consumers.py b/kfet/open/consumers.py index 8b800c76..1b190212 100644 --- a/kfet/open/consumers.py +++ b/kfet/open/consumers.py @@ -12,13 +12,15 @@ class OpenKfetConsumer(PermConsumerMixin, DjangoJsonWebsocketConsumer): """ - def connection_groups(self, user, **kwargs): - """Select which group the user should be connected.""" - if kfet_is_team(user): - return ["kfet.open.team"] - return ["kfet.open.base"] + async def open_status(self, event): + await self.send_json(event) - def connect(self, message, *args, **kwargs): + async def connect(self): """Send current status on connect.""" - super().connect(message, *args, **kwargs) - self.send(kfet_open.export(message.user)) + await super().connect() + + group = "team" if kfet_is_team(self.user) else "base" + + await self.channel_layer.group_add(f"kfet.open.{group}", self.channel_name) + + await self.send_json(kfet_open.export(self.user)) diff --git a/kfet/open/open.py b/kfet/open/open.py index d0e0c901..ec391de6 100644 --- a/kfet/open/open.py +++ b/kfet/open/open.py @@ -1,5 +1,6 @@ from datetime import timedelta +from channels.layers import get_channel_layer from django.utils import timezone from ..decorators import kfet_is_team @@ -77,7 +78,7 @@ class OpenKfet(CachedMixin, object): """ status = self.status() - base = {"status": status} + base = {"status": status, "type": "open.status"} restrict = { "admin_status": self.admin_status(status), "force_close": self.force_close, @@ -95,13 +96,14 @@ class OpenKfet(CachedMixin, object): base, team = self._export() return team if kfet_is_team(user) else base - def send_ws(self): + async def send_ws(self): """Send internal state to websocket channels.""" - from .consumers import OpenKfetConsumer - base, team = self._export() - OpenKfetConsumer.group_send("kfet.open.base", base) - OpenKfetConsumer.group_send("kfet.open.team", team) + + channel_layer = get_channel_layer() + + await channel_layer.group_send("kfet.open.base", base) + await channel_layer.group_send("kfet.open.team", team) kfet_open = OpenKfet() diff --git a/kfet/open/routing.py b/kfet/open/routing.py index 811ae56e..6b10b4ec 100644 --- a/kfet/open/routing.py +++ b/kfet/open/routing.py @@ -1,5 +1,10 @@ -from channels.routing import route_class +from channels.routing import URLRouter +from django.urls import path -from . import consumers +from .consumers import OpenKfetConsumer -routing = [route_class(consumers.OpenKfetConsumer)] +OpenRouter = URLRouter( + [ + path(r"", OpenKfetConsumer), + ] +) diff --git a/kfet/open/views.py b/kfet/open/views.py index 49b91f4a..e5b09fb9 100644 --- a/kfet/open/views.py +++ b/kfet/open/views.py @@ -1,3 +1,4 @@ +from asgiref.sync import async_to_sync from django.conf import settings from django.contrib.auth.decorators import permission_required from django.core.exceptions import PermissionDenied @@ -18,7 +19,7 @@ def raw_open(request): raise PermissionDenied raw_open = request.POST.get("raw_open") in TRUE_STR kfet_open.raw_open = raw_open - kfet_open.send_ws() + async_to_sync(kfet_open.send_ws)() return HttpResponse() @@ -27,5 +28,5 @@ def raw_open(request): def force_close(request): force_close = request.POST.get("force_close") in TRUE_STR kfet_open.force_close = force_close - kfet_open.send_ws() + async_to_sync(kfet_open.send_ws)() return HttpResponse() diff --git a/kfet/routing.py b/kfet/routing.py index ceafca06..492f6e4f 100644 --- a/kfet/routing.py +++ b/kfet/routing.py @@ -1,8 +1,13 @@ -from channels.routing import include, route_class +from channels.routing import URLRouter +from django.urls import path -from . import consumers +from kfet.open.routing import OpenRouter -routing = [ - route_class(consumers.KPsul, path=r"^/k-psul/$"), - include("kfet.open.routing.routing", path=r"^/open"), -] +from .consumers import KPsul + +KFRouter = URLRouter( + [ + path("k-psul/", KPsul), + path("open", OpenRouter), + ] +) diff --git a/kfet/utils.py b/kfet/utils.py index 0c4f170a..b1facf1e 100644 --- a/kfet/utils.py +++ b/kfet/utils.py @@ -1,8 +1,7 @@ import json import math -from channels.channel import Group -from channels.generic.websockets import JsonWebsocketConsumer +from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.core.cache import cache from django.core.serializers.json import DjangoJSONEncoder @@ -63,7 +62,7 @@ class CachedMixin: # Consumers -class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): +class DjangoJsonWebsocketConsumer(AsyncJsonWebsocketConsumer): """Custom Json Websocket Consumer. Encode to JSON with DjangoJSONEncoder. @@ -71,7 +70,7 @@ class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): """ @classmethod - def encode_json(cls, content): + async def encode_json(cls, content): return json.dumps(content, cls=DjangoJSONEncoder) @@ -89,31 +88,35 @@ class PermConsumerMixin: http_user = True # Enable message.user perms_connect = [] - def connect(self, message, **kwargs): + async def connect(self): """Check permissions on connection.""" - if message.user.has_perms(self.perms_connect): - super().connect(message, **kwargs) + self.user = self.scope["user"] + + if self.user.has_perms(self.perms_connect): + await super().connect() else: - self.close() + await self.close() - def raw_connect(self, message, **kwargs): - # Same as original raw_connect method of JsonWebsocketConsumer - # We add user to connection_groups call. - groups = self.connection_groups(user=message.user, **kwargs) - for group in groups: - Group(group, channel_layer=message.channel_layer).add(message.reply_channel) - self.connect(message, **kwargs) - - def raw_disconnect(self, message, **kwargs): - # Same as original raw_connect method of JsonWebsocketConsumer - # We add user to connection_groups call. - groups = self.connection_groups(user=message.user, **kwargs) - for group in groups: - Group(group, channel_layer=message.channel_layer).discard( - message.reply_channel - ) - self.disconnect(message, **kwargs) - - def connection_groups(self, user, **kwargs): - """`message.user` is available as `user` arg. Original behavior.""" - return super().connection_groups(user=user, **kwargs) + # async def raw_connect(self, message, **kwargs): + # # Same as original raw_connect method of JsonWebsocketConsumer + # # We add user to connection_groups call. + # groups = self.connection_groups(user=message.user, **kwargs) + # for group in groups: + # await self.channel_layer.group_add(group, message.reply_channel) + # # Group(group, channel_layer=message.channel_layer).add(message.reply_channel) + # self.connect(message, **kwargs) + # + # async def raw_disconnect(self, message, **kwargs): + # # Same as original raw_connect method of JsonWebsocketConsumer + # # We add user to connection_groups call. + # groups = self.connection_groups(user=message.user, **kwargs) + # for group in groups: + # await self.channel_layer.group_discard(group, message.reply_channel) + # # Group(group, channel_layer=message.channel_layer).discard( + # # message.reply_channel + # # ) + # self.disconnect(message, **kwargs) + # + # def connection_groups(self, user, **kwargs): + # """`message.user` is available as `user` arg. Original behavior.""" + # return super().connection_groups(user=user, **kwargs) diff --git a/kfet/views.py b/kfet/views.py index 154be949..d60a3a07 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -6,6 +6,8 @@ from decimal import Decimal from typing import List, Tuple from urllib.parse import urlencode +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required @@ -44,7 +46,7 @@ from django.views.generic.detail import BaseDetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView from gestioncof.models import CofProfile -from kfet import KFET_DELETED_TRIGRAMME, consumers +from kfet import KFET_DELETED_TRIGRAMME from kfet.auth.decorators import kfet_password_auth from kfet.autocomplete import kfet_account_only_autocomplete, kfet_autocomplete from kfet.config import kfet_config @@ -1054,8 +1056,14 @@ def kpsul_update_addcost(request): kfet_config.set(addcost_for=account, addcost_amount=amount) - data = {"addcost": {"for": account and account.trigramme or None, "amount": amount}} - consumers.KPsul.group_send("kfet.kpsul", data) + data = { + "addcost": {"for": account and account.trigramme or None, "amount": amount}, + "type": "kpsul", + } + + channel_layer = get_channel_layer() + + async_to_sync(channel_layer.group_send)("kfet.kpsul", data) return JsonResponse(data) @@ -1239,7 +1247,7 @@ def kpsul_perform_operations(request): ) # Websocket data - websocket_data = {} + websocket_data = {"type": "kpsul"} websocket_data["groups"] = [ { "add": True, @@ -1286,7 +1294,10 @@ def kpsul_perform_operations(request): websocket_data["articles"].append( {"id": article["id"], "stock": article["stock"]} ) - consumers.KPsul.group_send("kfet.kpsul", websocket_data) + + channel_layer = get_channel_layer() + + async_to_sync(channel_layer.group_send)("kfet.kpsul", websocket_data) return JsonResponse(data) @@ -1481,7 +1492,7 @@ def cancel_operations(request): articles = Article.objects.values("id", "stock").filter(pk__in=articles_pk) # Websocket data - websocket_data = {"checkouts": [], "articles": []} + websocket_data = {"checkouts": [], "articles": [], "type": "kpsul"} for checkout in checkouts: websocket_data["checkouts"].append( {"id": checkout["id"], "balance": checkout["balance"]} @@ -1490,7 +1501,10 @@ def cancel_operations(request): websocket_data["articles"].append( {"id": article["id"], "stock": article["stock"]} ) - consumers.KPsul.group_send("kfet.kpsul", websocket_data) + + channel_layer = get_channel_layer() + + async_to_sync(channel_layer.group_send)("kfet.kpsul", websocket_data) data["canceled"] = list(opes) data["opegroups_to_update"] = list(opegroups) diff --git a/requirements-prod.txt b/requirements-prod.txt index 6d6fd334..fc7ed097 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -6,7 +6,7 @@ psycopg2<2.8 # Redis django-redis-cache==2.1.* redis~=2.10.6 -asgi-redis==1.4.* +channels-redis==2.4.* # ASGI protocol and HTTP server asgiref~=1.1.2 diff --git a/requirements.txt b/requirements.txt index c5685b03..b4594ce3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ Django==2.2.* Pillow==7.2.0 authens==0.1b0 -channels==1.1.* +channels==2.4.* configparser==3.5.0 django-autocomplete-light==3.3.* django-bootstrap-form==3.3