1
1
(* This file is part of Learn-OCaml.
2
2
*
3
- * Copyright (C) 2019 OCaml Software Foundation.
3
+ * Copyright (C) 2019-2020 OCaml Software Foundation.
4
4
* Copyright (C) 2016-2018 OCamlPro.
5
5
*
6
6
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -434,30 +434,34 @@ let get_state_as_save_file ?(include_reports = false) () =
434
434
all_exercise_toplevel_histories = retrieve all_exercise_toplevel_histories;
435
435
}
436
436
437
- let rec sync_save token save_file =
437
+ let rec sync_save token save_file on_sync =
438
438
Server_caller. request (Learnocaml_api. Update_save (token, save_file))
439
439
>> = function
440
- | Ok save -> set_state_from_save_file ~token save; Lwt. return save
440
+ | Ok save ->
441
+ set_state_from_save_file ~token save;
442
+ on_sync () ;
443
+ Lwt. return save
441
444
| Error (`Not_found _ ) ->
442
445
Server_caller. request_exn
443
446
(Learnocaml_api. Create_token (" " , Some token, None )) >> = fun _token ->
444
447
assert (_token = token);
445
448
Server_caller. request_exn
446
449
(Learnocaml_api. Update_save (token, save_file)) >> = fun save ->
447
450
set_state_from_save_file ~token save;
451
+ on_sync () ;
448
452
Lwt. return save
449
453
| Error e ->
450
454
lwt_alert ~title: [% i" SYNC FAILED" ] [
451
455
H. p [H. txt [% i" Could not synchronise save with the server" ]];
452
456
H. code [H. txt (Server_caller. string_of_error e)];
453
457
] ~buttons: [
454
- [% i" Retry" ], (fun () -> sync_save token save_file);
455
- [% i" Ignore" ], (fun () -> Lwt. return save_file);
458
+ [% i" Retry" ], (fun () -> sync_save token save_file on_sync );
459
+ [% i" Ignore" ], (fun () -> Lwt. return save_file);
456
460
]
457
461
458
- let sync token = sync_save token (get_state_as_save_file () )
462
+ let sync token on_sync = sync_save token (get_state_as_save_file () ) on_sync
459
463
460
- let sync_exercise token ?answer ?editor id =
464
+ let sync_exercise token ?answer ?editor id on_sync =
461
465
let handle_serverless () =
462
466
(* save the text at least locally (but not the report & grade, that could
463
467
be misleading) *)
@@ -494,7 +498,7 @@ let sync_exercise token ?answer ?editor id =
494
498
} in
495
499
match token with
496
500
| Some token ->
497
- Lwt. catch (fun () -> sync_save token save_file)
501
+ Lwt. catch (fun () -> sync_save token save_file on_sync )
498
502
(fun e ->
499
503
handle_serverless () ;
500
504
raise e)
@@ -708,11 +712,72 @@ let mouseover_toggle_signal elt sigvalue setter =
708
712
in
709
713
Manip.Ev. onmouseover elt hdl
710
714
715
+ (*
716
+
717
+ If a user has made no change to a solution for the exercise [id]
718
+ for 180 seconds, [check_valid_editor_state id] ensures that there is
719
+ no more recent version of this solution in the server. If this is
720
+ the case, the user is asked if we should download this solution
721
+ from the server.
722
+
723
+ This function reduces the risk of an involuntary overwriting of a
724
+ student solution when the solution is open in several clients.
725
+
726
+ *)
727
+ let is_synchronized_with_server_callback = ref (fun () -> false )
728
+
729
+ let is_synchronized_with_server () = ! is_synchronized_with_server_callback ()
730
+
731
+ let check_valid_editor_state id =
732
+ let last_changed = ref (Unix. gettimeofday () ) in
733
+ fun update_content focus_back on_sync ->
734
+ let update_local_copy checking_time () =
735
+ let get_solution () =
736
+ Learnocaml_local_storage. (retrieve (exercise_state id)).Answer. solution in
737
+ try let mtime =
738
+ Learnocaml_local_storage. (retrieve (exercise_state id)).Answer. mtime in
739
+ if mtime > checking_time then begin
740
+ let buttons =
741
+ if is_synchronized_with_server () then
742
+ [
743
+ [% i " Fetch from server" ],
744
+ (fun () -> let solution = get_solution () in
745
+ Lwt. return (focus_back () ; update_content solution; on_sync () ));
746
+ [% i " Ignore & keep editing" ],
747
+ (fun () -> Lwt. return (focus_back () ));
748
+ ]
749
+ else
750
+ [
751
+ [% i " Ignore & keep editing" ],
752
+ (fun () -> Lwt. return (focus_back () ));
753
+ [% i " Fetch from server & overwrite" ],
754
+ (fun () -> let solution = get_solution () in
755
+ Lwt. return (focus_back () ; update_content solution; on_sync () ));
756
+ ]
757
+ in
758
+ lwt_alert ~title: " Question"
759
+ ~buttons
760
+ [ H. p [H. txt [% i " A more recent answer exists on the server. \
761
+ Do you want to fetch the new version?" ] ] ]
762
+ end else Lwt. return_unit
763
+ with
764
+ | Not_found -> Lwt. return ()
765
+ in
766
+ let now = Unix. gettimeofday () in
767
+ if now -. ! last_changed > 180. then (
768
+ let checking_time = ! last_changed in
769
+ last_changed := now;
770
+ Lwt. async (update_local_copy checking_time)
771
+ ) else
772
+ last_changed := now
773
+
774
+
711
775
let ace_display tab =
712
776
let ace = lazy (
713
777
let answer =
714
778
Ocaml_mode. create_ocaml_editor
715
779
(Tyxml_js.To_dom. of_div tab)
780
+ (fun _ _ _ -> () )
716
781
in
717
782
let ace = Ocaml_mode. get_editor answer in
718
783
Ace. set_font_size ace 16 ;
874
939
875
940
module Editor_button (E : Editor_info ) = struct
876
941
877
- let editor_button = button ~container: E. buttons_container ~theme: " light"
942
+ let editor_button =
943
+ button ~container: E. buttons_container ~theme: " light"
878
944
879
945
let cleanup template =
880
946
editor_button
@@ -901,16 +967,26 @@ module Editor_button (E : Editor_info) = struct
901
967
select_tab " toplevel" ;
902
968
Lwt. return_unit
903
969
904
- let sync token id =
905
- editor_button
970
+ let sync token id on_sync =
971
+ let state = button_state () in
972
+ (editor_button
973
+ ~state
906
974
~icon: " sync" [% i" Sync" ] @@ fun () ->
907
975
token >> = fun token ->
908
- sync_exercise token id ~editor: (Ace. get_contents E. ace) > |= fun _save -> ()
976
+ sync_exercise token id ~editor: (Ace. get_contents E. ace) on_sync
977
+ > |= fun _save -> () );
978
+ Ace. register_sync_observer E. ace (fun sync ->
979
+ if sync then disable_button state else enable_button state)
980
+
909
981
end
910
982
911
- let setup_editor solution =
983
+ let setup_editor id solution =
912
984
let editor_pane = find_component " learnocaml-exo-editor-pane" in
913
- let editor = Ocaml_mode. create_ocaml_editor (Tyxml_js.To_dom. of_div editor_pane) in
985
+ let editor =
986
+ Ocaml_mode. create_ocaml_editor
987
+ (Tyxml_js.To_dom. of_div editor_pane)
988
+ (check_valid_editor_state id)
989
+ in
914
990
let ace = Ocaml_mode. get_editor editor in
915
991
Ace. set_contents ace ~reset_undo: true solution;
916
992
Ace. set_font_size ace 18 ;
@@ -1022,7 +1098,7 @@ let setup_prelude_pane ace prelude =
1022
1098
(fun _ -> state := not ! state ; update () ; true ) ;
1023
1099
Manip. appendChildren prelude_pane
1024
1100
[ prelude_title ; prelude_container ]
1025
-
1101
+
1026
1102
let get_token ?(has_server = true ) () =
1027
1103
if not has_server then
1028
1104
Lwt. return None
@@ -1041,7 +1117,7 @@ let get_token ?(has_server = true) () =
1041
1117
> |= fun token ->
1042
1118
Learnocaml_local_storage. (store sync_token) token;
1043
1119
Some token
1044
-
1120
+
1045
1121
module Display_exercise =
1046
1122
functor (
1047
1123
Q : sig
0 commit comments