Files
training-software/cms/models.py
Paperclip CTO b087a63b56
Some checks failed
CI / Tests (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / OpenAPI Schema (push) Has been cancelled
feat(TRA-237): CMS content blocks, media upload pipeline, and course theme
- MediaAsset model with file metadata and AV scan status tracking
- ContentBlock model (richtext/image/video/embed/download) with ordered
  blocks per page and unique constraint on (page, order)
- CourseTheme one-to-one per course with primary/secondary color and logo
- validate_upload/save_upload helpers with extension and size enforcement
- scan_media_asset_task Celery stub (marks clean; replace with ClamAV)
- REST API: media upload, page content blocks CRUD, block patch/delete
- Admin registrations for all three models
- Factory-boy factories and pytest test suite for views, upload, and task

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 09:28:54 +02:00

95 lines
3.3 KiB
Python

import uuid
from django.conf import settings
from django.db import models
class BlockType(models.TextChoices):
RICHTEXT = "richtext", "Rich Text"
IMAGE = "image", "Image"
VIDEO = "video", "Video"
EMBED = "embed", "Embed"
DOWNLOAD = "download", "Download"
class ScanStatus(models.TextChoices):
PENDING = "pending", "Pending"
CLEAN = "clean", "Clean"
INFECTED = "infected", "Infected"
ERROR = "error", "Error"
class MediaAsset(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="media_assets"
)
org_id = models.UUIDField(db_index=True)
file_name = models.CharField(max_length=255)
file_path = models.CharField(max_length=1000)
file_size = models.PositiveBigIntegerField() # bytes
mime_type = models.CharField(max_length=100, blank=True)
extension = models.CharField(max_length=20, blank=True)
scan_status = models.CharField(
max_length=20, choices=ScanStatus.choices, default=ScanStatus.PENDING
)
scan_result = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "cms_media_asset"
ordering = ["-created_at"]
def __str__(self):
return self.file_name
class ContentBlock(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
page = models.ForeignKey(
"courses.Page", on_delete=models.CASCADE, related_name="content_blocks"
)
order = models.PositiveIntegerField(default=0)
block_type = models.CharField(max_length=20, choices=BlockType.choices)
# Richtext: HTML body string
body = models.TextField(blank=True)
# Image/video/download: reference to MediaAsset
media_asset = models.ForeignKey(
MediaAsset, null=True, blank=True, on_delete=models.SET_NULL, related_name="blocks"
)
# Embed: external URL
embed_url = models.URLField(max_length=1000, blank=True)
# Shared metadata
caption = models.CharField(max_length=500, blank=True)
extra = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "cms_content_block"
ordering = ["order"]
constraints = [
models.UniqueConstraint(fields=["page", "order"], name="unique_page_block_order"),
]
def __str__(self):
return f"{self.page_id} / {self.block_type} block {self.order}"
class CourseTheme(models.Model):
"""Per-course visual theme configuration."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
course = models.OneToOneField(
"courses.Course", on_delete=models.CASCADE, related_name="theme"
)
primary_color = models.CharField(max_length=7, default="#000000") # hex
secondary_color = models.CharField(max_length=7, default="#ffffff")
logo = models.ForeignKey(
MediaAsset, null=True, blank=True, on_delete=models.SET_NULL, related_name="course_logos"
)
extra = models.JSONField(default=dict, blank=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "cms_course_theme"