feat: upload a script PDF from the script pane

Backend: POST /api/projects/<id>/script/ (owner/collaborator) saves the script
FileField and returns the refreshed detail. Frontend: when a project has no
script, the script pane shows a PDF file picker that uploads on selection; the
returned detail carries the new URL so the pane renders it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-30 01:09:16 -04:00
parent 9c4b56dbc6
commit b05b440922
6 changed files with 46 additions and 2 deletions

View file

@ -9,5 +9,6 @@ urlpatterns = [
path("projects/", views.projects),
path("projects/<int:pk>/", views.project),
path("projects/<int:pk>/scene/", views.scene),
path("projects/<int:pk>/script/", views.script),
path("projects/<int:pk>/revisions/", views.revisions),
]

View file

@ -174,6 +174,22 @@ def scene(request, pk):
return HttpResponseNotAllowed(["GET", "PUT"])
@csrf_exempt
def script(request, pk):
"""POST (owner/collaborator) — upload or replace the project's script PDF."""
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
if (resp := _auth(request)):
return resp
p = _editable(request, pk)
f = request.FILES.get("script")
if not f:
return JsonResponse({"detail": "script file is required"}, status=400)
p.script = f
p.save(update_fields=["script", "updated"])
return JsonResponse(_meta(request, p))
def revisions(request, pk):
"""GET /api/projects/<pk>/revisions/ — the save history (who/when/what)."""
if (resp := _auth(request)):

View file

@ -12,6 +12,7 @@
:net-error nil ; backend unreachable (fetch-me / logout)
:projects-error nil ; project-list fetch failure
:save-error nil ; annotation save/delete failure
:script-error nil ; script PDF upload failure
:fps (/ 24000 1001)
;; the whole scene graph (see tl.scene). Seeded from OTIO at load.

View file

@ -253,6 +253,22 @@
(rf/reg-event-db ::set-pane (fn [db [_ p]] (assoc-in db [:view :pane] p)))
;; upload (or replace) the project's script PDF; on success the refreshed detail
;; carries the new script URL so the script pane renders it.
(rf/reg-event-fx ::upload-script
(fn [{:keys [db]} [_ file]]
(let [id (get-in db [:project :id])
fd (doto (js/FormData.) (.append "script" file))]
{:db (assoc db :script-error nil)
:http-xhrio (api/upload (str "/api/projects/" id "/script/") fd
{:on-success [::script-uploaded]
:on-failure [::script-upload-error]})})))
(rf/reg-event-db ::script-uploaded
(fn [db [_ detail]] (assoc db :project detail :script-error nil)))
(rf/reg-event-db ::script-upload-error
(fn [db [_ failure]]
(assoc db :script-error (api/error-message failure "Script upload failed."))))
;; Script highlighting is an edit-mode sub-task: "Add script annotation" arms it
;; for the annotation being edited (the draft) and jumps to the Script tab; the
;; rects land on the draft's :script and ride its save. ✓ keeps, ✕ restores the

View file

@ -23,6 +23,7 @@
(rf/reg-sub ::net-error (fn [db] (:net-error db)))
(rf/reg-sub ::projects-error (fn [db] (:projects-error db)))
(rf/reg-sub ::save-error (fn [db] (:save-error db)))
(rf/reg-sub ::script-error (fn [db] (:script-error db)))
(rf/reg-sub ::load-error (fn [db] (get-in db [:load :error])))
(rf/reg-sub ::authed? (fn [db] (boolean (get-in db [:auth :authenticated]))))
(rf/reg-sub ::username (fn [db] (get-in db [:auth :username])))

View file

@ -851,6 +851,8 @@
anns @(rf/subscribe [::subs/annotations])
hl @(rf/subscribe [::subs/highlighting])
active @(rf/subscribe [::subs/active-annotation])
authed? @(rf/subscribe [::subs/authed?])
script-err @(rf/subscribe [::subs/script-error])
armed (:gid hl)
hl-name (some #(when (= (:id %) armed) (:name %)) anns)]
(when (and url (exists? js/pdfjsLib) (not= url @loaded))
@ -877,8 +879,15 @@
[:button.hl-ok {:title "Done" :on-click #(rf/dispatch [::events/finish-highlighting])} "✓"]
[:button.hl-cancel {:title "Cancel" :on-click #(rf/dispatch [::events/cancel-highlighting])} "✕"]]]
[:div.script-hint
(if url "Playing follows the script to each note. Edit an annotation → “Add script annotation” to link a passage."
"No script uploaded for this project.")])
(cond
url "Playing follows the script to each note. Edit an annotation → “Add script annotation” to link a passage."
authed? [:span.script-upload
[:label.file-row "Upload a script (PDF) "
[:input {:type "file" :accept ".pdf,application/pdf"
:on-change #(when-let [f (-> % .-target .-files (aget 0))]
(rf/dispatch [::events/upload-script f]))}]]
(when script-err [:span.err " " script-err])]
:else "No script uploaded for this project.")])
[:div.script-pages {:ref #(reset! el %)}
(if (seq @pages)
(doall (for [[n {:keys [pg w h]}] @pages]