Files
training-software/cms/upload.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

73 lines
2.1 KiB
Python

"""
Upload validation helpers.
Validates file extension and size against settings before persisting to storage.
"""
from __future__ import annotations
import os
from django.conf import settings
class UploadValidationError(Exception):
pass
def validate_upload(file) -> None:
"""
Validate an uploaded file against configured size and extension limits.
Raises UploadValidationError on violation.
"""
ext = os.path.splitext(file.name)[1].lower()
allowed = [e.lower() for e in getattr(settings, "ALLOWED_UPLOAD_EXTENSIONS", [])]
max_size = getattr(settings, "MAX_UPLOAD_SIZE_BYTES", 100 * 1024 * 1024)
if allowed and ext not in allowed:
raise UploadValidationError(
f"File type '{ext}' is not allowed. Permitted: {', '.join(allowed)}"
)
if file.size > max_size:
max_mb = max_size // (1024 * 1024)
raise UploadValidationError(
f"File size {file.size} bytes exceeds limit of {max_mb} MB."
)
def save_upload(file, org_id: str, uploader) -> "cms.models.MediaAsset":
"""Persist an uploaded file and create a MediaAsset record."""
import uuid
import mimetypes
from django.conf import settings as s
from .models import MediaAsset, ScanStatus
ext = os.path.splitext(file.name)[1].lower()
storage_root = getattr(s, "MEDIA_ROOT", "/tmp/media")
relative_path = os.path.join("uploads", str(org_id), f"{uuid.uuid4().hex}{ext}")
full_path = os.path.join(storage_root, relative_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "wb") as dest:
for chunk in file.chunks():
dest.write(chunk)
mime_type, _ = mimetypes.guess_type(file.name)
asset = MediaAsset.objects.create(
uploaded_by=uploader,
org_id=org_id,
file_name=file.name,
file_path=relative_path,
file_size=file.size,
mime_type=mime_type or "",
extension=ext,
scan_status=ScanStatus.PENDING,
)
# Enqueue async scan
from .tasks import scan_media_asset_task
scan_media_asset_task.delay(str(asset.id))
return asset