Merge branch 'HFP-1174-overall-feedback'

pull/15/head
Frode Petterson 2017-07-13 16:12:42 +02:00
commit f89204789c
6 changed files with 264 additions and 148 deletions

View File

@ -225,30 +225,27 @@
font-size: 1.25em; font-size: 1.25em;
} }
.questionset-results .feedback-section .feedback-text {
font-weight: normal;
color: #777;
}
.questionset-results .buttons { .questionset-results .buttons {
margin-bottom: 1.5em; margin: 2em 0 1em 0;
} }
.questionset-results .result-header, .questionset-results .result-header,
.questionset-results .result-text { .questionset-results .result-text,
.questionset-results .feedback-section .feedback-text {
color: #1a73d9; color: #1a73d9;
font-weight: bold; font-weight: bold;
} }
.questionset-results .result-header { .questionset-results .result-header {
font-size: 2em; font-size: 2em;
margin-top: 1em; margin: 1em 0.5em 0.5em 0.5em;
} }
.questionset-results .result-text { .questionset-results .result-text,
.questionset-results .feedback-section .feedback-text {
font-size: 1.25em; font-size: 1.25em;
line-height: 1.25em; line-height: 1.25em;
margin: 1em 1em 2.25em; margin: 1em;
} }
/* No margin for questions when no frame */ /* No margin for questions when no frame */

View File

