diff --git a/submission/api.py b/submission/api.py index 4680a45..7745aee 100644 --- a/submission/api.py +++ b/submission/api.py @@ -86,6 +86,8 @@ def list_submissions(request, filters: SubmissionFilter = Query(...)): submissions = submissions.filter(flag__isnull=False) else: submissions = submissions.filter(flag=filters.flag) + if filters.zone: + submissions = submissions.filter(zone=filters.zone) if filters.score_lt_threshold is not None: submissions = submissions.filter(score__lt=filters.score_lt_threshold) diff --git a/submission/management/commands/recalculate_scores.py b/submission/management/commands/recalculate_scores.py new file mode 100644 index 0000000..de9b43e --- /dev/null +++ b/submission/management/commands/recalculate_scores.py @@ -0,0 +1,27 @@ +from django.core.management.base import BaseCommand + +from submission.models import Submission + + +class Command(BaseCommand): + help = "Recalculate score, raw_score, and zone for all rated submissions" + + def add_arguments(self, parser): + parser.add_argument("--dry-run", action="store_true", help="Show what would change without saving") + + def handle(self, *args, **options): + dry_run = options["dry_run"] + qs = Submission.objects.filter(ratings__isnull=False).distinct() + total = qs.count() + self.stdout.write(f"Found {total} rated submission(s).") + + if dry_run: + self.stdout.write("Dry run — no changes made.") + return + + for i, s in enumerate(qs, 1): + old_score = s.score + s.update_score() + self.stdout.write(f"[{i}/{total}] {s.user.username}/{s.task.title}: {old_score:.3f} → {s.score:.3f} zone={s.zone}") + + self.stdout.write(self.style.SUCCESS("Done.")) diff --git a/submission/migrations/0008_add_zone.py b/submission/migrations/0008_add_zone.py new file mode 100644 index 0000000..b792853 --- /dev/null +++ b/submission/migrations/0008_add_zone.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-03-30 11:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0007_remove_nominated'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='zone', + field=models.CharField(blank=True, choices=[('featured', '精选'), ('low', '待改进'), ('pending', '待评')], db_index=True, default=None, max_length=10, null=True, verbose_name='分区'), + ), + ] diff --git a/submission/models.py b/submission/models.py index bbd3c41..d2875c3 100644 --- a/submission/models.py +++ b/submission/models.py @@ -19,6 +19,12 @@ class FlagChoices(models.TextChoices): YELLOW = "yellow", "需要改进" +class ZoneChoices(models.TextChoices): + FEATURED = "featured", "精选" + LOW = "low", "待改进" + PENDING = "pending", "待评" + + class Submission(TimeStampedModel): id = models.UUIDField( primary_key=True, @@ -49,6 +55,15 @@ class Submission(TimeStampedModel): verbose_name="标记", ) raw_score = models.FloatField(default=0.0, verbose_name="原始加权分") + zone = models.CharField( + max_length=10, + choices=ZoneChoices.choices, + null=True, + blank=True, + default=None, + db_index=True, + verbose_name="分区", + ) class Meta: ordering = ("-created",) @@ -62,6 +77,19 @@ class Submission(TimeStampedModel): """ return self.task.task_type + def _update_zone(self, n: int): + if n < 5: + new_zone = ZoneChoices.PENDING + elif self.score >= 4.0: + new_zone = ZoneChoices.FEATURED + elif self.score < 3.0: + new_zone = ZoneChoices.LOW + else: + new_zone = None + if self.zone != new_zone: + self.zone = new_zone + self.save(update_fields=["zone"]) + def update_score(self): ratings = list(self.ratings.select_related("user").all()) n = len(ratings) @@ -70,15 +98,18 @@ class Submission(TimeStampedModel): self.raw_score = 0.0 self.score = 0.0 self.save(update_fields=["raw_score", "score"]) + self._update_zone(n) return - weighted_sum = sum( - r.score * (0.5 if r.user.role == RoleChoices.SUPER - else 0.3 if r.user.role == RoleChoices.ADMIN - else 0.2) + role_weights = [ + 0.5 if r.user.role == RoleChoices.SUPER + else 0.3 if r.user.role == RoleChoices.ADMIN + else 0.2 for r in ratings - ) - self.raw_score = weighted_sum / n + ] + weighted_sum = sum(r.score * w for r, w in zip(ratings, role_weights)) + weight_total = sum(role_weights) + self.raw_score = weighted_sum / weight_total C = 3 global_mean = ( @@ -89,6 +120,7 @@ class Submission(TimeStampedModel): self.score = (C * global_mean + n * self.raw_score) / (C + n) self.save(update_fields=["raw_score", "score"]) + self._update_zone(n) def save(self, *args, **kwargs): super().save(*args, **kwargs) diff --git a/submission/schemas.py b/submission/schemas.py index dd0bb43..88e93bf 100644 --- a/submission/schemas.py +++ b/submission/schemas.py @@ -26,6 +26,7 @@ class SubmissionOut(Schema): js: Optional[str] = None conversation_id: Optional[UUID] = None flag: Optional[str] = None + zone: Optional[str] = None submit_count: int = 0 created: str modified: str @@ -107,6 +108,7 @@ class SubmissionFilter(Schema): username: Optional[str] = None user_id: Optional[int] = None flag: Optional[Literal["red", "blue", "green", "yellow", "any"]] = None + zone: Optional[Literal["featured", "low", "pending"]] = None score_min: Optional[float] = None score_max_exclusive: Optional[float] = None score_lt_threshold: Optional[float] = None