224 lines
6.3 KiB
Python
224 lines
6.3 KiB
Python
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
import uuid
|
|
|
|
from accounts.models import CustomUser
|
|
from events.models import Event
|
|
from events.scoping import EventScopedManager
|
|
from games.models import EventGame
|
|
|
|
|
|
class OCRStatus(models.TextChoices):
|
|
TO_PROCESS = "to_process", _("To Process")
|
|
PROCESSING = "processing", _("Processing")
|
|
PROCESSED = "processed", _("Processed")
|
|
ERROR = "error", _("Error processing")
|
|
|
|
|
|
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)
|
|
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"{event_path.lstrip('/')}/{game_path.lstrip('/')}/{mid}/{user}/{instance.submission.id}/{new_filename}"
|
|
|
|
|
|
class BaseModel(models.Model):
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
|
|
# Timestamps
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
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,
|
|
on_delete=models.CASCADE,
|
|
related_name="submissions",
|
|
)
|
|
|
|
scope = models.ForeignKey(
|
|
Scope,
|
|
on_delete=models.CASCADE,
|
|
related_name="submissions",
|
|
help_text="The match this submission's score is for.",
|
|
)
|
|
|
|
user = models.ForeignKey(
|
|
CustomUser,
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
help_text="User who made the submission (null for anonymous)",
|
|
related_name="user_submissions",
|
|
)
|
|
|
|
# Submission metadata
|
|
notes = models.TextField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Optional notes about the submission",
|
|
)
|
|
|
|
objects = models.Manager()
|
|
for_event = EventScopedManager()
|
|
|
|
|
|
class SubmissionFile(BaseModel):
|
|
submission = models.ForeignKey(
|
|
Submission,
|
|
on_delete=models.CASCADE,
|
|
related_name="files",
|
|
)
|
|
|
|
file = models.FileField(
|
|
upload_to=submission_file_upload_path,
|
|
help_text=_("Uploaded file"),
|
|
)
|
|
|
|
original_filename = models.CharField(
|
|
max_length=255,
|
|
help_text=_("Original filename as uploaded by user"),
|
|
)
|
|
file_size = models.PositiveIntegerField(help_text=_("File size in bytes"))
|
|
content_type = models.CharField(
|
|
max_length=100,
|
|
help_text=_("MIME type of the file"),
|
|
)
|
|
|
|
# OCR metadata
|
|
ocr_status = models.CharField(
|
|
max_length=20,
|
|
choices=OCRStatus.choices,
|
|
default=OCRStatus.TO_PROCESS,
|
|
help_text=_("OCR processing status for the entire file"),
|
|
)
|
|
ocr_raw_data = models.TextField(
|
|
blank=True,
|
|
help_text=_("Raw OCR data for the entire file"),
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ["file", "created_at"]
|
|
verbose_name = "Submission File"
|
|
verbose_name_plural = "Submission Files"
|
|
|
|
@property
|
|
def file_url(self) -> str | None:
|
|
if self.file:
|
|
return self.file.url
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.file and not self.file_size:
|
|
self.file_size = self.file.size
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class ImageCrop(BaseModel):
|
|
submission_file = models.ForeignKey(
|
|
SubmissionFile,
|
|
on_delete=models.CASCADE,
|
|
related_name="crops",
|
|
)
|
|
|
|
# Crop coordinates (pixels): x1, y1, x2, y2
|
|
x1 = models.PositiveIntegerField(help_text=_("Top-left x coordinate"))
|
|
y1 = models.PositiveIntegerField(help_text=_("Top-left y coordinate"))
|
|
x2 = models.PositiveIntegerField(help_text=_("Bottom-right x coordinate"))
|
|
y2 = models.PositiveIntegerField(help_text=_("Bottom-right y coordinate"))
|
|
|
|
# Label/name for the crop
|
|
label = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
help_text=_("Optional label for this crop region"),
|
|
)
|
|
|
|
# OCR results for this crop
|
|
ocr_status = models.CharField(
|
|
max_length=20,
|
|
choices=OCRStatus.choices,
|
|
default=OCRStatus.TO_PROCESS,
|
|
help_text=_("OCR processing status for this crop"),
|
|
)
|
|
ocr_raw_data = models.TextField(
|
|
blank=True,
|
|
help_text=_("Raw OCR data for this crop"),
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ["created_at"]
|
|
verbose_name = "Image Crop"
|
|
verbose_name_plural = "Image Crops"
|
|
|
|
def __str__(self):
|
|
return f"{self.label or f'Crop ({self.x1}, {self.y1}, {self.x2}, {self.y2})'}"
|