@ -112,11 +112,13 @@ H5P.QuestionSet = function (options, contentId, contentData) {
showResultPage: true, showResultPage: true,
noResultMessage: 'Finished', noResultMessage: 'Finished',
message: 'Your result:', message: 'Your result:',
successGreeting: 'Congratulations!', oldFeedback: {
successComment: 'You have enough correct answers to pass the test.', successGreeting: '',
failGreeting: 'Sorry!', successComment: '',
failComment: "You don't have enough correct answers to pass this test.", failGreeting: '',
scoreString: 'You got @score of @total points', failComment: ''
},
overallFeedback: [],
finishButtonText: 'Finish', finishButtonText: 'Finish',
solutionButtonText: 'Show solution', solutionButtonText: 'Show solution',
retryButtonText: 'Retry', retryButtonText: 'Retry',
@ -158,27 +160,27 @@ H5P.QuestionSet = function (options, contentId, contentData) {
var randomizeQuestionOrdering = function (questions) { var randomizeQuestionOrdering = function (questions) {
// Save the original order of the questions in a multidimensional array [[question0,0],[question1,1]... // Save the original order of the questions in a multidimensional array [[question0,0],[question1,1]...
var questionOrdering = questions.map(function(questionInstance, index) { return [questionInstance, index] }); var questionOrdering = questions.map(function (questionInstance, index) { return [questionInstance, index]; });
// Shuffle the multidimensional array // Shuffle the multidimensional array
questionOrdering = H5P.shuffleArray(questionOrdering); questionOrdering = H5P.shuffleArray(questionOrdering);
// Retrieve question objects from the first index // Retrieve question objects from the first index
var questions = []; questions = [];
for (var i = 0; i < questionOrdering.length; i++) { for (var i = 0; i < questionOrdering.length; i++) {
questions[i] = questionOrdering[i][0]; questions[i] = questionOrdering[i][0];
} }
// Retrieve the new shuffled order from the second index // Retrieve the new shuffled order from the second index
var newOrder = []; var newOrder = [];
for (var i = 0; i< questionOrdering.length; i++) { for (var j = 0; j < questionOrdering.length; j++) {
// Use a previous order if it exists // Use a previous order if it exists
if(contentData.previousState && contentData.previousState.questionOrder) { if (contentData.previousState && contentData.previousState.questionOrder) {
newOrder[i] = questionOrder[questionOrdering[i][1]]; newOrder[j] = questionOrder[questionOrdering[j][1]];
} }
else { else {
newOrder[i] = questionOrdering[i][1]; newOrder[j] = questionOrdering[j][1];
} }
} }
@ -193,7 +195,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
if (params.poolSize > 0) { if (params.poolSize > 0) {
// If a previous pool exists, recreate it // If a previous pool exists, recreate it
if(contentData.previousState && contentData.previousState.poolOrder) { if (contentData.previousState && contentData.previousState.poolOrder) {
poolOrder = contentData.previousState.poolOrder; poolOrder = contentData.previousState.poolOrder;
// Recreate the pool from the saved data // Recreate the pool from the saved data
@ -247,7 +249,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
* @param {object} questions H5P content types to be created as instances * @param {object} questions H5P content types to be created as instances
* @return {array} Array of questions instances * @return {array} Array of questions instances
*/ */
var createQuestionInstancesFromQuestions = function(questions) { var createQuestionInstancesFromQuestions = function (questions) {
var result = []; var result = [];
// Create question instances from questions // Create question instances from questions
// Instantiate question instances // Instantiate question instances
@ -312,8 +314,8 @@ H5P.QuestionSet = function (options, contentId, contentData) {
var _updateButtons = function () { var _updateButtons = function () {
// Verify that current question is answered when backward nav is disabled // Verify that current question is answered when backward nav is disabled
if (params.disableBackwardsNavigation) { if (params.disableBackwardsNavigation) {
if (questionInstances[currentQuestion].getAnswerGiven() if (questionInstances[currentQuestion].getAnswerGiven() &&
&& questionInstances.length-1 !== currentQuestion) { questionInstances.length-1 !== currentQuestion) {
questionInstances[currentQuestion].showButton('next'); questionInstances[currentQuestion].showButton('next');
} }
else { else {
@ -418,15 +420,19 @@ H5P.QuestionSet = function (options, contentId, contentData) {
var currentQuestion = params.questions[currentQuestionIndex]; var currentQuestion = params.questions[currentQuestionIndex];
var hasAutoPlay = currentQuestion var hasAutoPlay = currentQuestion &&
&& currentQuestion.params.media currentQuestion.params.media &&
&& currentQuestion.params.media.params.playback.autoplay; currentQuestion.params.media.params &&
currentQuestion.params.media.params.playback &&
currentQuestion.params.media.params.playback.autoplay;
if (hasAutoPlay) { if (hasAutoPlay && typeof questionInstances[currentQuestionIndex].play === 'function') {
questionInstances[currentQuestionIndex].play(); questionInstances[currentQuestionIndex].play();
} }
}; };
/** /**
* Show solutions for subcontent, and hide subcontent buttons. * Show solutions for subcontent, and hide subcontent buttons.
* Used for contracts with integrated content. * Used for contracts with integrated content.
@ -520,7 +526,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
//Force the last page to be reRendered //Force the last page to be reRendered
rendered = false; rendered = false;
if(params.poolSize > 0){ if (params.poolSize > 0) {
// Make new pool from params.questions // Make new pool from params.questions
// Randomize and get the results // Randomize and get the results
@ -573,7 +579,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
var replaceQuestionsInDOM = function (questionInstances) { var replaceQuestionsInDOM = function (questionInstances) {
// Find all question containers and detach questions from them // Find all question containers and detach questions from them
$('.question-container', $myDom).each(function (){ $('.question-container', $myDom).each(function () {
$(this).children().detach(); $(this).children().detach();
}); });
@ -588,19 +594,19 @@ H5P.QuestionSet = function (options, contentId, contentData) {
question.attach($('.question-container:eq(' + i + ')', $myDom)); question.attach($('.question-container:eq(' + i + ')', $myDom));
//Show buttons if necessary //Show buttons if necessary
if(questionInstances[questionInstances.length -1] === question if (questionInstances[questionInstances.length -1] === question &&
&& question.hasButton('finish')) { question.hasButton('finish')) {
question.showButton('finish'); question.showButton('finish');
} }
if(questionInstances[questionInstances.length -1] !== question if (questionInstances[questionInstances.length -1] !== question &&
&& question.hasButton('next')) { question.hasButton('next')) {
question.showButton('next'); question.showButton('next');
} }
if(questionInstances[0] !== question if (questionInstances[0] !== question &&
&& question.hasButton('prev') question.hasButton('prev') &&
&& !params.disableBackwardsNavigation) { !params.disableBackwardsNavigation) {
question.showButton('prev'); question.showButton('prev');
} }
@ -641,7 +647,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
* @param {number} dotIndex Index of dot * @param {number} dotIndex Index of dot
* @param {boolean} isAnswered True if is answered, False if not answered * @param {boolean} isAnswered True if is answered, False if not answered
*/ */
var toggleAnsweredDot = function(dotIndex, isAnswered) { var toggleAnsweredDot = function (dotIndex, isAnswered) {
var $el = $('.progress-dot:eq(' + dotIndex +')', $myDom); var $el = $('.progress-dot:eq(' + dotIndex +')', $myDom);
// Skip current button // Skip current button
@ -703,12 +709,9 @@ H5P.QuestionSet = function (options, contentId, contentData) {
// Get total score. // Get total score.
var finals = self.getScore(); var finals = self.getScore();
var totals = self.getMaxScore(); var totals = self.getMaxScore();
var scoreString = params.endGame.scoreString.replace("@score", finals).replace("@total", totals);
var scoreString = H5P.Question.determineOverallFeedback(params.endGame.overallFeedback, finals / totals).replace('@score', finals).replace('@total', totals);
var success = ((100 * finals / totals) >= params.passPercentage); var success = ((100 * finals / totals) >= params.passPercentage);
var eventData = {
score: scoreString,
passed: success
};
/** /**
* Makes our buttons behave like other buttons. * Makes our buttons behave like other buttons.
@ -731,8 +734,8 @@ H5P.QuestionSet = function (options, contentId, contentData) {
var eparams = { var eparams = {
message: params.endGame.showResultPage ? params.endGame.message : params.endGame.noResultMessage, message: params.endGame.showResultPage ? params.endGame.message : params.endGame.noResultMessage,
comment: params.endGame.showResultPage ? (success ? params.endGame.successGreeting : params.endGame.failGreeting) : undefined, comment: params.endGame.showResultPage ? (success ? params.endGame.oldFeedback.successGreeting : params.endGame.oldFeedback.failGreeting) : undefined,
resulttext: params.endGame.showResultPage ? (success ? params.endGame.successComment : params.endGame.failComment) : undefined, resulttext: params.endGame.showResultPage ? (success ? params.endGame.oldFeedback.successComment : params.endGame.oldFeedback.failComment) : undefined,
finishButtonText: params.endGame.finishButtonText, finishButtonText: params.endGame.finishButtonText,
solutionButtonText: params.endGame.solutionButtonText, solutionButtonText: params.endGame.solutionButtonText,
retryButtonText: params.endGame.retryButtonText retryButtonText: params.endGame.retryButtonText
@ -743,10 +746,6 @@ H5P.QuestionSet = function (options, contentId, contentData) {
$myDom.append(endTemplate.render(eparams)); $myDom.append(endTemplate.render(eparams));
if (params.endGame.showResultPage) { if (params.endGame.showResultPage) {
// Add event handlers to summary buttons
hookUpButton('.qs-finishbutton', function () {
self.trigger('h5pQuestionSetFinished', eventData);
});
hookUpButton('.qs-solutionbutton', function () { hookUpButton('.qs-solutionbutton', function () {
showSolutions(); showSolutions();
$myDom.children().hide().filter('.questionset').show(); $myDom.children().hide().filter('.questionset').show();
@ -876,7 +875,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
}); });
// Hide next button if it is the last question // Hide next button if it is the last question
if(questionInstances[questionInstances.length -1] === question) { if (questionInstances[questionInstances.length -1] === question) {
question.hideButton('next'); question.hideButton('next');
} }
@ -1104,10 +1103,10 @@ H5P.QuestionSet = function (options, contentId, contentData) {
return info; return info;
}; };
this.getQuestions = function() { this.getQuestions = function () {
return questionInstances; return questionInstances;
}; };
this.showSolutions = function() { this.showSolutions = function () {
renderSolutions = true; renderSolutions = true;
}; };
@ -1165,7 +1164,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
/** /**
* Add the question itself to the definition part of an xAPIEvent * Add the question itself to the definition part of an xAPIEvent
*/ */
var addQuestionToXAPI = function(xAPIEvent) { var addQuestionToXAPI = function (xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']); var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
$.extend(definition, getxAPIDefinition()); $.extend(definition, getxAPIDefinition());
}; };
@ -1176,8 +1175,8 @@ H5P.QuestionSet = function (options, contentId, contentData) {
* @param {Object} metaContentType * @param {Object} metaContentType
* @returns {array} * @returns {array}
*/ */
var getXAPIDataFromChildren = function(metaContentType) { var getXAPIDataFromChildren = function (metaContentType) {
return metaContentType.getQuestions().map(function(question) { return metaContentType.getQuestions().map(function (question) {
return question.getXAPIData(); return question.getXAPIData();
}); });
}; };
@ -1188,7 +1187,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
* *
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6} * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/ */
this.getXAPIData = function(){ this.getXAPIData = function () {
var xAPIEvent = this.createXAPIEventTemplate('answered'); var xAPIEvent = this.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent); addQuestionToXAPI(xAPIEvent);
xAPIEvent.setScoredResult(this.getScore(), xAPIEvent.setScoredResult(this.getScore(),
@ -1200,7 +1199,7 @@ H5P.QuestionSet = function (options, contentId, contentData) {
return { return {
statement: xAPIEvent.data.statement, statement: xAPIEvent.data.statement,
children: getXAPIDataFromChildren(this) children: getXAPIDataFromChildren(this)
} };
}; };
}; };

View File

@ -141,30 +141,48 @@
"description": "This heading will be displayed at the end of the quiz when the user has answered all questions." "description": "This heading will be displayed at the end of the quiz when the user has answered all questions."
}, },
{ {
"label": "Score display text", "label": "Overall Feedback",
"description": "Text used to display Total user score. \"@score\" will be replaced by calculated score, \"@total\" will be replaced by maximum possible score. ", "fields": [
"default": "You got @score of @total points" {
"widgets": [],
"label": "Define custom feedback for any score range",
"description": "Example: 0-20% Bad score, 21-91% Average Score, 91-100% Great Score!",
"entity": "range",
"field": {
"fields": [
{
"label": "Score Range"
},
{},
{
"label": "Feedback for defined score range",
"placeholder": "Fill in the feedback"
}
]
}
}
]
}, },
{ {
"label": "Quiz passed greeting", "label": "Old Feedback",
"placeholder": "Congratulations!", "fields": [
"default": "Congratulations!", {
"description": "This text will be displayed above the score if the user has successfully passed the quiz." "label": "Quiz passed greeting",
}, "description": "This text will be displayed above the score if the user has successfully passed the quiz."
{ },
"label": "Passed comment", {
"default": "You did very well!", "label": "Passed comment",
"description": "This comment will be displayed after the score if the user has successfully passed the quiz." "description": "This comment will be displayed after the score if the user has successfully passed the quiz."
}, },
{ {
"label": "Quiz failed title", "label": "Quiz failed title",
"default": "You did not pass this time.", "description": "This text will be displayed above the score if the user has failed the quiz."
"description": "This text will be displayed above the score if the user has failed the quiz." },
}, {
{ "label": "Failed comment",
"label": "Failed comment", "description": "This comment will be displayed after the score if the user has failed the quiz."
"default": "Have another try!", }
"description": "This comment will be displayed after the score if the user has failed the quiz." ]
}, },
{ {
"label": "Solution button label", "label": "Solution button label",

View File

@ -3,7 +3,7 @@
"description": "Put together a set of different questions that has to be solved. (Quiz)", "description": "Put together a set of different questions that has to be solved. (Quiz)",
"contentType": "question", "contentType": "question",
"majorVersion": 1, "majorVersion": 1,
"minorVersion": 15, "minorVersion": 13,
"patchVersion": 0, "patchVersion": 0,
"embedTypes": [ "embedTypes": [
"iframe" "iframe"
@ -14,7 +14,7 @@
"author": "Joubel", "author": "Joubel",
"coreApi": { "coreApi": {
"majorVersion": 1, "majorVersion": 1,
"minorVersion": 6 "minorVersion": 14
}, },
"license": "MIT", "license": "MIT",
"preloadedJs": [ "preloadedJs": [
@ -50,6 +50,11 @@
} }
], ],
"editorDependencies": [ "editorDependencies": [
{
"machineName": "H5PEditor.RangeList",
"majorVersion": 1,
"minorVersion": 0
},
{ {
"machineName": "H5PEditor.VerticalTabs", "machineName": "H5PEditor.VerticalTabs",
"majorVersion": 1, "majorVersion": 1,

View File

@ -276,7 +276,6 @@
"label": "Feedback heading", "label": "Feedback heading",
"importance": "low", "importance": "low",
"default": "Your result:", "default": "Your result:",
"optional": true,
"description": "This heading will be displayed at the end of the quiz when the user has answered all questions.", "description": "This heading will be displayed at the end of the quiz when the user has answered all questions.",
"tags": [ "tags": [
"strong", "strong",
@ -284,75 +283,127 @@
] ]
}, },
{ {
"name": "scoreString", "name": "overallFeedback",
"type": "text", "type": "group",
"label": "Score display text", "label": "Overall Feedback",
"importance": "low", "importance": "low",
"description": "Text used to display Total user score. \"@score\" will be replaced by calculated score, \"@total\" will be replaced by maximum possible score. ", "fields": [
"default": "You got @score of @total points", {
"optional": true "name": "overallFeedback",
}, "type": "list",
{ "widgets": [
"name": "successGreeting", {
"type": "text", "name": "RangeList",
"label": "Quiz passed greeting", "label": "Default"
"importance": "low", }
"placeholder": "Congratulations!", ],
"default": "Congratulations!", "importance": "high",
"optional": true, "label": "Define custom feedback for any score range",
"description": "This text will be displayed above the score if the user has successfully passed the quiz.", "description": "Example: 0-20% Bad score, 21-91% Average Score, 91-100% Great Score!",
"tags": [ "entity": "range",
"strong", "min": 1,
"em" "defaultNum": 1,
"optional": true,
"field": {
"name": "overallFeedback",
"type": "group",
"importance": "low",
"fields": [
{
"name": "from",
"type": "number",
"label": "Score Range",
"min": 0,
"max": 100,
"default": 0,
"unit": "%"
},
{
"name": "to",
"type": "number",
"min": 0,
"max": 100,
"default": 100,
"unit": "%"
},
{
"name": "feedback",
"type": "text",
"label": "Feedback for defined score range",
"importance": "low",
"placeholder": "Fill in the feedback",
"optional": true
}
]
}
}
] ]
}, },
{ {
"name": "successComment", "name": "oldFeedback",
"type": "text", "type": "group",
"widget": "html", "label": "Old Feedback",
"label": "Passed comment",
"importance": "low", "importance": "low",
"default": "You did very well!", "deprecated": true,
"optional": true, "fields": [
"description": "This comment will be displayed after the score if the user has successfully passed the quiz.", {
"tags": [ "name": "successGreeting",
"sub", "type": "text",
"sup", "label": "Quiz passed greeting",
"strong", "importance": "low",
"em", "optional": true,
"a", "description": "This text will be displayed above the score if the user has successfully passed the quiz.",
"p" "tags": [
] "strong",
}, "em"
{ ]
"name": "failGreeting", },
"type": "text", {
"label": "Quiz failed title", "name": "successComment",
"importance": "low", "type": "text",
"default": "You did not pass this time.", "widget": "html",
"optional": true, "label": "Passed comment",
"description": "This text will be displayed above the score if the user has failed the quiz.", "importance": "low",
"tags": [ "optional": true,
"strong", "description": "This comment will be displayed after the score if the user has successfully passed the quiz.",
"em" "tags": [
] "sub",
}, "sup",
{ "strong",
"name": "failComment", "em",
"type": "text", "a",
"widget": "html", "p"
"label": "Failed comment", ]
"importance": "low", },
"default": "Have another try!", {
"optional": true, "name": "failGreeting",
"description": "This comment will be displayed after the score if the user has failed the quiz.", "type": "text",
"tags": [ "label": "Quiz failed title",
"sub", "importance": "low",
"sup", "optional": true,
"strong", "description": "This text will be displayed above the score if the user has failed the quiz.",
"em", "tags": [
"a", "strong",
"p" "em"
]
},
{
"name": "failComment",
"type": "text",
"widget": "html",
"label": "Failed comment",
"importance": "low",
"optional": true,
"description": "This comment will be displayed after the score if the user has failed the quiz.",
"tags": [
"sub",
"sup",
"strong",
"em",
"a",
"p"
]
}
] ]
}, },
{ {

View File

@ -52,6 +52,52 @@ H5PUpgrades['H5P.QuestionSet'] = (function ($) {
// Remove old copyright dialog question label // Remove old copyright dialog question label
delete parameters.questionLabel; delete parameters.questionLabel;
finished(null, parameters);
},
/**
* Asynchronous content upgrade hook.
*
* Upgrade params to support overall feedback
*
* @param {Object} parameters
* @param {function} finished
*/
13: function (parameters, finished) {
parameters.endGame = parameters.endGame || {};
parameters.endGame.overallFeedback = [];
if (parameters.endGame.scoreString) {
parameters.endGame.overallFeedback.push({
from: 0,
to: 100,
feedback: parameters.endGame.scoreString
});
delete parameters.endGame.scoreString;
}
// Group old feedback fields
if (parameters.endGame.successGreeting ||
parameters.endGame.successComment ||
parameters.endGame.failGreeting ||
parameters.endGame.failComment) {
parameters.endGame.oldFeedback = {};
if (parameters.endGame.successGreeting) {
parameters.endGame.oldFeedback.successGreeting = parameters.endGame.successGreeting;
}
if (parameters.endGame.successComment) {
parameters.endGame.oldFeedback.successComment = parameters.endGame.successComment;
}
if (parameters.endGame.failGreeting) {
parameters.endGame.oldFeedback.failGreeting = parameters.endGame.failGreeting;
}
if (parameters.endGame.failComment) {
parameters.endGame.oldFeedback.failComment = parameters.endGame.failComment;
}
}
finished(null, parameters); finished(null, parameters);
} }
} }