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
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")
django_asgi_app = get_asgi_application()
from submissions.routing import websocket_urlpatterns
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
),
}
)
+3
View File
@@ -4,3 +4,6 @@ from django.apps import AppConfig
class SubmissionsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
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 uuid import UUID
from ninja import Schema
from pydantic import BaseModel, ConfigDict
class ImageCropIn(Schema):
@@ -11,7 +12,9 @@ class ImageCropIn(Schema):
label: str | None = None
class ImageCropOut(Schema):
class ImageCropOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
x1: int
y1: int
@@ -23,7 +26,9 @@ class ImageCropOut(Schema):
created_at: datetime
class SubmissionFileOut(Schema):
class SubmissionFileOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
original_filename: str
content_type: str
@@ -34,7 +39,9 @@ class SubmissionFileOut(Schema):
crops: list[ImageCropOut] = []
class SubmissionOut(Schema):
class SubmissionOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
game_id: int | 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,
}
)