Add guarded content draft and publication services

This commit is contained in:
2026-09-27 09:49:30 +03:30
parent 3bc9de2367
commit b8e79c950a
5 changed files with 517 additions and 0 deletions

View File

@@ -70,6 +70,12 @@ class SiteIdentity(models.Model):
class Meta:
constraints = [models.CheckConstraint(condition=Q(id=1), name="content_single_site")]
permissions = [
("change_content_draft", "Can change content drafts"),
("preview_content_draft", "Can preview content drafts"),
("publish_content", "Can publish content"),
("restore_content", "Can restore published content"),
]
def clean(self):
if self.published_revision_id and self.published_revision.site_id != self.pk:
@@ -114,6 +120,14 @@ class SiteRevision(models.Model):
if not self.logo_id and self.logo_alt:
raise ValidationError({"logo_alt": "Alt text requires a logo."})
def save(self, *args, **kwargs):
if self.pk:
previous = type(self).objects.get(pk=self.pk)
protected = ("site_id", "number", "official_name", "short_name", "faculty_subline", "description", "logo_id", "logo_alt", "navigation", "contacts", "footer_copy", "seo_title", "seo_description", "seo_image_id", "created_by_id", "source_seed_key", "source_seed_version", "content_checksum")
if any(getattr(self, field) != getattr(previous, field) for field in protected):
raise ValidationError("Site revision content is immutable.")
return super().save(*args, **kwargs)
class EditorialPage(models.Model):
slug = models.CharField(max_length=20, unique=True)
@@ -155,6 +169,16 @@ class PageRevision(models.Model):
for field in ("title", "intro", "seo_title", "seo_description"):
validators.text(getattr(self, field), self._meta.get_field(field).max_length, required=field != "intro")
def save(self, *args, **kwargs):
if self.pk:
previous = type(self).objects.get(pk=self.pk)
protected = ("page_id", "number", "title", "intro", "seo_title", "seo_description", "seo_image_id", "created_by_id", "source_seed_key", "source_seed_version")
if any(getattr(self, field) != getattr(previous, field) for field in protected):
raise ValidationError("Page revision content is immutable.")
if self.content_checksum != previous.content_checksum and previous.content_checksum:
raise ValidationError("Page revision checksum is immutable.")
return super().save(*args, **kwargs)
class PageSection(models.Model):
revision = models.ForeignKey(PageRevision, on_delete=models.CASCADE, related_name="sections")
@@ -226,6 +250,16 @@ class PageSection(models.Model):
if self.body and self.entries:
raise ValidationError("Use body or entries, not both.")
def save(self, *args, **kwargs):
if self.pk:
raise ValidationError("Page sections are immutable; create a new revision.")
if self.revision.status != "draft":
raise ValidationError("Sections can only be added to a draft revision.")
return super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
raise ValidationError("Page sections are immutable; create a new revision.")
class PublicationAudit(models.Model):
actor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
@@ -238,3 +272,11 @@ class PublicationAudit(models.Model):
class Meta:
constraints = [models.CheckConstraint(condition=(Q(site__isnull=False, page__isnull=True) | Q(site__isnull=True, page__isnull=False)), name="content_audit_one_target")]
def save(self, *args, **kwargs):
if self.pk:
raise ValidationError("Publication audit records are append only.")
return super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
raise ValidationError("Publication audit records are append only.")