diff --git a/mod/quiz/autosave.ajax.php b/mod/quiz/autosave.ajax.php index 9b6f1326612..dccfadc0736 100644 --- a/mod/quiz/autosave.ajax.php +++ b/mod/quiz/autosave.ajax.php @@ -60,3 +60,4 @@ if ($attemptobj->is_finished()) { $attemptobj->process_auto_save($timenow); $transaction->allow_commit(); +echo 'OK'; diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index 5a22fa12cdc..fa89dcf65d6 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -185,6 +185,12 @@ $string['confirmserverdelete'] = 'Are you sure you want to remove the server $string['confirmstartattemptlimit'] = 'Number of attempts allowed: {$a}. You are about to start a new attempt. Do you wish to proceed?'; $string['confirmstartattempttimelimit'] = 'This quiz has a time limit and is limited to {$a} attempt(s). You are about to start a new attempt. Do you wish to proceed?'; $string['confirmstarttimelimit'] = 'The quiz has a time limit. Are you sure that you wish to start?'; +$string['connectionok'] = 'Network connection restored. You may continue safely.'; +$string['connectionerror'] = 'Network connection lost. (Autosave failed). + +Make a note of any responses entered on this page in the last few minutes, then try to re-connect. + +Once connection has been re-established, your responses should be saved and this message will disappear.'; $string['containercategorycreated'] = 'This category has been created to store all the original categories moved to site level due to the causes specified below.'; $string['continueattemptquiz'] = 'Continue the last attempt'; $string['continuepreview'] = 'Continue the last preview'; diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index aca16a57c1b..6c36544ad1b 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -493,6 +493,8 @@ class mod_quiz_renderer extends plugin_renderer_base { $output .= html_writer::end_tag('div'); $output .= html_writer::end_tag('form'); + $output .= $this->connection_warning(); + return $output; } @@ -1163,6 +1165,18 @@ class mod_quiz_renderer extends plugin_renderer_base { return $this->heading($title, 3) . html_writer::tag('div', $graph, array('class' => 'graph')); } + + /** + * Output the connection warning messages, which are initially hidden, and + * only revealed by JavaScript if necessary. + */ + public function connection_warning() { + $options = array('filter' => false, 'newlines' => false); + $warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options); + $ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options); + return html_writer::tag('div', $warning, array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) . + html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert')); + } } class mod_quiz_links_to_other_attempts implements renderable { diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index 4a8051fd56e..efbf870ba59 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -28,6 +28,28 @@ body.jsenabled .questionflagcheckbox { display: none; } +#page-mod-quiz-attempt #connection-ok, +#page-mod-quiz-attempt #connection-error { + position: fixed; + top: 0; + width: 80%; + left: 10%; + color: #555; + border-radius: 0 0 10px 10px; + box-shadow: 5px 5px 20px 0 #666666; + padding: 1em 1em 0; + z-index: 10000; +} + +#page-mod-quiz-attempt #connection-error { + background-color: #fcc; +} +#page-mod-quiz-attempt #connection-ok { + background-color: #cfb; + width: 60%; + left: 20%; +} + /** Mod quiz attempt **/ .generalbox#passwordbox { /* Should probably match .generalbox#intro above */ diff --git a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js index 043c560b3e4..30353d3b31a 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js @@ -30,13 +30,17 @@ M.mod_quiz.autosave = { TINYMCE_DETECTION_DELAY: 500, TINYMCE_DETECTION_REPEATS: 20, WATCH_HIDDEN_DELAY: 1000, + FAILURES_BEFORE_NOTIFY: 1, + FIRST_SUCCESSFUL_SAVE: -1, /** Selectors. */ SELECTORS: { QUIZ_FORM: '#responseform', VALUE_CHANGE_ELEMENTS: 'input, textarea', CHANGE_ELEMENTS: 'input, select', - HIDDEN_INPUTS: 'input[type=hidden]' + HIDDEN_INPUTS: 'input[type=hidden]', + CONNECTION_ERROR: '#connection-error', + CONNECTION_OK: '#connection-ok' }, /** Script that handles the auto-saves. */ @@ -57,9 +61,13 @@ M.mod_quiz.autosave = { /** Y.io transaction for the save ajax request. */ save_transaction: null, + /** @property Failed saves count. */ + savefailures: 0, + /** Properly bound key change handler. */ editor_change_handler: null, + /** Record of the value of all the hidden fields, last time they were checked. */ hidden_field_values: {}, /** @@ -202,7 +210,10 @@ M.mod_quiz.autosave = { this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, { method: 'POST', form: {id: this.form}, - on: {complete: this.save_done}, + on: { + success: this.save_done, + failure: this.save_failed + }, context: this }); }, @@ -215,6 +226,29 @@ M.mod_quiz.autosave = { Y.log('Dirty after save.'); this.start_save_timer(); } + + if (this.savefailures > 0) { + Y.one(this.SELECTORS.CONNECTION_ERROR).hide(); + Y.one(this.SELECTORS.CONNECTION_OK).show(); + this.savefailures = this.FIRST_SUCCESSFUL_SAVE; + } else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) { + Y.one(this.SELECTORS.CONNECTION_OK).hide(); + this.savefailures = 0; + } + }, + + save_failed: function() { + Y.log('Save failed.'); + this.save_transaction = null; + + // We want to retry soon. + this.start_save_timer(); + + this.savefailures = Math.max(1, this.savefailures + 1); + if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) { + Y.one(this.SELECTORS.CONNECTION_ERROR).show(); + Y.one(this.SELECTORS.CONNECTION_OK).hide(); + } }, is_time_nearly_over: function() { diff --git a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js index 64d9bad04b1..aefb7ab8750 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js @@ -1 +1 @@ -YUI.add("moodle-mod_quiz-autosave",function(e,t){M.mod_quiz=M.mod_quiz||{},M.mod_quiz.autosave={TINYMCE_DETECTION_DELAY:500,TINYMCE_DETECTION_REPEATS:20,WATCH_HIDDEN_DELAY:1e3,SELECTORS:{QUIZ_FORM:"#responseform",VALUE_CHANGE_ELEMENTS:"input, textarea",CHANGE_ELEMENTS:"input, select",HIDDEN_INPUTS:"input[type=hidden]"},AUTOSAVE_HANDLER:M.cfg.wwwroot+"/mod/quiz/autosave.ajax.php",delay:12e4,form:null,dirty:!1,delay_timer:null,save_transaction:null,editor_change_handler:null,hidden_field_values:{},init:function(t){this.form=e.one(this.SELECTORS.QUIZ_FORM);if(!this.form)return;this.delay=t*1e3,this.form.delegate("valuechange",this.value_changed,this.SELECTORS.VALUE_CHANGE_ELEMENTS,this),this.form.delegate("change",this.value_changed,this.SELECTORS.CHANGE_ELEMENTS,this),this.form.on("submit",this.stop_autosaving,this),this.init_tinymce(this.TINYMCE_DETECTION_REPEATS),this.save_hidden_field_values(),this.watch_hidden_fields()},save_hidden_field_values:function(){this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(e){var t=e.get("name");if(!t)return;this.hidden_field_values[t]=e.get("value")},this)},watch_hidden_fields:function(){this.detect_hidden_field_changes(),e.later(this.WATCH_HIDDEN_DELAY,this,this.watch_hidden_fields)},detect_hidden_field_changes:function(){this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(e){var t=e.get("name"),n=e.get("value");if(!t)return;if(!(t in this.hidden_field_values)||n!==this.hidden_field_values[t])this.hidden_field_values[t]=n,this.value_changed({target:e})},this)},init_tinymce:function(t){if(typeof tinyMCE=="undefined"){t>0&&e.later(this.TINYMCE_DETECTION_DELAY,this,this.init_tinymce,[t-1]);return}this.editor_change_handler=e.bind(this.editor_changed,this),tinyMCE.onAddEditor.add(e.bind(this.init_tinymce_editor,this))},init_tinymce_editor:function(e,t){t.onChange.add(this.editor_change_handler),t.onRedo.add(this.editor_change_handler),t.onUndo.add(this.editor_change_handler),t.onKeyDown.add(this.editor_change_handler)},value_changed:function(e){if(e.target.get("name")==="thispage"||e.target.get("name")==="scrollpos"||e.target.get("name").match(/_:flagged$/))return;this.start_save_timer_if_necessary()},editor_changed:function(e){this.start_save_timer_if_necessary()},start_save_timer_if_necessary:function(){this.dirty=!0;if(this.delay_timer||this.save_transaction)return;this.start_save_timer()},start_save_timer:function(){this.cancel_delay(),this.delay_timer=e.later(this.delay,this,this.save_changes)},cancel_delay:function(){this.delay_timer&&this.delay_timer!==!0&&this.delay_timer.cancel(),this.delay_timer=null},save_changes:function(){this.cancel_delay(),this.dirty=!1;if(this.is_time_nearly_over()){this.stop_autosaving();return}typeof tinyMCE!="undefined"&&tinyMCE.triggerSave(),this.save_transaction=e.io(this.AUTOSAVE_HANDLER,{method:"POST",form:{id:this.form},on:{complete:this.save_done},context:this})},save_done:function(){this.save_transaction=null,this.dirty&&this.start_save_timer()},is_time_nearly_over:function(){return M.mod_quiz.timer&&M.mod_quiz.timer.endtime&&(new Date).getTime()+2*this.delay>M.mod_quiz.timer.endtime},stop_autosaving:function(){this.cancel_delay(),this.delay_timer=!0,this.save_transaction&&this.save_transaction.abort()}}},"@VERSION@",{requires:["base","node","event","event-valuechange","node-event-delegate","io-form"]}); +YUI.add("moodle-mod_quiz-autosave",function(e,t){M.mod_quiz=M.mod_quiz||{},M.mod_quiz.autosave={TINYMCE_DETECTION_DELAY:500,TINYMCE_DETECTION_REPEATS:20,WATCH_HIDDEN_DELAY:1e3,FAILURES_BEFORE_NOTIFY:1,FIRST_SUCCESSFUL_SAVE:-1,SELECTORS:{QUIZ_FORM:"#responseform",VALUE_CHANGE_ELEMENTS:"input, textarea",CHANGE_ELEMENTS:"input, select",HIDDEN_INPUTS:"input[type=hidden]",CONNECTION_ERROR:"#connection-error",CONNECTION_OK:"#connection-ok"},AUTOSAVE_HANDLER:M.cfg.wwwroot+"/mod/quiz/autosave.ajax.php",delay:12e4,form:null,dirty:!1,delay_timer:null,save_transaction:null,savefailures:0,editor_change_handler:null,hidden_field_values:{},init:function(t){this.form=e.one(this.SELECTORS.QUIZ_FORM);if(!this.form)return;this.delay=t*1e3,this.form.delegate("valuechange",this.value_changed,this.SELECTORS.VALUE_CHANGE_ELEMENTS,this),this.form.delegate("change",this.value_changed,this.SELECTORS.CHANGE_ELEMENTS,this),this.form.on("submit",this.stop_autosaving,this),this.init_tinymce(this.TINYMCE_DETECTION_REPEATS),this.save_hidden_field_values(),this.watch_hidden_fields()},save_hidden_field_values:function(){this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(e){var t=e.get("name");if(!t)return;this.hidden_field_values[t]=e.get("value")},this)},watch_hidden_fields:function(){this.detect_hidden_field_changes(),e.later(this.WATCH_HIDDEN_DELAY,this,this.watch_hidden_fields)},detect_hidden_field_changes:function(){this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(e){var t=e.get("name"),n=e.get("value");if(!t)return;if(!(t in this.hidden_field_values)||n!==this.hidden_field_values[t])this.hidden_field_values[t]=n,this.value_changed({target:e})},this)},init_tinymce:function(t){if(typeof tinyMCE=="undefined"){t>0&&e.later(this.TINYMCE_DETECTION_DELAY,this,this.init_tinymce,[t-1]);return}this.editor_change_handler=e.bind(this.editor_changed,this),tinyMCE.onAddEditor.add(e.bind(this.init_tinymce_editor,this))},init_tinymce_editor:function(e,t){t.onChange.add(this.editor_change_handler),t.onRedo.add(this.editor_change_handler),t.onUndo.add(this.editor_change_handler),t.onKeyDown.add(this.editor_change_handler)},value_changed:function(e){if(e.target.get("name")==="thispage"||e.target.get("name")==="scrollpos"||e.target.get("name").match(/_:flagged$/))return;this.start_save_timer_if_necessary()},editor_changed:function(e){this.start_save_timer_if_necessary()},start_save_timer_if_necessary:function(){this.dirty=!0;if(this.delay_timer||this.save_transaction)return;this.start_save_timer()},start_save_timer:function(){this.cancel_delay(),this.delay_timer=e.later(this.delay,this,this.save_changes)},cancel_delay:function(){this.delay_timer&&this.delay_timer!==!0&&this.delay_timer.cancel(),this.delay_timer=null},save_changes:function(){this.cancel_delay(),this.dirty=!1;if(this.is_time_nearly_over()){this.stop_autosaving();return}typeof tinyMCE!="undefined"&&tinyMCE.triggerSave(),this.save_transaction=e.io(this.AUTOSAVE_HANDLER,{method:"POST",form:{id:this.form},on:{success:this.save_done,failure:this.save_failed},context:this})},save_done:function(){this.save_transaction=null,this.dirty&&this.start_save_timer(),this.savefailures>0?(e.one(this.SELECTORS.CONNECTION_ERROR).hide(),e.one(this.SELECTORS.CONNECTION_OK).show(),this.savefailures=this.FIRST_SUCCESSFUL_SAVE):this.savefailures===this.FIRST_SUCCESSFUL_SAVE&&(e.one(this.SELECTORS.CONNECTION_OK).hide(),this.savefailures=0)},save_failed:function(){this.save_transaction=null,this.start_save_timer(),this.savefailures=Math.max(1,this.savefailures+1),this.savefailures===this.FAILURES_BEFORE_NOTIFY&&(e.one(this.SELECTORS.CONNECTION_ERROR).show(),e.one(this.SELECTORS.CONNECTION_OK).hide())},is_time_nearly_over:function(){return M.mod_quiz.timer&&M.mod_quiz.timer.endtime&&(new Date).getTime()+2*this.delay>M.mod_quiz.timer.endtime},stop_autosaving:function(){this.cancel_delay(),this.delay_timer=!0,this.save_transaction&&this.save_transaction.abort()}}},"@VERSION@",{requires:["base","node","event","event-valuechange","node-event-delegate","io-form"]}); diff --git a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js index b884cf2ec9e..55e11e9ce0b 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js @@ -30,13 +30,17 @@ M.mod_quiz.autosave = { TINYMCE_DETECTION_DELAY: 500, TINYMCE_DETECTION_REPEATS: 20, WATCH_HIDDEN_DELAY: 1000, + FAILURES_BEFORE_NOTIFY: 1, + FIRST_SUCCESSFUL_SAVE: -1, /** Selectors. */ SELECTORS: { QUIZ_FORM: '#responseform', VALUE_CHANGE_ELEMENTS: 'input, textarea', CHANGE_ELEMENTS: 'input, select', - HIDDEN_INPUTS: 'input[type=hidden]' + HIDDEN_INPUTS: 'input[type=hidden]', + CONNECTION_ERROR: '#connection-error', + CONNECTION_OK: '#connection-ok' }, /** Script that handles the auto-saves. */ @@ -57,9 +61,13 @@ M.mod_quiz.autosave = { /** Y.io transaction for the save ajax request. */ save_transaction: null, + /** @property Failed saves count. */ + savefailures: 0, + /** Properly bound key change handler. */ editor_change_handler: null, + /** Record of the value of all the hidden fields, last time they were checked. */ hidden_field_values: {}, /** @@ -194,7 +202,10 @@ M.mod_quiz.autosave = { this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, { method: 'POST', form: {id: this.form}, - on: {complete: this.save_done}, + on: { + success: this.save_done, + failure: this.save_failed + }, context: this }); }, @@ -205,6 +216,28 @@ M.mod_quiz.autosave = { if (this.dirty) { this.start_save_timer(); } + + if (this.savefailures > 0) { + Y.one(this.SELECTORS.CONNECTION_ERROR).hide(); + Y.one(this.SELECTORS.CONNECTION_OK).show(); + this.savefailures = this.FIRST_SUCCESSFUL_SAVE; + } else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) { + Y.one(this.SELECTORS.CONNECTION_OK).hide(); + this.savefailures = 0; + } + }, + + save_failed: function() { + this.save_transaction = null; + + // We want to retry soon. + this.start_save_timer(); + + this.savefailures = Math.max(1, this.savefailures + 1); + if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) { + Y.one(this.SELECTORS.CONNECTION_ERROR).show(); + Y.one(this.SELECTORS.CONNECTION_OK).hide(); + } }, is_time_nearly_over: function() { diff --git a/mod/quiz/yui/src/autosave/js/autosave.js b/mod/quiz/yui/src/autosave/js/autosave.js index feb80548af7..a3ac4719fe9 100644 --- a/mod/quiz/yui/src/autosave/js/autosave.js +++ b/mod/quiz/yui/src/autosave/js/autosave.js @@ -28,13 +28,17 @@ M.mod_quiz.autosave = { TINYMCE_DETECTION_DELAY: 500, TINYMCE_DETECTION_REPEATS: 20, WATCH_HIDDEN_DELAY: 1000, + FAILURES_BEFORE_NOTIFY: 1, + FIRST_SUCCESSFUL_SAVE: -1, /** Selectors. */ SELECTORS: { QUIZ_FORM: '#responseform', VALUE_CHANGE_ELEMENTS: 'input, textarea', CHANGE_ELEMENTS: 'input, select', - HIDDEN_INPUTS: 'input[type=hidden]' + HIDDEN_INPUTS: 'input[type=hidden]', + CONNECTION_ERROR: '#connection-error', + CONNECTION_OK: '#connection-ok' }, /** Script that handles the auto-saves. */ @@ -55,9 +59,13 @@ M.mod_quiz.autosave = { /** Y.io transaction for the save ajax request. */ save_transaction: null, + /** @property Failed saves count. */ + savefailures: 0, + /** Properly bound key change handler. */ editor_change_handler: null, + /** Record of the value of all the hidden fields, last time they were checked. */ hidden_field_values: {}, /** @@ -200,7 +208,10 @@ M.mod_quiz.autosave = { this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, { method: 'POST', form: {id: this.form}, - on: {complete: this.save_done}, + on: { + success: this.save_done, + failure: this.save_failed + }, context: this }); }, @@ -213,6 +224,29 @@ M.mod_quiz.autosave = { Y.log('Dirty after save.'); this.start_save_timer(); } + + if (this.savefailures > 0) { + Y.one(this.SELECTORS.CONNECTION_ERROR).hide(); + Y.one(this.SELECTORS.CONNECTION_OK).show(); + this.savefailures = this.FIRST_SUCCESSFUL_SAVE; + } else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) { + Y.one(this.SELECTORS.CONNECTION_OK).hide(); + this.savefailures = 0; + } + }, + + save_failed: function() { + Y.log('Save failed.'); + this.save_transaction = null; + + // We want to retry soon. + this.start_save_timer(); + + this.savefailures = Math.max(1, this.savefailures + 1); + if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) { + Y.one(this.SELECTORS.CONNECTION_ERROR).show(); + Y.one(this.SELECTORS.CONNECTION_OK).hide(); + } }, is_time_nearly_over: function() {