[OCaml] High Intensity Training Online
fix Prevent feedback submission races
Lock each feedback form after its first score or action so rapid clicks cannot schedule duplicate stale submissions. If an intercepted feedback response cannot replace the app shell, return to /logbook instead of navigating GET /feedback, which has no GET route. Assert stale steps redirect to the logbook.
Changed files
lib/web/pages.ml
@@ -1191,6 +1191,7 @@
1191
1191
action Routes.feedback;
1192
1192
post_form;
1193
1193
class_ "feedback-form";
1194
Added:
Dream_html.attr "data-hito-feedback-form";
1194
1195
Dream_html.attr "data-hito-app-form";
1195
1196
]
1196
1197
[ tag "fieldset" [] body ]
lib/web/workout_client.ml
@@ -275,6 +275,10 @@
275
275
let workout =
276
276
Js.to_bool (form##hasAttribute (Js.string "data-hito-workout-form"))
277
277
in
278
Added:
let feedback =
279
Added:
Js.to_bool
280
Added:
(form##hasAttribute (Js.string "data-hito-feedback-form"))
281
Added:
in
278
282
let submit =
279
283
match
280
284
Js.Opt.to_option
@@ -317,7 +321,7 @@
317
321
push_url response.url;
318
322
Lwt.return_unit)
319
323
else (
320
Removed:
fallback action;
324
Added:
fallback (if feedback then "/logbook" else action);
321
325
Lwt.return_unit)));
322
326
Js._false)
323
327
@@ -398,17 +402,30 @@
398
402
| None -> Js._true
399
403
| Some form -> (
400
404
match
401
Removed:
Js.Opt.to_option (action##getAttribute (Js.string "value"))
405
Added:
Js.Opt.to_option
406
Added:
(form##getAttribute
407
Added:
(Js.string "data-hito-feedback-pending"))
402
408
with
403
Removed:
| None -> Js._true
404
Removed:
| Some value -> (
409
Added:
| Some _ ->
410
Added:
Dom.preventDefault event;
411
Added:
Js._false
412
Added:
| None -> (
405
413
match
406
Removed:
query_one form "[data-hito-feedback-action-value]"
414
Added:
Js.Opt.to_option
415
Added:
(action##getAttribute (Js.string "value"))
407
416
with
408
417
| None -> Js._true
409
Removed:
| Some hidden ->
410
Removed:
hidden##setAttribute (Js.string "value") value;
411
Removed:
Js._true)))))
418
Added:
| Some value -> (
419
Added:
match
420
Added:
query_one form "[data-hito-feedback-action-value]"
421
Added:
with
422
Added:
| None -> Js._true
423
Added:
| Some hidden ->
424
Added:
form##setAttribute
425
Added:
(Js.string "data-hito-feedback-pending")
426
Added:
(Js.string "");
427
Added:
hidden##setAttribute (Js.string "value") value;
428
Added:
Js._true))))))
412
429
413
430
(* Selecting a score advances to the next factor without a separate Next
414
431
action. Handle click rather than change so selecting the current score again
@@ -421,17 +438,27 @@
421
438
match closest "form" choice with
422
439
| None -> Js._true
423
440
| Some form -> (
424
Removed:
match query_one form "[data-hito-feedback-action-value]" with
425
Removed:
| None -> Js._true
426
Removed:
| Some hidden ->
427
Removed:
ignore
428
Removed:
(Dom_html.window##setTimeout
429
Removed:
(Js.wrap_callback (fun () ->
430
Removed:
hidden##setAttribute (Js.string "value")
431
Removed:
(Js.string "next");
432
Removed:
ignore (Js.Unsafe.meth_call form "requestSubmit" [||])))
433
Removed:
(Js.number_of_float 0.));
434
Removed:
Js._true))
441
Added:
match
442
Added:
Js.Opt.to_option
443
Added:
(form##getAttribute (Js.string "data-hito-feedback-pending"))
444
Added:
with
445
Added:
| Some _ -> Js._false
446
Added:
| None -> (
447
Added:
match query_one form "[data-hito-feedback-action-value]" with
448
Added:
| None -> Js._true
449
Added:
| Some hidden ->
450
Added:
form##setAttribute
451
Added:
(Js.string "data-hito-feedback-pending")
452
Added:
(Js.string "");
453
Added:
ignore
454
Added:
(Dom_html.window##setTimeout
455
Added:
(Js.wrap_callback (fun () ->
456
Added:
hidden##setAttribute (Js.string "value")
457
Added:
(Js.string "next");
458
Added:
ignore
459
Added:
(Js.Unsafe.meth_call form "requestSubmit" [||])))
460
Added:
(Js.number_of_float 0.));
461
Added:
Js._true)))
435
462
436
463
(* The exercise dropdown navigates the moment its selection changes. It lives in
437
464
a GET form ([data-hito-exercise-form]) that submits to the workout path. The
test/test_web.ml
@@ -1586,6 +1586,9 @@
1586
1586
in
1587
1587
Alcotest.(check int)
1588
1588
"mismatched step redirects" 303 (status mismatched);
1589
Added:
Alcotest.(check string)
1590
Added:
"stale feedback returns to the logbook" "/logbook"
1591
Added:
(location mismatched);
1589
1592
(* The mismatched post advanced nothing and stored no report. *)
1590
1593
Alcotest.(check int)
1591
1594
"no feedback stored" 0