feat(submissions): scoped submissions per user/matchs, sync from polylan
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user