diff --git a/.changelogs/feature_quiz-resume-4.yml b/.changelogs/feature_quiz-resume-4.yml
new file mode 100644
index 0000000000..a86a9cfe7c
--- /dev/null
+++ b/.changelogs/feature_quiz-resume-4.yml
@@ -0,0 +1,4 @@
+significance: patch
+type: fixed
+entry: Fixed reference in `LLMS_Ajax_Handler::quiz_start()` to
+ `LLMS_Quiz_Attempt::get_status()` method removed since LifterLMS 4.0.0.
diff --git a/.changelogs/feature_quiz-resume-akash-3.yml b/.changelogs/feature_quiz-resume-akash-3.yml
new file mode 100644
index 0000000000..f979f75b7c
--- /dev/null
+++ b/.changelogs/feature_quiz-resume-akash-3.yml
@@ -0,0 +1,3 @@
+significance: minor
+type: changed
+entry: Added support for image upload in Result Clarifications box for quizzes.
diff --git a/.changelogs/feature_quiz-resume-akash-5.yml b/.changelogs/feature_quiz-resume-akash-5.yml
new file mode 100644
index 0000000000..5efe2f8866
--- /dev/null
+++ b/.changelogs/feature_quiz-resume-akash-5.yml
@@ -0,0 +1,4 @@
+significance: minor
+type: dev
+entry: Added filter `llms_quiz_attempt_resume_time_period` for updating quiz resume
+ allowed time period.
diff --git a/.changelogs/feature_quiz-resume.yml b/.changelogs/feature_quiz-resume.yml
new file mode 100644
index 0000000000..0e4979123b
--- /dev/null
+++ b/.changelogs/feature_quiz-resume.yml
@@ -0,0 +1,3 @@
+significance: minor
+type: added
+entry: "Added new feature: Quiz Resume."
diff --git a/assets/js/builder/Models/Quiz.js b/assets/js/builder/Models/Quiz.js
index 4251c5848c..71be58af24 100644
--- a/assets/js/builder/Models/Quiz.js
+++ b/assets/js/builder/Models/Quiz.js
@@ -52,6 +52,7 @@ define( [
* @since 3.16.0
* @since 7.4.0 Added filter for filtering defaults.
* @since 7.5.0 Replaced unused `random_answers` property with `random_questions`.
+ * @since [version] Added filter for filtering defaults and `can_be_resumed` property.
*
* @return {Object}
*/
@@ -75,6 +76,7 @@ define( [
random_questions: 'no',
time_limit: 30,
show_correct_answer: 'no',
+ can_be_resumed: 'no',
disable_retake: 'no',
questions: [],
diff --git a/assets/js/builder/Schemas/Quiz.js b/assets/js/builder/Schemas/Quiz.js
index 31d13a8483..3c0b53b0d2 100644
--- a/assets/js/builder/Schemas/Quiz.js
+++ b/assets/js/builder/Schemas/Quiz.js
@@ -4,7 +4,9 @@
* @since 3.17.6
* @since 7.4.0 Added upsell for Question Bank and condition in `random_questions` schema.
* @since 7.6.2 Added `disable_retake` schema.
- * @version 7.6.2
+ * @since [version] Added `can_be_resumed` option.
+ * @version [version]
+
*/
define( [], function() {
@@ -56,6 +58,17 @@ define( [], function() {
type: 'switch-number',
},
], [
+
+ {
+ attribute: 'can_be_resumed',
+ id: 'resume',
+ label: LLMS.l10n.translate( 'Can be resumed' ),
+ tip: LLMS.l10n.translate( 'Allow a new attempt on this quiz to be resumed' ),
+ type: 'switch',
+ condition: function() {
+ return 'yes' === this.get( 'limit_time' ) ? false : true;
+ }
+ },
{
attribute: 'show_correct_answer',
id: 'show-correct-answer',
diff --git a/assets/js/builder/Views/Question.js b/assets/js/builder/Views/Question.js
index a91ee52382..c911397d99 100644
--- a/assets/js/builder/Views/Question.js
+++ b/assets/js/builder/Views/Question.js
@@ -1,7 +1,8 @@
/**
- * Single Question View
- * @since 3.16.0
- * @version 3.27.0
+ * Single Question View.
+ *
+ * @since 3.16.0
+ * @version [version]
*/
define( [
'Views/_Detachable',
@@ -82,10 +83,12 @@ define( [
},
/**
- * Compiles the template and renders the view
- * @return self (for chaining)
- * @since 3.16.0
- * @version 3.16.0
+ * Compiles the template and renders the view.
+ *
+ * @since 3.16.0
+ * @since [version] Added support for image upload in tinyMCE editor.
+ *
+ * @return self (for chaining)
*/
render: function() {
@@ -124,7 +127,7 @@ define( [
if ( this.model.get( 'clarifications_enabled' ) ) {
this.init_editor( 'question-clarifications--' + this.model.get( 'id' ), {
- mediaButtons: false,
+ mediaButtons: true,
tinymce: {
toolbar1: 'bold,italic,strikethrough,bullist,numlist,alignleft,aligncenter,alignright',
toolbar2: '',
diff --git a/assets/js/llms-quiz.js b/assets/js/llms-quiz.js
index 3c46bc1324..3398afb37c 100644
--- a/assets/js/llms-quiz.js
+++ b/assets/js/llms-quiz.js
@@ -2,11 +2,12 @@
/* jshint strict: true */
/**
- * Front End Quiz Class
+ * Front End Quiz Class.
*
- * @type {Object}
- * @since 1.0.0
- * @version 3.24.3
+ * @type {Object}
+ *
+ * @since 1.0.0
+ * @version [version]
*/( function( $ ) {
var quiz = {
@@ -14,93 +15,111 @@
/**
* Selector of all the available button elements
*
- * @type obj
+ * @type {Object}
*/
$buttons: null,
/**
- * Main Question Container Element
+ * Main Question Container Element.
*
- * @type obj
+ * @type {Object}
*/
$container: null,
/**
- * Main Quiz container UI element
+ * Main Quiz container UI element.
*
- * @type obj
+ * @type {Object}
*/
$ui: null,
/**
- * Attempt key for the current quiz
+ * Attempt key for the current quiz.
*
- * @type {[type]}
+ * @type {[type]}
*/
attempt_key: null,
/**
- * Question ID of the current question
+ * Question ID of the current question.
*
- * @type {Number}
+ * @type {Number}
*/
current_question: 0,
/**
- * Total number of questions in the current quiz
+ * Total number of questions in the current quiz.
*
- * @type {Number}
+ * @type {Number}
*/
total_questions: 0,
/**
- * Object of quiz question HTML
+ * Object of quiz question HTML.
*
- * @type {Object}
+ * @type {Object}
*/
questions: {},
/**
- * Validator functions for question types
- * Third party custom question types can register validators for use when answering questions
+ * Validator functions for question type.
+ * Third party custom question types can register validators for use when answering questions.
*
- * @type {Object}
+ * @type {Object}
*/
validators: {},
/**
- * Records current status of a quiz session
+ * Records current status of a quiz session.
* If a user attempts to navigate away from a quiz
* while taking the quiz they'll be warned that their progress
- * will not be saved if this status is not null
+ * will not be saved if this status is not null.
*
- * @type boolean
+ * @type {Bool}
*/
status: null,
/**
- * Bind DOM events
+ * Records if the quiz can be resumed.
+ */
+ resumable: null,
+
+ /**
+ * Flag if the user is exiting the quiz.
+ */
+ exiting_quiz: false,
+
+ /**
+ * Bind DOM events.
*
- * @return void
- * @since 1.0.0
- * @version 3.16.6
+ * @since 1.0.0
+ * @since 3.16.6 Unknown.
+ * @since [version] Add quiz resume and hide leave warning if quiz is resumable.
+ *
+ * @return {Void}
*/
bind: function() {
var self = this;
- // start quiz
+ // Start quiz.
$( '#llms_start_quiz' ).on( 'click', function( e ) {
e.preventDefault();
self.start_quiz();
} );
- // draw quiz grade circular chart
+ // Resume quiz.
+ $( '#llms_resume_quiz' ).on( 'click', function( e ) {
+ e.preventDefault();
+ self.resume_quiz();
+ } );
+
+ // Draw quiz grade circular chart.
$( '.llms-donut' ).each( function() {
LLMS.Donut( $( this ) );
} );
- // redirect to attempt on attempt selection change
+ // Redirect to attempt on attempt selection change.
$( '#llms-quiz-attempt-select' ).on( 'change', function() {
var val = $( this ).val();
if ( val ) {
@@ -108,23 +127,25 @@
}
} );
- // warn when quiz is running and user tries to leave the page
+ // Warn when quiz is running and user tries to leave the page when quiz is not resumable.
$( window ).on( 'beforeunload', function() {
- if ( self.status ) {
+ if ( self.status && ! self.exiting_quiz ) {
return LLMS.l10n.translate( 'Are you sure you wish to quit this quiz attempt?' );
}
+
+ return;
} );
- // complete the quiz attempt when user leaves if the quiz is running
+ // Complete the quiz attempt when user leaves if the quiz is running.
$( window ).on( 'unload', function() {
- if ( self.status ) {
+ if ( self.status && ! self.resumable ) {
self.complete_quiz();
}
} );
$( document ).on( 'llms-post-append-question', self.post_append_question );
- // register validators
+ // Register validators.
this.register_validator( 'content', this.validate );
this.register_validator( 'choice', this.validate_choice );
this.register_validator( 'picture_choice', this.validate_choice );
@@ -157,6 +178,45 @@
},
+ save_question: function( options ) {
+ var self = this,
+ $question = this.$container.find( '.llms-question-wrapper' ),
+ type = $question.attr( 'data-type' ),
+ valid;
+
+ if ( ! this.validators[ type ] ) {
+ console.log( 'No validator registered for question type ' + type );
+ return;
+ }
+
+ valid = this.validators[ type ]( $question );
+
+ var requestData = {
+ action: 'quiz_answer_question',
+ answer: valid.answer,
+ attempt_key: self.attempt_key,
+ question_id: $question.attr( 'data-id' ),
+ question_type: $question.attr( 'data-type' ),
+ };
+
+ if ( options && options.exit_quiz ) {
+ requestData.via_exit_quiz = true;
+ }
+
+ if ( options && options.previous_question ) {
+ requestData.via_previous_question = true;
+ }
+
+ LLMS.Ajax.call( {
+ data: requestData,
+ success: function( r ) {
+ if (options && typeof options.callback === 'function') {
+ options.callback();
+ }
+ },
+ });
+ },
+
/**
* Answer a Question
*
@@ -334,32 +394,75 @@
},
/**
- * Return to the previous question
+ * Return to the previous question.
*
- * @return void
- * @since 1.0.0
- * @version 3.16.6
+ * @since 1.0.0
+ * @since 3.16.6 Unknown.
+ * @since [version] Retrieve question HTML from the server when not cached.
+ *
+ * @return {Void}
*/
previous_question: function() {
var self = this;
- self.toggle_loader( 'show', LLMS.l10n.translate( 'Loading Question...' ) );
- self.update_progress_bar( 'decrement' );
+ this.save_question( {
+ previous_question: true,
+ callback: function() {
- var ids = Object.keys( self.questions ),
- curr = ids.indexOf( 'q-' + self.current_question ),
- prev_id = ids[0];
+ self.toggle_loader( 'show', LLMS.l10n.translate( 'Loading Question...' ) );
+ self.update_progress_bar( 'decrement' );
- if ( curr >= 1 ) {
- prev_id = ids[ curr - 1 ];
- }
+ var ids = Object.keys( self.questions ),
+ curr = ids.indexOf( 'q-' + self.current_question ),
+ prev_id = ids[0];
- setTimeout( function() {
- self.toggle_loader( 'hide' );
- self.load_question( self.questions[ prev_id ] );
- }, 100 );
+ if ( curr >= 1 ) {
+ prev_id = ids[ curr - 1 ];
+ }
+
+ // Retrieve previous question HTML from the server.
+ if ( ! self.questions[ prev_id ] ) {
+ LLMS.Ajax.call( {
+ data: {
+ action : 'quiz_get_question',
+ attempt_key: self.attempt_key,
+ question_id: prev_id.substring(2), // Remove 'q-'.
+ },
+ success: function( r ) {
+
+ self.toggle_loader( 'hide' );
+ if ( r.data && r.data.html ) {
+
+ self.load_question( r.data.html );
+
+ } else if ( r.data && r.data.redirect ) {
+
+ self.redirect( r.data.redirect );
+
+ } else if ( r.message ) {
+ self.$container.append( '
' + r.message + '
' );
+
+ } else {
+
+ var msg = LLMS.l10n.translate( 'An unknown error occurred. Please try again.' );
+ self.$container.append( '' + msg + '
' );
+
+ }
+
+ }
+
+ } );
+
+ } else {
+ setTimeout( function() {
+ self.toggle_loader( 'hide' );
+ self.load_question( self.questions[ prev_id ] );
+ }, 100 );
+ }
+ }
+ });
},
/**
@@ -378,44 +481,96 @@
},
/**
- * Start a Quiz via AJAX call
+ * Start a Quiz.
*
- * @return void
- * @since 1.0.0
- * @version 3.24.3
+ * @since 1.0.0
+ * @since 3.24.3 Unknown.
+ * @since [version] Abstracted the function in `init_quiz`.
+ *
+ * @return {Void}
*/
start_quiz: function () {
+ this.init_quiz( 'quiz_start' );
+ },
+
+ /**
+ * Resume a Quiz.
+ *
+ * @since [version]
+ *
+ * @return {Void}
+ */
+ resume_quiz: function () {
+
+ this.init_quiz( 'quiz_resume' );
+ },
+
+ /**
+ * Initiate 'Start' or 'Resume' action on a Quiz via AJAX call.
+ *
+ * @since [version]
+ *
+ * @return {Void}
+ */
+ init_quiz: function ( action ) {
+
var self = this;
+ if( 'quiz_resume' === action ) {
+ // Disable resume button.
+ $( '#llms_resume_quiz' ).attr( 'disabled', 'disabled' );
+ }
+
this.load_ui_elements();
this.$ui = $( '#llms-quiz-ui' );
this.$buttons = $( '#llms-quiz-nav button' );
this.$container = $( '#llms-quiz-question-wrapper' );
- // bind submission event for answering questions
+ // Bind submission event for answering questions.
$( '#llms-next-question, #llms-complete-quiz' ).on( 'click', function( e ) {
e.preventDefault();
self.answer_question( $( this ) );
} );
- // bind submission event for navigating backwards
+ // Bind submission event for navigating backwards.
$( '#llms-prev-question' ).on( 'click', function( e ) {
e.preventDefault();
self.previous_question();
} );
- LLMS.Ajax.call( {
- data: {
+ // Bind exit event for quiz.
+ $( '#llms-quiz-nav' ).on( 'click', '#llms-exit-quiz', function( e ) {
+ e.preventDefault();
+ self.save_question( {
+ exit_quiz: true,
+ callback: function() {
+ self.exiting_quiz = true;
+ window.location.reload();
+ }
+ });
+ } );
+
+ if ( 'quiz_resume' === action ) {
+ data = {
+ action: 'quiz_resume',
+ attempt_key: $( '#llms-attempt-key' ).val(),
+ };
+ } else {
+ data = {
action: 'quiz_start',
attempt_key: $( '#llms-attempt-key' ).val(),
lesson_id : $( '#llms-lesson-id' ).val(),
quiz_id : $( '#llms-quiz-id' ).val(),
- },
+ };
+ }
+
+ LLMS.Ajax.call( {
+ data: data,
beforeSend: function() {
self.status = true;
- $( '#llms-quiz-wrapper, #quiz-start-button' ).remove();
+ $( '#llms-quiz-wrapper, #quiz-start-button, #quiz-resume-button' ).remove();
$( 'html, body' ).stop().animate( {scrollTop: 0 }, 500 );
self.toggle_loader( 'show', LLMS.l10n.translate( 'Loading Quiz...' ) );
@@ -429,16 +584,27 @@
if ( r.data && r.data.html ) {
- // start the quiz timer when a time limit is set
- if ( r.data.time_limit ) {
+ self.attempt_key = r.data.attempt_key;
+ self.total_questions = r.data.total;
+ self.resumable = r.data.can_be_resumed;
+
+ if( 'quiz_resume' === action ) {
+ r.data.question_ids.forEach( id => self.questions[`q-${id}`] = '' );
+ } else if ( r.data.time_limit ) {
self.start_quiz_timer( r.data.time_limit );
}
- self.attempt_key = r.data.attempt_key;
- self.total_questions = r.data.total;
+ // Adding Exit Button in Layout if quiz is resumable.
+ if ( self.resumable ) {
+ $( '#llms-quiz-nav' ).append( '' );
+ }
self.load_question( r.data.html );
+ if ( 'quiz_resume' === action ) {
+ self.update_progress_bar( 'reload' );
+ }
+
} else if ( r.message ) {
self.$container.append( '' + r.message + '
' );
@@ -469,7 +635,6 @@
} );
}
-
},
/**
@@ -600,6 +765,11 @@
*/
load_ui_elements: function() {
+ // Removing the quiz UI elements if they already exist.
+ if ( $( '#llms-quiz-ui').length > 0 ) {
+ $( '#llms-quiz-ui' ).remove();
+ }
+
var $html = $( '' ),
$header = $( '' )
$footer = $( '' );
@@ -670,17 +840,17 @@
},
/**
- * Update the progress bar and toggle button availability based on question the question being shown
+ * Update the progress bar and toggle button availability based on question the question being shown.
*
- * @param {[type]} qid [description]
- * @return {[type]}
- * @since 3.16.0
- * @version 3.16.0
+ * @since 3.16.0
+ * @since [version] Show counter and set the total as when needed.
+ *
+ * @param {Int} qid Question ID.
+ * @return {Void}
*/
update_progress: function( qid ) {
- var index = this.get_question_index( qid ),
- progress;
+ var index = this.get_question_index( qid );
if ( -1 === index ) {
return;
@@ -689,12 +859,12 @@
index++;
$( '#llms-quiz-counter .llms-current' ).text( index );
- if ( index === 1 ) {
+ if ( index > 0 && ! $( '#llms-quiz-counter .llms-total' ).text() ) {
$( '#llms-quiz-counter .llms-total' ).text( this.total_questions );
$( '#llms-quiz-counter' ).show();
}
- // handle prev question
+ // Handle prev question.
if ( index >= 2 ) {
$( '#llms-prev-question' ).show();
} else {
diff --git a/assets/js/llms-quiz.min.js b/assets/js/llms-quiz.min.js
deleted file mode 100644
index 597c95192d..0000000000
--- a/assets/js/llms-quiz.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-!function(c){var t={$buttons:null,$container:null,$ui:null,attempt_key:null,current_question:0,total_questions:0,questions:{},validators:{},status:null,bind:function(){var e=this;c("#llms_start_quiz").on("click",function(t){t.preventDefault(),e.start_quiz()}),c(".llms-donut").each(function(){LLMS.Donut(c(this))}),c("#llms-quiz-attempt-select").on("change",function(){var t=c(this).val();t&&(window.location.href=t)}),c(window).on("beforeunload",function(){if(e.status)return LLMS.l10n.translate("Are you sure you wish to quit this quiz attempt?")}),c(window).on("unload",function(){e.status&&e.complete_quiz()}),c(document).on("llms-post-append-question",e.post_append_question),this.register_validator("content",this.validate),this.register_validator("choice",this.validate_choice),this.register_validator("picture_choice",this.validate_choice),this.register_validator("true_false",this.validate_choice)},add_error:function(t){this.$container.find(".llms-error").remove();var e=c(''+t+'
');e.on("click","a",function(t){t.preventDefault(),e.fadeOut("200"),setTimeout(function(){e.remove()},210)}),this.$container.append(e)},answer_question:function(e){var t,n=this,s=this.$container.find(".llms-question-wrapper"),i=s.attr("data-type");if(this.validators[i]){if(!(t=this.validators[i](s))||!0!==t.valid||!t.answer)return n.add_error(t.valid);LLMS.Ajax.call({data:{action:"quiz_answer_question",answer:t.answer,attempt_key:n.attempt_key,question_id:s.attr("data-id"),question_type:s.attr("data-type")},beforeSend:function(){var t=e.hasClass("llms-button-quiz-complete")?LLMS.l10n.translate("Grading Quiz..."):LLMS.l10n.translate("Loading Question...");n.toggle_loader("show",t),n.update_progress_bar("increment")},success:function(t){n.toggle_loader("hide"),t.data&&t.data.html?t.data.question_id&&n.questions["q-"+t.data.question_id]?n.load_question(n.questions["q-"+t.data.question_id]):n.load_question(t.data.html):t.data&&t.data.redirect?n.redirect(t.data.redirect):t.message?n.$container.append(""+t.message+"
"):(t=LLMS.l10n.translate("An unknown error occurred. Please try again."),n.$container.append(""+t+"
"))},error:function(t,e,s){n.reload_question(),n.add_error(LLMS.l10n.translate("An unknown error occurred. Please try again.")),console.log(s)}})}else console.log("No validator registered for question type "+i)},complete_quiz:function(){var e=this;LLMS.Ajax.call({data:{action:"quiz_end",attempt_key:e.attempt_key},beforeSend:function(){e.toggle_loader("show","Grading Quiz...")},success:function(t){e.toggle_loader("hide"),t.data&&t.data.redirect?e.redirect(t.data.redirect):t.message?e.$container.append(""+t.message+"
"):(t=LLMS.l10n.translate("An unknown error occurred. Please try again."),e.$container.append(""+t+"
"))}})},get_question_index:function(t){return Object.keys(this.questions).indexOf("q-"+t)},redirect:function(t){this.toggle_loader("show","Grading Quiz..."),this.status=null,window.location.href=t},reload_question:function(){var t=this;t.toggle_loader("show",LLMS.l10n.translate("Loading Question...")),t.update_progress_bar("reload"),setTimeout(function(){t.toggle_loader("hide"),t.load_question(t.questions["q-"+t.current_question])},100)},previous_question:function(){var t=this,e=(t.toggle_loader("show",LLMS.l10n.translate("Loading Question...")),t.update_progress_bar("decrement"),Object.keys(t.questions)),s=e.indexOf("q-"+t.current_question),n=e[0];1<=s&&(n=e[s-1]),setTimeout(function(){t.toggle_loader("hide"),t.load_question(t.questions[n])},100)},register_validator:function(t,e){this.validators[t]=e},start_quiz:function(){var e=this;this.load_ui_elements(),this.$ui=c("#llms-quiz-ui"),this.$buttons=c("#llms-quiz-nav button"),this.$container=c("#llms-quiz-question-wrapper"),c("#llms-next-question, #llms-complete-quiz").on("click",function(t){t.preventDefault(),e.answer_question(c(this))}),c("#llms-prev-question").on("click",function(t){t.preventDefault(),e.previous_question()}),LLMS.Ajax.call({data:{action:"quiz_start",attempt_key:c("#llms-attempt-key").val(),lesson_id:c("#llms-lesson-id").val(),quiz_id:c("#llms-quiz-id").val()},beforeSend:function(){e.status=!0,c("#llms-quiz-wrapper, #quiz-start-button").remove(),c("html, body").stop().animate({scrollTop:0},500),e.toggle_loader("show",LLMS.l10n.translate("Loading Quiz..."))},error:function(t,e,s){console.log(t,e,s)},success:function(t){e.toggle_loader("hide"),t.data&&t.data.html?(t.data.time_limit&&e.start_quiz_timer(t.data.time_limit),e.attempt_key=t.data.attempt_key,e.total_questions=t.data.total,e.load_question(t.data.html)):t.message?e.$container.append(""+t.message+"
"):(t=LLMS.l10n.translate("An unknown error occurred. Please try again."),e.$container.append(""+t+"
"))}}),LLMS.is_touch_device()||(this.$ui.on("mouseenter","li.llms-choice label",function(){c(this).addClass("hovered")}),this.$ui.on("mouseleave","li.llms-choice label",function(){c(this).removeClass("hovered")}))},start_quiz_timer:function(t){var e,s,n,i,a=c(''),o=LLMS.l10n.translate("Time Remaining"),l=(a.append(''+o+""),a.append(''),c("#llms-quiz-header").append(a),this),r=(new Date).getTime()+60*t*1e3,u=60*t*1e3,d=document.getElementById("llms-tiles");setTimeout(function(){l.complete_quiz()},1e3+u),this.getCountdown(t,r,u,e,s,n,i,d),setInterval(function(){l.getCountdown(t,r,u,e,s,n,i,d)},1e3)},trigger:function(t){"answer_question"===t&&(this.get_question_index(this.current_question)===this.total_questions?c("#llms-complete-quiz"):c("#llms-next-question")).trigger("click")},load_question:function(t){var t=c(t),e=t.attr("data-id");this.questions["q-"+e]||(this.questions["q-"+e]=t),this.update_progress(e),this.current_question=e,c(document).trigger("llms-pre-append-question",t),this.$container.append(t),c(document).trigger("llms-post-append-question",t)},load_ui_elements:function(){var t=c(''),e=c('');($footer=c('')).append('"),$footer.append('"),$footer.append('"),e.append(''),$footer.append('/
'),t.append(e).append('').append($footer),c("#llms-quiz-wrapper").after(t)},post_append_question:function(t,e){c(e).find("audio").length&&wp.mediaelement.initialize()},toggle_loader:function(t,e){"show"===t?(e=e||LLMS.l10n.translate("Loading..."),this.$buttons.attr("disabled","disabled"),this.$container.empty(),LLMS.Spinner.start(this.$container),this.$container.append(''+LLMS.l10n.translate(e)+"
")):(LLMS.Spinner.stop(this.$container),this.$buttons.removeAttr("disabled"),this.$container.find(".llms-quiz-loading").remove())},update_progress:function(t){t=this.get_question_index(t);-1!==t&&(t++,c("#llms-quiz-counter .llms-current").text(t),1==t&&(c("#llms-quiz-counter .llms-total").text(this.total_questions),c("#llms-quiz-counter").show()),2<=t?c("#llms-prev-question").show():c("#llms-prev-question").hide(),t===this.total_questions?(c("#llms-next-question").hide(),c("#llms-complete-quiz").show()):(c("#llms-next-question").show(),c("#llms-complete-quiz").hide()))},update_progress_bar:function(t){var e=this.get_question_index(this.current_question);switch(t){case"increment":e++;break;case"decrement":e--}progress=e/this.total_questions*100,this.$ui.find(".progress-bar-complete").css("width",progress+"%")},getCountdown:function(t,e,s,n,i,a,o,l){e=(e-(new Date).getTime())/1e3;0<=e&&(1e3*e'+i+':'+a+':'+o+"")},pad:function(t){return(t<10?"0":"")+t},validate:function(t){return{answer:[],valid:!0}},validate_choice:function(t){var e=window.llms.quizzes.validate(t),t=t.find("input:checked");return t.length?t.each(function(){e.answer.push(c(this).val())}):e.valid=LLMS.l10n.translate("You must select an answer to continue."),e}};t.bind(),window.llms=window.llms||{},window.llms.quizzes=t}(jQuery);
-//# sourceMappingURL=../maps/js/llms-quiz.min.js.map
diff --git a/assets/scss/admin/_llms-table.scss b/assets/scss/admin/_llms-table.scss
index 6f286150d6..9d02556df3 100644
--- a/assets/scss/admin/_llms-table.scss
+++ b/assets/scss/admin/_llms-table.scss
@@ -118,6 +118,10 @@
}
}
+ .llms-clear-resumable-attempts {
+ float: left;
+ }
+
.llms-table-pagination {
float: right;
}
diff --git a/includes/abstracts/abstract.llms.admin.table.php b/includes/abstracts/abstract.llms.admin.table.php
index 4d289bdf53..c734c143fe 100644
--- a/includes/abstracts/abstract.llms.admin.table.php
+++ b/includes/abstracts/abstract.llms.admin.table.php
@@ -5,7 +5,7 @@
* @package LifterLMS/Abstracts/Classes
*
* @since 3.2.0
- * @version 7.3.0
+ * @version [version]
*/
defined( 'ABSPATH' ) || exit;
@@ -576,6 +576,7 @@ public function output_table_filters_html() {
* @since 3.2.0
* @since 3.17.8 Unknown.
* @since 3.37.7 Use correct argument order for implode to fix php 7.4 deprecation.
+ * @since [version] Added button for clearing resumable attempts.
*
* @return string
* @deprecated 7.7.0 Use output_table_html() instead.
@@ -787,6 +788,19 @@ public function output_tfoot_html() {
+ has_resumable_attempts() ) : ?>
+
+
+
+
+
is_exportable ) : ?>
+ can_be_resumed() ) : // Show the clear resume attempt button only if quiz can be resumed. ?>
+
+
';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+ can_be_resumed_by_student() ) : ?>
+
+
- get_next_lesson() && llms_is_complete( get_current_user_id(), $lesson->get( 'id' ), 'lesson' ) ) : ?>
-
-
-
+ get_next_lesson() && llms_is_complete( get_current_user_id(), $lesson->get( 'id' ), 'lesson' ) ) : ?>
+
+
diff --git a/tests/assets/import-with-quiz.json b/tests/assets/import-with-quiz.json
index d51bb86676..f3cc5ed149 100644
--- a/tests/assets/import-with-quiz.json
+++ b/tests/assets/import-with-quiz.json
@@ -182,6 +182,7 @@
"modified_gmt": "2020-08-04 23:10:23",
"name": "new-lesson-quiz",
"passing_percent": 85,
+ "can_be_resumed": "yes",
"password": "",
"permalink": "http://localhost:8080/quiz/new-lesson-quiz/",
"ping_status": "closed",
diff --git a/tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-quizzes.php b/tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-quizzes.php
index 7249262926..48d8eb788f 100644
--- a/tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-quizzes.php
+++ b/tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-quizzes.php
@@ -190,9 +190,10 @@ public function test_quiz_answer_question_test_attempts_limit() {
)
);
+ // The status is "pass" so it won't be possible to answer the question again anyway (500 vs 400).
$this->assertIsWPError( $res );
- $this->assertWPErrorCodeEquals( 400, $res );
- $this->assertWPErrorMessageEquals( "You've reached the maximum number of attempts for this quiz.", $res );
+ $this->assertWPErrorCodeEquals( 500, $res );
+ $this->assertWPErrorMessageEquals( "There was an error recording your answer. Please return to the lesson and begin again.", $res );
// Reset.
$this->quiz->set( 'limit_attempts', 'no' );
|