MDL-42504 quiz autosave: alert users if connection lost.

When the auto-save fails, we alert the user that they may have lost their
internet connection so that they don't do more work that they might just
lose.
This commit is contained in:
Tim Hunt 2013-10-25 17:45:00 +01:00
parent 5e6da548d7
commit 52b612f08b
8 changed files with 151 additions and 7 deletions

View File

@ -60,3 +60,4 @@ if ($attemptobj->is_finished()) {
$attemptobj->process_auto_save($timenow);
$transaction->allow_commit();
echo 'OK';

View File

@ -185,6 +185,12 @@ $string['confirmserverdelete'] = 'Are you sure you want to remove the server <b>
$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';

View File

@ -492,6 +492,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;
}
@ -1162,6 +1164,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 {

View File

@ -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 */

View File

@ -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() {

View File

@ -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"]});

View File

@ -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() {

View File

@ -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() {