feat(websocket): submissions backend

This commit is contained in:
2026-05-25 02:45:17 +02:00
parent c02a0aa985
commit b64161b371
6 changed files with 128 additions and 4 deletions
+7 -1
View File
@@ -10,14 +10,20 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
import os import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polylan_submitter.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polylan_submitter.settings")
django_asgi_app = get_asgi_application() django_asgi_app = get_asgi_application()
from submissions.routing import websocket_urlpatterns
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {
"http": django_asgi_app, "http": django_asgi_app,
"websocket": AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
),
} }
) )
+3
View File
@@ -4,3 +4,6 @@ from django.apps import AppConfig
class SubmissionsConfig(AppConfig): class SubmissionsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "submissions" name = "submissions"
def ready(self):
import submissions.signals # noqa
@@ -0,0 +1,35 @@
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from submissions.models import Submission, SubmissionFile, ImageCrop
from submissions.schemas import SubmissionOut, SubmissionFileOut, ImageCropOut
class SubmissionsConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.user = self.scope["user"]
if isinstance(self.user, AnonymousUser):
await self.close()
return
self.user_group_name = f"submissions_user_{self.user.id}"
await self.channel_layer.group_add(self.user_group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.user_group_name, self.channel_name)
async def submission_updated(self, event):
submission_data = event["submission"]
await self.send(text_data=json.dumps(submission_data))
async def submission_file_updated(self, event):
file_data = event["file"]
await self.send(text_data=json.dumps(file_data))
async def submission_crop_updated(self, event):
crop_data = event["crop"]
await self.send(text_data=json.dumps(crop_data))
+6
View File
@@ -0,0 +1,6 @@
from django.urls import re_path
from submissions.consumers import SubmissionsConsumer
websocket_urlpatterns = [
re_path(r"ws/submissions/$", SubmissionsConsumer.as_asgi()),
]
+10 -3
View File
@@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from uuid import UUID from uuid import UUID
from ninja import Schema from ninja import Schema
from pydantic import BaseModel, ConfigDict
class ImageCropIn(Schema): class ImageCropIn(Schema):
@@ -11,7 +12,9 @@ class ImageCropIn(Schema):
label: str | None = None label: str | None = None
class ImageCropOut(Schema): class ImageCropOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID id: UUID
x1: int x1: int
y1: int y1: int
@@ -23,7 +26,9 @@ class ImageCropOut(Schema):
created_at: datetime created_at: datetime
class SubmissionFileOut(Schema): class SubmissionFileOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID id: UUID
original_filename: str original_filename: str
content_type: str content_type: str
@@ -34,7 +39,9 @@ class SubmissionFileOut(Schema):
crops: list[ImageCropOut] = [] crops: list[ImageCropOut] = []
class SubmissionOut(Schema): class SubmissionOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID id: UUID
game_id: int | None = None game_id: int | None = None
notes: str | None = None notes: str | None = None
+67
View File
@@ -0,0 +1,67 @@
import json
from django.db.models.signals import post_save
from django.dispatch import receiver
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from submissions.models import Submission, SubmissionFile, ImageCrop
from submissions.schemas import SubmissionOut, SubmissionFileOut, ImageCropOut
channel_layer = get_channel_layer()
@receiver(post_save, sender=Submission)
def broadcast_submission_update(sender, instance, created, **kwargs):
if not instance.user:
return
user_group_name = f"submissions_user_{instance.user.id}"
schema = SubmissionOut.model_validate(instance)
submission_data = schema.model_dump(mode="json")
async_to_sync(channel_layer.group_send)(
user_group_name,
{
"type": "submission.updated",
"submission": submission_data,
}
)
@receiver(post_save, sender=SubmissionFile)
def broadcast_submission_file_update(sender, instance, created, **kwargs):
submission = instance.submission
if not submission.user:
return
user_group_name = f"submissions_user_{submission.user.id}"
schema = SubmissionFileOut.model_validate(instance)
file_data = schema.model_dump(mode="json")
async_to_sync(channel_layer.group_send)(
user_group_name,
{
"type": "submission.file.updated",
"file": file_data,
}
)
@receiver(post_save, sender=ImageCrop)
def broadcast_image_crop_update(sender, instance, created, **kwargs):
submission = instance.submission_file.submission
if not submission.user:
return
user_group_name = f"submissions_user_{submission.user.id}"
schema = ImageCropOut.model_validate(instance)
crop_data = schema.model_dump(mode="json")
async_to_sync(channel_layer.group_send)(
user_group_name,
{
"type": "submission.crop.updated",
"crop": crop_data,
}
)