Coverage for baibu/submissions/models.py: 100%
46 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1import uuid
3from django.conf import settings
4from django.db import models
5from django.utils.translation import gettext_lazy as _
7from baibu.users.models import ConsentRecord
10class Submission(models.Model):
11 """A piece of writing contributed by a user.
13 The full text lives in private file storage (``raw_key``, then
14 ``clean_key`` once cleaned); the database keeps an excerpt of the cleaned
15 text, counts and the cleaning outcome.
16 """
18 class Status(models.TextChoices):
19 PENDING = "pending", _("Pending")
20 VERIFIED = "verified", _("Verified")
21 NEEDS_REVIEW = "needs_review", _("Needs review")
22 REJECTED = "rejected", _("Rejected")
23 ISSUE = "issue", _("Issue")
25 # Cleaning moves a pending submission on; staff decide the rest. "Clean
26 # again" sends a submission back to pending.
27 TRANSITIONS = {
28 Status.PENDING: {Status.VERIFIED, Status.NEEDS_REVIEW, Status.ISSUE},
29 Status.NEEDS_REVIEW: {Status.VERIFIED, Status.REJECTED, Status.PENDING},
30 Status.ISSUE: {Status.PENDING, Status.REJECTED},
31 Status.VERIFIED: {Status.REJECTED, Status.PENDING},
32 Status.REJECTED: {Status.PENDING},
33 }
35 id = models.UUIDField(primary_key=True, default=uuid.uuid7, editable=False)
36 user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="submissions")
37 language_code = models.CharField(_("Language"), max_length=24)
38 status = models.CharField(max_length=16, choices=Status.choices, default=Status.PENDING, db_index=True)
40 # Consent in force when the text was submitted.
41 consent_tier = models.CharField(max_length=32, choices=ConsentRecord.Tier.choices)
42 consent_record = models.ForeignKey(
43 ConsentRecord,
44 on_delete=models.SET_NULL,
45 null=True,
46 blank=True,
47 related_name="+",
48 )
50 # Storage keys (see baibu.core.storage), not URLs.
51 raw_key = models.CharField(max_length=512)
52 clean_key = models.CharField(max_length=512, blank=True)
53 # SHA-256 of the normalised raw text, to refuse duplicates.
54 content_hash = models.CharField(max_length=64, unique=True)
55 word_count = models.PositiveIntegerField(default=0)
56 character_count = models.PositiveIntegerField(default=0)
57 # Start of the cleaned text; empty until cleaning has run.
58 excerpt = models.TextField(blank=True)
60 # Cleaning outcome.
61 cleaner = models.CharField(max_length=128, blank=True, help_text=_("Cleaner (and model) that produced it."))
62 redaction_count = models.PositiveIntegerField(null=True, blank=True)
63 toxicity_detected = models.BooleanField(null=True, blank=True)
64 quality_score = models.PositiveSmallIntegerField(null=True, blank=True, help_text=_("0 to 100."))
65 detected_language = models.CharField(max_length=24, blank=True)
66 flags = models.JSONField(default=list, blank=True)
67 decision_reason = models.CharField(max_length=512, blank=True)
68 cleaned_at = models.DateTimeField(null=True, blank=True)
70 staff_notes = models.TextField(blank=True)
71 reviewed_by = models.ForeignKey(
72 settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="+"
73 )
74 reviewed_at = models.DateTimeField(null=True, blank=True)
75 created_at = models.DateTimeField(auto_now_add=True)
76 updated_at = models.DateTimeField(auto_now=True)
78 class Meta:
79 ordering = ["-created_at"]
80 verbose_name = _("Submission")
81 verbose_name_plural = _("Submissions")
83 def __str__(self) -> str:
84 return f"Submission {self.pk}"
86 def can_transition_to(self, new_status: str) -> bool:
87 return new_status in self.TRANSITIONS.get(self.status, set())