feat(submissions): scoped submissions per user/matchs, sync from polylan

This commit is contained in:
2026-09-16 13:12:22 +02:00
parent 85e67599af
commit fabf06e6d4
35 changed files with 1008 additions and 92 deletions
+67 -8
View File
@@ -6,7 +6,7 @@ import uuid
from accounts.models import CustomUser
from events.models import Event
from events.scoping import EventScopedManager
from games.models import Game
from games.models import EventGame
class OCRStatus(models.TextChoices):
@@ -16,14 +16,20 @@ class OCRStatus(models.TextChoices):
ERROR = "error", _("Error processing")
def submission_file_upload_path(instance, filename):
def submission_file_upload_path(instance: "SubmissionFile", filename: str):
ext = filename.split(".")[-1] if "." in filename else ""
new_filename = f"{instance.id}_{filename}" if ext else str(instance.id)
date = instance.created_at.strftime("%Y/%m/%d")
user = instance.submission.user.username
game_path = instance.submission.scope.event_game.game.path
event_path = instance.submission.scope.event_game.event.path
mid = (
instance.submission.scope.external_id
if instance.submission.scope.external_id
else instance.submission.scope.name
)
return f"{instance.submission.game.path.lstrip('/')}/{date}/{user}/{instance.submission.id}/{new_filename}"
return f"{event_path.lstrip('/')}/{game_path.lstrip('/')}/{mid}/{user}/{instance.submission.id}/{new_filename}"
class BaseModel(models.Model):
@@ -37,6 +43,60 @@ class BaseModel(models.Model):
abstract = True
class Scope(BaseModel):
"""A "match" that submissions can be scored against.
Scopes are either created manually (admin/CLI) or auto-synced from the
legacy PolyLAN AAndT API (see submissions.services.aandt_sync), in which
case `external_id` holds the remote match id for idempotent upserts.
"""
event_game = models.ForeignKey(
EventGame,
on_delete=models.CASCADE,
related_name="scopes",
)
external_id = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Legacy AAndT match id, set when synced automatically.",
)
name = models.CharField(max_length=255)
assigned_users = models.ManyToManyField(
CustomUser,
blank=True,
related_name="scopes",
help_text="Users allowed to submit scores for this scope.",
)
is_active = models.BooleanField(
default=True,
help_text="Whether submissions are currently accepted for this scope.",
)
synced_at = models.DateTimeField(
null=True,
blank=True,
help_text="Last time this scope was synced from the AAndT API.",
)
class Meta:
ordering = ["-created_at"]
constraints = [
models.UniqueConstraint(
fields=["event_game", "external_id"],
condition=models.Q(external_id__isnull=False),
name="unique_scope_external_id_per_event_game",
),
]
def __str__(self) -> str:
return f"{self.name} ({self.event_game})"
class Submission(BaseModel):
event = models.ForeignKey(
Event,
@@ -44,12 +104,11 @@ class Submission(BaseModel):
related_name="submissions",
)
game = models.ForeignKey(
Game,
scope = models.ForeignKey(
Scope,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="submissions",
help_text="The match this submission's score is for.",
)
user = models.ForeignKey(