11(* This file is part of Learn-OCaml.
22 *
3- * Copyright (C) 2019 OCaml Software Foundation.
3+ * Copyright (C) 2019-2020 OCaml Software Foundation.
44 * Copyright (C) 2016-2018 OCamlPro.
55 *
66 * 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) () =
434434 all_exercise_toplevel_histories = retrieve all_exercise_toplevel_histories;
435435 }
436436
437- let rec sync_save token save_file =
437+ let rec sync_save token save_file on_sync =
438438 Server_caller. request (Learnocaml_api. Update_save (token, save_file))
439439 >> = 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
441444 | Error (`Not_found _ ) ->
442445 Server_caller. request_exn
443446 (Learnocaml_api. Create_token (" " , Some token, None )) >> = fun _token ->
444447 assert (_token = token);
445448 Server_caller. request_exn
446449 (Learnocaml_api. Update_save (token, save_file)) >> = fun save ->
447450 set_state_from_save_file ~token save;
451+ on_sync () ;
448452 Lwt. return save
449453 | Error e ->
450454 lwt_alert ~title: [% i" SYNC FAILED" ] [
451455 H. p [H. txt [% i" Could not synchronise save with the server" ]];
452456 H. code [H. txt (Server_caller. string_of_error e)];
453457 ] ~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);
456460 ]
457461
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
459463
460- let sync_exercise token ?answer ?editor id =
464+ let sync_exercise token ?answer ?editor id on_sync =
461465 let handle_serverless () =
462466 (* save the text at least locally (but not the report & grade, that could
463467 be misleading) *)
@@ -494,7 +498,7 @@ let sync_exercise token ?answer ?editor id =
494498 } in
495499 match token with
496500 | Some token ->
497- Lwt. catch (fun () -> sync_save token save_file)
501+ Lwt. catch (fun () -> sync_save token save_file on_sync )
498502 (fun e ->
499503 handle_serverless () ;
500504 raise e)
@@ -708,11 +712,72 @@ let mouseover_toggle_signal elt sigvalue setter =
708712 in
709713 Manip.Ev. onmouseover elt hdl
710714
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+
711775let ace_display tab =
712776 let ace = lazy (
713777 let answer =
714778 Ocaml_mode. create_ocaml_editor
715779 (Tyxml_js.To_dom. of_div tab)
780+ (fun _ _ _ -> () )
716781 in
717782 let ace = Ocaml_mode. get_editor answer in
718783 Ace. set_font_size ace 16 ;
874939
875940module Editor_button (E : Editor_info ) = struct
876941
877- let editor_button = button ~container: E. buttons_container ~theme: " light"
942+ let editor_button =
943+ button ~container: E. buttons_container ~theme: " light"
878944
879945 let cleanup template =
880946 editor_button
@@ -901,16 +967,26 @@ module Editor_button (E : Editor_info) = struct
901967 select_tab " toplevel" ;
902968 Lwt. return_unit
903969
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
906974 ~icon: " sync" [% i" Sync" ] @@ fun () ->
907975 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+
909981end
910982
911- let setup_editor solution =
983+ let setup_editor id solution =
912984 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
914990 let ace = Ocaml_mode. get_editor editor in
915991 Ace. set_contents ace ~reset_undo: true solution;
916992 Ace. set_font_size ace 18 ;
@@ -1022,7 +1098,7 @@ let setup_prelude_pane ace prelude =
10221098 (fun _ -> state := not ! state ; update () ; true ) ;
10231099 Manip. appendChildren prelude_pane
10241100 [ prelude_title ; prelude_container ]
1025-
1101+
10261102let get_token ?(has_server = true ) () =
10271103 if not has_server then
10281104 Lwt. return None
@@ -1041,7 +1117,7 @@ let get_token ?(has_server = true) () =
10411117 > |= fun token ->
10421118 Learnocaml_local_storage. (store sync_token) token;
10431119 Some token
1044-
1120+
10451121module Display_exercise =
10461122 functor (
10471123 Q : sig
0 commit comments