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
@@ -432,30 +432,34 @@ let get_state_as_save_file ?(include_reports = false) () =
432432 all_exercise_toplevel_histories = retrieve all_exercise_toplevel_histories;
433433 }
434434
435- let rec sync_save token save_file =
435+ let rec sync_save token save_file on_sync =
436436 Server_caller. request (Learnocaml_api. Update_save (token, save_file))
437437 >> = function
438- | Ok save -> set_state_from_save_file ~token save; Lwt. return save
438+ | Ok save ->
439+ set_state_from_save_file ~token save;
440+ on_sync () ;
441+ Lwt. return save
439442 | Error (`Not_found _ ) ->
440443 Server_caller. request_exn
441444 (Learnocaml_api. Create_token (" " , Some token, None )) >> = fun _token ->
442445 assert (_token = token);
443446 Server_caller. request_exn
444447 (Learnocaml_api. Update_save (token, save_file)) >> = fun save ->
445448 set_state_from_save_file ~token save;
449+ on_sync () ;
446450 Lwt. return save
447451 | Error e ->
448452 lwt_alert ~title: [% i" SYNC FAILED" ] [
449453 H. p [H. txt [% i" Could not synchronise save with the server" ]];
450454 H. code [H. txt (Server_caller. string_of_error e)];
451455 ] ~buttons: [
452- [% i" Retry" ], (fun () -> sync_save token save_file);
453- [% i" Ignore" ], (fun () -> Lwt. return save_file);
456+ [% i" Retry" ], (fun () -> sync_save token save_file on_sync );
457+ [% i" Ignore" ], (fun () -> Lwt. return save_file);
454458 ]
455459
456- let sync token = sync_save token (get_state_as_save_file () )
460+ let sync token on_sync = sync_save token (get_state_as_save_file () ) on_sync
457461
458- let sync_exercise token ?answer ?editor id =
462+ let sync_exercise token ?answer ?editor id on_sync =
459463 let handle_serverless () =
460464 (* save the text at least locally (but not the report & grade, that could
461465 be misleading) *)
@@ -492,7 +496,7 @@ let sync_exercise token ?answer ?editor id =
492496 } in
493497 match token with
494498 | Some token ->
495- Lwt. catch (fun () -> sync_save token save_file)
499+ Lwt. catch (fun () -> sync_save token save_file on_sync )
496500 (fun e ->
497501 handle_serverless () ;
498502 raise e)
@@ -706,11 +710,51 @@ let mouseover_toggle_signal elt sigvalue setter =
706710 in
707711 Manip.Ev. onmouseover elt hdl
708712
713+ (*
714+
715+ If a user has made no change to a solution for the exercise [id]
716+ for 180 seconds, [check_valid_editor_state id] ensures that there is
717+ no more recent version of this solution in the server. If this is
718+ the case, the user is asked if we should download this solution
719+ from the server.
720+
721+ This function reduces the risk of an involuntary overwriting of a
722+ student solution when the solution is open in several clients.
723+
724+ *)
725+ let check_valid_editor_state id =
726+ let last_changed = ref (Unix. gettimeofday () ) in
727+ fun update_content ->
728+ let update_local_copy checking_time () =
729+ match id with
730+ | None -> Lwt. return ()
731+ | Some id ->
732+ match Learnocaml_local_storage. (retrieve (exercise_state id)) with
733+ | { Answer. mtime; solution; _ } ->
734+ if mtime > checking_time then (
735+ if Js_utils. confirm
736+ [% i " A more recent answer exists on the server. \
737+ Do you want to update the current one?" ]
738+ then
739+ update_content solution;
740+ );
741+ Lwt. return ()
742+ | exception Not_found -> Lwt. return ()
743+ in
744+ let now = Unix. gettimeofday () in
745+ if now -. ! last_changed > 180. then (
746+ let checking_time = ! last_changed in
747+ last_changed := now;
748+ Lwt. async (update_local_copy checking_time)
749+ )
750+
751+
709752let ace_display tab =
710753 let ace = lazy (
711754 let answer =
712755 Ocaml_mode. create_ocaml_editor
713756 (Tyxml_js.To_dom. of_div tab)
757+ (check_valid_editor_state None )
714758 in
715759 let ace = Ocaml_mode. get_editor answer in
716760 Ace. set_font_size ace 16 ;
872916
873917module Editor_button (E : Editor_info ) = struct
874918
875- let editor_button = button ~container: E. buttons_container ~theme: " light"
919+ let editor_button =
920+ button ~container: E. buttons_container ~theme: " light"
876921
877922 let cleanup template =
878923 editor_button
@@ -898,16 +943,26 @@ module Editor_button (E : Editor_info) = struct
898943 select_tab " toplevel" ;
899944 Lwt. return_unit
900945
901- let sync token id =
902- editor_button
946+ let sync token id on_sync =
947+ let state = button_state () in
948+ (editor_button
949+ ~state
903950 ~icon: " sync" [% i" Sync" ] @@ fun () ->
904951 token >> = fun token ->
905- 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+
906957end
907958
908- let setup_editor solution =
959+ let setup_editor id solution =
909960 let editor_pane = find_component " learnocaml-exo-editor-pane" in
910- 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 (Some id))
965+ in
911966 let ace = Ocaml_mode. get_editor editor in
912967 Ace. set_contents ace ~reset_undo: true solution;
913968 Ace. set_font_size ace 18 ;
0 commit comments