mutiple fixes

This commit is contained in:
2025-11-28 14:05:26 +01:00
parent 9ee45463a8
commit fa76fbce92
36 changed files with 694 additions and 242 deletions
+2 -1
View File
@@ -1,7 +1,7 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils import timezone
from .models import (
from submissions.models import (
SteamAPIKey,
SteamCollection,
SteamCollectionItem,
@@ -148,6 +148,7 @@ class SteamCollectionItemAdmin(admin.ModelAdmin):
("Author Information", {"fields": ("author_name", "author_steam_id")}),
("Metadata", {"fields": ("tags",)}),
("Timestamps", {"fields": ("created_at", "updated_at")}),
("Points factor", {"fields": ("points_factor", "points_value")}),
)
+9 -1
View File
@@ -112,6 +112,14 @@ def create_submission(
ocr_confidence_cost=response_data.ocr_confidence_cost,
ocr_confidence_cycles=response_data.ocr_confidence_cycles,
ocr_confidence_area=response_data.ocr_confidence_area,
**{
# Put validated if not manual validation is needed
"validated_cost": response_data.cost,
"validated_cycles": response_data.cycles,
"validated_area": response_data.area,
}
if not data.manual_validation_requested
else {},
)
# Process files for this response
@@ -197,7 +205,7 @@ def validate_response(request, response_id: int, data: ValidationIn):
@router.put("/responses/{response_id}/validate/auto", response=PuzzleResponseOut)
def validate_response(request, response_id: int):
def validate_auto(request, response_id: int):
"""Try to auto validate a puzzle response"""
if not request.user.is_authenticated or not request.user.is_staff:
@@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-11-23 22:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('animations', '0001_initial'),
('submissions', '0008_alter_puzzleresponse_unique_together'),
]
operations = [
migrations.AddField(
model_name='steamcollectionitem',
name='points_factor',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='animations.puzzlepointsfactor'),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-11-23 23:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('submissions', '0009_steamcollectionitem_points_factor'),
]
operations = [
migrations.AlterField(
model_name='puzzleresponse',
name='validated_area',
field=models.IntegerField(blank=True, help_text='Manually validated area value'),
),
migrations.AlterField(
model_name='puzzleresponse',
name='validated_cost',
field=models.IntegerField(blank=True, help_text='Manually validated cost value'),
),
migrations.AlterField(
model_name='puzzleresponse',
name='validated_cycles',
field=models.IntegerField(blank=True, help_text='Manually validated cycles value'),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-11-23 23:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('submissions', '0010_alter_puzzleresponse_validated_area_and_more'),
]
operations = [
migrations.AlterField(
model_name='puzzleresponse',
name='area',
field=models.IntegerField(blank=True, help_text='Area value from OCR'),
),
migrations.AlterField(
model_name='puzzleresponse',
name='cost',
field=models.IntegerField(blank=True, help_text='Cost value from OCR'),
),
migrations.AlterField(
model_name='puzzleresponse',
name='cycles',
field=models.IntegerField(blank=True, help_text='Cycles value from OCR'),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-11-23 23:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('submissions', '0011_alter_puzzleresponse_area_alter_puzzleresponse_cost_and_more'),
]
operations = [
migrations.AlterField(
model_name='puzzleresponse',
name='validated_area',
field=models.IntegerField(blank=True, help_text='Manually validated area value', null=True),
),
migrations.AlterField(
model_name='puzzleresponse',
name='validated_cost',
field=models.IntegerField(blank=True, help_text='Manually validated cost value', null=True),
),
migrations.AlterField(
model_name='puzzleresponse',
name='validated_cycles',
field=models.IntegerField(blank=True, help_text='Manually validated cycles value', null=True),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-11-24 01:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('animations', '0002_puzzlepointsvalue'),
('submissions', '0012_alter_puzzleresponse_validated_area_and_more'),
]
operations = [
migrations.AddField(
model_name='steamcollectionitem',
name='points_value',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='animations.puzzlepointsvalue'),
),
]
+99 -11
View File
@@ -1,11 +1,43 @@
from typing import Self
from django.db import models
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
import uuid
from django.db.models.expressions import Window
from django.db.models.functions import Cast, Rank, RowNumber
from django.db.models.query import F
User = get_user_model()
class JsonIndex(models.Func):
function = ""
template = "%(json_field)s -> (%(index)s::int)"
def __init__(self, json_field, index_expression, **extra):
super().__init__(json_field, index_expression, **extra)
self.output_field = models.IntegerField()
def resolve_expression(
self, query, allow_joins=True, reuse=None, summarize=False, for_save=False
):
# Resolve both expressions in the query context to ensure joins are set up correctly
clone = self.copy()
clone.source_expressions = [
expr.resolve_expression(query, allow_joins, reuse, summarize, for_save)
for expr in self.source_expressions
]
return clone
def as_sql(self, compiler, connection):
json_sql, json_params = compiler.compile(self.source_expressions[0])
idx_sql, idx_params = compiler.compile(self.source_expressions[1])
sql = self.template % {"json_field": json_sql, "index": idx_sql}
params = json_params + idx_params
return sql, params
class SteamAPIKey(models.Model):
"""Model to store Steam API key configuration - Admin only"""
@@ -178,6 +210,19 @@ class SteamCollectionItem(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Puzzle points
points_factor = models.ForeignKey(
"animations.PuzzlePointsFactor",
null=True,
on_delete=models.SET_NULL,
)
points_value = models.ForeignKey(
"animations.PuzzlePointsValue",
null=True,
on_delete=models.SET_NULL,
)
class Meta:
ordering = ["collection", "order_index"]
unique_together = ["collection", "steam_item_id"]
@@ -271,6 +316,48 @@ class Submission(models.Model):
return self.responses.filter(needs_manual_validation=True).exists()
class PuzzleResponseQuerySet(models.QuerySet):
def annotate_rank_points(self) -> Self:
return (
self.annotate(
points=F("puzzle__points_factor__cost") * F("validated_cost")
+ F("puzzle__points_factor__cycles") * F("validated_cycles")
+ F("puzzle__points_factor__area") * F("validated_area")
)
.annotate(
user_response_rank=Window(
expression=RowNumber(),
partition_by=[F("puzzle"), F("submission__user")],
order_by=F("points").asc(),
)
)
# .filter(user_response_rank=1)
.annotate(
puzzle_user_rank=Window(
expression=Rank(),
partition_by=[F("puzzle")],
order_by=F("points").asc(),
)
)
.annotate(
rank_points=Cast(
JsonIndex(
F("puzzle__points_value__points"),
Cast(F("puzzle_user_rank") - 1, models.IntegerField()),
),
models.IntegerField(),
)
)
)
def filter_user_best_response(self) -> Self:
return self.annotate_rank_points().filter(user_response_rank=1)
class PuzzleResponseManager(models.Manager.from_queryset(PuzzleResponseQuerySet)):
pass
class PuzzleResponse(models.Model):
"""Model representing a response/solution for a specific puzzle"""
@@ -289,11 +376,9 @@ class PuzzleResponse(models.Model):
puzzle_name = models.CharField(
max_length=255, help_text="Puzzle name as detected by OCR"
)
cost = models.CharField(max_length=20, blank=True, help_text="Cost value from OCR")
cycles = models.CharField(
max_length=20, blank=True, help_text="Cycles value from OCR"
)
area = models.CharField(max_length=20, blank=True, help_text="Area value from OCR")
cost = models.IntegerField(blank=True, help_text="Cost value from OCR")
cycles = models.IntegerField(blank=True, help_text="Cycles value from OCR")
area = models.IntegerField(blank=True, help_text="Area value from OCR")
# Validation flags
needs_manual_validation = models.BooleanField(
@@ -311,24 +396,27 @@ class PuzzleResponse(models.Model):
)
# Manual validation overrides
validated_cost = models.CharField(
max_length=20, blank=True, help_text="Manually validated cost value"
validated_cost = models.IntegerField(
null=True, blank=True, help_text="Manually validated cost value"
)
validated_cycles = models.CharField(
max_length=20, blank=True, help_text="Manually validated cycles value"
validated_cycles = models.IntegerField(
null=True, blank=True, help_text="Manually validated cycles value"
)
validated_area = models.CharField(
max_length=20, blank=True, help_text="Manually validated area value"
validated_area = models.IntegerField(
null=True, blank=True, help_text="Manually validated area value"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = PuzzleResponseManager()
class Meta:
ordering = ["submission", "puzzle__order_index"]
verbose_name = "Puzzle Response"
verbose_name_plural = "Puzzle Responses"
_base_manager_name = "objects"
def __str__(self):
return f"Response for {self.puzzle_name} in {self.submission}"
+9 -9
View File
@@ -20,9 +20,9 @@ class PuzzleResponseIn(Schema):
puzzle_id: int
puzzle_name: str
cost: Optional[str] = None
cycles: Optional[str] = None
area: Optional[str] = None
cost: Optional[int] = None
cycles: Optional[int] = None
area: Optional[int] = None
needs_manual_validation: bool = False
ocr_confidence_cost: Optional[float] = None
ocr_confidence_cycles: Optional[float] = None
@@ -61,9 +61,9 @@ class PuzzleResponseOut(ModelSchema):
"""Schema for puzzle response output"""
files: List[SubmissionFileOut]
final_cost: Optional[str]
final_cycles: Optional[str]
final_area: Optional[str]
final_cost: Optional[int]
final_cycles: Optional[int]
final_area: Optional[int]
class Meta:
model = PuzzleResponse
@@ -126,9 +126,9 @@ class ValidationIn(Schema):
"""Schema for manual validation input"""
puzzle: Optional[int] = None
validated_cost: Optional[str] = None
validated_cycles: Optional[str] = None
validated_area: Optional[str] = None
validated_cost: Optional[int] = None
validated_cycles: Optional[int] = None
validated_area: Optional[int] = None
# Collection Schemas
+7 -2
View File
@@ -4,7 +4,7 @@ Utilities for fetching Steam Workshop collection data using Steam Web API
import re
import requests
from submissions.models import SteamCollection, SubmissionFile
from submissions.models import SteamCollection, SteamCollectionItem, SubmissionFile
from datetime import datetime
from django.utils import timezone
from django.conf import settings
@@ -485,7 +485,7 @@ def verify_ocr_data_for_file(file: str) -> tuple[str, int, int, int]:
# cv2.imwrite("output_debug.jpg", output)
os.remove("temp.jpg")
return puzzle, cost, cycles, area
return puzzle, int(cost), int(cycles), int(area)
def verify_and_validate_ocr_date_for_submission(file: SubmissionFile):
@@ -496,10 +496,15 @@ def verify_and_validate_ocr_date_for_submission(file: SubmissionFile):
f"{r.submission.user}: ({r.cost: >4} {r.cycles: >4} {r.area: >4}) -> ({ocr_data[1]: >4} {ocr_data[2]: >4} {ocr_data[3]: >4})"
)
if puzzle := SteamCollectionItem.objects.filter(title=ocr_data[0]).first():
# print(f"{r.puzzle.title} -> {ocr_data[0]}")
r.puzzle = puzzle
valid_count = 0
for index, field in enumerate(["cost", "cycles", "area"]):
value = getattr(r, field, -1)
# print(f"{value} -> {ocr_data[index + 1]}")
if value == ocr_data[index + 1]:
setattr(r, f"validated_{field}", value)
valid_count += 1