from django.db import transaction from django.db.models.signals import post_delete, 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, Scope from submissions.schemas import SubmissionOut, SubmissionFileOut, ImageCropOut # Staff see (and OCR/crop) every player's submissions, so they get every update STAFF_GROUP_NAME = "submissions_staff" def _broadcast_submission_event(submission: Submission, message: dict): channel_layer = get_channel_layer() groups = [STAFF_GROUP_NAME] if submission.user_id: groups.append(f"submissions_user_{submission.user_id}") for group_name in groups: async_to_sync(channel_layer.group_send)(group_name, message) @receiver(post_save, sender=Submission) def broadcast_submission_update(sender, instance, created, **kwargs): _broadcast_submission_event( instance, { "type": "submission.updated", "submission": SubmissionOut.from_orm(instance).dict(), }, ) @receiver(post_save, sender=SubmissionFile) def broadcast_submission_file_update(sender, instance, created, **kwargs): _broadcast_submission_event( instance.submission, { "type": "submission.file.updated", "file": SubmissionFileOut.from_orm(instance).dict(), }, ) @receiver(post_save, sender=ImageCrop) def broadcast_image_crop_update(sender, instance, created, **kwargs): _broadcast_submission_event( instance.submission_file.submission, { "type": "submission.crop.updated", "crop": ImageCropOut.from_orm(instance).dict(), }, ) @receiver(post_delete, sender=ImageCrop) def broadcast_image_crop_delete(sender, instance, **kwargs): _broadcast_submission_event( instance.submission_file.submission, { "type": "submission.crop.deleted", "crop": { "id": str(instance.id), "submission_file_id": str(instance.submission_file_id), }, }, ) def broadcast_scope_updates(changes: dict[Scope, set[int]], /): """Tell users their scope changed (closed/opened, (un)assigned), once the current transaction commits, so the frontend can move them to the right match without a refresh. Args: changes: Changed scopes, each with every user id that was or now is assigned to it. """ def send(): channel_layer = get_channel_layer() for scope, user_ids in changes.items(): assigned_ids = set(scope.assigned_users.values_list("id", flat=True)) for user_id in user_ids: async_to_sync(channel_layer.group_send)( f"submissions_user_{user_id}", { "type": "scope.updated", "scope": { "id": str(scope.id), "is_active": scope.is_active, "assigned": user_id in assigned_ids, }, }, ) if changes: # robust: a websocket hiccup only gets logged, it mustn't fail the # (already committed) sync or admin save transaction.on_commit(send, robust=True)