A11y:
- Add alt_text field to MediaAsset and ContentBlock (WCAG 2.1 AA 1.1.1)
- Expose alt_text in MediaAssetSerializer and ContentBlockSerializer so
frontends can render <img alt="..."> and <video aria-label="..."> correctly
- Migration 0002 adds the two alt_text columns
i18n:
- Add LocaleMiddleware to MIDDLEWARE stack (after SessionMiddleware per Django docs)
- Add LANGUAGES = [("de", ...), ("en", ...)] and LOCALE_PATHS = [BASE_DIR/"locale"]
- Add USE_L10N = True
- Seed locale/de/LC_MESSAGES/django.po and locale/en/LC_MESSAGES/django.po with
translations for all user-facing API strings (upload errors, notification titles)
Tests (tests/a11y/test_a11y_i18n.py):
- alt_text field round-trip on MediaAsset and ContentBlock
- Serializer exposes and accepts alt_text on create, list, PATCH
- LANGUAGES/LOCALE_PATHS/LocaleMiddleware settings assertions
- Accept-Language header switching smoke test against /api/v1/notifications/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
3.5 KiB
Python
98 lines
3.5 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)
|
|
alt_text = models.CharField(max_length=500, blank=True) # WCAG 2.1 AA
|
|
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)
|
|
# WCAG 2.1 AA: alt text required for image blocks
|
|
alt_text = models.CharField(max_length=500, 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"
|