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,48 @@ 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 check_valid_editor_state id =
728+ let last_changed = ref (Unix. gettimeofday () ) in
729+ fun update_content ->
730+ let update_local_copy checking_time () =
731+ match Learnocaml_local_storage. (retrieve (exercise_state id)) with
732+ | { Answer. mtime; solution; _ } ->
733+ if mtime > checking_time then (
734+ if Js_utils. confirm
735+ [% i " A more recent answer exists on the server. \
736+ Do you want to update the current one?" ]
737+ then
738+ update_content solution;
739+ );
740+ Lwt. return ()
741+ | exception Not_found -> Lwt. return ()
742+ in
743+ let now = Unix. gettimeofday () in
744+ if now -. ! last_changed > 180. then (
745+ let checking_time = ! last_changed in
746+ last_changed := now;
747+ Lwt. async (update_local_copy checking_time)
748+ )
749+
750+
711751let ace_display tab =
712752 let ace = lazy (
713753 let answer =
714754 Ocaml_mode. create_ocaml_editor
715755 (Tyxml_js.To_dom. of_div tab)
756+ ignore
716757 in
717758 let ace = Ocaml_mode. get_editor answer in
718759 Ace. set_font_size ace 16 ;
874915
875916module Editor_button (E : Editor_info ) = struct
876917
877- let editor_button = button ~container: E. buttons_container ~theme: " light"
918+ let editor_button =
919+ button ~container: E. buttons_container ~theme: " light"
878920
879921 let cleanup template =
880922 editor_button
@@ -901,16 +943,26 @@ module Editor_button (E : Editor_info) = struct
901943 select_tab " toplevel" ;
902944 Lwt. return_unit
903945
904- let sync token id =
905- editor_button
946+ let sync token id on_sync =
947+ let state = button_state () in
948+ (editor_button
949+ ~state
906950 ~icon: " sync" [% i" Sync" ] @@ fun () ->
907951 token >> = fun token ->
908- sync_exercise token id ~editor: (Ace. get_contents E. ace) > |= fun _save -> ()
952+ sync_exercise token id ~editor: (Ace. get_contents E. ace) on_sync
953+ > |= fun _save -> () );
954+ Ace. register_sync_observer E. ace (fun sync ->
955+ if sync then disable_button state else enable_button state)
956+
909957end
910958
911- let setup_editor solution =
959+ let setup_editor id solution =
912960 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
961+ let editor =
962+ Ocaml_mode. create_ocaml_editor
963+ (Tyxml_js.To_dom. of_div editor_pane)
964+ (check_valid_editor_state id)
965+ in
914966 let ace = Ocaml_mode. get_editor editor in
915967 Ace. set_contents ace ~reset_undo: true solution;
916968 Ace. set_font_size ace 18 ;
0 commit comments