[OCaml] High Intensity Training Online
feat add application feedback actions
Changed files
lib/app/memory_repo.ml
@@ -324,3 +324,45 @@
324
324
t.app_feedback;
325
325
Lwt.return true
326
326
end
327
Added:
328
Added:
let update_app_feedback t ~author id ~message =
329
Added:
let author_id = Trainee.id_to_string author in
330
Added:
let updated = ref false in
331
Added:
t.app_feedback <-
332
Added:
List.map
333
Added:
(fun (owner, report) ->
334
Added:
if
335
Added:
String.equal (Trainee.id_to_string owner) author_id
336
Added:
&& String.equal
337
Added:
(Repository.app_feedback_id_to_string
338
Added:
report.Repository.feedback_id)
339
Added:
(Repository.app_feedback_id_to_string id)
340
Added:
then begin
341
Added:
updated := true;
342
Added:
(owner, { report with message })
343
Added:
end
344
Added:
else (owner, report))
345
Added:
t.app_feedback;
346
Added:
Lwt.return !updated
347
Added:
348
Added:
let delete_app_feedback t ~author id =
349
Added:
let author_id = Trainee.id_to_string author in
350
Added:
let owned (owner, report) =
351
Added:
String.equal (Trainee.id_to_string owner) author_id
352
Added:
&& String.equal
353
Added:
(Repository.app_feedback_id_to_string report.Repository.feedback_id)
354
Added:
(Repository.app_feedback_id_to_string id)
355
Added:
in
356
Added:
let removed = List.exists owned t.app_feedback in
357
Added:
if removed then begin
358
Added:
t.app_feedback <- List.filter (fun item -> not (owned item)) t.app_feedback;
359
Added:
t.app_feedback_votes <-
360
Added:
List.filter
361
Added:
(fun (feedback_id, _) ->
362
Added:
not
363
Added:
(String.equal
364
Added:
(Repository.app_feedback_id_to_string feedback_id)
365
Added:
(Repository.app_feedback_id_to_string id)))
366
Added:
t.app_feedback_votes
367
Added:
end;
368
Added:
Lwt.return removed
lib/app/repository.ml
@@ -73,4 +73,10 @@
73
73
74
74
val upvote_app_feedback :
75
75
t -> voter:Trainee.id -> app_feedback_id -> bool Lwt.t
76
Added:
77
Added:
val update_app_feedback :
78
Added:
t -> author:Trainee.id -> app_feedback_id -> message:string -> bool Lwt.t
79
Added:
80
Added:
val delete_app_feedback :
81
Added:
t -> author:Trainee.id -> app_feedback_id -> bool Lwt.t
76
82
end
lib/app/repository.mli
@@ -124,4 +124,12 @@
124
124
val upvote_app_feedback :
125
125
t -> voter:Trainee.id -> app_feedback_id -> bool Lwt.t
126
126
(** Add one vote from another trainee. [false] means no vote was added. *)
127
Added:
128
Added:
val update_app_feedback :
129
Added:
t -> author:Trainee.id -> app_feedback_id -> message:string -> bool Lwt.t
130
Added:
(** Update a report only when [author] owns it. *)
131
Added:
132
Added:
val delete_app_feedback :
133
Added:
t -> author:Trainee.id -> app_feedback_id -> bool Lwt.t
134
Added:
(** Remove a report and its votes only when [author] owns it. *)
127
135
end
lib/app/service.ml
@@ -31,7 +31,7 @@
31
31
| Rejected e -> Evidence.Workout.pp_error ppf e
32
32
33
33
type edit_error = Unknown_workout | Rejected_edit of Evidence.Workout.error
34
Removed:
type app_feedback_error = [ `Empty_message ]
34
Added:
type app_feedback_error = [ `Empty_message | `Unknown_feedback ]
35
35
36
36
(* Shared helpers over a routine and a log, used by more than one use case. *)
37
37
@@ -271,6 +271,15 @@
271
271
272
272
let list t trainee = R.app_feedback t.repo ~viewer:trainee
273
273
let upvote t trainee id = R.upvote_app_feedback t.repo ~voter:trainee id
274
Added:
275
Added:
let edit t trainee id ~message =
276
Added:
if String.trim message = "" then Lwt.return (Error `Empty_message)
277
Added:
else
278
Added:
R.update_app_feedback t.repo ~author:trainee id ~message >|= function
279
Added:
| true -> Ok ()
280
Added:
| false -> Error `Unknown_feedback
281
Added:
282
Added:
let remove t trainee id = R.delete_app_feedback t.repo ~author:trainee id
274
283
end
275
284
276
285
(* Re-export the use cases as one flat service, matching Service.mli. *)
@@ -302,4 +311,6 @@
302
311
let record_app_feedback = App_feedback.record
303
312
let app_feedback = App_feedback.list
304
313
let upvote_app_feedback = App_feedback.upvote
314
Added:
let edit_app_feedback = App_feedback.edit
315
Added:
let remove_app_feedback = App_feedback.remove
305
316
end
lib/app/service.mli
@@ -196,8 +196,8 @@
196
196
197
197
(** {2 Application feedback} *)
198
198
199
Removed:
type app_feedback_error = [ `Empty_message ]
200
Removed:
(** Why an application feedback message was refused. *)
199
Added:
type app_feedback_error = [ `Empty_message | `Unknown_feedback ]
200
Added:
(** Why an application feedback operation was refused. *)
201
201
202
202
val record_app_feedback :
203
203
t ->
@@ -213,4 +213,16 @@
213
213
val upvote_app_feedback :
214
214
t -> Trainee.id -> Repository.app_feedback_id -> bool Lwt.t
215
215
(** Try to add the trainee's vote to another trainee's feedback. *)
216
Added:
217
Added:
val edit_app_feedback :
218
Added:
t ->
219
Added:
Trainee.id ->
220
Added:
Repository.app_feedback_id ->
221
Added:
message:string ->
222
Added:
(unit, app_feedback_error) result Lwt.t
223
Added:
(** Edit one of the trainee's own messages. *)
224
Added:
225
Added:
val remove_app_feedback :
226
Added:
t -> Trainee.id -> Repository.app_feedback_id -> bool Lwt.t
227
Added:
(** Remove one of the trainee's own messages. *)
216
228
end
lib/app/sqlite_repo.ml
@@ -119,6 +119,19 @@
119
119
(t3 string int string ->. unit)
120
120
"INSERT INTO app_feedback_vote (feedback_trainee_id, feedback_seq, \
121
121
voter_id) VALUES (?, ?, ?)"
122
Added:
123
Added:
let update_app_feedback =
124
Added:
(t3 string string int ->. unit)
125
Added:
"UPDATE app_feedback SET message = ? WHERE trainee_id = ? AND seq = ?"
126
Added:
127
Added:
let delete_feedback_votes =
128
Added:
(t2 string int ->. unit)
129
Added:
"DELETE FROM app_feedback_vote WHERE feedback_trainee_id = ? AND \
130
Added:
feedback_seq = ?"
131
Added:
132
Added:
let delete_app_feedback =
133
Added:
(t2 string int ->. unit)
134
Added:
"DELETE FROM app_feedback WHERE trainee_id = ? AND seq = ?"
122
135
end
123
136
124
137
(* --- pool helper --- *)
@@ -428,3 +441,32 @@
428
441
run t (fun (module Db : Caqti_lwt.CONNECTION) ->
429
442
Db.exec Q.insert_vote (trainee, seq, voter))
430
443
>|= fun () -> true))
444
Added:
445
Added:
let update_app_feedback t ~author id ~message =
446
Added:
match split_app_feedback_id id with
447
Added:
| None -> Lwt.return false
448
Added:
| Some (trainee, seq) -> (
449
Added:
run t (fun (module Db : Caqti_lwt.CONNECTION) ->
450
Added:
Db.find_opt Q.feedback_owner (trainee, seq))
451
Added:
>>= function
452
Added:
| Some owner when String.equal owner (Trainee.id_to_string author) ->
453
Added:
run t (fun (module Db : Caqti_lwt.CONNECTION) ->
454
Added:
Db.exec Q.update_app_feedback (message, trainee, seq))
455
Added:
>|= fun () -> true
456
Added:
| _ -> Lwt.return false)
457
Added:
458
Added:
let delete_app_feedback t ~author id =
459
Added:
match split_app_feedback_id id with
460
Added:
| None -> Lwt.return false
461
Added:
| Some (trainee, seq) -> (
462
Added:
run t (fun (module Db : Caqti_lwt.CONNECTION) ->
463
Added:
Db.find_opt Q.feedback_owner (trainee, seq))
464
Added:
>>= function
465
Added:
| Some owner when String.equal owner (Trainee.id_to_string author) ->
466
Added:
run t (fun (module Db : Caqti_lwt.CONNECTION) ->
467
Added:
Db.exec Q.delete_feedback_votes (trainee, seq))
468
Added:
>>= fun () ->
469
Added:
run t (fun (module Db : Caqti_lwt.CONNECTION) ->
470
Added:
Db.exec Q.delete_app_feedback (trainee, seq))
471
Added:
>|= fun () -> true
472
Added:
| _ -> Lwt.return false)
lib/web/assets/hito.css
@@ -915,3 +915,29 @@
915
915
color: var(--paper-raised);
916
916
background: var(--oxblood);
917
917
}
918
Added:
919
Added:
.app-feedback-actions {
920
Added:
display: flex;
921
Added:
flex-wrap: wrap;
922
Added:
gap: 0.6rem;
923
Added:
align-items: flex-start;
924
Added:
}
925
Added:
.app-feedback-edit-form {
926
Added:
display: grid;
927
Added:
gap: 0.45rem;
928
Added:
flex: 1 1 24rem;
929
Added:
}
930
Added:
.app-feedback-edit-form textarea {
931
Added:
width: 100%;
932
Added:
border: 1px solid var(--rule-strong);
933
Added:
border-radius: var(--radius);
934
Added:
padding: 0.65rem;
935
Added:
color: var(--ink);
936
Added:
background: var(--paper-raised);
937
Added:
font: 400 1rem/1.45 var(--sans);
938
Added:
resize: vertical;
939
Added:
}
940
Added:
.app-feedback-remove-form input,
941
Added:
.app-feedback-vote input {
942
Added:
min-height: 2.75rem;
943
Added:
}
lib/web/handlers.ml
@@ -590,7 +590,10 @@
590
590
"Feedback submitted."
591
591
| Error `Empty_message ->
592
592
redirect_with_flash_raw request "/app-feedback?tab=write"
593
Removed:
Present.app_feedback_empty)
593
Added:
Present.app_feedback_empty
594
Added:
| Error `Unknown_feedback ->
595
Added:
redirect_with_flash_raw request "/app-feedback?tab=submitted"
596
Added:
"That feedback is no longer available.")
594
597
595
598
let upvote t trainee request id =
596
599
guard_csrf request >>= function
@@ -604,6 +607,39 @@
604
607
redirect_with_flash_raw request "/app-feedback?tab=submitted"
605
608
(if added then "Vote recorded." else "Vote was not added.")
606
609
610
Added:
let edit t trainee request id =
611
Added:
decode_form Decode.app_feedback request >>= function
612
Added:
| Error _ ->
613
Added:
redirect_with_flash_raw request "/app-feedback?tab=submitted"
614
Added:
Present.form_invalid
615
Added:
| Ok message -> (
616
Added:
Service.edit_app_feedback t.service trainee.Trainee.id
617
Added:
(Repository.app_feedback_id id)
618
Added:
~message
619
Added:
>>= function
620
Added:
| Ok () ->
621
Added:
redirect_with_flash_raw request "/app-feedback?tab=submitted"
622
Added:
"Feedback updated."
623
Added:
| Error `Empty_message ->
624
Added:
redirect_with_flash_raw request "/app-feedback?tab=submitted"
625
Added:
Present.app_feedback_empty
626
Added:
| Error `Unknown_feedback ->
627
Added:
redirect_with_flash_raw request "/app-feedback?tab=submitted"
628
Added:
"That feedback is no longer available.")
629
Added:
630
Added:
let remove t trainee request id =
631
Added:
guard_csrf request >>= function
632
Added:
| Error _ ->
633
Added:
redirect_with_flash_raw request "/app-feedback?tab=submitted"
634
Added:
Present.form_invalid
635
Added:
| Ok () ->
636
Added:
Service.remove_app_feedback t.service trainee.Trainee.id
637
Added:
(Repository.app_feedback_id id)
638
Added:
>>= fun removed ->
639
Added:
redirect_with_flash_raw request "/app-feedback?tab=submitted"
640
Added:
(if removed then "Feedback removed."
641
Added:
else "That feedback is no longer available.")
642
Added:
607
643
let routes t =
608
644
[
609
645
Dream_html.get Routes.app_feedback (fun request ->
@@ -612,6 +648,10 @@
612
648
authenticated t request (fun trainee -> submit t trainee request));
613
649
Dream_html.post Routes.upvote_app_feedback (fun request id ->
614
650
authenticated t request (fun trainee -> upvote t trainee request id));
651
Added:
Dream_html.post Routes.edit_app_feedback (fun request id ->
652
Added:
authenticated t request (fun trainee -> edit t trainee request id));
653
Added:
Dream_html.post Routes.remove_app_feedback (fun request id ->
654
Added:
authenticated t request (fun trainee -> remove t trainee request id));
615
655
]
616
656
end
617
657
lib/web/pages.ml
@@ -1381,6 +1381,64 @@
1381
1381
[ type_ "submit"; Dream_html.string_attr "value" "%s" label ];
1382
1382
]
1383
1383
1384
Added:
let app_feedback_edit_form request report =
1385
Added:
tag "form"
1386
Added:
[
1387
Added:
action Routes.edit_app_feedback
1388
Added:
(Repository.app_feedback_id_to_string report.Repository.feedback_id);
1389
Added:
post_form;
1390
Added:
class_ "app-feedback-edit-form";
1391
Added:
]
1392
Added:
[
1393
Added:
Dream_html.csrf_tag request;
1394
Added:
tag "label"
1395
Added:
[
1396
Added:
Dream_html.string_attr "for" "app-feedback-edit-%s"
1397
Added:
(Repository.app_feedback_id_to_string report.Repository.feedback_id);
1398
Added:
]
1399
Added:
[ txt "Edit feedback" ];
1400
Added:
tag "textarea"
1401
Added:
[
1402
Added:
name "message";
1403
Added:
Dream_html.string_attr "id" "app-feedback-edit-%s"
1404
Added:
(Repository.app_feedback_id_to_string report.Repository.feedback_id);
1405
Added:
Dream_html.string_attr "rows" "4";
1406
Added:
required;
1407
Added:
]
1408
Added:
[ txt "%s" report.Repository.message ];
1409
Added:
void "input" [ type_ "submit"; value "Save edit" ];
1410
Added:
]
1411
Added:
1412
Added:
let app_feedback_remove_form request report =
1413
Added:
tag "form"
1414
Added:
[
1415
Added:
action Routes.remove_app_feedback
1416
Added:
(Repository.app_feedback_id_to_string report.Repository.feedback_id);
1417
Added:
post_form;
1418
Added:
class_ "app-feedback-remove-form";
1419
Added:
]
1420
Added:
[
1421
Added:
Dream_html.csrf_tag request;
1422
Added:
void "input" [ type_ "submit"; value "Remove feedback" ];
1423
Added:
]
1424
Added:
1425
Added:
let app_feedback_actions request ~trainee report =
1426
Added:
let own =
1427
Added:
String.equal report.Repository.author
1428
Added:
(Trainee.username_to_string trainee.Trainee.username)
1429
Added:
in
1430
Added:
if own then
1431
Added:
tag "div"
1432
Added:
[ class_ "app-feedback-actions" ]
1433
Added:
[
1434
Added:
app_feedback_edit_form request report;
1435
Added:
app_feedback_remove_form request report;
1436
Added:
]
1437
Added:
else
1438
Added:
tag "div"
1439
Added:
[ class_ "app-feedback-actions" ]
1440
Added:
[ app_feedback_vote_form request ~trainee report ]
1441
Added:
1384
1442
let app_feedback request ?(logging = false) ~trainee ?(tab = `Write) ?error
1385
1443
reports =
1386
1444
let panel =
@@ -1424,7 +1482,7 @@
1424
1482
tag "p"
1425
1483
[ class_ "app-feedback-upvotes" ]
1426
1484
[ txt "Upvotes: %d" report.upvotes ];
1427
Removed:
app_feedback_vote_form request ~trainee report;
1485
Added:
app_feedback_actions request ~trainee report;
1428
1486
])
1429
1487
reports));
1430
1488
]
lib/web/routes.ml
@@ -20,6 +20,8 @@
20
20
let%path app_feedback = "/app-feedback"
21
21
let%path submit_app_feedback = "/app-feedback/submit"
22
22
let%path upvote_app_feedback = "/app-feedback/%s/upvote"
23
Added:
let%path edit_app_feedback = "/app-feedback/%s/edit"
24
Added:
let%path remove_app_feedback = "/app-feedback/%s/remove"
23
25
let%path feedback = "/feedback"
24
26
let%path record = "/logbook/%s"
25
27
let%path record_slot = "/logbook/%s/slots/%d"
test/test_service.ml
@@ -628,6 +628,8 @@
628
628
(S.record_app_feedback s t ~submitted_at:(day 1) ~message:" \n ")
629
629
with
630
630
| Error `Empty_message -> ()
631
Added:
| Error `Unknown_feedback ->
632
Added:
Alcotest.fail "unexpected unknown feedback error"
631
633
| Ok _ -> Alcotest.fail "expected a blank message to be refused" );
632
634
( "app feedback is stored newest first and visible globally",
633
635
`Quick,
@@ -716,6 +718,51 @@
716
718
Alcotest.(check int) "upvote count" 1 voted_bob.Repository.upvotes;
717
719
Alcotest.(check bool)
718
720
"viewer vote state" true voted_bob.Repository.viewer_upvoted );
721
Added:
( "application feedback CRUD is owner-scoped",
722
Added:
`Quick,
723
Added:
fun () ->
724
Added:
let s, alice = fixture () in
725
Added:
let bob =
726
Added:
(ok (run (S.register s ~username:"bobby" ~password:"heavyduty1")))
727
Added:
.Trainee.id
728
Added:
in
729
Added:
let report =
730
Added:
ok
731
Added:
(run
732
Added:
(S.record_app_feedback s alice ~submitted_at:(day 1)
733
Added:
~message:"Original"))
734
Added:
in
735
Added:
(match
736
Added:
run
737
Added:
(S.edit_app_feedback s bob report.Repository.feedback_id
738
Added:
~message:"Not yours")
739
Added:
with
740
Added:
| Error `Unknown_feedback -> ()
741
Added:
| _ -> Alcotest.fail "another trainee edited the report");
742
Added:
(match
743
Added:
run
744
Added:
(S.edit_app_feedback s alice report.Repository.feedback_id
745
Added:
~message:"Edited")
746
Added:
with
747
Added:
| Ok () -> ()
748
Added:
| _ -> Alcotest.fail "owner edit failed");
749
Added:
let edited =
750
Added:
List.find
751
Added:
(fun current ->
752
Added:
String.equal
753
Added:
(Repository.app_feedback_id_to_string
754
Added:
current.Repository.feedback_id)
755
Added:
(Repository.app_feedback_id_to_string
756
Added:
report.Repository.feedback_id))
757
Added:
(run (S.app_feedback s bob))
758
Added:
in
759
Added:
Alcotest.(check string) "edited message" "Edited" edited.message;
760
Added:
Alcotest.(check bool)
761
Added:
"owner remove succeeds" true
762
Added:
(run (S.remove_app_feedback s alice report.Repository.feedback_id));
763
Added:
Alcotest.(check int)
764
Added:
"removed from the list" 0
765
Added:
(List.length (run (S.app_feedback s bob))) );
719
766
]
720
767
721
768
let suite =
test/test_sqlite_repo.ml
@@ -397,6 +397,72 @@
397
397
Alcotest.(check bool)
398
398
"owner does not see a self vote" false
399
399
own_view.Repository.viewer_upvoted) );
400
Added:
( "application feedback CRUD survives and enforces ownership",
401
Added:
`Quick,
402
Added:
fun () ->
403
Added:
let path, uri = temp_uri () in
404
Added:
Fun.protect
405
Added:
~finally:(fun () -> cleanup path)
406
Added:
(fun () ->
407
Added:
let report_id =
408
Added:
let repo = connect uri in
409
Added:
let s = S.make ~repo in
410
Added:
let alice =
411
Added:
(ok
412
Added:
(run (S.register s ~username:"alice" ~password:"heavyduty1")))
413
Added:
.Trainee.id
414
Added:
in
415
Added:
let bob =
416
Added:
(ok
417
Added:
(run (S.register s ~username:"bobby" ~password:"heavyduty1")))
418
Added:
.Trainee.id
419
Added:
in
420
Added:
let report =
421
Added:
ok
422
Added:
(run
423
Added:
(S.record_app_feedback s alice ~submitted_at:(day 1)
424
Added:
~message:"Original"))
425
Added:
in
426
Added:
(match
427
Added:
run
428
Added:
(S.edit_app_feedback s bob report.Repository.feedback_id
429
Added:
~message:"Not yours")
430
Added:
with
431
Added:
| Error `Unknown_feedback -> ()
432
Added:
| _ -> Alcotest.fail "another trainee edited the report");
433
Added:
(match
434
Added:
run
435
Added:
(S.edit_app_feedback s alice report.Repository.feedback_id
436
Added:
~message:"Edited")
437
Added:
with
438
Added:
| Ok () -> ()
439
Added:
| _ -> Alcotest.fail "owner edit failed");
440
Added:
report.Repository.feedback_id
441
Added:
in
442
Added:
let repo = connect uri in
443
Added:
let s = S.make ~repo in
444
Added:
let alice =
445
Added:
Option.get
446
Added:
(run
447
Added:
(S.authenticate s ~username:"alice" ~password:"heavyduty1"))
448
Added:
in
449
Added:
let edited =
450
Added:
List.find
451
Added:
(fun report ->
452
Added:
String.equal
453
Added:
(Repository.app_feedback_id_to_string
454
Added:
report.Repository.feedback_id)
455
Added:
(Repository.app_feedback_id_to_string report_id))
456
Added:
(run (S.app_feedback s alice.Trainee.id))
457
Added:
in
458
Added:
Alcotest.(check string)
459
Added:
"edited message persists" "Edited" edited.Repository.message;
460
Added:
Alcotest.(check bool)
461
Added:
"owner remove succeeds" true
462
Added:
(run (S.remove_app_feedback s alice.Trainee.id report_id));
463
Added:
Alcotest.(check int)
464
Added:
"removed report is absent" 0
465
Added:
(List.length (run (S.app_feedback s alice.Trainee.id)))) );
400
466
( "a username and password change survive a reconnect",
401
467
`Quick,
402
468
fun () ->
test/test_web.ml
@@ -1126,8 +1126,8 @@
1126
1126
(contains ~substring:"id=\"app-feedback-message\"" write_page
1127
1127
&& contains ~substring:"<textarea" write_page);
1128
1128
Alcotest.(check bool)
1129
Removed:
"posts to the app feedback endpoint" true
1130
Removed:
(contains ~substring:"action=\"/app-feedback/submit\"" write_page);
1129
Added:
"renders one primary submit action" true
1130
Added:
(contains ~substring:"value=\"Submit feedback\"" write_page);
1131
1131
let submitted_page = body (get c "/app-feedback?tab=submitted") in
1132
1132
Alcotest.(check bool)
1133
1133
"submitted tab is selected" true
@@ -1220,8 +1220,30 @@
1220
1220
(contains ~substring:"value=\"Upvoted\"" ranked);
1221
1221
let bob_page = body (get bob "/app-feedback?tab=submitted") in
1222
1222
Alcotest.(check bool)
1223
Removed:
"identifies the owner's feedback" true
1224
Removed:
(contains ~substring:"Your feedback" bob_page) );
1223
Added:
"shows owner actions" true
1224
Added:
(contains ~substring:"action=\"/app-feedback/t2:1/edit\"" bob_page
1225
Added:
&& contains ~substring:"action=\"/app-feedback/t2:1/remove\""
1226
Added:
bob_page);
1227
Added:
let edit_token = Option.get (csrf_token bob_page) in
1228
Added:
let edited =
1229
Added:
post bob "/app-feedback/t2:1/edit"
1230
Added:
[ ("dream.csrf", edit_token); ("message", "Edited report") ]
1231
Added:
in
1232
Added:
Alcotest.(check int) "edit redirects" 303 (status edited);
1233
Added:
let edited_page = body (get bob "/app-feedback?tab=submitted") in
1234
Added:
Alcotest.(check bool)
1235
Added:
"shows edited feedback" true
1236
Added:
(contains ~substring:"Edited report" edited_page);
1237
Added:
let remove_token = Option.get (csrf_token edited_page) in
1238
Added:
let removed =
1239
Added:
post bob "/app-feedback/t2:1/remove"
1240
Added:
[ ("dream.csrf", remove_token) ]
1241
Added:
in
1242
Added:
Alcotest.(check int) "remove redirects" 303 (status removed);
1243
Added:
let after_remove = body (get bob "/app-feedback?tab=submitted") in
1244
Added:
Alcotest.(check bool)
1245
Added:
"removes the owner's feedback" false
1246
Added:
(contains ~substring:"Edited report" after_remove) );
1225
1247
] );
1226
1248
( "web.profile",
1227
1249
[