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

1import uuid 

2 

3from django.conf import settings 

4from django.db import models 

5from django.utils.translation import gettext_lazy as _ 

6 

7from baibu.users.models import ConsentRecord 

8 

9 

10class Submission(models.Model): 

11 """A piece of writing contributed by a user. 

12 

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 """ 

17 

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") 

24 

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 } 

34 

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) 

39 

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 ) 

49 

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) 

59 

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) 

69 

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) 

77 

78 class Meta: 

79 ordering = ["-created_at"] 

80 verbose_name = _("Submission") 

81 verbose_name_plural = _("Submissions") 

82 

83 def __str__(self) -> str: 

84 return f"Submission {self.pk}" 

85 

86 def can_transition_to(self, new_status: str) -> bool: 

87 return new_status in self.TRANSITIONS.get(self.status, set())