2026-06-28 23:45:49 -04:00
|
|
|
import json
|
|
|
|
|
|
2026-06-29 00:12:13 -04:00
|
|
|
from django.db.models import Q
|
2026-06-28 23:45:49 -04:00
|
|
|
from django.http import FileResponse, HttpResponseNotAllowed, JsonResponse
|
|
|
|
|
from django.shortcuts import get_object_or_404
|
2026-06-29 00:12:13 -04:00
|
|
|
from django.utils import timezone
|
2026-06-28 23:45:49 -04:00
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
|
|
2026-06-29 00:12:13 -04:00
|
|
|
from .attribution import annotation_layer, apply_attribution
|
|
|
|
|
from .models import Project, Revision
|
2026-06-28 23:45:49 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _auth(request):
|
|
|
|
|
"""None if logged in, else a 401 JSON response (so fetch() doesn't get a
|
|
|
|
|
login redirect)."""
|
|
|
|
|
if not request.user.is_authenticated:
|
|
|
|
|
return JsonResponse({"detail": "authentication required"}, status=401)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
2026-06-29 00:12:13 -04:00
|
|
|
def _visible(request):
|
|
|
|
|
"""Projects the user owns or collaborates on."""
|
|
|
|
|
return Project.objects.filter(
|
|
|
|
|
Q(owner=request.user) | Q(collaborators=request.user)).distinct()
|
|
|
|
|
|
|
|
|
|
|
2026-06-28 23:45:49 -04:00
|
|
|
def _owned(request, pk):
|
2026-06-29 00:12:13 -04:00
|
|
|
return get_object_or_404(_visible(request), pk=pk)
|
2026-06-28 23:45:49 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def projects(request):
|
2026-06-29 00:12:13 -04:00
|
|
|
"""GET /api/projects/ — the projects the user owns or collaborates on."""
|
2026-06-28 23:45:49 -04:00
|
|
|
if (resp := _auth(request)):
|
|
|
|
|
return resp
|
2026-06-29 00:12:13 -04:00
|
|
|
rows = _visible(request).values("id", "name", "fps", "updated", "owner__username")
|
2026-06-28 23:45:49 -04:00
|
|
|
return JsonResponse(list(rows), safe=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@csrf_exempt # dev convenience: session-auth API without a CSRF token round-trip
|
|
|
|
|
def scene(request, pk):
|
|
|
|
|
"""GET/PUT /api/projects/<pk>/scene/ — read or replace the timeline scene."""
|
|
|
|
|
if (resp := _auth(request)):
|
|
|
|
|
return resp
|
|
|
|
|
project = _owned(request, pk)
|
|
|
|
|
if request.method == "GET":
|
|
|
|
|
return JsonResponse({"fps": project.fps, "scene": project.scene})
|
|
|
|
|
if request.method == "PUT":
|
|
|
|
|
data = json.loads(request.body or "{}")
|
2026-06-29 00:12:13 -04:00
|
|
|
summary = None
|
2026-06-28 23:45:49 -04:00
|
|
|
if "scene" in data:
|
2026-06-29 00:12:13 -04:00
|
|
|
project.scene, summary, _ = apply_attribution(
|
|
|
|
|
project.scene, data["scene"], request.user.get_username(),
|
|
|
|
|
timezone.now().isoformat())
|
|
|
|
|
Revision.objects.create(project=project, user=request.user,
|
|
|
|
|
summary=summary,
|
|
|
|
|
annotations=annotation_layer(project.scene))
|
2026-06-28 23:45:49 -04:00
|
|
|
if "fps" in data:
|
|
|
|
|
project.fps = data["fps"]
|
|
|
|
|
project.save(update_fields=["scene", "fps", "updated"])
|
2026-06-29 00:12:13 -04:00
|
|
|
return JsonResponse({"ok": True, "updated": project.updated, "summary": summary})
|
2026-06-28 23:45:49 -04:00
|
|
|
return HttpResponseNotAllowed(["GET", "PUT"])
|
|
|
|
|
|
|
|
|
|
|
2026-06-29 00:12:13 -04:00
|
|
|
def revisions(request, pk):
|
|
|
|
|
"""GET /api/projects/<pk>/revisions/ — the save history (who/when/what)."""
|
|
|
|
|
if (resp := _auth(request)):
|
|
|
|
|
return resp
|
|
|
|
|
project = _owned(request, pk)
|
|
|
|
|
rows = project.revisions.values("id", "user__username", "created", "summary")
|
|
|
|
|
return JsonResponse(list(rows), safe=False)
|
|
|
|
|
|
|
|
|
|
|
2026-06-28 23:45:49 -04:00
|
|
|
def otio(request, pk):
|
|
|
|
|
"""GET /api/projects/<pk>/otio/ — serve the uploaded OTIO file."""
|
|
|
|
|
if (resp := _auth(request)):
|
|
|
|
|
return resp
|
|
|
|
|
project = _owned(request, pk)
|
|
|
|
|
if not project.otio:
|
|
|
|
|
return JsonResponse({"detail": "no otio uploaded"}, status=404)
|
|
|
|
|
return FileResponse(project.otio.open("rb"), content_type="application/json")
|