Merge scene saves as per-id deltas instead of clobbering
PUT now takes {changed, deleted} and merges per annotation id, so concurrent
edits to different annotations both survive; same-annotation edits are
last-write-wins by arrival. No version/locking. No-op saves don't write a
revision. Tests cover merge, LWW, stale-client safety, attribution
(server-stamped/spoof-resistant, creator preserved), revisions, and access.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
36054ac329
commit
60fb4df795
2 changed files with 115 additions and 9 deletions
|
|
@ -45,19 +45,31 @@ def scene(request, pk):
|
|||
if request.method == "GET":
|
||||
return JsonResponse({"fps": project.fps, "scene": project.scene})
|
||||
if request.method == "PUT":
|
||||
# A delta, not the whole scene: {changed: {gid: annotation}, deleted: [gid]}.
|
||||
# Merging per-id means two users editing different annotations both land
|
||||
# (no clobber); same-annotation edits are last-write-wins by arrival.
|
||||
data = json.loads(request.body or "{}")
|
||||
changed = data.get("changed") or {}
|
||||
deleted = data.get("deleted") or []
|
||||
summary = None
|
||||
if "scene" in data:
|
||||
project.scene, summary, _ = apply_attribution(
|
||||
project.scene, data["scene"], request.user.get_username(),
|
||||
if changed or deleted:
|
||||
groups = dict((project.scene or {}).get("groups", {}))
|
||||
groups.update(changed)
|
||||
for gid in deleted:
|
||||
groups.pop(gid, None)
|
||||
merged = dict(project.scene or {}, groups=groups)
|
||||
project.scene, summary, counts = apply_attribution(
|
||||
project.scene, merged, request.user.get_username(),
|
||||
timezone.now().isoformat())
|
||||
Revision.objects.create(project=project, user=request.user,
|
||||
summary=summary,
|
||||
annotations=annotation_layer(project.scene))
|
||||
if any(counts.values()): # don't log a no-op save
|
||||
Revision.objects.create(project=project, user=request.user,
|
||||
summary=summary,
|
||||
annotations=annotation_layer(project.scene))
|
||||
if "fps" in data:
|
||||
project.fps = data["fps"]
|
||||
project.save(update_fields=["scene", "fps", "updated"])
|
||||
return JsonResponse({"ok": True, "updated": project.updated, "summary": summary})
|
||||
return JsonResponse({"ok": True, "updated": project.updated, "summary": summary,
|
||||
"annotations": annotation_layer(project.scene)})
|
||||
return HttpResponseNotAllowed(["GET", "PUT"])
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue