From 80ac20a3de839b27ddf78f53bb065e8a180c441f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 9 Feb 2016 13:58:43 +0100 Subject: [PATCH 001/101] MOBILE-1406 quiz: Implement quiz entry page --- www/addons/mod_quiz/controllers/index.js | 258 +++++ www/addons/mod_quiz/lang/en.json | 34 + www/addons/mod_quiz/main.js | 39 + www/addons/mod_quiz/scss/styles.scss | 22 + www/addons/mod_quiz/services/handlers.js | 72 ++ www/addons/mod_quiz/services/helper.js | 100 ++ www/addons/mod_quiz/services/quiz.js | 1243 ++++++++++++++++++++++ www/addons/mod_quiz/templates/index.html | 77 ++ www/core/components/course/lang/en.json | 1 + www/core/lang/en.json | 1 + www/core/lib/text.js | 25 + 11 files changed, 1872 insertions(+) create mode 100644 www/addons/mod_quiz/controllers/index.js create mode 100644 www/addons/mod_quiz/lang/en.json create mode 100644 www/addons/mod_quiz/main.js create mode 100644 www/addons/mod_quiz/scss/styles.scss create mode 100644 www/addons/mod_quiz/services/handlers.js create mode 100644 www/addons/mod_quiz/services/helper.js create mode 100644 www/addons/mod_quiz/services/quiz.js create mode 100644 www/addons/mod_quiz/templates/index.html diff --git a/www/addons/mod_quiz/controllers/index.js b/www/addons/mod_quiz/controllers/index.js new file mode 100644 index 00000000000..0406fd32ab7 --- /dev/null +++ b/www/addons/mod_quiz/controllers/index.js @@ -0,0 +1,258 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Quiz index controller. + * + * @module mm.addons.mod_quiz + * @ngdoc controller + * @name mmaModQuizIndexCtrl + */ +.controller('mmaModQuizIndexCtrl', function($scope, $stateParams, $mmaModQuiz, $mmCourse, $ionicPlatform, $q, $translate, + $mmUtil, $mmaModQuizHelper) { + var module = $stateParams.module || {}, + courseId = $stateParams.courseid, + quiz, + overallStats, + attempts, + options, + bestGrade, + gradebookData, + accessInfo, + moreAttempts; + + $scope.title = module.name; + $scope.description = module.description; + $scope.moduleUrl = module.url; + $scope.isTablet = $ionicPlatform.isTablet(); + $scope.courseId = courseId; + + // Convenience function to get Quiz data. + function fetchQuizData(refresh) { + return $mmaModQuiz.getQuiz(courseId, module.id).then(function(quizData) { + quiz = quizData; + quiz.gradeMethodReadable = $mmaModQuiz.getQuizGradeMethod(quiz.grademethod); + + $scope.now = new Date().getTime(); + $scope.title = quiz.name || $scope.title; + $scope.description = quiz.intro || $scope.description; + $scope.quiz = quiz; + + return getAttempts().catch(showError); + + }, function(message) { + if (!refresh) { + // Get quiz failed, retry without using cache since it might be a new activity. + return refreshData(); + } + return showError(message); + }); + } + + // Convenience function to get Quiz attempts. + function getAttempts() { + + // Get access information of last attempt (it also works if no attempts made). + return $mmaModQuiz.getAccessInformation(quiz.id, 0).then(function(info) { + accessInfo = info; + $scope.accessRules = accessInfo.accessrules; + quiz.showReviewColumn = accessInfo.canreviewmyattempts; + + // Get attempts. + return $mmaModQuiz.getUserAttempts(quiz.id).then(function(atts) { + attempts = atts; + + return treatAttempts().then(function() { + // Check if user can create/continue attempts. + if (attempts.length) { + var lastAttempt = attempts[attempts.length - 1]; + moreAttempts = !$mmaModQuiz.isAttemptFinished(lastAttempt.state) || !accessInfo.isfinished; + } else { + moreAttempts = !accessInfo.isfinished; + } + + $scope.attempts = attempts; + + getButtonText(); + return getResultInfo(); + }); + }); + }); + } + + // Treat user attempts. + function treatAttempts() { + if (!attempts || !attempts.length) { + return $q.when(); + } + + var lastFinished = $mmaModQuiz.getLastFinishedAttemptFromList(attempts), + promises = []; + + // Get combined review options. + promises.push($mmaModQuiz.getCombinedReviewOptions(quiz.id).then(function(result) { + options = result; + })); + + // Get best grade. + promises.push($mmaModQuiz.getUserBestGrade(quiz.id).then(function(best) { + bestGrade = best; + })); + + // Get gradebook grade. + promises.push($mmaModQuiz.getGradeFromGradebook(courseId, module.id).then(function(data) { + gradebookData = data; + })); + + return $q.all(promises).then(function() { + var quizGrade = typeof gradebookData.grade != 'undefined' ? gradebookData.grade : bestGrade.grade; + + // Calculate data to construct the header of the attempts table. + $mmaModQuizHelper.setQuizCalculatedData(quiz, options); + + overallStats = lastFinished && options.alloptions.marks >= $mmaModQuiz.QUESTION_OPTIONS_MARK_AND_MAX; + + // Calculate data to show for each attempt. + angular.forEach(attempts, function(attempt) { + // Highlight the highest grade if appropriate. + var shouldHighlight = overallStats && quiz.grademethod == $mmaModQuiz.GRADEHIGHEST && attempts.length > 1; + $mmaModQuizHelper.setAttemptCalculatedData(quiz, attempt, shouldHighlight, quizGrade); + }); + }); + } + + // Get result info to show. + function getResultInfo() { + if (attempts.length && quiz.showGradeColumn && bestGrade.hasgrade && typeof gradebookData.grade != 'undefined') { + + $scope.showResults = true; + $scope.gradeOverridden = gradebookData.grade != bestGrade.grade; + $scope.gradebookFeedback = gradebookData.feedback; + + if (overallStats) { + // Show the quiz grade. The message shown is different if the quiz is finished. + if (moreAttempts) { + $scope.gradeResult = $translate.instant('mma.mod_quiz.gradesofar', {$a: { + method: quiz.gradeMethodReadable, + mygrade: $mmaModQuiz.formatGrade(gradebookData.grade, quiz.decimalpoints), + quizgrade: quiz.gradeFormatted + }}); + } else { + var outOfShort = $translate.instant('mma.mod_quiz.outofshort', {$a: { + grade: $mmaModQuiz.formatGrade(gradebookData.grade, quiz.decimalpoints), + maxgrade: quiz.gradeFormatted + }}); + $scope.gradeResult = $translate.instant('mma.mod_quiz.yourfinalgradeis', {$a: outOfShort}); + } + } + + if (quiz.showFeedbackColumn) { + // Get the quiz overall feedback. + return $mmaModQuiz.getFeedbackForGrade(quiz.id, gradebookData.grade).then(function(response) { + $scope.overallFeedback = response.feedbacktext; + }); + } + } else { + $scope.showResults = false; + } + return $q.when(); + } + + // Get the text to show in the button. It also sets restriction messages if needed. + function getButtonText() { + $scope.buttonText = ''; + + if (quiz.hasquestions !== 0) { + if (attempts.length && !$mmaModQuiz.isAttemptFinished(attempts[attempts.length - 1].state)) { + // Last attempt is unfinished. + if (accessInfo.canattempt) { + $scope.buttonText = 'mma.mod_quiz.continueattemptquiz'; + } else if (accessInfo.canpreview) { + $scope.buttonText = 'mma.mod_quiz.continuepreview'; + } + } else { + // Last attempt is finished or no attempts. + if (accessInfo.canattempt) { + $scope.preventMessages = accessInfo.preventnewattemptreasons; + if (!$scope.preventMessages.length) { + if (!attempts.length) { + $scope.buttonText = 'mma.mod_quiz.attemptquiznow'; + } else { + $scope.buttonText = 'mma.mod_quiz.reattemptquiz'; + } + } + } else if (accessInfo.canpreview) { + $scope.buttonText = 'mma.mod_quiz.previewquiznow'; + } + } + } + + if ($scope.buttonText) { + // So far we think a button should be printed, check if they will be allowed to access it. + $scope.preventMessages = accessInfo.preventaccessreasons; + if (!moreAttempts) { + $scope.buttonText = ''; + } else if (accessInfo.canattempt && $scope.preventMessages.length) { + $scope.buttonText = ''; + } + } + } + + // Show error message and return a rejected promise. + function showError(message, defaultMessage) { + defaultMessage = defaultMessage || 'mma.mod_quiz.errorgetquiz'; + if (message) { + $mmUtil.showErrorModal(message); + } else { + $mmUtil.showErrorModal(defaultMessage, true); + } + return $q.reject(); + } + + // Refreshes data. + function refreshData() { + var promises = []; + promises.push($mmaModQuiz.invalidateQuizData(courseId)); + if (quiz) { + promises.push($mmaModQuiz.invalidateUserAttemptsForUser(quiz.id)); + promises.push($mmaModQuiz.invalidateAccessInformationForAttempt(quiz.id, 0)); + promises.push($mmaModQuiz.invalidateCombinedReviewOptionsForUser(quiz.id)); + promises.push($mmaModQuiz.invalidateUserBestGradeForUser(quiz.id)); + promises.push($mmaModQuiz.invalidateGradeFromGradebook(courseId)); + } + + return $q.all(promises).finally(function() { + return fetchQuizData(true); + }); + } + + // Fetch the Quiz data. + fetchQuizData().then(function() { + $mmaModQuiz.logViewQuiz(quiz.id).then(function() { + $mmCourse.checkModuleCompletion(courseId, module.completionstatus); + }); + }).finally(function() { + $scope.quizLoaded = true; + }); + + // Pull to refresh. + $scope.refreshQuiz = function() { + refreshData().finally(function() { + $scope.$broadcast('scroll.refreshComplete'); + }); + }; + +}); diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json new file mode 100644 index 00000000000..6fd5a367082 --- /dev/null +++ b/www/addons/mod_quiz/lang/en.json @@ -0,0 +1,34 @@ +{ + "attemptfirst": "First attempt", + "attemptlast": "Last attempt", + "attemptnumber": "Attempt", + "attemptquiznow": "Attempt quiz now", + "attemptstate": "State", + "comment": "Comment", + "continueattemptquiz": "Continue the last attempt", + "errorgetattempt": "Error getting attempt data.", + "errorgetquiz": "Error getting quiz data.", + "feedback": "Feedback", + "grade": "Grade", + "gradeaverage": "Average grade", + "gradehighest": "Highest grade", + "grademethod": "Grading method", + "gradesofar": "{{$a.method}}: {{$a.mygrade}} / {{$a.quizgrade}}.", + "marks": "Marks", + "noquestions": "No questions have been added yet", + "notyetgraded": "Not yet graded", + "outofshort": "{{$a.grade}}/{{$a.maxgrade}}", + "overallfeedback": "Overall feedback", + "preview": "Preview", + "previewquiznow": "Preview quiz now", + "reattemptquiz": "Re-attempt quiz", + "review": "Review", + "stateabandoned": "Never submitted", + "statefinished": "Finished", + "statefinisheddetails": "Submitted {{$a}}", + "stateinprogress": "In progress", + "stateoverdue": "Overdue", + "stateoverduedetails": "Must be submitted by {{$a}}", + "summaryofattempts": "Summary of your previous attempts", + "yourfinalgradeis": "Your final grade for this quiz is {{$a}}." +} diff --git a/www/addons/mod_quiz/main.js b/www/addons/mod_quiz/main.js new file mode 100644 index 00000000000..7d00afb4da5 --- /dev/null +++ b/www/addons/mod_quiz/main.js @@ -0,0 +1,39 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz', ['mm.core']) + +.config(function($stateProvider) { + + $stateProvider + + .state('site.mod_quiz', { + url: '/mod_quiz', + params: { + module: null, + courseid: null + }, + views: { + 'site': { + controller: 'mmaModQuizIndexCtrl', + templateUrl: 'addons/mod_quiz/templates/index.html' + } + } + }); + +}) + +.config(function($mmCourseDelegateProvider) { + $mmCourseDelegateProvider.registerContentHandler('mmaModQuiz', 'quiz', '$mmaModQuizHandlers.courseContentHandler'); +}); diff --git a/www/addons/mod_quiz/scss/styles.scss b/www/addons/mod_quiz/scss/styles.scss new file mode 100644 index 00000000000..a4e811df910 --- /dev/null +++ b/www/addons/mod_quiz/scss/styles.scss @@ -0,0 +1,22 @@ + +$mma-mod-quiz-warning-color: #c00 !default; +$mma-mod-quiz-highlight-color: #d9edf7 !default; + +p.mma-mod-quiz-warning { + color: $mma-mod-quiz-warning-color; +} + +.mma-mod-quiz-index-content { + .row { + min-height: $button-height + $grid-padding-width + 5px; + + .button.button-block { + margin: 0; + } + } +} + +.item.mma-mod-quiz-highlighted { + background-color: $mma-mod-quiz-highlight-color; +} + diff --git a/www/addons/mod_quiz/services/handlers.js b/www/addons/mod_quiz/services/handlers.js new file mode 100644 index 00000000000..d1bdcd13c3a --- /dev/null +++ b/www/addons/mod_quiz/services/handlers.js @@ -0,0 +1,72 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Mod Quiz handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizHandlers + */ +.factory('$mmaModQuizHandlers', function($mmCourse, $mmaModQuiz, $state) { + + var self = {}; + + /** + * Course content handler. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHandlers#courseContentHandler + */ + self.courseContentHandler = function() { + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return $mmaModQuiz.isPluginEnabled(); + }; + + /** + * Get the controller. + * + * @param {Object} module The module info. + * @param {Number} courseId The course ID. + * @return {Function} + */ + self.getController = function(module, courseId) { + return function($scope) { + $scope.icon = $mmCourse.getModuleIconSrc('quiz'); + $scope.title = module.name; + $scope.action = function(e) { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + $state.go('site.mod_quiz', {module: module, courseid: courseId}); + }; + }; + }; + + return self; + }; + + return self; +}); diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js new file mode 100644 index 00000000000..de70eea5718 --- /dev/null +++ b/www/addons/mod_quiz/services/helper.js @@ -0,0 +1,100 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_scorm') + +/** + * Helper to gather some common quiz functions. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizHelper + */ +.factory('$mmaModQuizHelper', function($mmaModQuiz, $mmUtil, $translate) { + + var self = {}; + + /** + * Add some calculated data to the attempt. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#setAttemptCalculatedData + * @param {Object} quiz Quiz. + * @param {Object} attempt Attempt. + * @param {Boolean} highlight True if we should check if attempt should be highlighted, false otherwise. + * @param {Number} [bestGrade] Quiz's best grade. Required if highlight=true. + * the due date if the attempt's state is "overdue". + * @return {Void} + */ + self.setAttemptCalculatedData = function(quiz, attempt, highlight, bestGrade) { + + attempt.rescaledGrade = $mmaModQuiz.rescaleGrade(attempt.sumgrades, quiz, false); + attempt.finished = $mmaModQuiz.isAttemptFinished(attempt.state); + attempt.readableState = $mmaModQuiz.getAttemptReadableState(quiz, attempt); + + if (quiz.showMarkColumn && attempt.finished) { + attempt.readableMark = $mmaModQuiz.formatGrade(attempt.sumgrades, quiz.decimalpoints); + } else { + attempt.readableMark = ''; + } + + if (quiz.showGradeColumn && attempt.finished) { + attempt.readableGrade = $mmaModQuiz.formatGrade(attempt.rescaledGrade, quiz.decimalpoints); + // Highlight the highest grade if appropriate. + attempt.highlightGrade = highlight && !attempt.preview && attempt.state == $mmaModQuiz.ATTEMPT_FINISHED && + attempt.rescaledGrade == bestGrade; + } else { + attempt.readableGrade = ''; + } + }; + + /** + * Add some calculated data to the quiz. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#setQuizCalculatedData + * @param {Object} quiz Quiz. + * @param {Object} options Options returned by $mmaModQuiz#getCombinedReviewOptions. + * @return {Void} + */ + self.setQuizCalculatedData = function(quiz, options) { + quiz.sumGradesFormatted = $mmaModQuiz.formatGrade(quiz.sumgrades, quiz.decimalpoints); + quiz.gradeFormatted = $mmaModQuiz.formatGrade(quiz.grade, quiz.decimalpoints); + + quiz.showAttemptColumn = quiz.attempts != 1; + quiz.showGradeColumn = options.someoptions.marks >= $mmaModQuiz.QUESTION_OPTIONS_MARK_AND_MAX && + $mmaModQuiz.quizHasGrades(quiz); + quiz.showMarkColumn = quiz.showGradeColumn && quiz.grade != quiz.sumgrades; + quiz.showFeedbackColumn = quiz.hasfeedback && options.alloptions.overallfeedback; + }; + + /** + * Show error because a SCORM download failed. + * + * @module mm.addons.mod_scorm + * @ngdoc method + * @name $mmaModScormHelper#showDownloadError + * @param {Object} scorm SCORM downloaded. + * @return {Void} + */ + self.showDownloadError = function(scorm) { + $translate('mma.mod_scorm.errordownloadscorm', {name: scorm.name}).then(function(message) { + $mmUtil.showErrorModal(message); + }); + }; + + return self; +}); diff --git a/www/addons/mod_quiz/services/quiz.js b/www/addons/mod_quiz/services/quiz.js new file mode 100644 index 00000000000..bd4c68a30a4 --- /dev/null +++ b/www/addons/mod_quiz/services/quiz.js @@ -0,0 +1,1243 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Quiz service. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuiz + */ +.factory('$mmaModQuiz', function($log, $mmSite, $mmSitesManager, $q, $translate, $mmUtil, $mmText) { + + $log = $log.getInstance('$mmaModQuiz'); + + var self = {}; + + // Constants. + + // Grade methods. + self.GRADEHIGHEST = 1; + self.GRADEAVERAGE = 2; + self.ATTEMPTFIRST = 3; + self.ATTEMPTLAST = 4; + + // Question options. + self.QUESTION_OPTIONS_MAX_ONLY = 1; + self.QUESTION_OPTIONS_MARK_AND_MAX = 2; + + // Attempt state. + self.ATTEMPT_IN_PROGRESS = 'inprogress'; + self.ATTEMPT_OVERDUE = 'overdue'; + self.ATTEMPT_FINISHED = 'finished'; + self.ATTEMPT_ABANDONED = 'abandoned'; + + /** + * Formats a grade to be displayed. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#formatGrade + * @param {Number} grade Grade. + * @param {Number} decimals Decimals to use. + * @return {String|Float} Grade to display. + */ + self.formatGrade = function(grade, decimals) { + if (typeof grade == 'undefined' || grade == -1 || grade == null) { + return $translate.instant('mma.mod_quiz.notyetgraded'); + } + return $mmUtil.roundToDecimals(grade, decimals); + }; + + /** + * Get cache key for get access information WS calls. + * + * @param {Number} quizId Quiz ID. + * @param {Number} attemptId Attempt ID. + * @return {String} Cache key. + */ + function getAccessInformationCacheKey(quizId, attemptId) { + return getAccessInformationCommonCacheKey(quizId) + ':' + attemptId; + } + + /** + * Get common cache key for get access information WS calls. + * + * @param {Number} quizId Quiz ID. + * @return {String} Cache key. + */ + function getAccessInformationCommonCacheKey(quizId) { + return 'mmaModQuiz:accessInformation:' + quizId; + } + + /** + * Get access information for an attempt. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getAccessInformation + * @param {Number} quizId Quiz ID. + * @param {Number} attemptId Attempt ID. 0 for user's last attempt. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the access information. + */ + self.getAccessInformation = function(quizId, attemptId, siteId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + var params = { + quizid: quizId, + attemptid: attemptId + }, + preSets = { + cacheKey: getAccessInformationCacheKey(quizId, attemptId) + }; + + return site.read('mod_quiz_get_access_information', params, preSets); + }); + }; + + /** + * Get cache key for get attempt data WS calls. + * + * @param {Number} attemptId Attempt ID. + * @param {Number} page Page. + * @return {String} Cache key. + */ + function getAttemptDataCacheKey(attemptId, page) { + return getAttemptDataCommonCacheKey(attemptId) + ':' + page; + } + + /** + * Get common cache key for get attempt data WS calls. + * + * @param {Number} attemptId Attempt ID. + * @return {String} Cache key. + */ + function getAttemptDataCommonCacheKey(attemptId) { + return 'mmaModQuiz:attemptData:' + attemptId; + } + + /** + * Get an attempt's data. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getAttemptData + * @param {Number} attemptId Attempt ID. + * @param {Number} page Page number. + * @param {Object} preflightData Preflight required data (like password). + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the attempt data. + */ + self.getAttemptData = function(attemptId, page, preflightData, siteId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + var params = { + attemptid: attemptId, + page: page, + preflightdata: preflightData + }, + preSets = { + cacheKey: getAttemptDataCacheKey(attemptId, page) + }; + + return site.read('mod_quiz_get_attempt_data', params, preSets).then(function(response) { + if (response && response.attempt) { + return response.attempt; + } + return $q.reject(); + }); + }); + }; + + /** + * Get an attempt's due date. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getAttemptDueDate + * @param {Object} quiz Quiz. + * @param {Object} attempt Attempt. + * @return {Number} Attempt's due date, 0 if no due date or invalid data. + */ + self.getAttemptDueDate = function(quiz, attempt) { + var deadlines = [], + dueDate; + + if (quiz.timelimit) { + deadlines.push(parseInt(attempt.timestart, 10) + parseInt(quiz.timelimit, 10)); + } + if (quiz.timeclose) { + deadlines.push(parseInt(quiz.timeclose, 10)); + } + + if (!deadlines.length) { + return 0; + } + + // Get min due date. + dueDate = Math.min.apply(null, deadlines); + if (!dueDate) { + return 0; + } + + switch (attempt.state) { + case self.ATTEMPT_IN_PROGRESS: + return dueDate * 1000; + + case self.ATTEMPT_OVERDUE: + return (dueDate + parseInt(quiz.graceperiod, 10)) * 1000; + + default: + $log.warn('Unexpected state when getting due date: ' + attempt.state); + return 0; + } + }; + + /** + * Turn attempt's state into a readable state. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getAttemptReadableState + * @param {Number} quiz Quiz. + * @param {Object} attempt Attempt. + * @return {String[]} List of state sentences. + */ + self.getAttemptReadableState = function(quiz, attempt) { + switch (attempt.state) { + case self.ATTEMPT_IN_PROGRESS: + return [$translate.instant('mma.mod_quiz.stateinprogress')]; + + case self.ATTEMPT_OVERDUE: + var sentences = [], + dueDate = self.getAttemptDueDate(quiz, attempt); + sentences.push($translate.instant('mma.mod_quiz.stateoverdue')); + if (dueDate) { + dueDate = moment(dueDate).format('LLL'); + sentences.push($translate.instant('mma.mod_quiz.stateoverduedetails', {$a: dueDate})); + } + return sentences; + + case self.ATTEMPT_FINISHED: + return [ + $translate.instant('mma.mod_quiz.statefinished'), + $translate.instant('mma.mod_quiz.statefinisheddetails', {$a: moment(attempt.timefinish * 1000).format('LLL')}) + ]; + + case self.ATTEMPT_ABANDONED: + return [$translate.instant('mma.mod_quiz.stateabandoned')]; + } + return []; + }; + + /** + * Get cache key for get attempt review WS calls. + * + * @param {Number} attemptId Attempt ID. + * @param {Number} page Page. + * @return {String} Cache key. + */ + function getAttemptReviewCacheKey(attemptId, page) { + return getAttemptReviewCommonCacheKey(attemptId) + ':' + page; + } + + /** + * Get common cache key for get attempt review WS calls. + * + * @param {Number} attemptId Attempt ID. + * @return {String} Cache key. + */ + function getAttemptReviewCommonCacheKey(attemptId) { + return 'mmaModQuiz:attemptReview:' + attemptId; + } + + /** + * Get an attempt's review. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getAttemptReview + * @param {Number} attemptId Attempt ID. + * @param {Number} page Page number, -1 for all the questions in all the pages. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the attempt review. + */ + self.getAttemptReview = function(attemptId, page, siteId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + var params = { + attemptid: attemptId, + page: page + }, + preSets = { + cacheKey: getAttemptReviewCacheKey(attemptId, page) + }; + + return site.read('mod_quiz_get_attempt_review', params, preSets); + }); + }; + + /** + * Get cache key for get attempt summary WS calls. + * + * @param {Number} attemptId Attempt ID. + * @return {String} Cache key. + */ + function getAttemptSummaryCacheKey(attemptId) { + return 'mmaModQuiz:attemptSummary:' + attemptId; + } + + /** + * Get an attempt's summary. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getAttemptSummary + * @param {Number} attemptId Attempt ID. + * @param {Object} preflightData Preflight required data (like password). + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the attempt summary. + */ + self.getAttemptSummary = function(attemptId, preflightData, siteId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + var params = { + attemptid: attemptId, + preflightdata: preflightData + }, + preSets = { + cacheKey: getAttemptSummaryCacheKey(attemptId) + }; + + return site.read('mod_quiz_get_attempt_summary', params, preSets).then(function(response) { + if (response && response.questions) { + return response.questions; + } + return $q.reject(); + }); + }); + }; + + /** + * Get cache key for get combined review options WS calls. + * + * @param {Number} quizId Quiz ID. + * @param {Number} userId User ID. + * @return {String} Cache key. + */ + function getCombinedReviewOptionsCacheKey(quizId, userId) { + return getCombinedReviewOptionsCommonCacheKey(quizId) + ':' + userId; + } + + /** + * Get common cache key for get combined review options WS calls. + * + * @param {Number} quizId Quiz ID. + * @return {String} Cache key. + */ + function getCombinedReviewOptionsCommonCacheKey(quizId) { + return 'mmaModQuiz:combinedReviewOptions:' + quizId; + } + + /** + * Get a quiz combined review options. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getCombinedReviewOptions + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved with the combined review options. + */ + self.getCombinedReviewOptions = function(quizId, siteId, userId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); + + var params = { + quizid: quizId, + userid: userId + }, + preSets = { + cacheKey: getCombinedReviewOptionsCacheKey(quizId, userId) + }; + + return site.read('mod_quiz_get_combined_review_options', params, preSets).then(function(response) { + if (response && response.someoptions && response.alloptions) { + // Convert the arrays to objects with name -> value. + var someOptions = {}, + allOptions = {}; + angular.forEach(response.someoptions, function(entry) { + someOptions[entry.name] = entry.value; + }); + angular.forEach(response.alloptions, function(entry) { + allOptions[entry.name] = entry.value; + }); + response.someoptions = someOptions; + response.alloptions = allOptions; + return response; + } + return $q.reject(); + }); + }); + }; + + /** + * Get cache key for get feedback for grade WS calls. + * + * @param {Number} quizId Quiz ID. + * @param {Number} grade Grade. + * @return {String} Cache key. + */ + function getFeedbackForGradeCacheKey(quizId, grade) { + return getFeedbackForGradeCommonCacheKey(quizId) + ':' + grade; + } + + /** + * Get common cache key for get feedback for grade WS calls. + * + * @param {Number} quizId Quiz ID. + * @return {String} Cache key. + */ + function getFeedbackForGradeCommonCacheKey(quizId) { + return 'mmaModQuiz:feedbackForGrade:' + quizId; + } + + /** + * Get the feedback for a certain grade. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getFeedbackForGrade + * @param {Number} quizId Quiz ID. + * @param {Number} grade Grade. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the feedback. + */ + self.getFeedbackForGrade = function(quizId, grade, siteId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + + var params = { + quizid: quizId, + grade: grade + }, + preSets = { + cacheKey: getFeedbackForGradeCacheKey(quizId, grade) + }; + + return site.read('mod_quiz_get_quiz_feedback_for_grade', params, preSets); + }); + }; + + /** + * Determine the correct number of decimal places required to format a grade. + * Based on Moodle's quiz_get_grade_format. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getGradeDecimals + * @param {Object} quiz Quiz. + * @return {Number} Number of decimals. + */ + self.getGradeDecimals = function(quiz) { + if (typeof quiz.questiondecimalpoints == 'undefined') { + quiz.questiondecimalpoints = -1; + } + + if (quiz.questiondecimalpoints == -1) { + return quiz.decimalpoints; + } + + return quiz.questiondecimalpoints; + }; + + /** + * Get cache key for get grade from gradebook WS calls. + * + * @param {Number} quizId Quiz ID. + * @param {Number} grade Grade. + * @return {String} Cache key. + */ + function getGradeFromGradebookCacheKey(courseId, userId) { + return 'mmaModQuiz:gradeFromGradebook:' + courseId + ':' + userId; + } + + /** + * Gets a quiz grade and feedback from the gradebook. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getGradeFromGradebook + * @param {Number} courseId Course ID. + * @param {Number} moduleId Quiz module ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved with an object containing the grade and the feedback. + */ + self.getGradeFromGradebook = function(courseId, moduleId, siteId, userId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); + + var params = { + courseid: courseId, + userid: userId + }, + preSets = { + cacheKey: getGradeFromGradebookCacheKey(courseId, userId) + }; + + return $mmSite.read('gradereport_user_get_grades_table', params, preSets).then(function(response) { + // Search the module we're looking for. + var quizEntry, + regex = /href="([^"]*\/mod\/quiz\/[^"|^\.]*\.php[^"]*)/, // Find href containing "/mod/quiz/xxx.php". + matches, + hrefParams, + result = {}, + grade; + + angular.forEach(response.tables, function(table) { + angular.forEach(table.tabledata, function(entry) { + if (entry.itemname && entry.itemname.content) { + matches = entry.itemname.content.match(regex); + if (matches && matches.length) { + hrefParams = $mmUtil.extractUrlParams(matches[1]); + if (hrefParams && hrefParams.id == moduleId) { + quizEntry = entry; + } + } + } + }); + }); + + if (quizEntry) { + if (quizEntry.feedback.content) { + result.feedback = $mmText.decodeHTML(quizEntry.feedback.content).trim(); + } else { + result.feedback = ''; + } + if (quizEntry.grade) { + grade = parseFloat(quizEntry.grade.content); + if (!isNaN(grade)) { + result.grade = grade; + } + } + return result; + } + return $q.reject(); + }); + }); + }; + + /** + * Given a list of attempts, returns the last finished attempt. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getLastFinishedAttemptFromList + * @param {Object[]} attempts Attempts. + * @return {Object} Last finished attempt. + */ + self.getLastFinishedAttemptFromList = function(attempts) { + if (attempts && attempts.length) { + for (var i = attempts.length - 1; i >= 0; i--) { + var attempt = attempts[i]; + if (self.isAttemptFinished(attempt.state)) { + return attempt; + } + } + } + }; + + /** + * Get cache key for Quiz data WS calls. + * + * @param {Number} courseId Course ID. + * @return {String} Cache key. + */ + function getQuizDataCacheKey(courseId) { + return 'mmaModQuiz:quiz:' + courseId; + } + + /** + * Get a Quiz with key=value. If more than one is found, only the first will be returned. + * + * @param {String} siteId Site ID. + * @param {Number} courseId Course ID. + * @param {String} key Name of the property to check. + * @param {Mixed} value Value to search. + * @return {Promise} Promise resolved when the Quiz is retrieved. + */ + function getQuiz(siteId, courseId, key, value) { + return $mmSitesManager.getSite(siteId).then(function(site) { + var params = { + courseids: [courseId] + }, + preSets = { + cacheKey: getQuizDataCacheKey(courseId) + }; + + return site.read('mod_quiz_get_quizzes_by_courses', params, preSets).then(function(response) { + if (response && response.quizzes) { + var currentQuiz; + angular.forEach(response.quizzes, function(quiz) { + if (!currentQuiz && quiz[key] == value) { + currentQuiz = quiz; + } + }); + if (currentQuiz) { + return currentQuiz; + } + } + return $q.reject(); + }); + }); + } + + /** + * Get a Quiz by module ID. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getQuiz + * @param {Number} courseId Course ID. + * @param {Number} cmid Course module ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the Quiz is retrieved. + */ + self.getQuiz = function(courseId, cmid, siteId) { + siteId = siteId || $mmSite.getId(); + return getQuiz(siteId, courseId, 'coursemodule', cmid); + }; + + /** + * Get a Quiz by Quiz ID. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getQuizById + * @param {Number} courseId Course ID. + * @param {Number} id Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the Quiz is retrieved. + */ + self.getQuizById = function(courseId, id, siteId) { + siteId = siteId || $mmSite.getId(); + return getQuiz(siteId, courseId, 'id', id); + }; + + /** + * Get a readable Quiz grade method. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getQuizGradeMethod + * @param {Number} method Grading method. + * @return {String} Readable grading method. + */ + self.getQuizGradeMethod = function(method) { + switch (parseInt(method, 10)) { + case self.GRADEHIGHEST: + return $translate.instant('mma.mod_quiz.gradehighest'); + case self.GRADEAVERAGE: + return $translate.instant('mma.mod_quiz.gradeaverage'); + case self.ATTEMPTFIRST: + return $translate.instant('mma.mod_quiz.attemptfirst'); + case self.ATTEMPTLAST: + return $translate.instant('mma.mod_quiz.attemptlast'); + } + return ''; + }; + + /** + * Get cache key for get user attempts WS calls. + * + * @param {Number} quizId Quiz ID. + * @param {Number} userId User ID. + * @return {String} Cache key. + */ + function getUserAttemptsCacheKey(quizId, userId) { + return getUserAttemptsCommonCacheKey(quizId) + ':' + userId; + } + + /** + * Get common cache key for get user attempts WS calls. + * + * @param {Number} quizId Quiz ID. + * @return {String} Cache key. + */ + function getUserAttemptsCommonCacheKey(quizId) { + return 'mmaModQuiz:userAttempts:' + quizId; + } + + /** + * Get quiz attempts for a certain user. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getUserAttempts + * @param {Number} quizId Quiz ID. + * @param {Number} [status] Status of the attempts to get. By default, 'all'. + * @param {Boolean} [includePreviews] True to include previews, false otherwise. Defaults to true. + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved with the attempts. + */ + self.getUserAttempts = function(quizId, status, includePreviews, siteId, userId) { + siteId = siteId || $mmSite.getId(); + status = status || 'all'; + if (typeof includePreviews == 'undefined') { + includePreviews = true; + } + + return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); + + var params = { + quizid: quizId, + userid: userId, + status: status, + includepreviews: includePreviews ? 1 : 0 + }, + preSets = { + cacheKey: getUserAttemptsCacheKey(quizId, userId) + }; + + return site.read('mod_quiz_get_user_attempts', params, preSets).then(function(response) { + if (response && response.attempts) { + return response.attempts; + } + return $q.reject(); + }); + }); + }; + + /** + * Get cache key for get user best grade WS calls. + * + * @param {Number} quizId Quiz ID. + * @param {Number} userId User ID. + * @return {String} Cache key. + */ + function getUserBestGradeCacheKey(quizId, userId) { + return getUserBestGradeCommonCacheKey(quizId) + ':' + userId; + } + + /** + * Get common cache key for get user best grade WS calls. + * + * @param {Number} quizId Quiz ID. + * @return {String} Cache key. + */ + function getUserBestGradeCommonCacheKey(quizId) { + return 'mmaModQuiz:userBestGrade:' + quizId; + } + + /** + * Get best grade in a quiz for a certain user. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getUserBestGrade + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved with the attempts. + */ + self.getUserBestGrade = function(quizId, siteId, userId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); + + var params = { + quizid: quizId, + userid: userId + }, + preSets = { + cacheKey: getUserBestGradeCacheKey(quizId, userId) + }; + + return site.read('mod_quiz_get_user_best_grade', params, preSets); + }); + }; + + /** + * Invalidates access information for all attempts in a quiz. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateAccessInformation + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateAccessInformation = function(quizId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKeyStartingWith(getAccessInformationCommonCacheKey(quizId)); + }); + }; + + /** + * Invalidates access information for an attempt. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateAccessInformationForAttempt + * @param {Number} quizId Quiz ID. + * @param {Number} attemptId Attempt ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateAccessInformationForAttempt = function(quizId, attemptId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKey(getAccessInformationCacheKey(quizId, attemptId)); + }); + }; + + /** + * Invalidates attempt data for all pages. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateAttemptData + * @param {Number} attemptId Attempt ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateAttemptData = function(attemptId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKeyStartingWith(getAttemptDataCommonCacheKey(attemptId)); + }); + }; + + /** + * Invalidates attempt data for a certain page. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateAttemptDataForPage + * @param {Number} attemptId Attempt ID. + * @param {Number} page Page. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateAttemptDataForPage = function(attemptId, page, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKey(getAttemptDataCacheKey(attemptId, page)); + }); + }; + + /** + * Invalidates attempt review for all pages. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateAttemptReview + * @param {Number} attemptId Attempt ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateAttemptReview = function(attemptId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKeyStartingWith(getAttemptReviewCommonCacheKey(attemptId)); + }); + }; + + /** + * Invalidates attempt review for a certain page. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateAttemptReviewForPage + * @param {Number} attemptId Attempt ID. + * @param {Number} page Page. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateAttemptReviewForPage = function(attemptId, page, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKey(getAttemptReviewCacheKey(attemptId, page)); + }); + }; + + /** + * Invalidates attempt summary. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateAttemptSummary + * @param {Number} attemptId Attempt ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateAttemptSummary = function(attemptId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKey(getAttemptSummaryCacheKey(attemptId)); + }); + }; + + /** + * Invalidates combined review options for all users. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateCombinedReviewOptions + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateCombinedReviewOptions = function(quizId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKeyStartingWith(getCombinedReviewOptionsCommonCacheKey(quizId)); + }); + }; + + /** + * Invalidates combined review options for a certain user. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateCombinedReviewOptionsForUser + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateCombinedReviewOptionsForUser = function(quizId, siteId, userId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); + return site.invalidateWsCacheForKey(getCombinedReviewOptionsCacheKey(quizId, userId)); + }); + }; + + /** + * Invalidates feedback for all grades of a quiz. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateFeedback + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateFeedback = function(quizId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKeyStartingWith(getFeedbackForGradeCommonCacheKey(quizId)); + }); + }; + + /** + * Invalidates feedback for a certain grade. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateFeedbackForGrade + * @param {Number} quizId Quiz ID. + * @param {Number} grade Grade. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateFeedbackForGrade = function(quizId, grade, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKey(getFeedbackForGradeCacheKey(quizId, grade)); + }); + }; + + /** + * Invalidates grade from gradebook for a certain user. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateGradeFromGradebook + * @param {Number} courseId Course ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateGradeFromGradebook = function(courseId, siteId, userId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); + return site.invalidateWsCacheForKey(getGradeFromGradebookCacheKey(courseId, userId)); + }); + }; + + /** + * Invalidates user attempts for all users. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateUserAttempts + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateUserAttempts = function(quizId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKeyStartingWith(getUserAttemptsCommonCacheKey(quizId)); + }); + }; + + /** + * Invalidates user attempts for a certain user. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateUserAttemptsForUser + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateUserAttemptsForUser = function(quizId, siteId, userId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); + return site.invalidateWsCacheForKey(getUserAttemptsCacheKey(quizId, userId)); + }); + }; + + /** + * Invalidates user best grade for all users. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateUserBestGrade + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateUserBestGrade = function(quizId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKeyStartingWith(getUserBestGradeCommonCacheKey(quizId)); + }); + }; + + /** + * Invalidates user best grade for a certain user. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateUserBestGradeForUser + * @param {Number} quizId Quiz ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Number} [userId] User ID. If not defined use site's current user. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateUserBestGradeForUser = function(quizId, siteId, userId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); + return site.invalidateWsCacheForKey(getUserBestGradeCacheKey(quizId, userId)); + }); + }; + + /** + * Invalidates Quiz data. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#invalidateQuizData + * @param {Number} courseId Course ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + self.invalidateQuizData = function(courseId, siteId) { + siteId = siteId || $mmSite.getId(); + return $mmSitesManager.getSite(siteId).then(function(site) { + return site.invalidateWsCacheForKey(getQuizDataCacheKey(courseId)); + }); + }; + + /** + * Check if an attempt is finished based on its state. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#isAttemptFinished + * @param {String} state Attempt's state. + * @return {Boolean} True if finished, false otherwise. + */ + self.isAttemptFinished = function(state) { + return state == self.ATTEMPT_FINISHED || state == self.ATTEMPT_ABANDONED; + }; + + /** + * Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the quiz WS are available. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#isPluginEnabled + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + self.isPluginEnabled = function(siteId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + // All WS were introduced at the same time so checking one is enough. + return site.wsAvailable('mod_quiz_get_attempt_review'); + }); + }; + + /** + * Report an attempt as being viewed. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#logViewAttempt + * @param {String} attemptId Attempt ID. + * @param {Number} [page=0] Page number. + * @return {Promise} Promise resolved when the WS call is successful. + */ + self.logViewAttempt = function(attemptId, page) { + if (typeof page == 'undefined') { + page = 0; + } + + var params = { + attemptid: attemptId, + page: page + }; + return $mmSite.write('mod_quiz_view_attempt', params); + }; + + /** + * Report an attempt's review as being viewed. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#logViewAttemptReview + * @param {String} attemptId Attempt ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + self.logViewAttemptReview = function(attemptId) { + var params = { + attemptid: attemptId + }; + return $mmSite.write('mod_quiz_view_attempt_review', params); + }; + + /** + * Report an attempt's summary as being viewed. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#logViewAttemptSummary + * @param {String} attemptId Attempt ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + self.logViewAttemptSummary = function(attemptId) { + var params = { + attemptid: attemptId + }; + return $mmSite.write('mod_quiz_view_attempt_summary', params); + }; + + /** + * Report a quiz as being viewed. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#logViewQuiz + * @param {String} id Module ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + self.logViewQuiz = function(id) { + if (id) { + var params = { + quizid: id + }; + return $mmSite.write('mod_quiz_view_quiz', params); + } + return $q.reject(); + }; + + /** + * Check if it's a graded quiz. Based on Moodle's quiz_has_grades. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#quizHasGrades + * @param {Object} quiz Quiz. + * @return {Boolean} True if quiz is graded, false otherwise. + */ + self.quizHasGrades = function(quiz) { + return quiz.grade >= 0.000005 && quiz.sumgrades >= 0.000005; + }; + + /** + * Convert the raw grade stored in $attempt into a grade out of the maximum grade for this quiz. + * Based on Moodle's quiz_rescale_grade. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#rescaleGrade + * @param {Number} rawGrade The unadjusted grade, for example attempt.sumgrades. + * @param {Object} quiz Quiz. + * @param {Boolean|String} format True to format the results for display, 'question' to format a question grade + * (different number of decimal places), false to not format it. + * @return {String|Float} Grade to display. + */ + self.rescaleGrade = function(rawGrade, quiz, format) { + var grade; + if (typeof format == 'undefined') { + format = true; + } + + rawGrade = parseInt(rawGrade, 10); + if (!isNaN(rawGrade)) { + if (quiz.sumgrades >= 0.000005) { + grade = rawGrade * quiz.grade / quiz.sumgrades; + } else { + grade = 0; + } + } + + if (format === 'question') { + grade = self.formatGrade(grade, self.getGradeDecimals(quiz)); + } else if (format) { + grade = self.formatGrade(grade, quiz.decimalpoints); + } + return grade; + }; + + return self; +}); diff --git a/www/addons/mod_quiz/templates/index.html b/www/addons/mod_quiz/templates/index.html new file mode 100644 index 00000000000..a5b61e26ee7 --- /dev/null +++ b/www/addons/mod_quiz/templates/index.html @@ -0,0 +1,77 @@ + + {{ title }} + + + + + + + + + +
+
+

{{ rule }}

+
+
+

{{ 'mma.mod_quiz.grademethod' | translate }}

+

{{ quiz.gradeMethodReadable }}

+
+
+ + +
+
{{ 'mma.mod_quiz.summaryofattempts' | translate }}
+
+

{{ 'mma.mod_quiz.attemptnumber' | translate }}

+

{{ 'mma.mod_quiz.attemptstate' | translate }}

+

{{ 'mma.mod_quiz.marks' | translate }} / {{ quiz.sumGradesFormatted }}

+

{{ 'mma.mod_quiz.grade' | translate }} / {{ quiz.gradeFormatted }}

+

{{ 'mma.mod_quiz.review' | translate }}

+

+
+
+

{{ 'mma.mod_quiz.preview' | translate }}

+

{{ attempt.attempt }}

+
+

{{ sentence }}

+
+

{{ attempt.readableMark }}

+

{{ attempt.readableGrade }}

+

+ +

+

+ +

+
+
+ + +
+

{{ gradeResult }}

+

{{ 'mm.course.overriddennotice' | translate }}

+
+

{{ 'mma.mod_quiz.comment' | translate }}

+

{{ gradebookFeedback }}

+
+
+

{{ 'mma.mod_quiz.overallfeedback' | translate }}

+

{{ overallFeedback }}

+
+
+ +
+
+

{{ message }}

+
+
+

{{ 'mma.mod_quiz.noquestions' | translate }}

+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/www/core/components/course/lang/en.json b/www/core/components/course/lang/en.json index 78449400ca6..4d83d49d078 100644 --- a/www/core/components/course/lang/en.json +++ b/www/core/components/course/lang/en.json @@ -11,6 +11,7 @@ "errorgetmodule": "Error getting module data.", "gotothesite": "Go to the site", "nocontentavailable": "No content available at the moment.", + "overriddennotice": "Your final grade from this activity was manually adjusted.", "showall": "Show all", "whoops": "Whoops!" } diff --git a/www/core/lang/en.json b/www/core/lang/en.json index c05bdd32b23..e81bf3fd1ea 100644 --- a/www/core/lang/en.json +++ b/www/core/lang/en.json @@ -99,6 +99,7 @@ "searching": "Searching", "sec" : "sec", "secs" : "secs", + "seemoredetail": "Click here to see more detail", "sending": "Sending", "serverconnection": "Error connecting to the server", "sizeb": "bytes", diff --git a/www/core/lib/text.js b/www/core/lib/text.js index b77d08d262a..cb6c23d45dc 100644 --- a/www/core/lib/text.js +++ b/www/core/lib/text.js @@ -230,6 +230,31 @@ angular.module('mm.core') .replace(/ /g, ' '); }; + /** + * Decode an escaped HTML text. This implementation is based on PHP's htmlspecialchars_decode. + * + * @module mm.core + * @ngdoc method + * @name $mmText#decodeHTML + * @param {String} text Text to decode. + * @return {String} Decoded text. + */ + self.decodeHTML = function(text) { + if (typeof text == 'undefined' || text === null || (typeof text == 'number' && isNaN(text))) { + return ''; + } else if (typeof text != 'string') { + return '' + text; + } + + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, ' '); + }; + /** * Add or remove 'www' from a URL. The url needs to have http or https protocol. * From 950c970b23d48dc475ea563e4e4ad1587fdb1fdc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 9 Feb 2016 13:59:08 +0100 Subject: [PATCH 002/101] MOBILE-1406 quiz: Implement attempt details --- www/addons/mod_quiz/controllers/attempt.js | 111 +++++++++++++++++++++ www/addons/mod_quiz/controllers/index.js | 19 +--- www/addons/mod_quiz/main.js | 15 +++ www/addons/mod_quiz/services/helper.js | 25 +++-- www/addons/mod_quiz/templates/attempt.html | 34 +++++++ www/addons/mod_quiz/templates/index.html | 4 +- 6 files changed, 182 insertions(+), 26 deletions(-) create mode 100644 www/addons/mod_quiz/controllers/attempt.js create mode 100644 www/addons/mod_quiz/templates/attempt.html diff --git a/www/addons/mod_quiz/controllers/attempt.js b/www/addons/mod_quiz/controllers/attempt.js new file mode 100644 index 00000000000..5f69fc694e8 --- /dev/null +++ b/www/addons/mod_quiz/controllers/attempt.js @@ -0,0 +1,111 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Quiz attempt controller. + * + * @module mm.addons.mod_quiz + * @ngdoc controller + * @name mmaModQuizAttemptCtrl + */ +.controller('mmaModQuizAttemptCtrl', function($scope, $stateParams, $mmaModQuiz, $q, $mmaModQuizHelper) { + var attemptId = $stateParams.attemptid, + quizId = $stateParams.quizid, + courseId = $stateParams.courseid, + quiz, + attempt; + + // Convenience function to get the quiz data. + function fetchData() { + return $mmaModQuiz.getQuizById(courseId, quizId).then(function(quizData) { + quiz = quizData; + $scope.quiz = quiz; + + return fetchAttempt(); + }).catch(function(message) { + return $mmaModQuizHelper.showError(message, 'mma.mod_quiz.errorgetattempt'); + }); + } + + // Convenience function to get the attempt. + function fetchAttempt() { + // Get all the attempts and search the one we want. + return $mmaModQuiz.getUserAttempts(quiz.id).then(function(attempts) { + angular.forEach(attempts, function(att) { + if (att.id == attemptId) { + attempt = att; + } + }); + + if (!attempt) { + // Attempt not found, error. + return $q.reject(); + } + + return $mmaModQuiz.getCombinedReviewOptions(quiz.id).then(function(options) { + return $mmaModQuiz.getAccessInformation(quiz.id, attempt.id).then(function(accessInfo) { + // Determine fields to show. + $mmaModQuizHelper.setQuizCalculatedData(quiz, options); + quiz.showReviewColumn = accessInfo.canreviewmyattempts; + + // Get readable data for the attempt. + $mmaModQuizHelper.setAttemptCalculatedData(quiz, attempt, false); + + if (quiz.showFeedbackColumn && $mmaModQuiz.isAttemptFinished(attempt.state) && + options.someoptions.overallfeedback && angular.isNumber(attempt.rescaledGrade)) { + return $mmaModQuiz.getFeedbackForGrade(quiz.id, attempt.rescaledGrade).then(function(response) { + attempt.feedback = response.feedbacktext; + }); + } else { + delete attempt.feedback; + } + + }).then(function() { + $scope.attempt = attempt; + }); + }); + }); + } + + // Refreshes data. + function refreshData() { + var promises = []; + promises.push($mmaModQuiz.invalidateQuizData(courseId)); + promises.push($mmaModQuiz.invalidateUserAttemptsForUser(quizId)); + promises.push($mmaModQuiz.invalidateAccessInformationForAttempt(quizId, attemptId)); + promises.push($mmaModQuiz.invalidateCombinedReviewOptionsForUser(quizId)); + if (typeof attempt.feedback != 'undefined') { + promises.push($mmaModQuiz.invalidateFeedback(quizId)); + } + + return $q.all(promises).finally(function() { + return fetchData(); + }); + } + + // Fetch the Quiz data. + fetchData().finally(function() { + $scope.attemptLoaded = true; + }); + + // Pull to refresh. + $scope.refreshAttempt = function() { + refreshData().finally(function() { + $scope.$broadcast('scroll.refreshComplete'); + }); + }; + +}); diff --git a/www/addons/mod_quiz/controllers/index.js b/www/addons/mod_quiz/controllers/index.js index 0406fd32ab7..be05ce67bd5 100644 --- a/www/addons/mod_quiz/controllers/index.js +++ b/www/addons/mod_quiz/controllers/index.js @@ -22,7 +22,7 @@ angular.module('mm.addons.mod_quiz') * @name mmaModQuizIndexCtrl */ .controller('mmaModQuizIndexCtrl', function($scope, $stateParams, $mmaModQuiz, $mmCourse, $ionicPlatform, $q, $translate, - $mmUtil, $mmaModQuizHelper) { + $mmaModQuizHelper) { var module = $stateParams.module || {}, courseId = $stateParams.courseid, quiz, @@ -51,14 +51,16 @@ angular.module('mm.addons.mod_quiz') $scope.description = quiz.intro || $scope.description; $scope.quiz = quiz; - return getAttempts().catch(showError); + return getAttempts().catch(function(message) { + return $mmaModQuizHelper.showError(message); + }); }, function(message) { if (!refresh) { // Get quiz failed, retry without using cache since it might be a new activity. return refreshData(); } - return showError(message); + return $mmaModQuizHelper.showError(message); }); } @@ -211,17 +213,6 @@ angular.module('mm.addons.mod_quiz') } } - // Show error message and return a rejected promise. - function showError(message, defaultMessage) { - defaultMessage = defaultMessage || 'mma.mod_quiz.errorgetquiz'; - if (message) { - $mmUtil.showErrorModal(message); - } else { - $mmUtil.showErrorModal(defaultMessage, true); - } - return $q.reject(); - } - // Refreshes data. function refreshData() { var promises = []; diff --git a/www/addons/mod_quiz/main.js b/www/addons/mod_quiz/main.js index 7d00afb4da5..229c209029e 100644 --- a/www/addons/mod_quiz/main.js +++ b/www/addons/mod_quiz/main.js @@ -30,6 +30,21 @@ angular.module('mm.addons.mod_quiz', ['mm.core']) templateUrl: 'addons/mod_quiz/templates/index.html' } } + }) + + .state('site.mod_quiz-attempt', { + url: '/mod_quiz-attempt', + params: { + courseid: null, + quizid: null, + attemptid: null + }, + views: { + 'site': { + controller: 'mmaModQuizAttemptCtrl', + templateUrl: 'addons/mod_quiz/templates/attempt.html' + } + } }); }) diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index de70eea5718..54fb65f4bdf 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -angular.module('mm.addons.mod_scorm') +angular.module('mm.addons.mod_quiz') /** * Helper to gather some common quiz functions. @@ -21,7 +21,7 @@ angular.module('mm.addons.mod_scorm') * @ngdoc service * @name $mmaModQuizHelper */ -.factory('$mmaModQuizHelper', function($mmaModQuiz, $mmUtil, $translate) { +.factory('$mmaModQuizHelper', function($mmaModQuiz, $mmUtil, $q) { var self = {}; @@ -82,18 +82,23 @@ angular.module('mm.addons.mod_scorm') }; /** - * Show error because a SCORM download failed. + * Show an error message and returns a rejected promise. * - * @module mm.addons.mod_scorm + * @module mm.addons.mod_quiz * @ngdoc method - * @name $mmaModScormHelper#showDownloadError - * @param {Object} scorm SCORM downloaded. - * @return {Void} + * @name $mmaModQuizHelper#showError + * @param {String} [message] Message to show. + * @param {String} [defaultMessage] Code of the message to show if message is not defined or empty. + * @return {Promise} Rejected promise. */ - self.showDownloadError = function(scorm) { - $translate('mma.mod_scorm.errordownloadscorm', {name: scorm.name}).then(function(message) { + self.showError = function(message, defaultMessage) { + defaultMessage = defaultMessage || 'mma.mod_quiz.errorgetquiz'; + if (message) { $mmUtil.showErrorModal(message); - }); + } else { + $mmUtil.showErrorModal(defaultMessage, true); + } + return $q.reject(); }; return self; diff --git a/www/addons/mod_quiz/templates/attempt.html b/www/addons/mod_quiz/templates/attempt.html new file mode 100644 index 00000000000..fc175153747 --- /dev/null +++ b/www/addons/mod_quiz/templates/attempt.html @@ -0,0 +1,34 @@ + + {{ quiz.name }} + + + +
+
+

{{ 'mma.mod_quiz.attemptnumber' | translate }}

+

{{ 'mma.mod_quiz.preview' | translate }}

+

{{ attempt.attempt }}

+
+
+

{{ 'mma.mod_quiz.attemptstate' | translate }}

+

{{ sentence }}

+
+
+

{{ 'mma.mod_quiz.marks' | translate }} / {{ quiz.sumGradesFormatted }}

+

{{ attempt.readableMark }}

+
+
+

{{ 'mma.mod_quiz.grade' | translate }} / {{ quiz.gradeFormatted }}

+

{{ attempt.readableGrade }}

+
+
+

{{ 'mma.mod_quiz.feedback' | translate }}

+

{{ attempt.feedback }}

+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/www/addons/mod_quiz/templates/index.html b/www/addons/mod_quiz/templates/index.html index a5b61e26ee7..2f9c87c95c2 100644 --- a/www/addons/mod_quiz/templates/index.html +++ b/www/addons/mod_quiz/templates/index.html @@ -42,7 +42,7 @@

- +

@@ -61,7 +61,7 @@ -
+

{{ message }}

From 1412af6c3c2aaae06dd01ac1410d11f8eca78249 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 9 Feb 2016 15:26:55 +0100 Subject: [PATCH 003/101] MOBILE-1406 quiz: Check if quiz has questions not supported --- www/addons/mod_quiz/controllers/index.js | 3 +++ www/addons/mod_quiz/lang/en.json | 1 + www/addons/mod_quiz/services/quiz.js | 23 ++++++++++++++++++++++- www/addons/mod_quiz/templates/index.html | 4 ++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/www/addons/mod_quiz/controllers/index.js b/www/addons/mod_quiz/controllers/index.js index be05ce67bd5..982996d0e25 100644 --- a/www/addons/mod_quiz/controllers/index.js +++ b/www/addons/mod_quiz/controllers/index.js @@ -72,6 +72,7 @@ angular.module('mm.addons.mod_quiz') accessInfo = info; $scope.accessRules = accessInfo.accessrules; quiz.showReviewColumn = accessInfo.canreviewmyattempts; + $scope.unsupportedQuestions = $mmaModQuiz.getUnsupportedQuestions(accessInfo.questiontypes); // Get attempts. return $mmaModQuiz.getUserAttempts(quiz.id).then(function(atts) { @@ -209,6 +210,8 @@ angular.module('mm.addons.mod_quiz') $scope.buttonText = ''; } else if (accessInfo.canattempt && $scope.preventMessages.length) { $scope.buttonText = ''; + } else if ($scope.unsupportedQuestions.length) { + $scope.buttonText = ''; } } } diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index 6fd5a367082..75891259eac 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -8,6 +8,7 @@ "continueattemptquiz": "Continue the last attempt", "errorgetattempt": "Error getting attempt data.", "errorgetquiz": "Error getting quiz data.", + "errorquestionsnotsupported": "This quiz can't be attempted in the app because it can contain questions not supported by the app:", "feedback": "Feedback", "grade": "Grade", "gradeaverage": "Average grade", diff --git a/www/addons/mod_quiz/services/quiz.js b/www/addons/mod_quiz/services/quiz.js index bd4c68a30a4..a43ba3831e1 100644 --- a/www/addons/mod_quiz/services/quiz.js +++ b/www/addons/mod_quiz/services/quiz.js @@ -25,7 +25,9 @@ angular.module('mm.addons.mod_quiz') $log = $log.getInstance('$mmaModQuiz'); - var self = {}; + var self = {}, + supportedQuestions = ['calculated', 'calculatedsimple', 'calculatedmulti', 'description', 'essay', 'gapselect', + 'match', 'multichoice', 'numerical', 'shortanswer', 'truefalse']; // Constants. @@ -672,6 +674,25 @@ angular.module('mm.addons.mod_quiz') return ''; }; + /** + * Given a list of question types, returns the types that aren't supported. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getUnsupportedQuestions + * @param {String[]} questionTypes Question types to check. + * @return {String[]} Not supported question types. + */ + self.getUnsupportedQuestions = function(questionTypes) { + var notSupported = []; + angular.forEach(questionTypes, function(type) { + if (supportedQuestions.indexOf(type) == -1 && notSupported.indexOf(type) == -1) { + notSupported.push(type); + } + }); + return notSupported; + }; + /** * Get cache key for get user attempts WS calls. * diff --git a/www/addons/mod_quiz/templates/index.html b/www/addons/mod_quiz/templates/index.html index 2f9c87c95c2..9ba54ceeba4 100644 --- a/www/addons/mod_quiz/templates/index.html +++ b/www/addons/mod_quiz/templates/index.html @@ -68,6 +68,10 @@

{{ 'mma.mod_quiz.noquestions' | translate }}

+
+

{{ 'mma.mod_quiz.errorquestionsnotsupported' | translate }}

+

{{ type }}

+
From fea06ebb0b2c1ea37df4782b6576037a5f55018c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 10 Feb 2016 15:41:41 +0100 Subject: [PATCH 004/101] MOBILE-1411 quiz: Allow starting or continuing a quiz (empty player) --- www/addons/mod_quiz/controllers/player.js | 123 ++++++++++++++++++ www/addons/mod_quiz/lang/en.json | 3 + www/addons/mod_quiz/main.js | 14 ++ www/addons/mod_quiz/services/helper.js | 28 +++- www/addons/mod_quiz/services/quiz.js | 95 +++++++++++--- www/addons/mod_quiz/templates/index.html | 2 +- .../mod_quiz/templates/password-modal.html | 19 +++ www/addons/mod_quiz/templates/player.html | 13 ++ 8 files changed, 280 insertions(+), 17 deletions(-) create mode 100644 www/addons/mod_quiz/controllers/player.js create mode 100644 www/addons/mod_quiz/templates/password-modal.html create mode 100644 www/addons/mod_quiz/templates/player.html diff --git a/www/addons/mod_quiz/controllers/player.js b/www/addons/mod_quiz/controllers/player.js new file mode 100644 index 00000000000..daef7643e99 --- /dev/null +++ b/www/addons/mod_quiz/controllers/player.js @@ -0,0 +1,123 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Quiz player controller. + * + * @module mm.addons.mod_quiz + * @ngdoc controller + * @name mmaModQuizPlayerCtrl + */ +.controller('mmaModQuizPlayerCtrl', function($scope, $stateParams, $mmaModQuiz, $mmaModQuizHelper, $q) { + var quizId = $stateParams.quizid, + courseId = $stateParams.courseid, + quiz, + accessInfo, + attempt, + preflightData = {}, // Preflight data to send to WS (like password). + newAttempt; + + $scope.password = ''; + + // Convenience function to start the player. + function start(password) { + var promise; + $scope.dataLoaded = false; + + if (typeof password != 'undefined') { + // Password submitted, get attempt data. + promise = getAttemptData(password); + } else { + // Fetch data. + promise = fetchData().then(function() { + return getAttemptData(); + }); + } + + promise.finally(function() { + $scope.dataLoaded = true; + }); + } + + // Convenience function to get the quiz data. + function fetchData() { + return $mmaModQuiz.getQuizById(courseId, quizId).then(function(quizData) { + quiz = quizData; + $scope.quiz = quiz; + + // Get access information for the quiz. + return $mmaModQuiz.getAccessInformation(quiz.id, 0, true).then(function(info) { + accessInfo = info; + + // Get attempts to determine last attempt. + return $mmaModQuiz.getUserAttempts(quiz.id, 'all', true, true).then(function(attempts) { + if (!attempts.length) { + newAttempt = true; + } else { + attempt = attempts[attempts.length - 1]; + newAttempt = $mmaModQuiz.isAttemptFinished(attempt.state); + } + }); + }); + }).catch(function(message) { + return $mmaModQuizHelper.showError(message); + }); + } + + // Convenience function to start/continue the attempt. + function getAttemptData(pwd) { + if (accessInfo.ispreflightcheckrequired && typeof pwd == 'undefined') { + // Quiz uses password. Ask the user. + if (!$scope.modal) { + $mmaModQuizHelper.initPasswordModal($scope).then(function() { + $scope.modal.show(); + }); + } else if (!$scope.modal.isShown()) { + $scope.modal.show(); + } + return $q.reject(); + } + + var promise; + preflightData.quizpassword = pwd; + + if (newAttempt) { + promise = $mmaModQuiz.startAttempt(quiz.id, preflightData).then(function(att) { + attempt = att; + }); + } else { + promise = $q.when(); + } + + return promise.then(function() { + return $mmaModQuiz.getAttemptData(attempt.id, 0, preflightData, true).then(function(data) { + $scope.closeModal && $scope.closeModal(); // Close modal if needed. + $scope.attempt = attempt; + }); + }).catch(function(message) { + $mmaModQuizHelper.showError(message, 'mm.core.error'); + }); + } + + // Start the player when the controller is loaded. + start(); + + // Start the player. + $scope.start = function(password) { + start(password); + }; + +}); diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index 75891259eac..fd9a8d8a5c9 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -22,8 +22,11 @@ "overallfeedback": "Overall feedback", "preview": "Preview", "previewquiznow": "Preview quiz now", + "quizpassword": "Quiz password", "reattemptquiz": "Re-attempt quiz", + "requirepasswordmessage": "To attempt this quiz you need to know the quiz password", "review": "Review", + "startattempt": "Start attempt", "stateabandoned": "Never submitted", "statefinished": "Finished", "statefinisheddetails": "Submitted {{$a}}", diff --git a/www/addons/mod_quiz/main.js b/www/addons/mod_quiz/main.js index 229c209029e..9dcac8cba0b 100644 --- a/www/addons/mod_quiz/main.js +++ b/www/addons/mod_quiz/main.js @@ -45,6 +45,20 @@ angular.module('mm.addons.mod_quiz', ['mm.core']) templateUrl: 'addons/mod_quiz/templates/attempt.html' } } + }) + + .state('site.mod_quiz-player', { + url: '/mod_quiz-player', + params: { + courseid: null, + quizid: null + }, + views: { + 'site': { + controller: 'mmaModQuizPlayerCtrl', + templateUrl: 'addons/mod_quiz/templates/player.html' + } + } }); }) diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index 54fb65f4bdf..ab0e75e0a8d 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -21,10 +21,36 @@ angular.module('mm.addons.mod_quiz') * @ngdoc service * @name $mmaModQuizHelper */ -.factory('$mmaModQuizHelper', function($mmaModQuiz, $mmUtil, $q) { +.factory('$mmaModQuizHelper', function($mmaModQuiz, $mmUtil, $q, $ionicModal) { var self = {}; + /** + * Init a password modal, adding it to the scope. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#initPasswordModal + * @param {Object} scope Scope. + * @return {Promise} Promise resolved when the modal is initialized. + */ + self.initPasswordModal = function(scope) { + return $ionicModal.fromTemplateUrl('addons/mod_quiz/templates/password-modal.html', { + scope: scope, + animation: 'slide-in-up' + }).then(function(modal) { + scope.modal = modal; + + scope.closeModal = function() { + scope.password = ''; + modal.hide(); + }; + scope.$on('$destroy', function() { + modal.remove(); + }); + }); + }; + /** * Add some calculated data to the attempt. * diff --git a/www/addons/mod_quiz/services/quiz.js b/www/addons/mod_quiz/services/quiz.js index a43ba3831e1..3b098aa5484 100644 --- a/www/addons/mod_quiz/services/quiz.js +++ b/www/addons/mod_quiz/services/quiz.js @@ -91,12 +91,13 @@ angular.module('mm.addons.mod_quiz') * @module mm.addons.mod_quiz * @ngdoc method * @name $mmaModQuiz#getAccessInformation - * @param {Number} quizId Quiz ID. - * @param {Number} attemptId Attempt ID. 0 for user's last attempt. - * @param {String} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with the access information. + * @param {Number} quizId Quiz ID. + * @param {Number} attemptId Attempt ID. 0 for user's last attempt. + * @param {Boolean} ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the access information. */ - self.getAccessInformation = function(quizId, attemptId, siteId) { + self.getAccessInformation = function(quizId, attemptId, ignoreCache, siteId) { siteId = siteId || $mmSite.getId(); return $mmSitesManager.getSite(siteId).then(function(site) { @@ -108,6 +109,11 @@ angular.module('mm.addons.mod_quiz') cacheKey: getAccessInformationCacheKey(quizId, attemptId) }; + if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + return site.read('mod_quiz_get_access_information', params, preSets); }); }; @@ -142,28 +148,29 @@ angular.module('mm.addons.mod_quiz') * @param {Number} attemptId Attempt ID. * @param {Number} page Page number. * @param {Object} preflightData Preflight required data (like password). + * @param {Boolean} ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param {String} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the attempt data. */ - self.getAttemptData = function(attemptId, page, preflightData, siteId) { + self.getAttemptData = function(attemptId, page, preflightData, ignoreCache, siteId) { siteId = siteId || $mmSite.getId(); return $mmSitesManager.getSite(siteId).then(function(site) { var params = { attemptid: attemptId, page: page, - preflightdata: preflightData + preflightdata: treatPreflightData(preflightData) }, preSets = { cacheKey: getAttemptDataCacheKey(attemptId, page) }; - return site.read('mod_quiz_get_attempt_data', params, preSets).then(function(response) { - if (response && response.attempt) { - return response.attempt; - } - return $q.reject(); - }); + if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + + return site.read('mod_quiz_get_attempt_data', params, preSets); }); }; @@ -323,7 +330,7 @@ angular.module('mm.addons.mod_quiz') return $mmSitesManager.getSite(siteId).then(function(site) { var params = { attemptid: attemptId, - preflightdata: preflightData + preflightdata: treatPreflightData(preflightData) }, preSets = { cacheKey: getAttemptSummaryCacheKey(attemptId) @@ -723,11 +730,12 @@ angular.module('mm.addons.mod_quiz') * @param {Number} quizId Quiz ID. * @param {Number} [status] Status of the attempts to get. By default, 'all'. * @param {Boolean} [includePreviews] True to include previews, false otherwise. Defaults to true. + * @param {Boolean} ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param {String} [siteId] Site ID. If not defined, current site. * @param {Number} [userId] User ID. If not defined use site's current user. * @return {Promise} Promise resolved with the attempts. */ - self.getUserAttempts = function(quizId, status, includePreviews, siteId, userId) { + self.getUserAttempts = function(quizId, status, includePreviews, ignoreCache, siteId, userId) { siteId = siteId || $mmSite.getId(); status = status || 'all'; if (typeof includePreviews == 'undefined') { @@ -747,6 +755,11 @@ angular.module('mm.addons.mod_quiz') cacheKey: getUserAttemptsCacheKey(quizId, userId) }; + if (ignoreCache) { + preSets.getFromCache = 0; + preSets.emergencyCache = 0; + } + return site.read('mod_quiz_get_user_attempts', params, preSets).then(function(response) { if (response && response.attempts) { return response.attempts; @@ -1260,5 +1273,57 @@ angular.module('mm.addons.mod_quiz') return grade; }; + /** + * Start an attempt. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#startAttempt + * @param {Number} quizId Quiz ID. + * @param {Object} preflightData Preflight required data (like password). + * @param {Boolean} forceNew Whether to force a new attempt or not. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the attempt data. + */ + self.startAttempt = function(quizId, preflightData, forceNew, siteId) { + siteId = siteId || $mmSite.getId(); + + return $mmSitesManager.getSite(siteId).then(function(site) { + var params = { + quizid: quizId, + preflightdata: treatPreflightData(preflightData), + forcenew: forceNew ? 1 : 0 + }; + + return site.write('mod_quiz_start_attempt', params).then(function(response) { + if (response && response.warnings && response.warnings.length) { + // Reject with the first warning. + return $q.reject(response.warnings[0].message); + } else if (response && response.attempt) { + return response.attempt; + } + return $q.reject(); + }); + }); + }; + + /** + * Treat preflight data to be sent to a WS. + * Converts an object of type key => value into an array of type 0 => {name: key, value: value}. + * + * @param {Object} data Data to treat. + * @return {Object[]} Treated data. + */ + function treatPreflightData(data) { + var treated = []; + angular.forEach(data, function(value, key) { + treated.push({ + name: key, + value: value + }); + }); + return treated; + } + return self; }); diff --git a/www/addons/mod_quiz/templates/index.html b/www/addons/mod_quiz/templates/index.html index 9ba54ceeba4..8901783c2a4 100644 --- a/www/addons/mod_quiz/templates/index.html +++ b/www/addons/mod_quiz/templates/index.html @@ -73,7 +73,7 @@

{{ type }}

- + {{ buttonText | translate }}
diff --git a/www/addons/mod_quiz/templates/password-modal.html b/www/addons/mod_quiz/templates/password-modal.html new file mode 100644 index 00000000000..3b423d2aabf --- /dev/null +++ b/www/addons/mod_quiz/templates/password-modal.html @@ -0,0 +1,19 @@ + + +

{{ 'mma.mod_quiz.startattempt' | translate}}

+ +
+ +
+
+

{{ 'mma.mod_quiz.requirepasswordmessage' | translate}}

+
+
+ +
+
+ +
+
+
+
diff --git a/www/addons/mod_quiz/templates/player.html b/www/addons/mod_quiz/templates/player.html new file mode 100644 index 00000000000..416f3e1ccd9 --- /dev/null +++ b/www/addons/mod_quiz/templates/player.html @@ -0,0 +1,13 @@ + + {{ quiz.name }} + + +
+ +
+
+ TODO +
+
+
+
\ No newline at end of file From 84b060e551a5e95c3d6dc0cdfd4fd13369e72c39 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 10 Feb 2016 16:32:22 +0100 Subject: [PATCH 005/101] MOBILE-1411 quiz: Show warning when starting timed quiz --- www/addons/mod_quiz/controllers/player.js | 28 ++++++++++++++----- www/addons/mod_quiz/lang/en.json | 2 ++ www/addons/mod_quiz/services/helper.js | 8 +++--- .../templates/confirmstart-modal.html | 23 +++++++++++++++ .../mod_quiz/templates/password-modal.html | 19 ------------- 5 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 www/addons/mod_quiz/templates/confirmstart-modal.html delete mode 100644 www/addons/mod_quiz/templates/password-modal.html diff --git a/www/addons/mod_quiz/controllers/player.js b/www/addons/mod_quiz/controllers/player.js index daef7643e99..3b914254ead 100644 --- a/www/addons/mod_quiz/controllers/player.js +++ b/www/addons/mod_quiz/controllers/player.js @@ -21,7 +21,7 @@ angular.module('mm.addons.mod_quiz') * @ngdoc controller * @name mmaModQuizPlayerCtrl */ -.controller('mmaModQuizPlayerCtrl', function($scope, $stateParams, $mmaModQuiz, $mmaModQuizHelper, $q) { +.controller('mmaModQuizPlayerCtrl', function($scope, $stateParams, $mmaModQuiz, $mmaModQuizHelper, $q, $mmUtil) { var quizId = $stateParams.quizid, courseId = $stateParams.courseid, quiz, @@ -30,7 +30,9 @@ angular.module('mm.addons.mod_quiz') preflightData = {}, // Preflight data to send to WS (like password). newAttempt; - $scope.password = ''; + $scope.preflightData = { + password: '' + }; // Convenience function to start the player. function start(password) { @@ -56,11 +58,20 @@ angular.module('mm.addons.mod_quiz') function fetchData() { return $mmaModQuiz.getQuizById(courseId, quizId).then(function(quizData) { quiz = quizData; + + if (quiz.timelimit > 0) { + $scope.isTimed = true; + $mmUtil.formatTime(quiz.timelimit).then(function(time) { + quiz.readableTimeLimit = time; + }); + } + $scope.quiz = quiz; // Get access information for the quiz. return $mmaModQuiz.getAccessInformation(quiz.id, 0, true).then(function(info) { accessInfo = info; + $scope.requirePassword = accessInfo.ispreflightcheckrequired; // Get attempts to determine last attempt. return $mmaModQuiz.getUserAttempts(quiz.id, 'all', true, true).then(function(attempts) { @@ -78,11 +89,14 @@ angular.module('mm.addons.mod_quiz') } // Convenience function to start/continue the attempt. - function getAttemptData(pwd) { - if (accessInfo.ispreflightcheckrequired && typeof pwd == 'undefined') { - // Quiz uses password. Ask the user. + function getAttemptData(password) { + // Check if we need to show a confirm modal (requires password or quiz has time limit). + // 'password' param will be != undefined even if password is not required, so we can use it to tell + // if the user clicked the modal button or not. + if (typeof password == 'undefined' && ($scope.requirePassword || $scope.isTimed)) { + // We need to show confirm modal and the user hasn't clicked the confirm button. Show the modal. if (!$scope.modal) { - $mmaModQuizHelper.initPasswordModal($scope).then(function() { + $mmaModQuizHelper.initConfirmStartModal($scope).then(function() { $scope.modal.show(); }); } else if (!$scope.modal.isShown()) { @@ -92,7 +106,7 @@ angular.module('mm.addons.mod_quiz') } var promise; - preflightData.quizpassword = pwd; + preflightData.quizpassword = password; if (newAttempt) { promise = $mmaModQuiz.startAttempt(quiz.id, preflightData).then(function(att) { diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index fd9a8d8a5c9..d7903060b9e 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -5,6 +5,8 @@ "attemptquiznow": "Attempt quiz now", "attemptstate": "State", "comment": "Comment", + "confirmstart": "The quiz has a time limit of {{$a}}. Time will count down from the moment you start your attempt and you must submit before it expires. Are you sure that you wish to start now?", + "confirmstartheader": "Timed quiz", "continueattemptquiz": "Continue the last attempt", "errorgetattempt": "Error getting attempt data.", "errorgetquiz": "Error getting quiz data.", diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index ab0e75e0a8d..bc163bc41ce 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -30,19 +30,19 @@ angular.module('mm.addons.mod_quiz') * * @module mm.addons.mod_quiz * @ngdoc method - * @name $mmaModQuizHelper#initPasswordModal + * @name $mmaModQuizHelper#initConfirmStartModal * @param {Object} scope Scope. * @return {Promise} Promise resolved when the modal is initialized. */ - self.initPasswordModal = function(scope) { - return $ionicModal.fromTemplateUrl('addons/mod_quiz/templates/password-modal.html', { + self.initConfirmStartModal = function(scope) { + return $ionicModal.fromTemplateUrl('addons/mod_quiz/templates/confirmstart-modal.html', { scope: scope, animation: 'slide-in-up' }).then(function(modal) { scope.modal = modal; scope.closeModal = function() { - scope.password = ''; + scope.preflightData.password = ''; modal.hide(); }; scope.$on('$destroy', function() { diff --git a/www/addons/mod_quiz/templates/confirmstart-modal.html b/www/addons/mod_quiz/templates/confirmstart-modal.html new file mode 100644 index 00000000000..8e3c33c8468 --- /dev/null +++ b/www/addons/mod_quiz/templates/confirmstart-modal.html @@ -0,0 +1,23 @@ + + +

{{ 'mma.mod_quiz.startattempt' | translate}}

+ +
+ +
+
+

{{ 'mma.mod_quiz.requirepasswordmessage' | translate}}

+
+
+ +
+
+

{{ 'mma.mod_quiz.confirmstartheader' | translate }}

+

{{ 'mma.mod_quiz.confirmstart' | translate:{$a: quiz.readableTimeLimit} }}

+
+
+ +
+
+
+
diff --git a/www/addons/mod_quiz/templates/password-modal.html b/www/addons/mod_quiz/templates/password-modal.html deleted file mode 100644 index 3b423d2aabf..00000000000 --- a/www/addons/mod_quiz/templates/password-modal.html +++ /dev/null @@ -1,19 +0,0 @@ - - -

{{ 'mma.mod_quiz.startattempt' | translate}}

- -
- -
-
-

{{ 'mma.mod_quiz.requirepasswordmessage' | translate}}

-
-
- -
-
- -
-
-
-
From 65e484582529bcd486d4eeb43cfb5d2d88fe8c55 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 11 Feb 2016 08:55:54 +0100 Subject: [PATCH 006/101] MOBILE-1411 quiz: Check supported access rules --- www/addons/mod_quiz/controllers/index.js | 3 ++- www/addons/mod_quiz/lang/en.json | 1 + www/addons/mod_quiz/services/quiz.js | 24 +++++++++++++++++++++++- www/addons/mod_quiz/templates/index.html | 4 ++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/www/addons/mod_quiz/controllers/index.js b/www/addons/mod_quiz/controllers/index.js index 982996d0e25..d6a8e373231 100644 --- a/www/addons/mod_quiz/controllers/index.js +++ b/www/addons/mod_quiz/controllers/index.js @@ -73,6 +73,7 @@ angular.module('mm.addons.mod_quiz') $scope.accessRules = accessInfo.accessrules; quiz.showReviewColumn = accessInfo.canreviewmyattempts; $scope.unsupportedQuestions = $mmaModQuiz.getUnsupportedQuestions(accessInfo.questiontypes); + $scope.unsupportedRules = $mmaModQuiz.getUnsupportedRules(accessInfo.activerulenames); // Get attempts. return $mmaModQuiz.getUserAttempts(quiz.id).then(function(atts) { @@ -210,7 +211,7 @@ angular.module('mm.addons.mod_quiz') $scope.buttonText = ''; } else if (accessInfo.canattempt && $scope.preventMessages.length) { $scope.buttonText = ''; - } else if ($scope.unsupportedQuestions.length) { + } else if ($scope.unsupportedQuestions.length || $scope.unsupportedRules.length) { $scope.buttonText = ''; } } diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index d7903060b9e..e4aaf8b4afe 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -11,6 +11,7 @@ "errorgetattempt": "Error getting attempt data.", "errorgetquiz": "Error getting quiz data.", "errorquestionsnotsupported": "This quiz can't be attempted in the app because it can contain questions not supported by the app:", + "errorrulesnotsupported": "This quiz can't be attempted in the app because it has active rules not supported by the app:", "feedback": "Feedback", "grade": "Grade", "gradeaverage": "Average grade", diff --git a/www/addons/mod_quiz/services/quiz.js b/www/addons/mod_quiz/services/quiz.js index 3b098aa5484..66d55388f5d 100644 --- a/www/addons/mod_quiz/services/quiz.js +++ b/www/addons/mod_quiz/services/quiz.js @@ -27,7 +27,10 @@ angular.module('mm.addons.mod_quiz') var self = {}, supportedQuestions = ['calculated', 'calculatedsimple', 'calculatedmulti', 'description', 'essay', 'gapselect', - 'match', 'multichoice', 'numerical', 'shortanswer', 'truefalse']; + 'match', 'multichoice', 'numerical', 'shortanswer', 'truefalse'], + supportedRules = ['quizaccess_delaybetweenattempts', 'quizaccess_ipaddress', 'quizaccess_numattempts', + 'quizaccess_openclosedate', 'quizaccess_password', 'quizaccess_safebrowser', + 'quizaccess_securewindow', 'quizaccess_timelimit']; // Constants. @@ -700,6 +703,25 @@ angular.module('mm.addons.mod_quiz') return notSupported; }; + /** + * Given a list of access rules names, returns the rules that aren't supported. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuiz#getUnsupportedRules + * @param {String[]} rulesNames Rules to check. + * @return {String[]} Not supported rules names. + */ + self.getUnsupportedRules = function(rulesNames) { + var notSupported = []; + angular.forEach(rulesNames, function(name) { + if (supportedRules.indexOf(name) == -1 && notSupported.indexOf(name) == -1) { + notSupported.push(name); + } + }); + return notSupported; + }; + /** * Get cache key for get user attempts WS calls. * diff --git a/www/addons/mod_quiz/templates/index.html b/www/addons/mod_quiz/templates/index.html index 8901783c2a4..d5836bf416d 100644 --- a/www/addons/mod_quiz/templates/index.html +++ b/www/addons/mod_quiz/templates/index.html @@ -72,6 +72,10 @@

{{ 'mma.mod_quiz.errorquestionsnotsupported' | translate }}

{{ type }}

+
+

{{ 'mma.mod_quiz.errorrulesnotsupported' | translate }}

+

{{ name }}

+
From b4b200ad9b81edf233cf1b7c2da324aec3575b8e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 11 Feb 2016 16:11:26 +0100 Subject: [PATCH 007/101] MOBILE-1416 quiz: Implement questions delegate --- www/addons/mod_quiz/main.js | 5 + www/addons/mod_quiz/services/delegate.js | 156 +++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 www/addons/mod_quiz/services/delegate.js diff --git a/www/addons/mod_quiz/main.js b/www/addons/mod_quiz/main.js index 9dcac8cba0b..f72b6adb128 100644 --- a/www/addons/mod_quiz/main.js +++ b/www/addons/mod_quiz/main.js @@ -65,4 +65,9 @@ angular.module('mm.addons.mod_quiz', ['mm.core']) .config(function($mmCourseDelegateProvider) { $mmCourseDelegateProvider.registerContentHandler('mmaModQuiz', 'quiz', '$mmaModQuizHandlers.courseContentHandler'); +}) + +.run(function($mmEvents, mmCoreEventLogin, mmCoreEventSiteUpdated, $mmaModQuizQuestionsDelegate) { + $mmEvents.on(mmCoreEventLogin, $mmaModQuizQuestionsDelegate.updateQuestionHandlers); + $mmEvents.on(mmCoreEventSiteUpdated, $mmaModQuizQuestionsDelegate.updateQuestionHandlers); }); diff --git a/www/addons/mod_quiz/services/delegate.js b/www/addons/mod_quiz/services/delegate.js new file mode 100644 index 00000000000..1e2816b1d8a --- /dev/null +++ b/www/addons/mod_quiz/services/delegate.js @@ -0,0 +1,156 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Delegate to register question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionsDelegate + * @description + * + * Delegate to register question handlers. + * You can use this service to register your own question handlers to be used in a quiz. + * + * Important: This service mustn't be injected using Angular's dependency injection. This is because custom apps could have this + * addon disabled or removed, so you can't guarantee that the service exists. Please inject it using {@link $mmAddonManager}. + * + */ +.factory('$mmaModQuizQuestionsDelegate', function($log, $q, $mmUtil, $mmSite) { + + $log = $log.getInstance('$mmaModQuizQuestionsDelegate'); + + var handlers = {}, + enabledHandlers = {}, + self = {}; + + /** + * Get the directive to use for a certain question type. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizQuestionsDelegate#getDirectiveForQuestion + * @param {Object} question Question to get the directive for. + * @return {String} Directive name. Undefined if no directive found. + */ + self.getDirectiveForQuestion = function(question) { + var type = question.type; + if (typeof enabledHandlers[type] != 'undefined') { + return enabledHandlers[type].getDirectiveName(question); + } + }; + + /** + * Register a question handler. The handler will be used when we need to render a question of the type defined. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizQuestionsDelegate#registerHandler + * @param {String} name Handler's name. + * @param {String} questionType Question type the handler supports. + * @param {String|Object|Function} handler Must be resolved to an object defining the following properties. Or to a function + * returning an object defining these properties. See {@link $mmUtil#resolveObject}. + * - isEnabled (Boolean|Promise) Whether or not the handler is enabled on a site level. + * When using a promise, it should return a boolean. + * - getDirectiveName(question) (String) Returns the name of the directive to render the question. + * There's no need to check the question type in this function. + */ + self.registerHandler = function(name, questionType, handler) { + if (typeof handlers[questionType] !== 'undefined') { + $log.debug("Addon '" + name + "' already registered as handler for '" + questionType + "'"); + return false; + } + $log.debug("Registered handler '" + name + "' for question type '" + questionType + "'"); + handlers[questionType] = { + addon: name, + instance: undefined, + handler: handler + }; + + // It's possible that this handler is registered after updateQuestionHandlers has been called for the current + // site. Let's call updateQuestionHandler just in case. + self.updateQuestionHandler(questionType, handlers[questionType]); + }; + + /** + * Check if a handler is enabled for a certain site and add/remove it to enabledHandlers. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizQuestionsDelegate#updateQuestionHandler + * @param {String} questionType The question type this handler handles. + * @param {Object} handlerInfo The handler details. + * @return {Promise} Resolved when done. + * @protected + */ + self.updateQuestionHandler = function(questionType, handlerInfo) { + var promise, + siteId = $mmSite.getId(); + + if (typeof handlerInfo.instance === 'undefined') { + handlerInfo.instance = $mmUtil.resolveObject(handlerInfo.handler, true); + } + + if (!$mmSite.isLoggedIn()) { + promise = $q.reject(); + } else { + promise = $q.when(handlerInfo.instance.isEnabled()); + } + + // Checks if the handler is enabled. + return promise.catch(function() { + return false; + }).then(function(enabled) { + // Check that site hasn't changed since the check started. + if ($mmSite.isLoggedIn() && $mmSite.getId() === siteId) { + if (enabled) { + enabledHandlers[questionType] = handlerInfo.instance; + } else { + delete enabledHandlers[questionType]; + } + } + }); + }; + + /** + * Update the enabled handlers for the current site. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizQuestionsDelegate#updateQuestionHandlers + * @return {Promise} Resolved when done. + * @protected + */ + self.updateQuestionHandlers = function() { + var promises = []; + + $log.debug('Updating question handlers for current site.'); + + // Loop over all the handlers. + angular.forEach(handlers, function(handlerInfo, questionType) { + promises.push(self.updateQuestionHandler(questionType, handlerInfo)); + }); + + return $q.all(promises).then(function() { + return true; + }, function() { + // Never reject. + return true; + }); + }; + + return self; +}); From 2269761978c7f338665550a557a751ec72be24ed Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 12 Feb 2016 10:30:31 +0100 Subject: [PATCH 008/101] MOBILE-1416 quiz: Apply questions delegate to player --- www/addons/mod_quiz/controllers/player.js | 5 ++ www/addons/mod_quiz/directives/question.js | 56 ++++++++++++++++++ www/addons/mod_quiz/lang/en.json | 4 ++ www/addons/mod_quiz/services/helper.js | 57 +++++++++++++++++++ www/addons/mod_quiz/templates/player.html | 16 +++++- .../templates/questionnotsupported.html | 1 + 6 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 www/addons/mod_quiz/directives/question.js create mode 100644 www/addons/mod_quiz/templates/questionnotsupported.html diff --git a/www/addons/mod_quiz/controllers/player.js b/www/addons/mod_quiz/controllers/player.js index 3b914254ead..781d29e7696 100644 --- a/www/addons/mod_quiz/controllers/player.js +++ b/www/addons/mod_quiz/controllers/player.js @@ -120,6 +120,11 @@ angular.module('mm.addons.mod_quiz') return $mmaModQuiz.getAttemptData(attempt.id, 0, preflightData, true).then(function(data) { $scope.closeModal && $scope.closeModal(); // Close modal if needed. $scope.attempt = attempt; + $scope.questions = data.questions; + + angular.forEach($scope.questions, function(question) { + question.readableMark = $mmaModQuizHelper.getQuestionMarkFromHtml(question.html); + }); }); }).catch(function(message) { $mmaModQuizHelper.showError(message, 'mm.core.error'); diff --git a/www/addons/mod_quiz/directives/question.js b/www/addons/mod_quiz/directives/question.js new file mode 100644 index 00000000000..7994743cf76 --- /dev/null +++ b/www/addons/mod_quiz/directives/question.js @@ -0,0 +1,56 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a question. + * It will search for the right directive to render the question based on the question type. + * See {@link $mmaModQuizQuestionsDelegate}. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestion + */ +.directive('mmaModQuizQuestion', function($compile, $mmaModQuizQuestionsDelegate, $mmaModQuizHelper) { + + // We set priority to a high number to ensure that it will be compiled before other directives. + // With terminal set to true, the other directives will be skipped after this directive is compiled. + return { + restrict: 'A', + priority: 1000, + terminal: true, + templateUrl: 'addons/mod_quiz/templates/questionnotsupported.html', + scope: { + question: '=', + }, + link: function(scope, element) { + if (scope.question) { + // Search the right directive to render the question. + var directive = $mmaModQuizQuestionsDelegate.getDirectiveForQuestion(scope.question); + if (directive) { + // Treat the question before starting the directive. + $mmaModQuizHelper.extractQuestionInfoBox(scope.question); + + // Add the directive to the element. + element.attr(directive, ''); + // Remove current directive, otherwise we would cause an infinite loop when compiling. + element.removeAttr('mma-mod-quiz-question'); + // Compile the new directive. + $compile(element)(scope); + } + } + } + }; +}); diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index e4aaf8b4afe..5fe7f121210 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -10,14 +10,17 @@ "continueattemptquiz": "Continue the last attempt", "errorgetattempt": "Error getting attempt data.", "errorgetquiz": "Error getting quiz data.", + "errorquestionnotsupported": "This type of question isn't supported by the app in your site: {{$a}}.", "errorquestionsnotsupported": "This quiz can't be attempted in the app because it can contain questions not supported by the app:", "errorrulesnotsupported": "This quiz can't be attempted in the app because it has active rules not supported by the app:", "feedback": "Feedback", + "flagged": "Flagged", "grade": "Grade", "gradeaverage": "Average grade", "gradehighest": "Highest grade", "grademethod": "Grading method", "gradesofar": "{{$a.method}}: {{$a.mygrade}} / {{$a.quizgrade}}.", + "information": "Information", "marks": "Marks", "noquestions": "No questions have been added yet", "notyetgraded": "Not yet graded", @@ -25,6 +28,7 @@ "overallfeedback": "Overall feedback", "preview": "Preview", "previewquiznow": "Preview quiz now", + "questionno": "Question {{$a}}", "quizpassword": "Quiz password", "reattemptquiz": "Re-attempt quiz", "requirepasswordmessage": "To attempt this quiz you need to know the quiz password", diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index bc163bc41ce..fafe38f6b92 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -25,6 +25,63 @@ angular.module('mm.addons.mod_quiz') var self = {}; + /** + * Removes the info box (flag, question number, etc.) from a question's HTML and adds it in a new infoBox property. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#extractQuestionInfoBox + * @param {Object} question Question. + * @return {Void} + */ + self.extractQuestionInfoBox = function(question) { + var el = angular.element(question.html)[0], + info; + if (el) { + info = el.querySelector('.info'); + if (info) { + question.infoBox = info.innerHTML; + info.remove(); + question.html = el.innerHTML; + } + } + }; + + /** + * Returns the contents of a certain selection in a DOM element. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#getContentsOfElement + * @param {Object} element DOM element to search in. + * @param {String} className Class to search. + * @return {String} Div contents. + */ + self.getContentsOfElement = function(element, selector) { + if (element) { + var el = element[0] || element, // Convert from jqLite to plain JS if needed. + div = el.querySelector(selector); + if (div) { + return div.innerHTML; + } + } + return ''; + }; + + /** + * Gets the mark string from a question HTML. + * Example result: "Marked out of 1.00". + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#getQuestionMarkFromHtml + * @param {String} html Question's HTML. + * @return {String} Question's mark. + */ + self.getQuestionMarkFromHtml = function(html) { + return self.getContentsOfElement(angular.element(html), '.grade'); + }; + /** * Init a password modal, adding it to the scope. * diff --git a/www/addons/mod_quiz/templates/player.html b/www/addons/mod_quiz/templates/player.html index 416f3e1ccd9..731eed17266 100644 --- a/www/addons/mod_quiz/templates/player.html +++ b/www/addons/mod_quiz/templates/player.html @@ -5,8 +5,20 @@
-
- TODO +
+
+
+

{{ 'mma.mod_quiz.questionno' | translate:{$a: question.number} }}

+

{{ 'mma.mod_quiz.information' | translate }}

+
+
+

{{question.status}}

+
+
+

{{question.readableMark}}

+
+ {{ 'mma.mod_quiz.flagged' | translate }} +
diff --git a/www/addons/mod_quiz/templates/questionnotsupported.html b/www/addons/mod_quiz/templates/questionnotsupported.html new file mode 100644 index 00000000000..59aabe26bf7 --- /dev/null +++ b/www/addons/mod_quiz/templates/questionnotsupported.html @@ -0,0 +1 @@ +

{{ 'mma.mod_quiz.errorquestionnotsupported' | translate:{$a: question.type} }}

From 5a47c5b0f84cc177f177b4e5493d4bae2cab3e80 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 12 Feb 2016 12:14:33 +0100 Subject: [PATCH 009/101] MOBILE-1416 quiz: Extract scripts from question HTML --- www/addons/mod_quiz/directives/question.js | 1 + www/addons/mod_quiz/services/helper.js | 45 +++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/www/addons/mod_quiz/directives/question.js b/www/addons/mod_quiz/directives/question.js index 7994743cf76..c2e9a8aa7bb 100644 --- a/www/addons/mod_quiz/directives/question.js +++ b/www/addons/mod_quiz/directives/question.js @@ -42,6 +42,7 @@ angular.module('mm.addons.mod_quiz') if (directive) { // Treat the question before starting the directive. $mmaModQuizHelper.extractQuestionInfoBox(scope.question); + $mmaModQuizHelper.extractQuestionScripts(scope.question); // Add the directive to the element. element.attr(directive, ''); diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index fafe38f6b92..da6688ccaaf 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -40,13 +40,54 @@ angular.module('mm.addons.mod_quiz') if (el) { info = el.querySelector('.info'); if (info) { - question.infoBox = info.innerHTML; + question.infoBox = info.outerHTML; info.remove(); - question.html = el.innerHTML; + question.html = el.outerHTML; } } }; + /** + * Removes the scripts from a question's HTML and adds it in a new 'scriptsCode' property. + * It will also search for init_question functions of the question type and add the object to an 'initObjects' property. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#extractQuestionScripts + * @param {Object} question Question. + * @return {Void} + */ + self.extractQuestionScripts = function(question) { + var matches; + + question.scriptsCode = ''; + question.initObjects = []; + + if (question.html) { + // Search the scripts. + matches = question.html.match(/]*>[\s\S]*?<\/script>/mg); + angular.forEach(matches, function(match) { + // Add the script to scriptsCode and remove it from html. + question.scriptsCode += match; + question.html = question.html.replace(match, ''); + + // Search init_question functions for this type. + var initMatches = match.match(new RegExp('M\.' + question.type + '\.init_question\\(.*?}\\);', 'mg')); + angular.forEach(initMatches, function(initMatch) { + // Remove start and end of the match, we only want the object. + initMatch = initMatch.replace('M.' + question.type + '.init_question(', ''); + initMatch = initMatch.substr(0, initMatch.length - 2); + + // Try to convert it to an object and add it to the question. + try { + initMatch = JSON.parse(initMatch); + question.initObjects.push(initMatch); + } catch(ex) {} + }); + }); + } + }; + /** * Returns the contents of a certain selection in a DOM element. * From 579a53304cf171688aca16fec0d5c54d78285766 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 12 Feb 2016 12:15:24 +0100 Subject: [PATCH 010/101] MOBILE-1416 quiz: Use delegate to detect supported question types --- www/addons/mod_quiz/services/delegate.js | 13 +++++++++++++ www/addons/mod_quiz/services/quiz.js | 6 ++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/www/addons/mod_quiz/services/delegate.js b/www/addons/mod_quiz/services/delegate.js index 1e2816b1d8a..6c1d37e14c3 100644 --- a/www/addons/mod_quiz/services/delegate.js +++ b/www/addons/mod_quiz/services/delegate.js @@ -53,6 +53,19 @@ angular.module('mm.addons.mod_quiz') } }; + /** + * Check if a question type is supported. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizQuestionsDelegate#isQuestionSupported + * @param {String} type Question type. + * @return {Boolean} True if supported, false otherwise. + */ + self.isQuestionSupported = function(type) { + return typeof enabledHandlers[type] != 'undefined'; + }; + /** * Register a question handler. The handler will be used when we need to render a question of the type defined. * diff --git a/www/addons/mod_quiz/services/quiz.js b/www/addons/mod_quiz/services/quiz.js index 66d55388f5d..419bbf1d6f9 100644 --- a/www/addons/mod_quiz/services/quiz.js +++ b/www/addons/mod_quiz/services/quiz.js @@ -21,13 +21,11 @@ angular.module('mm.addons.mod_quiz') * @ngdoc service * @name $mmaModQuiz */ -.factory('$mmaModQuiz', function($log, $mmSite, $mmSitesManager, $q, $translate, $mmUtil, $mmText) { +.factory('$mmaModQuiz', function($log, $mmSite, $mmSitesManager, $q, $translate, $mmUtil, $mmText, $mmaModQuizQuestionsDelegate) { $log = $log.getInstance('$mmaModQuiz'); var self = {}, - supportedQuestions = ['calculated', 'calculatedsimple', 'calculatedmulti', 'description', 'essay', 'gapselect', - 'match', 'multichoice', 'numerical', 'shortanswer', 'truefalse'], supportedRules = ['quizaccess_delaybetweenattempts', 'quizaccess_ipaddress', 'quizaccess_numattempts', 'quizaccess_openclosedate', 'quizaccess_password', 'quizaccess_safebrowser', 'quizaccess_securewindow', 'quizaccess_timelimit']; @@ -696,7 +694,7 @@ angular.module('mm.addons.mod_quiz') self.getUnsupportedQuestions = function(questionTypes) { var notSupported = []; angular.forEach(questionTypes, function(type) { - if (supportedQuestions.indexOf(type) == -1 && notSupported.indexOf(type) == -1) { + if (type != 'random' && type != 'randomsamatch' && !$mmaModQuizQuestionsDelegate.isQuestionSupported('qtype_'+type)) { notSupported.push(type); } }); From d927e65dd3a3c00cf4d3b7a8cd9bcdcd47c8e6b5 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 12 Feb 2016 14:01:06 +0100 Subject: [PATCH 011/101] MOBILE-1416 quiz: Allow aborting quiz from question handlers --- www/addons/mod_quiz/controllers/player.js | 8 +++++ www/addons/mod_quiz/directives/question.js | 9 +++++ www/addons/mod_quiz/lang/en.json | 1 + www/addons/mod_quiz/main.js | 3 +- www/addons/mod_quiz/templates/index.html | 2 +- www/addons/mod_quiz/templates/player.html | 39 ++++++++++++++-------- 6 files changed, 46 insertions(+), 16 deletions(-) diff --git a/www/addons/mod_quiz/controllers/player.js b/www/addons/mod_quiz/controllers/player.js index 781d29e7696..4a14931bb2c 100644 --- a/www/addons/mod_quiz/controllers/player.js +++ b/www/addons/mod_quiz/controllers/player.js @@ -24,12 +24,15 @@ angular.module('mm.addons.mod_quiz') .controller('mmaModQuizPlayerCtrl', function($scope, $stateParams, $mmaModQuiz, $mmaModQuizHelper, $q, $mmUtil) { var quizId = $stateParams.quizid, courseId = $stateParams.courseid, + moduleUrl = $stateParams.moduleurl, quiz, accessInfo, attempt, preflightData = {}, // Preflight data to send to WS (like password). newAttempt; + $scope.moduleUrl = moduleUrl; + $scope.quizAborted = false; $scope.preflightData = { password: '' }; @@ -139,4 +142,9 @@ angular.module('mm.addons.mod_quiz') start(password); }; + // Function to call to abort the quiz. + $scope.abortQuiz = function() { + $scope.quizAborted = true; + }; + }); diff --git a/www/addons/mod_quiz/directives/question.js b/www/addons/mod_quiz/directives/question.js index c2e9a8aa7bb..282e687c4d3 100644 --- a/www/addons/mod_quiz/directives/question.js +++ b/www/addons/mod_quiz/directives/question.js @@ -22,6 +22,14 @@ angular.module('mm.addons.mod_quiz') * @module mm.addons.mod_quiz * @ngdoc directive * @name mmaModQuizQuestion + * @description + * + * The directives to render the question will receive the following parameters in the scope: + * + * @param {Object} question The question to render. + * @param {Function} abortQuiz A function to call to abort the quiz execution. Please use it if there's any problem initializing + * your question. If this function is called, all questions will disappear from the screen and they'll + * be replaced by an error message and a button to attempt the quiz in the browser. */ .directive('mmaModQuizQuestion', function($compile, $mmaModQuizQuestionsDelegate, $mmaModQuizHelper) { @@ -34,6 +42,7 @@ angular.module('mm.addons.mod_quiz') templateUrl: 'addons/mod_quiz/templates/questionnotsupported.html', scope: { question: '=', + abortQuiz: '&' }, link: function(scope, element) { if (scope.question) { diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index 5fe7f121210..9898988db25 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -10,6 +10,7 @@ "continueattemptquiz": "Continue the last attempt", "errorgetattempt": "Error getting attempt data.", "errorgetquiz": "Error getting quiz data.", + "errorparsequestions": "An error occurred while treating the questions. Please attempt this quiz in a browser.", "errorquestionnotsupported": "This type of question isn't supported by the app in your site: {{$a}}.", "errorquestionsnotsupported": "This quiz can't be attempted in the app because it can contain questions not supported by the app:", "errorrulesnotsupported": "This quiz can't be attempted in the app because it has active rules not supported by the app:", diff --git a/www/addons/mod_quiz/main.js b/www/addons/mod_quiz/main.js index f72b6adb128..52482291ca1 100644 --- a/www/addons/mod_quiz/main.js +++ b/www/addons/mod_quiz/main.js @@ -51,7 +51,8 @@ angular.module('mm.addons.mod_quiz', ['mm.core']) url: '/mod_quiz-player', params: { courseid: null, - quizid: null + quizid: null, + moduleurl: null // Module URL to open it in browser. }, views: { 'site': { diff --git a/www/addons/mod_quiz/templates/index.html b/www/addons/mod_quiz/templates/index.html index d5836bf416d..2b4e2e2e7ba 100644 --- a/www/addons/mod_quiz/templates/index.html +++ b/www/addons/mod_quiz/templates/index.html @@ -77,7 +77,7 @@

{{ name }}

diff --git a/www/addons/mod_quiz/templates/player.html b/www/addons/mod_quiz/templates/player.html index 731eed17266..6cb177daf69 100644 --- a/www/addons/mod_quiz/templates/player.html +++ b/www/addons/mod_quiz/templates/player.html @@ -5,21 +5,32 @@
-
-
-
-

{{ 'mma.mod_quiz.questionno' | translate:{$a: question.number} }}

-

{{ 'mma.mod_quiz.information' | translate }}

-
-
-

{{question.status}}

-
-
-

{{question.readableMark}}

-
- {{ 'mma.mod_quiz.flagged' | translate }} -
+
+
+
+
+

{{ 'mma.mod_quiz.questionno' | translate:{$a: question.number} }}

+

{{ 'mma.mod_quiz.information' | translate }}

+
+
+

{{question.status}}

+
+
+

{{question.readableMark}}

+
+ {{ 'mma.mod_quiz.flagged' | translate }} +
+
+
+
+
+

{{ 'mma.mod_quiz.errorparsequestions' | translate }}

+
+ +
\ No newline at end of file From e325bc615ed48dd8dc65ec56b21c9114ff96dc45 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 12 Feb 2016 14:25:32 +0100 Subject: [PATCH 012/101] MOBILE-1416 quiz: Improve accessibility in entry page and player --- www/addons/mod_quiz/templates/index.html | 6 ++++-- www/addons/mod_quiz/templates/player.html | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/www/addons/mod_quiz/templates/index.html b/www/addons/mod_quiz/templates/index.html index 2b4e2e2e7ba..84d505c87ee 100644 --- a/www/addons/mod_quiz/templates/index.html +++ b/www/addons/mod_quiz/templates/index.html @@ -1,7 +1,7 @@ {{ title }} - + @@ -21,7 +21,9 @@
-
{{ 'mma.mod_quiz.summaryofattempts' | translate }}
+
+

{{ 'mma.mod_quiz.summaryofattempts' | translate }}

+

{{ 'mma.mod_quiz.attemptnumber' | translate }}

{{ 'mma.mod_quiz.attemptstate' | translate }}

diff --git a/www/addons/mod_quiz/templates/player.html b/www/addons/mod_quiz/templates/player.html index 6cb177daf69..dedc2b09ca8 100644 --- a/www/addons/mod_quiz/templates/player.html +++ b/www/addons/mod_quiz/templates/player.html @@ -9,8 +9,8 @@
-

{{ 'mma.mod_quiz.questionno' | translate:{$a: question.number} }}

-

{{ 'mma.mod_quiz.information' | translate }}

+

{{ 'mma.mod_quiz.questionno' | translate:{$a: question.number} }}

+

{{ 'mma.mod_quiz.information' | translate }}

{{question.status}}

From e531aaf0b67be5b3bd9da4b65a340e87dc926a82 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 12 Feb 2016 15:54:46 +0100 Subject: [PATCH 013/101] MOBILE-1420 quiz: Retrieve sequencecheck of questions --- www/addons/mod_quiz/controllers/player.js | 12 +++++++++- www/addons/mod_quiz/directives/question.js | 1 + www/addons/mod_quiz/services/helper.js | 27 ++++++++++++++++++++++ www/addons/mod_quiz/templates/player.html | 2 +- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/www/addons/mod_quiz/controllers/player.js b/www/addons/mod_quiz/controllers/player.js index 4a14931bb2c..8daacfcf3f4 100644 --- a/www/addons/mod_quiz/controllers/player.js +++ b/www/addons/mod_quiz/controllers/player.js @@ -21,7 +21,9 @@ angular.module('mm.addons.mod_quiz') * @ngdoc controller * @name mmaModQuizPlayerCtrl */ -.controller('mmaModQuizPlayerCtrl', function($scope, $stateParams, $mmaModQuiz, $mmaModQuizHelper, $q, $mmUtil) { +.controller('mmaModQuizPlayerCtrl', function($log, $scope, $stateParams, $mmaModQuiz, $mmaModQuizHelper, $q, $mmUtil) { + $log = $log.getInstance('mmaModQuizPlayerCtrl'); + var quizId = $stateParams.quizid, courseId = $stateParams.courseid, moduleUrl = $stateParams.moduleurl, @@ -33,6 +35,7 @@ angular.module('mm.addons.mod_quiz') $scope.moduleUrl = moduleUrl; $scope.quizAborted = false; + $scope.answers = {}; $scope.preflightData = { password: '' }; @@ -127,6 +130,13 @@ angular.module('mm.addons.mod_quiz') angular.forEach($scope.questions, function(question) { question.readableMark = $mmaModQuizHelper.getQuestionMarkFromHtml(question.html); + var seqCheck = $mmaModQuizHelper.getQuestionSequenceCheckFromHtml(question.html); + if (seqCheck) { + $scope.answers[seqCheck.name] = seqCheck.value; + } else { + $log.warn('Aborting quiz because couldn\'t retrieve sequence check.', question.name); + $scope.quizAborted = true; + } }); }); }).catch(function(message) { diff --git a/www/addons/mod_quiz/directives/question.js b/www/addons/mod_quiz/directives/question.js index 282e687c4d3..8b9407ecd5c 100644 --- a/www/addons/mod_quiz/directives/question.js +++ b/www/addons/mod_quiz/directives/question.js @@ -42,6 +42,7 @@ angular.module('mm.addons.mod_quiz') templateUrl: 'addons/mod_quiz/templates/questionnotsupported.html', scope: { question: '=', + answers: '=', abortQuiz: '&' }, link: function(scope, element) { diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index da6688ccaaf..188ca2802cd 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -123,6 +123,33 @@ angular.module('mm.addons.mod_quiz') return self.getContentsOfElement(angular.element(html), '.grade'); }; + /** + * Get the sequence check from a question HTML. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#getQuestionSequenceCheckFromHtml + * @param {String} html Question's HTML. + * @return {Object} Object with the sequencecheck name and value. + */ + self.getQuestionSequenceCheckFromHtml = function(html) { + var el, + input; + + if (html) { + el = angular.element(html)[0]; + + // Search the input holding the sequencecheck. + input = el.querySelector('input[name*=sequencecheck]'); + if (input && typeof input.name != 'undefined' && typeof input.value != 'undefined') { + return { + name: input.name, + value: input.value + }; + } + } + }; + /** * Init a password modal, adding it to the scope. * diff --git a/www/addons/mod_quiz/templates/player.html b/www/addons/mod_quiz/templates/player.html index dedc2b09ca8..c94a3d5bd89 100644 --- a/www/addons/mod_quiz/templates/player.html +++ b/www/addons/mod_quiz/templates/player.html @@ -19,7 +19,7 @@

{{ 'mma.mod_quiz.information' | translate }}

{{question.readableMark}}

{{ 'mma.mod_quiz.flagged' | translate }} -
+
From 87baf59635e89a6ff1a9a05cf0a4e47d2b3c9428 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 15 Feb 2016 13:36:53 +0100 Subject: [PATCH 014/101] MOBILE-1420 quiz: Create question helper for directives --- .../mod_quiz/services/questionhelper.js | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 www/addons/mod_quiz/services/questionhelper.js diff --git a/www/addons/mod_quiz/services/questionhelper.js b/www/addons/mod_quiz/services/questionhelper.js new file mode 100644 index 00000000000..86957ec39f6 --- /dev/null +++ b/www/addons/mod_quiz/services/questionhelper.js @@ -0,0 +1,172 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Helper to gather some common functions for question directives. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuestionHelper + */ +.factory('$mmaModQuestionHelper', function($mmaModQuizHelper, $mmUtil) { + + var self = {}; + + /** + * Convenience function to initialize a question directive. + * Performs some common checks and extracts the question's text. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuestionHelper#directiveInit + * @param {Object} scope Directive's scope. + * @param {Object} log $log instance to log messages. + * @return {Object} Angular DOM element of the question's HTML. Undefined if an error happens. + */ + self.directiveInit = function(scope, log) { + var question = scope.question, + questionEl; + + if (!question) { + log.warn('Aborting quiz because of no question received.'); + return self.showDirectiveError(scope); + } + + questionEl = angular.element(question.html); + + // Extract question text. + question.text = $mmaModQuizHelper.getContentsOfElement(questionEl, '.qtext'); + if (!question.text) { + log.warn('Aborting quiz because of an error parsing question.', question.name); + return self.showDirectiveError(scope); + } + + return questionEl; + }; + + /** + * Generic link function for question directives with an input of type "text". + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuestionHelper#inputTextDirective + * @param {Object} scope Directive's scope. + * @param {Object} log $log instance to log messages. + * @return {Void} + */ + self.inputTextDirective = function(scope, log) { + var questionEl = self.directiveInit(scope, log); + if (questionEl) { + // Get the input element. + input = questionEl[0].querySelector('input[type="text"][name*=answer]'); + if (!input) { + log.warn('Aborting quiz because couldn\'t find input.', question.name); + return self.showDirectiveError(scope); + } + + scope.input = { + id: input.id, + name: input.name + }; + } + }; + + /** + * Generic link function for question directives with a multi choice input. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuestionHelper#multiChoiceDirective + * @param {Object} scope Directive's scope. + * @param {Object} log $log instance to log messages. + * @return {Void} + */ + self.multiChoiceDirective = function(scope, log) { + var questionEl = self.directiveInit(scope, log), + question = scope.question; + + if (questionEl) { + questionEl = questionEl[0] || questionEl; // Convert from jqLite to plain JS if needed. + + // Get the prompt. + question.prompt = $mmaModQuizHelper.getContentsOfElement(questionEl, '.prompt'); + + // Search radio buttons first (single choice). + var options = questionEl.querySelectorAll('input[type="radio"]'); + if (!options || !options.length) { + // Radio buttons not found, it should be a multi answer. Search for checkbox. + question.multi = true; + options = questionEl.querySelectorAll('input[type="checkbox"]'); + + if (!options || !options.length) { + // No checkbox found either. Abort the quiz. + log.warn('Aborting quiz because of no radio and checkbox found.', question.name); + return self.showDirectiveError(scope); + } + } + + question.options = []; + + angular.forEach(options, function(element) { + + var option = { + id: element.id, + name: element.name, + value: element.value, + }, + label; + + // Get the label with the question text. + label = questionEl.querySelector('label[for="' + option.id + '"]'); + if (label) { + option.text = label.innerHTML; + + // Check that we were able to successfully extract options required data. + if (typeof option.name != 'undefined' && typeof option.value != 'undefined' && + typeof option.text != 'undefined') { + + // If the option is checked we store the data in the model. + if (element.checked) { + scope.answers[option.name] = question.multi ? true : option.value; + } + question.options.push(option); + return; + } + } + + // Something went wrong when extracting the questions data. Abort. + log.warn('Aborting quiz because of an error parsing options.', question.name, option.name); + return self.showDirectiveError(scope); + }); + } + }; + + /** + * Convenience function to show a parsing error and abort a quiz. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuestionHelper#showDirectiveError + * @param {Object} scope Directive scope. + * @return {Void} + */ + self.showDirectiveError = function(scope) { + $mmUtil.showErrorModal('Error parsing question. Please make sure you don\'t have a custom theme that can affect this.'); + scope.abortQuiz(); + }; + + return self; +}); From dcf7e263e40f64c35ef4f933fef094062395b1b0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 15 Feb 2016 13:47:36 +0100 Subject: [PATCH 015/101] MOBILE-1420 quiz: Implement multichoice, truefalse and calculatedmulti --- www/addons/mod_quiz/questions/base/multi.html | 18 ++++++ .../questions/calculatedmulti/directive.js | 35 +++++++++++ .../questions/calculatedmulti/handlers.js | 58 +++++++++++++++++++ .../questions/multichoice/directive.js | 35 +++++++++++ .../questions/multichoice/handlers.js | 58 +++++++++++++++++++ .../mod_quiz/questions/truefalse/directive.js | 35 +++++++++++ .../mod_quiz/questions/truefalse/handlers.js | 58 +++++++++++++++++++ 7 files changed, 297 insertions(+) create mode 100644 www/addons/mod_quiz/questions/base/multi.html create mode 100644 www/addons/mod_quiz/questions/calculatedmulti/directive.js create mode 100644 www/addons/mod_quiz/questions/calculatedmulti/handlers.js create mode 100644 www/addons/mod_quiz/questions/multichoice/directive.js create mode 100644 www/addons/mod_quiz/questions/multichoice/handlers.js create mode 100644 www/addons/mod_quiz/questions/truefalse/directive.js create mode 100644 www/addons/mod_quiz/questions/truefalse/handlers.js diff --git a/www/addons/mod_quiz/questions/base/multi.html b/www/addons/mod_quiz/questions/base/multi.html new file mode 100644 index 00000000000..710db7946e9 --- /dev/null +++ b/www/addons/mod_quiz/questions/base/multi.html @@ -0,0 +1,18 @@ +
+
+

{{ question.text }}

+
+

{{ question.prompt }}

+
+
  • + +

    {{option.text}}

    +
  • + +
    diff --git a/www/addons/mod_quiz/questions/calculatedmulti/directive.js b/www/addons/mod_quiz/questions/calculatedmulti/directive.js new file mode 100644 index 00000000000..722f5883492 --- /dev/null +++ b/www/addons/mod_quiz/questions/calculatedmulti/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a calculated multi question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionCalculatedMulti + */ +.directive('mmaModQuizQuestionCalculatedMulti', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionCalculatedMulti'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/multi.html', + link: function(scope) { + $mmaModQuestionHelper.multiChoiceDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/calculatedmulti/handlers.js b/www/addons/mod_quiz/questions/calculatedmulti/handlers.js new file mode 100644 index 00000000000..17a74d8cb39 --- /dev/null +++ b/www/addons/mod_quiz/questions/calculatedmulti/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Calculated multi question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionCalculatedMultiHandler + */ +.factory('$mmaModQuizQuestionCalculatedMultiHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-calculated-multi'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizCalculatedMulti', 'qtype_calculatedmulti', + '$mmaModQuizQuestionCalculatedMultiHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/multichoice/directive.js b/www/addons/mod_quiz/questions/multichoice/directive.js new file mode 100644 index 00000000000..b78069644b7 --- /dev/null +++ b/www/addons/mod_quiz/questions/multichoice/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a multichoice question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionMultichoice + */ +.directive('mmaModQuizQuestionMultichoice', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionMultichoice'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/multi.html', + link: function(scope) { + $mmaModQuestionHelper.multiChoiceDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/multichoice/handlers.js b/www/addons/mod_quiz/questions/multichoice/handlers.js new file mode 100644 index 00000000000..04400e90b5f --- /dev/null +++ b/www/addons/mod_quiz/questions/multichoice/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Multi choice question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionMultichoiceHandlers + */ +.factory('$mmaModQuizQuestionMultichoiceHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-multichoice'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizMultichoice', 'qtype_multichoice', + '$mmaModQuizQuestionMultichoiceHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/truefalse/directive.js b/www/addons/mod_quiz/questions/truefalse/directive.js new file mode 100644 index 00000000000..09ce782743c --- /dev/null +++ b/www/addons/mod_quiz/questions/truefalse/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a true false question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionTruefalse + */ +.directive('mmaModQuizQuestionTruefalse', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionTruefalse'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/multi.html', + link: function(scope) { + $mmaModQuestionHelper.multiChoiceDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/truefalse/handlers.js b/www/addons/mod_quiz/questions/truefalse/handlers.js new file mode 100644 index 00000000000..e7bd9a9a6d1 --- /dev/null +++ b/www/addons/mod_quiz/questions/truefalse/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * True false question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionTruefalseHandler + */ +.factory('$mmaModQuizQuestionTruefalseHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-truefalse'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizTruefalse', 'qtype_truefalse', + '$mmaModQuizQuestionTruefalseHandler'); + } +}); From 2e5071749807458c195a83888442e400024a0987 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 15 Feb 2016 13:59:51 +0100 Subject: [PATCH 016/101] MOBILE-1420 quiz: Implement input text questions --- www/addons/mod_quiz/lang/en.json | 1 + www/addons/mod_quiz/questions/base/text.html | 8 +++ .../questions/calculated/directive.js | 35 +++++++++++ .../mod_quiz/questions/calculated/handlers.js | 58 +++++++++++++++++++ .../questions/calculatedsimple/directive.js | 35 +++++++++++ .../questions/calculatedsimple/handlers.js | 58 +++++++++++++++++++ .../mod_quiz/questions/numerical/directive.js | 35 +++++++++++ .../mod_quiz/questions/numerical/handlers.js | 58 +++++++++++++++++++ .../questions/shortanswer/directive.js | 35 +++++++++++ .../questions/shortanswer/handlers.js | 58 +++++++++++++++++++ .../mod_quiz/services/questionhelper.js | 9 ++- 11 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 www/addons/mod_quiz/questions/base/text.html create mode 100644 www/addons/mod_quiz/questions/calculated/directive.js create mode 100644 www/addons/mod_quiz/questions/calculated/handlers.js create mode 100644 www/addons/mod_quiz/questions/calculatedsimple/directive.js create mode 100644 www/addons/mod_quiz/questions/calculatedsimple/handlers.js create mode 100644 www/addons/mod_quiz/questions/numerical/directive.js create mode 100644 www/addons/mod_quiz/questions/numerical/handlers.js create mode 100644 www/addons/mod_quiz/questions/shortanswer/directive.js create mode 100644 www/addons/mod_quiz/questions/shortanswer/handlers.js diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index 9898988db25..90d9c053468 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -1,4 +1,5 @@ { + "answer": "Answer", "attemptfirst": "First attempt", "attemptlast": "Last attempt", "attemptnumber": "Attempt", diff --git a/www/addons/mod_quiz/questions/base/text.html b/www/addons/mod_quiz/questions/base/text.html new file mode 100644 index 00000000000..6eec42470e6 --- /dev/null +++ b/www/addons/mod_quiz/questions/base/text.html @@ -0,0 +1,8 @@ +
    +
    +

    {{ question.text }}

    +
    + +
    diff --git a/www/addons/mod_quiz/questions/calculated/directive.js b/www/addons/mod_quiz/questions/calculated/directive.js new file mode 100644 index 00000000000..982bd2b3514 --- /dev/null +++ b/www/addons/mod_quiz/questions/calculated/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a calculated question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionCalculated + */ +.directive('mmaModQuizQuestionCalculated', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionCalculated'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/text.html', + link: function(scope) { + $mmaModQuestionHelper.inputTextDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/calculated/handlers.js b/www/addons/mod_quiz/questions/calculated/handlers.js new file mode 100644 index 00000000000..410dcca3650 --- /dev/null +++ b/www/addons/mod_quiz/questions/calculated/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Calculated question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionCalculatedHandler + */ +.factory('$mmaModQuizQuestionCalculatedHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-calculated'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizCalculated', 'qtype_calculated', + '$mmaModQuizQuestionCalculatedHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/calculatedsimple/directive.js b/www/addons/mod_quiz/questions/calculatedsimple/directive.js new file mode 100644 index 00000000000..d75a4761e61 --- /dev/null +++ b/www/addons/mod_quiz/questions/calculatedsimple/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a calculated simple question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionCalculatedSimple + */ +.directive('mmaModQuizQuestionCalculatedSimple', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionCalculatedSimple'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/text.html', + link: function(scope) { + $mmaModQuestionHelper.inputTextDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/calculatedsimple/handlers.js b/www/addons/mod_quiz/questions/calculatedsimple/handlers.js new file mode 100644 index 00000000000..af2751087db --- /dev/null +++ b/www/addons/mod_quiz/questions/calculatedsimple/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Calculated simple question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionCalculatedSimpleHandler + */ +.factory('$mmaModQuizQuestionCalculatedSimpleHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-calculated-simple'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizCalculatedSimple', 'qtype_calculatedsimple', + '$mmaModQuizQuestionCalculatedSimpleHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/numerical/directive.js b/www/addons/mod_quiz/questions/numerical/directive.js new file mode 100644 index 00000000000..99152c60a5f --- /dev/null +++ b/www/addons/mod_quiz/questions/numerical/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a numerical question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionNumerical + */ +.directive('mmaModQuizQuestionNumerical', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionNumerical'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/text.html', + link: function(scope) { + $mmaModQuestionHelper.inputTextDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/numerical/handlers.js b/www/addons/mod_quiz/questions/numerical/handlers.js new file mode 100644 index 00000000000..8018ddbf429 --- /dev/null +++ b/www/addons/mod_quiz/questions/numerical/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Numerical question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionNumericalHandler + */ +.factory('$mmaModQuizQuestionNumericalHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-numerical'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizNumerical', 'qtype_numerical', + '$mmaModQuizQuestionNumericalHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/shortanswer/directive.js b/www/addons/mod_quiz/questions/shortanswer/directive.js new file mode 100644 index 00000000000..91c5fe82740 --- /dev/null +++ b/www/addons/mod_quiz/questions/shortanswer/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a short answer question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionShortAnswer + */ +.directive('mmaModQuizQuestionShortAnswer', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionShortAnswer'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/text.html', + link: function(scope) { + $mmaModQuestionHelper.inputTextDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/shortanswer/handlers.js b/www/addons/mod_quiz/questions/shortanswer/handlers.js new file mode 100644 index 00000000000..4ab72c53abb --- /dev/null +++ b/www/addons/mod_quiz/questions/shortanswer/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Short answer question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionShortAnswerHandler + */ +.factory('$mmaModQuizQuestionShortAnswerHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-short-answer'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizShortAnswer', 'qtype_shortanswer', + '$mmaModQuizQuestionShortAnswerHandler'); + } +}); diff --git a/www/addons/mod_quiz/services/questionhelper.js b/www/addons/mod_quiz/services/questionhelper.js index 86957ec39f6..f9a71ed6177 100644 --- a/www/addons/mod_quiz/services/questionhelper.js +++ b/www/addons/mod_quiz/services/questionhelper.js @@ -70,13 +70,20 @@ angular.module('mm.addons.mod_quiz') self.inputTextDirective = function(scope, log) { var questionEl = self.directiveInit(scope, log); if (questionEl) { + questionEl = questionEl[0] || questionEl; // Convert from jqLite to plain JS if needed. + // Get the input element. - input = questionEl[0].querySelector('input[type="text"][name*=answer]'); + input = questionEl.querySelector('input[type="text"][name*=answer]'); if (!input) { log.warn('Aborting quiz because couldn\'t find input.', question.name); return self.showDirectiveError(scope); } + // Add current value to model if set. + if (input.value) { + scope.answers[input.name] = input.value; + } + scope.input = { id: input.id, name: input.name From 4bb9e0e5bfbbbf5ab27f7ef3f580c77364d8be5b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 15 Feb 2016 14:26:16 +0100 Subject: [PATCH 017/101] MOBILE-1420 quiz: Implement description and essay --- .../questions/description/directive.js | 43 +++++++++++++ .../questions/description/handlers.js | 58 +++++++++++++++++ .../questions/description/template.html | 5 ++ .../mod_quiz/questions/essay/directive.js | 63 +++++++++++++++++++ .../mod_quiz/questions/essay/handlers.js | 58 +++++++++++++++++ .../mod_quiz/questions/essay/template.html | 8 +++ www/addons/mod_quiz/scss/styles.scss | 5 ++ 7 files changed, 240 insertions(+) create mode 100644 www/addons/mod_quiz/questions/description/directive.js create mode 100644 www/addons/mod_quiz/questions/description/handlers.js create mode 100644 www/addons/mod_quiz/questions/description/template.html create mode 100644 www/addons/mod_quiz/questions/essay/directive.js create mode 100644 www/addons/mod_quiz/questions/essay/handlers.js create mode 100644 www/addons/mod_quiz/questions/essay/template.html diff --git a/www/addons/mod_quiz/questions/description/directive.js b/www/addons/mod_quiz/questions/description/directive.js new file mode 100644 index 00000000000..1f877c453a9 --- /dev/null +++ b/www/addons/mod_quiz/questions/description/directive.js @@ -0,0 +1,43 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a description question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionDescription + */ +.directive('mmaModQuizQuestionDescription', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionDescription'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/description/template.html', + link: function(scope) { + var questionEl = $mmaModQuestionHelper.directiveInit(scope, $log), + input; + if (questionEl) { + // Get the "seen" hidden input. + input = questionEl[0].querySelector('input[type="hidden"][name*=seen]'); + if (input) { + scope.answers[input.name] = input.value; + } + } + } + }; +}); diff --git a/www/addons/mod_quiz/questions/description/handlers.js b/www/addons/mod_quiz/questions/description/handlers.js new file mode 100644 index 00000000000..407455fad7f --- /dev/null +++ b/www/addons/mod_quiz/questions/description/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Description question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionDescriptionHandler + */ +.factory('$mmaModQuizQuestionDescriptionHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-description'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizDescription', 'qtype_description', + '$mmaModQuizQuestionDescriptionHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/description/template.html b/www/addons/mod_quiz/questions/description/template.html new file mode 100644 index 00000000000..05f8e994cad --- /dev/null +++ b/www/addons/mod_quiz/questions/description/template.html @@ -0,0 +1,5 @@ +
    +
    +

    {{ question.text }}

    +
    +
    diff --git a/www/addons/mod_quiz/questions/essay/directive.js b/www/addons/mod_quiz/questions/essay/directive.js new file mode 100644 index 00000000000..1ce7ed4ff14 --- /dev/null +++ b/www/addons/mod_quiz/questions/essay/directive.js @@ -0,0 +1,63 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render an essay question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionEssay + */ +.directive('mmaModQuizQuestionEssay', function($log, $mmaModQuestionHelper, $mmText) { + $log = $log.getInstance('mmaModQuizQuestionEssay'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/essay/template.html', + link: function(scope) { + var questionEl = $mmaModQuestionHelper.directiveInit(scope, $log), + question = scope.question; + + if (questionEl) { + questionEl = questionEl[0] || questionEl; // Convert from jqLite to plain JS if needed. + + var textarea = questionEl.querySelector('textarea[id*=answer_id]'), + input = questionEl.querySelector('input[type="hidden"][name*=answerformat]'), + content = textarea.innerHTML; + + if (!textarea) { + $log.warn('Aborting quiz because couldn\'t find textarea.', question.name); + return $mmaModQuestionHelper.showDirectiveError(scope); + } + + // Add current value to model if set. + if (content) { + scope.answers[textarea.name] = $mmText.decodeHTML(content); + } + + scope.textarea = { + id: textarea.id, + name: textarea.name + }; + + if (input) { + scope.answers[input.name] = input.value; + } + } + } + }; +}); diff --git a/www/addons/mod_quiz/questions/essay/handlers.js b/www/addons/mod_quiz/questions/essay/handlers.js new file mode 100644 index 00000000000..47a4af2729a --- /dev/null +++ b/www/addons/mod_quiz/questions/essay/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Essay question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionEssayHandler + */ +.factory('$mmaModQuizQuestionEssayHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-essay'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizEssay', 'qtype_essay', + '$mmaModQuizQuestionEssayHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/essay/template.html b/www/addons/mod_quiz/questions/essay/template.html new file mode 100644 index 00000000000..ed8b66d3935 --- /dev/null +++ b/www/addons/mod_quiz/questions/essay/template.html @@ -0,0 +1,8 @@ +
    +
    +

    {{ question.text }}

    +
    + +
    diff --git a/www/addons/mod_quiz/scss/styles.scss b/www/addons/mod_quiz/scss/styles.scss index a4e811df910..8bee3595ef4 100644 --- a/www/addons/mod_quiz/scss/styles.scss +++ b/www/addons/mod_quiz/scss/styles.scss @@ -20,3 +20,8 @@ p.mma-mod-quiz-warning { background-color: $mma-mod-quiz-highlight-color; } +.mma-quiz-textarea { + width: 100%; + height: 250px; + resize: none; +} From f49d952fd16ec37d48bfea3689aff1e7faf82793 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Feb 2016 09:47:26 +0100 Subject: [PATCH 018/101] MOBILE-1420 quiz: Fix scripts extraction --- www/addons/mod_quiz/directives/question.js | 2 +- www/addons/mod_quiz/services/helper.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/www/addons/mod_quiz/directives/question.js b/www/addons/mod_quiz/directives/question.js index 8b9407ecd5c..8991a6b9fd9 100644 --- a/www/addons/mod_quiz/directives/question.js +++ b/www/addons/mod_quiz/directives/question.js @@ -51,8 +51,8 @@ angular.module('mm.addons.mod_quiz') var directive = $mmaModQuizQuestionsDelegate.getDirectiveForQuestion(scope.question); if (directive) { // Treat the question before starting the directive. - $mmaModQuizHelper.extractQuestionInfoBox(scope.question); $mmaModQuizHelper.extractQuestionScripts(scope.question); + $mmaModQuizHelper.extractQuestionInfoBox(scope.question); // Add the directive to the element. element.attr(directive, ''); diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index 188ca2802cd..d4c12935b0f 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -27,6 +27,7 @@ angular.module('mm.addons.mod_quiz') /** * Removes the info box (flag, question number, etc.) from a question's HTML and adds it in a new infoBox property. + * Please take into account that all scripts will also be removed due to angular.element. * * @module mm.addons.mod_quiz * @ngdoc method From 125a99c6ef0eed3ccbf3e68425eedfc9dcf4e0fc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Feb 2016 09:49:01 +0100 Subject: [PATCH 019/101] MOBILE-1420 quiz: Implement matching question --- www/addons/mod_quiz/questions/base/match.html | 14 +++ .../mod_quiz/questions/match/directive.js | 35 +++++++ .../mod_quiz/questions/match/handlers.js | 58 ++++++++++++ .../questions/randomsamatch/directive.js | 35 +++++++ .../questions/randomsamatch/handlers.js | 58 ++++++++++++ .../questions/randomsamatch/scss/styles.scss | 19 ++++ .../questions/randomsamatch/template.html | 5 + www/addons/mod_quiz/scss/styles.scss | 4 + .../mod_quiz/services/questionhelper.js | 93 ++++++++++++++++++- www/addons/mod_quiz/services/quiz.js | 2 +- www/core/scss/styles.scss | 8 ++ 11 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 www/addons/mod_quiz/questions/base/match.html create mode 100644 www/addons/mod_quiz/questions/match/directive.js create mode 100644 www/addons/mod_quiz/questions/match/handlers.js create mode 100644 www/addons/mod_quiz/questions/randomsamatch/directive.js create mode 100644 www/addons/mod_quiz/questions/randomsamatch/handlers.js create mode 100644 www/addons/mod_quiz/questions/randomsamatch/scss/styles.scss create mode 100644 www/addons/mod_quiz/questions/randomsamatch/template.html diff --git a/www/addons/mod_quiz/questions/base/match.html b/www/addons/mod_quiz/questions/base/match.html new file mode 100644 index 00000000000..4b345c730a9 --- /dev/null +++ b/www/addons/mod_quiz/questions/base/match.html @@ -0,0 +1,14 @@ +
    +
    +

    {{ question.text }}

    +
    +
    +
    +

    {{ row.text }}

    +
    +
    + + +
    +
    +
    diff --git a/www/addons/mod_quiz/questions/match/directive.js b/www/addons/mod_quiz/questions/match/directive.js new file mode 100644 index 00000000000..8951de3d165 --- /dev/null +++ b/www/addons/mod_quiz/questions/match/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a match question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionMatch + */ +.directive('mmaModQuizQuestionMatch', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionMatch'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/match.html', + link: function(scope) { + $mmaModQuestionHelper.matchingDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/match/handlers.js b/www/addons/mod_quiz/questions/match/handlers.js new file mode 100644 index 00000000000..b0e10098715 --- /dev/null +++ b/www/addons/mod_quiz/questions/match/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Match question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionMatchHandler + */ +.factory('$mmaModQuizQuestionMatchHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-match'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizMatchin', 'qtype_match', + '$mmaModQuizQuestionMatchHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/randomsamatch/directive.js b/www/addons/mod_quiz/questions/randomsamatch/directive.js new file mode 100644 index 00000000000..b8f2bfca18f --- /dev/null +++ b/www/addons/mod_quiz/questions/randomsamatch/directive.js @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a random short-answer matching question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionRandomSaMatch + */ +.directive('mmaModQuizQuestionRandomSaMatch', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionRandomSaMatch'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/base/match.html', + link: function(scope) { + $mmaModQuestionHelper.matchingDirective(scope, $log); + } + }; +}); diff --git a/www/addons/mod_quiz/questions/randomsamatch/handlers.js b/www/addons/mod_quiz/questions/randomsamatch/handlers.js new file mode 100644 index 00000000000..886007588e4 --- /dev/null +++ b/www/addons/mod_quiz/questions/randomsamatch/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Random short-answer matching question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionRandomSaMatchHandler + */ +.factory('$mmaModQuizQuestionRandomSaMatchHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-random-sa-match'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizRandomSaMatch', 'qtype_randomsamatch', + '$mmaModQuizQuestionRandomSaMatchHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/randomsamatch/scss/styles.scss b/www/addons/mod_quiz/questions/randomsamatch/scss/styles.scss new file mode 100644 index 00000000000..1920bb8877d --- /dev/null +++ b/www/addons/mod_quiz/questions/randomsamatch/scss/styles.scss @@ -0,0 +1,19 @@ +// Style gapselect content a bit. All these styles are copied from Moodle. +.mm-quiz-gapselect-container { + p { + margin: 0 0 .5em; + } + + select { + height: 30px; + line-height: 30px; + display: inline-block; + border: 1px solid #ccc; + padding: 4px 6px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + margin-bottom: 10px; + } +} + diff --git a/www/addons/mod_quiz/questions/randomsamatch/template.html b/www/addons/mod_quiz/questions/randomsamatch/template.html new file mode 100644 index 00000000000..71f1830e77c --- /dev/null +++ b/www/addons/mod_quiz/questions/randomsamatch/template.html @@ -0,0 +1,5 @@ +
    +
    +

    {{ question.text }}

    +
    +
    diff --git a/www/addons/mod_quiz/scss/styles.scss b/www/addons/mod_quiz/scss/styles.scss index 8bee3595ef4..18b7b64d51d 100644 --- a/www/addons/mod_quiz/scss/styles.scss +++ b/www/addons/mod_quiz/scss/styles.scss @@ -25,3 +25,7 @@ p.mma-mod-quiz-warning { height: 250px; resize: none; } + +.item.row.mma-quiz-item-padding { + padding: $item-padding; // Prevent .row from overridding .item padding. +} diff --git a/www/addons/mod_quiz/services/questionhelper.js b/www/addons/mod_quiz/services/questionhelper.js index f9a71ed6177..db494a36c52 100644 --- a/www/addons/mod_quiz/services/questionhelper.js +++ b/www/addons/mod_quiz/services/questionhelper.js @@ -23,7 +23,8 @@ angular.module('mm.addons.mod_quiz') */ .factory('$mmaModQuestionHelper', function($mmaModQuizHelper, $mmUtil) { - var self = {}; + var self = {}, + lastErrorShown = 0; /** * Convenience function to initialize a question directive. @@ -91,6 +92,89 @@ angular.module('mm.addons.mod_quiz') } }; + /** + * Generic link function for question directives with a "matching" (selects). + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuestionHelper#matchingDirective + * @param {Object} scope Directive's scope. + * @param {Object} log $log instance to log messages. + * @return {Void} + */ + self.matchingDirective = function(scope, log) { + var questionEl = self.directiveInit(scope, log), + question = scope.question, + rows; + + if (questionEl) { + questionEl = questionEl[0] || questionEl; // Convert from jqLite to plain JS if needed. + + // Find rows. + rows = questionEl.querySelectorAll('tr'); + if (!rows || !rows.length) { + log.warn('Aborting quiz because couldn\'t find any row.', question.name); + return self.showDirectiveError(scope); + } + + question.rows = []; + + angular.forEach(rows, function(row) { + var rowModel = {}, + select, + options, + accessibilityLabel, + columns = row.querySelectorAll('td'); + + if (!columns || columns.length < 2) { + log.warn('Aborting quiz because couldn\'t find the right columns.', question.name); + return self.showDirectiveError(scope); + } + + // Get the row's text. It should be in the first column. + rowModel.text = columns[0].innerHTML; + + // Get the select and the options. + select = columns[1].querySelector('select'); + options = columns[1].querySelectorAll('option'); + + if (!select || !options || !options.length) { + log.warn('Aborting quiz because couldn\'t find select or options.', question.name); + return self.showDirectiveError(scope); + } + + rowModel.id = select.id; + rowModel.name = select.name; + rowModel.options = []; + + // Treat each option. + angular.forEach(options, function(option) { + if (typeof option.value == 'undefined') { + log.warn('Aborting quiz because couldn\'t find option value.', question.name); + return self.showDirectiveError(scope); + } + + rowModel.options.push({ + value: option.value, + label: option.innerHTML + }); + + if (option.selected) { + scope.answers[select.name] = option.value; + } + }); + + // Get the accessibility label. + accessibilityLabel = columns[1].querySelector('label.accesshide'); + rowModel.accessibilityLabel = accessibilityLabel.innerHTML; + + question.rows.push(rowModel); + }); + + question.loaded = true; + } + }; + /** * Generic link function for question directives with a multi choice input. * @@ -171,7 +255,12 @@ angular.module('mm.addons.mod_quiz') * @return {Void} */ self.showDirectiveError = function(scope) { - $mmUtil.showErrorModal('Error parsing question. Please make sure you don\'t have a custom theme that can affect this.'); + // Prevent consecutive errors. + var now = new Date().getTime(); + if (now - lastErrorShown > 500) { + lastErrorShown = now; + $mmUtil.showErrorModal('Error parsing question. Please make sure you don\'t have a custom theme that can affect this.'); + } scope.abortQuiz(); }; diff --git a/www/addons/mod_quiz/services/quiz.js b/www/addons/mod_quiz/services/quiz.js index 419bbf1d6f9..4b2e1626ee3 100644 --- a/www/addons/mod_quiz/services/quiz.js +++ b/www/addons/mod_quiz/services/quiz.js @@ -694,7 +694,7 @@ angular.module('mm.addons.mod_quiz') self.getUnsupportedQuestions = function(questionTypes) { var notSupported = []; angular.forEach(questionTypes, function(type) { - if (type != 'random' && type != 'randomsamatch' && !$mmaModQuizQuestionsDelegate.isQuestionSupported('qtype_'+type)) { + if (type != 'random' && !$mmaModQuizQuestionsDelegate.isQuestionSupported('qtype_'+type)) { notSupported.push(type); } }); diff --git a/www/core/scss/styles.scss b/www/core/scss/styles.scss index 7dc80959fe2..fa4b05d00b4 100644 --- a/www/core/scss/styles.scss +++ b/www/core/scss/styles.scss @@ -287,6 +287,14 @@ mm-navigation-bar { } } +// Accessibility: text 'seen' by screen readers but not visual users. +.accesshide { + position: absolute; + left: -10000px; + font-weight: normal; + font-size: 1em; +} + // Extra attributes to ionic classes to force frames via directive 100% height. .scroll { From ff992abb037768238b3392bfd9b4f7cfda54aff3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Feb 2016 12:40:16 +0100 Subject: [PATCH 020/101] MOBILE-1420 quiz: Implement multianswer (cloze) question --- .../questions/multianswer/directive.js | 76 +++++++++++++++++++ .../questions/multianswer/handlers.js | 58 ++++++++++++++ .../questions/multianswer/scss/styles.scss | 44 +++++++++++ .../questions/multianswer/template.html | 5 ++ www/addons/mod_quiz/services/helper.js | 32 ++++++-- 5 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 www/addons/mod_quiz/questions/multianswer/directive.js create mode 100644 www/addons/mod_quiz/questions/multianswer/handlers.js create mode 100644 www/addons/mod_quiz/questions/multianswer/scss/styles.scss create mode 100644 www/addons/mod_quiz/questions/multianswer/template.html diff --git a/www/addons/mod_quiz/questions/multianswer/directive.js b/www/addons/mod_quiz/questions/multianswer/directive.js new file mode 100644 index 00000000000..abf3f831278 --- /dev/null +++ b/www/addons/mod_quiz/questions/multianswer/directive.js @@ -0,0 +1,76 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a multianswer (cloze) question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionMultianswer + */ +.directive('mmaModQuizQuestionMultianswer', function($log, $mmaModQuestionHelper, $mmaModQuizHelper) { + $log = $log.getInstance('mmaModQuizQuestionMultianswer'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/multianswer/template.html', + link: function(scope) { + var question = scope.question, + questionEl, + content, + inputs; + + if (!question) { + $log.warn('Aborting quiz because of no question received.'); + return $mmaModQuestionHelper.showDirectiveError(scope); + } + + questionEl = angular.element(question.html); + + // Get question content. + content = questionEl[0].querySelector('.formulation'); + if (!content) { + log.warn('Aborting quiz because of an error parsing question.', question.name); + return $mmaModQuestionHelper.showDirectiveError(scope); + } + + // Remove sequencecheck. + $mmaModQuizHelper.removeElement(content, 'input[name*=sequencecheck]'); + + // Find inputs of type text, radio and select and add ng-model to them. + inputs = content.querySelectorAll('input[type="text"],input[type="radio"],select'); + angular.forEach(inputs, function(input) { + input.setAttribute('ng-model', 'answers["' + input.name + '"]'); + + if ((input.type == 'text' && input.value) || (input.type == 'radio' && input.checked)) { + // Store the value in the model. + scope.answers[input.name] = input.value; + } else if (input.tagName.toLowerCase() == 'select') { + // Search if there's any option selected. + var selected = input.querySelector('option[selected]'); + if (selected && selected.value !== '' && typeof selected.value != 'undefined') { + // Store the value in the model. + scope.answers[input.name] = selected.value; + } + } + }); + + // Set the question text. + question.text = content.innerHTML; + } + }; +}); diff --git a/www/addons/mod_quiz/questions/multianswer/handlers.js b/www/addons/mod_quiz/questions/multianswer/handlers.js new file mode 100644 index 00000000000..4acdebe3455 --- /dev/null +++ b/www/addons/mod_quiz/questions/multianswer/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Multi answer (cloze) question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionMultianswerHandler + */ +.factory('$mmaModQuizQuestionMultianswerHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-multianswer'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizMultianswer', 'qtype_multianswer', + '$mmaModQuizQuestionMultianswerHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/multianswer/scss/styles.scss b/www/addons/mod_quiz/questions/multianswer/scss/styles.scss new file mode 100644 index 00000000000..2684db0fd74 --- /dev/null +++ b/www/addons/mod_quiz/questions/multianswer/scss/styles.scss @@ -0,0 +1,44 @@ +// Style multianswer content a bit. All these styles are copied from Moodle. +.mm-quiz-multianswer-container { + p { + margin: 0 0 .5em; + } + + .answer div.r0, .answer div.r1 { + padding: 0.3em; + } + + table { + width: 100%; + display: table; + } + + tr { + display: table-row; + } + + td { + display: table-cell; + } + + input, select { + display: inline-block; + border: 1px solid #ccc; + padding: 4px 6px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + margin-bottom: 10px; + } + + select { + height: 30px; + line-height: 30px; + } + + input[type="radio"], input[type="checkbox"] { + margin-top: -4px; + margin-right: 7px; + } +} + diff --git a/www/addons/mod_quiz/questions/multianswer/template.html b/www/addons/mod_quiz/questions/multianswer/template.html new file mode 100644 index 00000000000..2a0921159a8 --- /dev/null +++ b/www/addons/mod_quiz/questions/multianswer/template.html @@ -0,0 +1,5 @@ +
    +
    +

    {{ question.text }}

    +
    +
    diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index d4c12935b0f..7bad36cbc21 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -95,16 +95,16 @@ angular.module('mm.addons.mod_quiz') * @module mm.addons.mod_quiz * @ngdoc method * @name $mmaModQuizHelper#getContentsOfElement - * @param {Object} element DOM element to search in. - * @param {String} className Class to search. - * @return {String} Div contents. + * @param {Object} element DOM element to search in. + * @param {String} selector Selector to search. + * @return {String} Selection contents. */ self.getContentsOfElement = function(element, selector) { if (element) { var el = element[0] || element, // Convert from jqLite to plain JS if needed. - div = el.querySelector(selector); - if (div) { - return div.innerHTML; + selected = el.querySelector(selector); + if (selected) { + return selected.innerHTML; } } return ''; @@ -177,6 +177,26 @@ angular.module('mm.addons.mod_quiz') }); }; + /** + * Search and remove a certain element from inside another element. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#removeElement + * @param {Object} element DOM element to search in. + * @param {String} selector Selector to search. + * @return {Void} + */ + self.removeElement = function(element, selector) { + if (element) { + var el = element[0] || element, // Convert from jqLite to plain JS if needed. + selected = el.querySelector(selector); + if (selected) { + selected.remove(); + } + } + }; + /** * Add some calculated data to the attempt. * From 8da0340419c85cc5425be76e8983280a3133212d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Feb 2016 13:33:41 +0100 Subject: [PATCH 021/101] MOBILE-1420 quiz: Handle validation errors --- www/addons/mod_quiz/controllers/player.js | 5 +++++ .../mod_quiz/questions/multianswer/directive.js | 1 + www/addons/mod_quiz/services/helper.js | 13 +++++++++++++ www/addons/mod_quiz/templates/player.html | 5 +++++ 4 files changed, 24 insertions(+) diff --git a/www/addons/mod_quiz/controllers/player.js b/www/addons/mod_quiz/controllers/player.js index 8daacfcf3f4..d55c2f7c15e 100644 --- a/www/addons/mod_quiz/controllers/player.js +++ b/www/addons/mod_quiz/controllers/player.js @@ -123,13 +123,18 @@ angular.module('mm.addons.mod_quiz') } return promise.then(function() { + // Get the attempt data. return $mmaModQuiz.getAttemptData(attempt.id, 0, preflightData, true).then(function(data) { $scope.closeModal && $scope.closeModal(); // Close modal if needed. $scope.attempt = attempt; $scope.questions = data.questions; angular.forEach($scope.questions, function(question) { + // Get the readable mark and validation error for each question. question.readableMark = $mmaModQuizHelper.getQuestionMarkFromHtml(question.html); + question.validationError = $mmaModQuizHelper.getValidationErrorFromHtml(question.html); + + // Get the sequence check (hidden input) and add it to the model. This is required. var seqCheck = $mmaModQuizHelper.getQuestionSequenceCheckFromHtml(question.html); if (seqCheck) { $scope.answers[seqCheck.name] = seqCheck.value; diff --git a/www/addons/mod_quiz/questions/multianswer/directive.js b/www/addons/mod_quiz/questions/multianswer/directive.js index abf3f831278..4f203701845 100644 --- a/www/addons/mod_quiz/questions/multianswer/directive.js +++ b/www/addons/mod_quiz/questions/multianswer/directive.js @@ -50,6 +50,7 @@ angular.module('mm.addons.mod_quiz') // Remove sequencecheck. $mmaModQuizHelper.removeElement(content, 'input[name*=sequencecheck]'); + $mmaModQuizHelper.removeElement(content, '.validationerror'); // Find inputs of type text, radio and select and add ng-model to them. inputs = content.querySelectorAll('input[type="text"],input[type="radio"],select'); diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index 7bad36cbc21..35584c145c9 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -151,6 +151,19 @@ angular.module('mm.addons.mod_quiz') } }; + /** + * Get the validation error message from a question HTML if it's there. + * + * @module mm.addons.mod_quiz + * @ngdoc method + * @name $mmaModQuizHelper#getValidationErrorFromHtml + * @param {String} html Question's HTML. + * @return {Object} Validation error message if present. + */ + self.getValidationErrorFromHtml = function(html) { + return self.getContentsOfElement(angular.element(html), '.validationerror'); + }; + /** * Init a password modal, adding it to the scope. * diff --git a/www/addons/mod_quiz/templates/player.html b/www/addons/mod_quiz/templates/player.html index c94a3d5bd89..de3d2b76220 100644 --- a/www/addons/mod_quiz/templates/player.html +++ b/www/addons/mod_quiz/templates/player.html @@ -5,6 +5,7 @@
    +
    @@ -20,9 +21,13 @@

    {{ 'mma.mod_quiz.information' | translate }}

    {{ 'mma.mod_quiz.flagged' | translate }}
    +
    +

    {{ question.validationError }}

    +
    +

    {{ 'mma.mod_quiz.errorparsequestions' | translate }}

    From a7c415394c5befbc45536aede8f815b742d25ad3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 16 Feb 2016 15:05:06 +0100 Subject: [PATCH 022/101] MOBILE-1420 quiz: Implement gapselect question --- .../mod_quiz/questions/gapselect/directive.js | 68 +++++++++++++++++++ .../mod_quiz/questions/gapselect/handlers.js | 58 ++++++++++++++++ .../questions/gapselect/scss/styles.scss | 19 ++++++ .../questions/gapselect/template.html | 5 ++ 4 files changed, 150 insertions(+) create mode 100644 www/addons/mod_quiz/questions/gapselect/directive.js create mode 100644 www/addons/mod_quiz/questions/gapselect/handlers.js create mode 100644 www/addons/mod_quiz/questions/gapselect/scss/styles.scss create mode 100644 www/addons/mod_quiz/questions/gapselect/template.html diff --git a/www/addons/mod_quiz/questions/gapselect/directive.js b/www/addons/mod_quiz/questions/gapselect/directive.js new file mode 100644 index 00000000000..680afbbb59e --- /dev/null +++ b/www/addons/mod_quiz/questions/gapselect/directive.js @@ -0,0 +1,68 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Directive to render a gap select question. + * + * @module mm.addons.mod_quiz + * @ngdoc directive + * @name mmaModQuizQuestionGapSelect + */ +.directive('mmaModQuizQuestionGapSelect', function($log, $mmaModQuestionHelper) { + $log = $log.getInstance('mmaModQuizQuestionGapSelect'); + + return { + restrict: 'A', + priority: 100, + templateUrl: 'addons/mod_quiz/questions/gapselect/template.html', + link: function(scope) { + var question = scope.question, + questionEl, + content, + selects; + + if (!question) { + $log.warn('Aborting quiz because of no question received.'); + return $mmaModQuestionHelper.showDirectiveError(scope); + } + + questionEl = angular.element(question.html); + + // Get question content. + content = questionEl[0].querySelector('.qtext'); + if (!content) { + log.warn('Aborting quiz because of an error parsing question.', question.name); + return $mmaModQuestionHelper.showDirectiveError(scope); + } + + // Find selects and add ng-model to them. + selects = content.querySelectorAll('select'); + angular.forEach(selects, function(select) { + select.setAttribute('ng-model', 'answers["' + select.name + '"]'); + + // Search if there's any option selected. + var selected = select.querySelector('option[selected]'); + if (selected && selected.value !== '' && typeof selected.value != 'undefined') { + // Store the value in the model. + scope.answers[select.name] = selected.value; + } + }); + + // Set the question text. + question.text = content.innerHTML; + } + }; +}); diff --git a/www/addons/mod_quiz/questions/gapselect/handlers.js b/www/addons/mod_quiz/questions/gapselect/handlers.js new file mode 100644 index 00000000000..b6a311227ca --- /dev/null +++ b/www/addons/mod_quiz/questions/gapselect/handlers.js @@ -0,0 +1,58 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.addons.mod_quiz') + +/** + * Gap select question handlers. + * + * @module mm.addons.mod_quiz + * @ngdoc service + * @name $mmaModQuizQuestionGapSelectHandler + */ +.factory('$mmaModQuizQuestionGapSelectHandler', function() { + + var self = {}; + + /** + * Whether or not the module is enabled for the site. + * + * @return {Boolean} + */ + self.isEnabled = function() { + return true; + }; + + /** + * Get the directive. + * + * @param {Object} question The question. + * @return {String} Directive's name. + */ + self.getDirectiveName = function(question) { + return 'mma-mod-quiz-question-gap-select'; + }; + + return self; +}) + +.run(function($mmAddonManager) { + // It shouldn't be mandatory for us to inject the delegate using $mmAddonManager because we're inside mod_quiz addon, + // but this way it can be used as an example for external question handlers. + var $mmaModQuizQuestionsDelegate = $mmAddonManager.get('$mmaModQuizQuestionsDelegate'); + if ($mmaModQuizQuestionsDelegate) { + $mmaModQuizQuestionsDelegate.registerHandler('mmaModQuizGapSelect', 'qtype_gapselect', + '$mmaModQuizQuestionGapSelectHandler'); + } +}); diff --git a/www/addons/mod_quiz/questions/gapselect/scss/styles.scss b/www/addons/mod_quiz/questions/gapselect/scss/styles.scss new file mode 100644 index 00000000000..1920bb8877d --- /dev/null +++ b/www/addons/mod_quiz/questions/gapselect/scss/styles.scss @@ -0,0 +1,19 @@ +// Style gapselect content a bit. All these styles are copied from Moodle. +.mm-quiz-gapselect-container { + p { + margin: 0 0 .5em; + } + + select { + height: 30px; + line-height: 30px; + display: inline-block; + border: 1px solid #ccc; + padding: 4px 6px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + margin-bottom: 10px; + } +} + diff --git a/www/addons/mod_quiz/questions/gapselect/template.html b/www/addons/mod_quiz/questions/gapselect/template.html new file mode 100644 index 00000000000..71f1830e77c --- /dev/null +++ b/www/addons/mod_quiz/questions/gapselect/template.html @@ -0,0 +1,5 @@ +
    +
    +

    {{ question.text }}

    +
    +
    From 5dd9b4bb0737ae44974d75c30068a520af77f1ad Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Feb 2016 13:22:01 +0100 Subject: [PATCH 023/101] MOBILE-1420 quiz: Move some helper functions to mmUtil --- .../questions/multianswer/directive.js | 6 +- www/core/lib/util.js | 74 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/www/addons/mod_quiz/questions/multianswer/directive.js b/www/addons/mod_quiz/questions/multianswer/directive.js index 4f203701845..e7a9a92e49a 100644 --- a/www/addons/mod_quiz/questions/multianswer/directive.js +++ b/www/addons/mod_quiz/questions/multianswer/directive.js @@ -21,7 +21,7 @@ angular.module('mm.addons.mod_quiz') * @ngdoc directive * @name mmaModQuizQuestionMultianswer */ -.directive('mmaModQuizQuestionMultianswer', function($log, $mmaModQuestionHelper, $mmaModQuizHelper) { +.directive('mmaModQuizQuestionMultianswer', function($log, $mmaModQuestionHelper, $mmUtil) { $log = $log.getInstance('mmaModQuizQuestionMultianswer'); return { @@ -49,8 +49,8 @@ angular.module('mm.addons.mod_quiz') } // Remove sequencecheck. - $mmaModQuizHelper.removeElement(content, 'input[name*=sequencecheck]'); - $mmaModQuizHelper.removeElement(content, '.validationerror'); + $mmUtil.removeElement(content, 'input[name*=sequencecheck]'); + $mmUtil.removeElement(content, '.validationerror'); // Find inputs of type text, radio and select and add ng-model to them. inputs = content.querySelectorAll('input[type="text"],input[type="radio"],select'); diff --git a/www/core/lib/util.js b/www/core/lib/util.js index 3c72ecd4869..10c0b53f167 100644 --- a/www/core/lib/util.js +++ b/www/core/lib/util.js @@ -1206,6 +1206,80 @@ angular.module('mm.core') } }; + /** + * Returns the contents of a certain selection in a DOM element. + * + * @module mm.core + * @ngdoc method + * @name $mmUtil#getContentsOfElement + * @param {Object} element DOM element to search in. + * @param {String} selector Selector to search. + * @return {String} Selection contents. + */ + self.getContentsOfElement = function(element, selector) { + if (element) { + var el = element[0] || element, // Convert from jqLite to plain JS if needed. + selected = el.querySelector(selector); + if (selected) { + return selected.innerHTML; + } + } + return ''; + }; + + /** + * Search and remove a certain element from inside another element. + * + * @module mm.core + * @ngdoc method + * @name $mmUtil#removeElement + * @param {Object} element DOM element to search in. + * @param {String} selector Selector to search. + * @return {Void} + */ + self.removeElement = function(element, selector) { + if (element) { + var el = element[0] || element, // Convert from jqLite to plain JS if needed. + selected = el.querySelector(selector); + if (selected) { + selected.remove(); + } + } + }; + + /** + * Search and remove a certain element from an HTML code. + * + * @module mm.core + * @ngdoc method + * @name $mmUtil#removeElementFromHtml + * @param {String} html HTML code to change. + * @param {String} selector Selector to search. + * @param {Boolean} removeAll True if it should remove all matches found, false if it should only remove the first one. + * @return {String} HTML without the element. + */ + self.removeElementFromHtml = function(html, selector, removeAll) { + // Create a fake div element so we can search using querySelector. + var div = document.createElement('div'), + selected; + + div.innerHTML = html; + + if (removeAll) { + selected = div.querySelectorAll(selector); + angular.forEach(selected, function(el) { + el.remove(); + }); + } else { + selected = div.querySelector(selector); + if (selected) { + selected.remove(); + } + } + + return div.innerHTML; + }; + return self; }; }); From e2625a48ec67e7ddb91ce5c8fb6777f623879eba Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Feb 2016 13:49:44 +0100 Subject: [PATCH 024/101] MOBILE-1420 quiz: Move question to a component --- www/addons/mod_quiz/controllers/player.js | 14 +- www/addons/mod_quiz/directives/question.js | 67 ------- www/addons/mod_quiz/lang/en.json | 1 - www/addons/mod_quiz/main.js | 5 - www/addons/mod_quiz/services/delegate.js | 169 ---------------- www/addons/mod_quiz/services/helper.js | 147 +------------- www/addons/mod_quiz/services/quiz.js | 4 +- www/addons/mod_quiz/templates/player.html | 5 +- .../templates/questionnotsupported.html | 1 - .../question/directives/question.js | 79 ++++++++ www/core/components/question/lang/en.json | 3 + www/core/components/question/main.js | 20 ++ .../components/question/services/delegate.js | 180 ++++++++++++++++++ .../components/question/services/helper.js} | 139 +++++++++++--- .../question/templates/question.html | 9 + 15 files changed, 408 insertions(+), 435 deletions(-) delete mode 100644 www/addons/mod_quiz/directives/question.js delete mode 100644 www/addons/mod_quiz/services/delegate.js delete mode 100644 www/addons/mod_quiz/templates/questionnotsupported.html create mode 100644 www/core/components/question/directives/question.js create mode 100644 www/core/components/question/lang/en.json create mode 100644 www/core/components/question/main.js create mode 100644 www/core/components/question/services/delegate.js rename www/{addons/mod_quiz/services/questionhelper.js => core/components/question/services/helper.js} (61%) create mode 100644 www/core/components/question/templates/question.html diff --git a/www/addons/mod_quiz/controllers/player.js b/www/addons/mod_quiz/controllers/player.js index d55c2f7c15e..696a63ad0a1 100644 --- a/www/addons/mod_quiz/controllers/player.js +++ b/www/addons/mod_quiz/controllers/player.js @@ -130,18 +130,10 @@ angular.module('mm.addons.mod_quiz') $scope.questions = data.questions; angular.forEach($scope.questions, function(question) { - // Get the readable mark and validation error for each question. + // Get the readable mark for each question. question.readableMark = $mmaModQuizHelper.getQuestionMarkFromHtml(question.html); - question.validationError = $mmaModQuizHelper.getValidationErrorFromHtml(question.html); - - // Get the sequence check (hidden input) and add it to the model. This is required. - var seqCheck = $mmaModQuizHelper.getQuestionSequenceCheckFromHtml(question.html); - if (seqCheck) { - $scope.answers[seqCheck.name] = seqCheck.value; - } else { - $log.warn('Aborting quiz because couldn\'t retrieve sequence check.', question.name); - $scope.quizAborted = true; - } + // Remove the question info box so it's not in the question HTML anymore. + question.html = $mmUtil.removeElementFromHtml(question.html, '.info'); }); }); }).catch(function(message) { diff --git a/www/addons/mod_quiz/directives/question.js b/www/addons/mod_quiz/directives/question.js deleted file mode 100644 index 8991a6b9fd9..00000000000 --- a/www/addons/mod_quiz/directives/question.js +++ /dev/null @@ -1,67 +0,0 @@ -// (C) Copyright 2015 Martin Dougiamas -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -angular.module('mm.addons.mod_quiz') - -/** - * Directive to render a question. - * It will search for the right directive to render the question based on the question type. - * See {@link $mmaModQuizQuestionsDelegate}. - * - * @module mm.addons.mod_quiz - * @ngdoc directive - * @name mmaModQuizQuestion - * @description - * - * The directives to render the question will receive the following parameters in the scope: - * - * @param {Object} question The question to render. - * @param {Function} abortQuiz A function to call to abort the quiz execution. Please use it if there's any problem initializing - * your question. If this function is called, all questions will disappear from the screen and they'll - * be replaced by an error message and a button to attempt the quiz in the browser. - */ -.directive('mmaModQuizQuestion', function($compile, $mmaModQuizQuestionsDelegate, $mmaModQuizHelper) { - - // We set priority to a high number to ensure that it will be compiled before other directives. - // With terminal set to true, the other directives will be skipped after this directive is compiled. - return { - restrict: 'A', - priority: 1000, - terminal: true, - templateUrl: 'addons/mod_quiz/templates/questionnotsupported.html', - scope: { - question: '=', - answers: '=', - abortQuiz: '&' - }, - link: function(scope, element) { - if (scope.question) { - // Search the right directive to render the question. - var directive = $mmaModQuizQuestionsDelegate.getDirectiveForQuestion(scope.question); - if (directive) { - // Treat the question before starting the directive. - $mmaModQuizHelper.extractQuestionScripts(scope.question); - $mmaModQuizHelper.extractQuestionInfoBox(scope.question); - - // Add the directive to the element. - element.attr(directive, ''); - // Remove current directive, otherwise we would cause an infinite loop when compiling. - element.removeAttr('mma-mod-quiz-question'); - // Compile the new directive. - $compile(element)(scope); - } - } - } - }; -}); diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index 90d9c053468..e44a1254dee 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -12,7 +12,6 @@ "errorgetattempt": "Error getting attempt data.", "errorgetquiz": "Error getting quiz data.", "errorparsequestions": "An error occurred while treating the questions. Please attempt this quiz in a browser.", - "errorquestionnotsupported": "This type of question isn't supported by the app in your site: {{$a}}.", "errorquestionsnotsupported": "This quiz can't be attempted in the app because it can contain questions not supported by the app:", "errorrulesnotsupported": "This quiz can't be attempted in the app because it has active rules not supported by the app:", "feedback": "Feedback", diff --git a/www/addons/mod_quiz/main.js b/www/addons/mod_quiz/main.js index 52482291ca1..9428a109be6 100644 --- a/www/addons/mod_quiz/main.js +++ b/www/addons/mod_quiz/main.js @@ -66,9 +66,4 @@ angular.module('mm.addons.mod_quiz', ['mm.core']) .config(function($mmCourseDelegateProvider) { $mmCourseDelegateProvider.registerContentHandler('mmaModQuiz', 'quiz', '$mmaModQuizHandlers.courseContentHandler'); -}) - -.run(function($mmEvents, mmCoreEventLogin, mmCoreEventSiteUpdated, $mmaModQuizQuestionsDelegate) { - $mmEvents.on(mmCoreEventLogin, $mmaModQuizQuestionsDelegate.updateQuestionHandlers); - $mmEvents.on(mmCoreEventSiteUpdated, $mmaModQuizQuestionsDelegate.updateQuestionHandlers); }); diff --git a/www/addons/mod_quiz/services/delegate.js b/www/addons/mod_quiz/services/delegate.js deleted file mode 100644 index 6c1d37e14c3..00000000000 --- a/www/addons/mod_quiz/services/delegate.js +++ /dev/null @@ -1,169 +0,0 @@ -// (C) Copyright 2015 Martin Dougiamas -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -angular.module('mm.addons.mod_quiz') - -/** - * Delegate to register question handlers. - * - * @module mm.addons.mod_quiz - * @ngdoc service - * @name $mmaModQuizQuestionsDelegate - * @description - * - * Delegate to register question handlers. - * You can use this service to register your own question handlers to be used in a quiz. - * - * Important: This service mustn't be injected using Angular's dependency injection. This is because custom apps could have this - * addon disabled or removed, so you can't guarantee that the service exists. Please inject it using {@link $mmAddonManager}. - * - */ -.factory('$mmaModQuizQuestionsDelegate', function($log, $q, $mmUtil, $mmSite) { - - $log = $log.getInstance('$mmaModQuizQuestionsDelegate'); - - var handlers = {}, - enabledHandlers = {}, - self = {}; - - /** - * Get the directive to use for a certain question type. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizQuestionsDelegate#getDirectiveForQuestion - * @param {Object} question Question to get the directive for. - * @return {String} Directive name. Undefined if no directive found. - */ - self.getDirectiveForQuestion = function(question) { - var type = question.type; - if (typeof enabledHandlers[type] != 'undefined') { - return enabledHandlers[type].getDirectiveName(question); - } - }; - - /** - * Check if a question type is supported. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizQuestionsDelegate#isQuestionSupported - * @param {String} type Question type. - * @return {Boolean} True if supported, false otherwise. - */ - self.isQuestionSupported = function(type) { - return typeof enabledHandlers[type] != 'undefined'; - }; - - /** - * Register a question handler. The handler will be used when we need to render a question of the type defined. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizQuestionsDelegate#registerHandler - * @param {String} name Handler's name. - * @param {String} questionType Question type the handler supports. - * @param {String|Object|Function} handler Must be resolved to an object defining the following properties. Or to a function - * returning an object defining these properties. See {@link $mmUtil#resolveObject}. - * - isEnabled (Boolean|Promise) Whether or not the handler is enabled on a site level. - * When using a promise, it should return a boolean. - * - getDirectiveName(question) (String) Returns the name of the directive to render the question. - * There's no need to check the question type in this function. - */ - self.registerHandler = function(name, questionType, handler) { - if (typeof handlers[questionType] !== 'undefined') { - $log.debug("Addon '" + name + "' already registered as handler for '" + questionType + "'"); - return false; - } - $log.debug("Registered handler '" + name + "' for question type '" + questionType + "'"); - handlers[questionType] = { - addon: name, - instance: undefined, - handler: handler - }; - - // It's possible that this handler is registered after updateQuestionHandlers has been called for the current - // site. Let's call updateQuestionHandler just in case. - self.updateQuestionHandler(questionType, handlers[questionType]); - }; - - /** - * Check if a handler is enabled for a certain site and add/remove it to enabledHandlers. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizQuestionsDelegate#updateQuestionHandler - * @param {String} questionType The question type this handler handles. - * @param {Object} handlerInfo The handler details. - * @return {Promise} Resolved when done. - * @protected - */ - self.updateQuestionHandler = function(questionType, handlerInfo) { - var promise, - siteId = $mmSite.getId(); - - if (typeof handlerInfo.instance === 'undefined') { - handlerInfo.instance = $mmUtil.resolveObject(handlerInfo.handler, true); - } - - if (!$mmSite.isLoggedIn()) { - promise = $q.reject(); - } else { - promise = $q.when(handlerInfo.instance.isEnabled()); - } - - // Checks if the handler is enabled. - return promise.catch(function() { - return false; - }).then(function(enabled) { - // Check that site hasn't changed since the check started. - if ($mmSite.isLoggedIn() && $mmSite.getId() === siteId) { - if (enabled) { - enabledHandlers[questionType] = handlerInfo.instance; - } else { - delete enabledHandlers[questionType]; - } - } - }); - }; - - /** - * Update the enabled handlers for the current site. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizQuestionsDelegate#updateQuestionHandlers - * @return {Promise} Resolved when done. - * @protected - */ - self.updateQuestionHandlers = function() { - var promises = []; - - $log.debug('Updating question handlers for current site.'); - - // Loop over all the handlers. - angular.forEach(handlers, function(handlerInfo, questionType) { - promises.push(self.updateQuestionHandler(questionType, handlerInfo)); - }); - - return $q.all(promises).then(function() { - return true; - }, function() { - // Never reject. - return true; - }); - }; - - return self; -}); diff --git a/www/addons/mod_quiz/services/helper.js b/www/addons/mod_quiz/services/helper.js index 35584c145c9..c685f59c194 100644 --- a/www/addons/mod_quiz/services/helper.js +++ b/www/addons/mod_quiz/services/helper.js @@ -25,91 +25,6 @@ angular.module('mm.addons.mod_quiz') var self = {}; - /** - * Removes the info box (flag, question number, etc.) from a question's HTML and adds it in a new infoBox property. - * Please take into account that all scripts will also be removed due to angular.element. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizHelper#extractQuestionInfoBox - * @param {Object} question Question. - * @return {Void} - */ - self.extractQuestionInfoBox = function(question) { - var el = angular.element(question.html)[0], - info; - if (el) { - info = el.querySelector('.info'); - if (info) { - question.infoBox = info.outerHTML; - info.remove(); - question.html = el.outerHTML; - } - } - }; - - /** - * Removes the scripts from a question's HTML and adds it in a new 'scriptsCode' property. - * It will also search for init_question functions of the question type and add the object to an 'initObjects' property. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizHelper#extractQuestionScripts - * @param {Object} question Question. - * @return {Void} - */ - self.extractQuestionScripts = function(question) { - var matches; - - question.scriptsCode = ''; - question.initObjects = []; - - if (question.html) { - // Search the scripts. - matches = question.html.match(/]*>[\s\S]*?<\/script>/mg); - angular.forEach(matches, function(match) { - // Add the script to scriptsCode and remove it from html. - question.scriptsCode += match; - question.html = question.html.replace(match, ''); - - // Search init_question functions for this type. - var initMatches = match.match(new RegExp('M\.' + question.type + '\.init_question\\(.*?}\\);', 'mg')); - angular.forEach(initMatches, function(initMatch) { - // Remove start and end of the match, we only want the object. - initMatch = initMatch.replace('M.' + question.type + '.init_question(', ''); - initMatch = initMatch.substr(0, initMatch.length - 2); - - // Try to convert it to an object and add it to the question. - try { - initMatch = JSON.parse(initMatch); - question.initObjects.push(initMatch); - } catch(ex) {} - }); - }); - } - }; - - /** - * Returns the contents of a certain selection in a DOM element. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizHelper#getContentsOfElement - * @param {Object} element DOM element to search in. - * @param {String} selector Selector to search. - * @return {String} Selection contents. - */ - self.getContentsOfElement = function(element, selector) { - if (element) { - var el = element[0] || element, // Convert from jqLite to plain JS if needed. - selected = el.querySelector(selector); - if (selected) { - return selected.innerHTML; - } - } - return ''; - }; - /** * Gets the mark string from a question HTML. * Example result: "Marked out of 1.00". @@ -121,47 +36,7 @@ angular.module('mm.addons.mod_quiz') * @return {String} Question's mark. */ self.getQuestionMarkFromHtml = function(html) { - return self.getContentsOfElement(angular.element(html), '.grade'); - }; - - /** - * Get the sequence check from a question HTML. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizHelper#getQuestionSequenceCheckFromHtml - * @param {String} html Question's HTML. - * @return {Object} Object with the sequencecheck name and value. - */ - self.getQuestionSequenceCheckFromHtml = function(html) { - var el, - input; - - if (html) { - el = angular.element(html)[0]; - - // Search the input holding the sequencecheck. - input = el.querySelector('input[name*=sequencecheck]'); - if (input && typeof input.name != 'undefined' && typeof input.value != 'undefined') { - return { - name: input.name, - value: input.value - }; - } - } - }; - - /** - * Get the validation error message from a question HTML if it's there. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizHelper#getValidationErrorFromHtml - * @param {String} html Question's HTML. - * @return {Object} Validation error message if present. - */ - self.getValidationErrorFromHtml = function(html) { - return self.getContentsOfElement(angular.element(html), '.validationerror'); + return $mmUtil.getContentsOfElement(angular.element(html), '.grade'); }; /** @@ -190,26 +65,6 @@ angular.module('mm.addons.mod_quiz') }); }; - /** - * Search and remove a certain element from inside another element. - * - * @module mm.addons.mod_quiz - * @ngdoc method - * @name $mmaModQuizHelper#removeElement - * @param {Object} element DOM element to search in. - * @param {String} selector Selector to search. - * @return {Void} - */ - self.removeElement = function(element, selector) { - if (element) { - var el = element[0] || element, // Convert from jqLite to plain JS if needed. - selected = el.querySelector(selector); - if (selected) { - selected.remove(); - } - } - }; - /** * Add some calculated data to the attempt. * diff --git a/www/addons/mod_quiz/services/quiz.js b/www/addons/mod_quiz/services/quiz.js index 4b2e1626ee3..f03d9a9700b 100644 --- a/www/addons/mod_quiz/services/quiz.js +++ b/www/addons/mod_quiz/services/quiz.js @@ -21,7 +21,7 @@ angular.module('mm.addons.mod_quiz') * @ngdoc service * @name $mmaModQuiz */ -.factory('$mmaModQuiz', function($log, $mmSite, $mmSitesManager, $q, $translate, $mmUtil, $mmText, $mmaModQuizQuestionsDelegate) { +.factory('$mmaModQuiz', function($log, $mmSite, $mmSitesManager, $q, $translate, $mmUtil, $mmText, $mmQuestionDelegate) { $log = $log.getInstance('$mmaModQuiz'); @@ -694,7 +694,7 @@ angular.module('mm.addons.mod_quiz') self.getUnsupportedQuestions = function(questionTypes) { var notSupported = []; angular.forEach(questionTypes, function(type) { - if (type != 'random' && !$mmaModQuizQuestionsDelegate.isQuestionSupported('qtype_'+type)) { + if (type != 'random' && !$mmQuestionDelegate.isQuestionSupported('qtype_'+type)) { notSupported.push(type); } }); diff --git a/www/addons/mod_quiz/templates/player.html b/www/addons/mod_quiz/templates/player.html index de3d2b76220..982916b1dbc 100644 --- a/www/addons/mod_quiz/templates/player.html +++ b/www/addons/mod_quiz/templates/player.html @@ -20,10 +20,7 @@

    {{ 'mma.mod_quiz.information' | translate }}

    {{question.readableMark}}

    {{ 'mma.mod_quiz.flagged' | translate }} -
    -
    -

    {{ question.validationError }}

    -
    +
    diff --git a/www/addons/mod_quiz/templates/questionnotsupported.html b/www/addons/mod_quiz/templates/questionnotsupported.html deleted file mode 100644 index 59aabe26bf7..00000000000 --- a/www/addons/mod_quiz/templates/questionnotsupported.html +++ /dev/null @@ -1 +0,0 @@ -

    {{ 'mma.mod_quiz.errorquestionnotsupported' | translate:{$a: question.type} }}

    diff --git a/www/core/components/question/directives/question.js b/www/core/components/question/directives/question.js new file mode 100644 index 00000000000..7b90be8fa06 --- /dev/null +++ b/www/core/components/question/directives/question.js @@ -0,0 +1,79 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.core.question') + +/** + * Directive to render a question. + * It will search for the right directive to render the question based on the question type. + * See {@link $mmQuestionDelegate}. + * + * @module mm.core.question + * @ngdoc directive + * @name mmQuestion + * @description + * + * The directives to render the question will receive the following parameters in the scope: + * + * @param {Object} question The question to render. + * @param {Object} answers Object to store the answers in (model). + * @param {Function} abort A function to call to abort the execution. + * Directives implementing questions should use it if there's a critical error. + * Addons using this directive should provide a function that allows aborting the execution of the + * addon, so if any question calls it the whole feature is aborted. + */ +.directive('mmQuestion', function($log, $compile, $mmQuestionDelegate, $mmQuestionHelper) { + $log = $log.getInstance('mmQuestion'); + + return { + restrict: 'E', + templateUrl: 'core/components/question/templates/question.html', + scope: { + question: '=', + answers: '=', + abort: '&' + }, + link: function(scope, element) { + var question = scope.question, + questionContainer = element[0].querySelector('#mm-question-container'); + + if (question && questionContainer) { + // Search the right directive to render the question. + var directive = $mmQuestionDelegate.getDirectiveForQuestion(question); + if (directive) { + // Treat the question before starting the directive. + $mmQuestionHelper.extractQuestionScripts(question); + + // Extract the validation error of the question. + question.validationError = $mmQuestionHelper.getValidationErrorFromHtml(question.html); + + // Get the sequence check (hidden input) and add it to the model. This is required. + var seqCheck = $mmQuestionHelper.getQuestionSequenceCheckFromHtml(question.html); + if (seqCheck) { + scope.answers[seqCheck.name] = seqCheck.value; + } else { + $log.warn('Aborting question because couldn\'t retrieve sequence check.', question.name); + scope.abort(); + return; + } + + // Add the directive to the element. + questionContainer.setAttribute(directive, ''); + // Compile the new directive. + $compile(questionContainer)(scope); + } + } + } + }; +}); diff --git a/www/core/components/question/lang/en.json b/www/core/components/question/lang/en.json new file mode 100644 index 00000000000..886f2bdcf0a --- /dev/null +++ b/www/core/components/question/lang/en.json @@ -0,0 +1,3 @@ +{ + "errorquestionnotsupported": "This type of question isn't supported by the app in your site: {{$a}}." +} diff --git a/www/core/components/question/main.js b/www/core/components/question/main.js new file mode 100644 index 00000000000..33a2709f17a --- /dev/null +++ b/www/core/components/question/main.js @@ -0,0 +1,20 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.core.question', []) + +.run(function($mmEvents, mmCoreEventLogin, mmCoreEventSiteUpdated, $mmQuestionDelegate) { + $mmEvents.on(mmCoreEventLogin, $mmQuestionDelegate.updateQuestionHandlers); + $mmEvents.on(mmCoreEventSiteUpdated, $mmQuestionDelegate.updateQuestionHandlers); +}); diff --git a/www/core/components/question/services/delegate.js b/www/core/components/question/services/delegate.js new file mode 100644 index 00000000000..9dd5ceab83b --- /dev/null +++ b/www/core/components/question/services/delegate.js @@ -0,0 +1,180 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.core.question') + +/** + * Delegate to register question handlers. + * + * @module mm.core.question + * @ngdoc provider + * @name $mmQuestionDelegate + * @description + * + * Delegate to register question handlers. + * You can use this provider to register your own question handlers to be used in a quiz or other places. + * + * To register a question handler: + * + * $mmQuestionDelegateProvider.registerHandler('mmaYourAddon', 'questionType', 'handlerName'); + * + * Example: + * + * .config(function($mmQuestionDelegateProvider) { + * $mmQuestionDelegateProvider.registerHandler('mmaQtypeCalculated', 'qtype_calculated', '$mmaQtypeCalculatedHandler'); + * }) + * + * @see $mmQuestionDelegateProvider#registerHandler to see the methods your handle needs to implement. + */ +.provider('$mmQuestionDelegate', function() { + + var handlers = {}, + self = {}; + + /** + * Register a question handler. The handler will be used when we need to render a question of the type defined. + * + * @module mm.core.question + * @ngdoc method + * @name $mmQuestionDelegateProvider#registerHandler + * @param {String} name Handler's name. + * @param {String} questionType Question type the handler supports. + * @param {String|Object|Function} handler Must be resolved to an object defining the following properties. Or to a function + * returning an object defining these properties. See {@link $mmUtil#resolveObject}. + * - isEnabled (Boolean|Promise) Whether or not the handler is enabled on a site level. + * When using a promise, it should return a boolean. + * - getDirectiveName(question) (String) Returns the name of the directive to render the question. + * There's no need to check the question type in this function. + */ + self.registerHandler = function(name, questionType, handler) { + if (typeof handlers[questionType] !== 'undefined') { + console.log("$mmQuestionDelegateProvider: Addon '" + name + "' already registered as handler for '" + questionType + "'"); + return false; + } + console.log("$mmQuestionDelegateProvider: Registered handler '" + name + "' for question type '" + questionType + "'"); + handlers[questionType] = { + addon: name, + instance: undefined, + handler: handler + }; + }; + + self.$get = function($log, $q, $mmUtil, $mmSite) { + + $log = $log.getInstance('$mmQuestionDelegate'); + + var enabledHandlers = {}, + self = {}; + + /** + * Get the directive to use for a certain question type. + * + * @module mm.core.question + * @ngdoc method + * @name $mmQuestionDelegate#getDirectiveForQuestion + * @param {Object} question Question to get the directive for. + * @return {String} Directive name. Undefined if no directive found. + */ + self.getDirectiveForQuestion = function(question) { + var type = question.type; + if (typeof enabledHandlers[type] != 'undefined') { + return enabledHandlers[type].getDirectiveName(question); + } + }; + + /** + * Check if a question type is supported. + * + * @module mm.core.question + * @ngdoc method + * @name $mmQuestionDelegate#isQuestionSupported + * @param {String} type Question type. + * @return {Boolean} True if supported, false otherwise. + */ + self.isQuestionSupported = function(type) { + return typeof enabledHandlers[type] != 'undefined'; + }; + + /** + * Check if a handler is enabled for a certain site and add/remove it to enabledHandlers. + * + * @module mm.core.question + * @ngdoc method + * @name $mmQuestionDelegate#updateQuestionHandler + * @param {String} questionType The question type this handler handles. + * @param {Object} handlerInfo The handler details. + * @return {Promise} Resolved when done. + * @protected + */ + self.updateQuestionHandler = function(questionType, handlerInfo) { + var promise, + siteId = $mmSite.getId(); + + if (typeof handlerInfo.instance === 'undefined') { + handlerInfo.instance = $mmUtil.resolveObject(handlerInfo.handler, true); + } + + if (!$mmSite.isLoggedIn()) { + promise = $q.reject(); + } else { + promise = $q.when(handlerInfo.instance.isEnabled()); + } + + // Checks if the handler is enabled. + return promise.catch(function() { + return false; + }).then(function(enabled) { + // Check that site hasn't changed since the check started. + if ($mmSite.isLoggedIn() && $mmSite.getId() === siteId) { + if (enabled) { + enabledHandlers[questionType] = handlerInfo.instance; + } else { + delete enabledHandlers[questionType]; + } + } + }); + }; + + /** + * Update the enabled handlers for the current site. + * + * @module mm.core.question + * @ngdoc method + * @name $mmQuestionDelegate#updateQuestionHandlers + * @return {Promise} Resolved when done. + * @protected + */ + self.updateQuestionHandlers = function() { + var promises = []; + + $log.debug('Updating question handlers for current site.'); + + // Loop over all the handlers. + angular.forEach(handlers, function(handlerInfo, questionType) { + promises.push(self.updateQuestionHandler(questionType, handlerInfo)); + }); + + return $q.all(promises).then(function() { + return true; + }, function() { + // Never reject. + return true; + }); + }; + + return self; + }; + + return self; +}); diff --git a/www/addons/mod_quiz/services/questionhelper.js b/www/core/components/question/services/helper.js similarity index 61% rename from www/addons/mod_quiz/services/questionhelper.js rename to www/core/components/question/services/helper.js index db494a36c52..dc0b77af5c7 100644 --- a/www/addons/mod_quiz/services/questionhelper.js +++ b/www/core/components/question/services/helper.js @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -angular.module('mm.addons.mod_quiz') +angular.module('mm.core.question') /** * Helper to gather some common functions for question directives. * - * @module mm.addons.mod_quiz + * @module mm.core.question * @ngdoc service - * @name $mmaModQuestionHelper + * @name $mmQuestionHelper */ -.factory('$mmaModQuestionHelper', function($mmaModQuizHelper, $mmUtil) { +.factory('$mmQuestionHelper', function($mmUtil) { var self = {}, lastErrorShown = 0; @@ -30,9 +30,9 @@ angular.module('mm.addons.mod_quiz') * Convenience function to initialize a question directive. * Performs some common checks and extracts the question's text. * - * @module mm.addons.mod_quiz + * @module mm.core.question * @ngdoc method - * @name $mmaModQuestionHelper#directiveInit + * @name $mmQuestionHelper#directiveInit * @param {Object} scope Directive's scope. * @param {Object} log $log instance to log messages. * @return {Object} Angular DOM element of the question's HTML. Undefined if an error happens. @@ -42,28 +42,109 @@ angular.module('mm.addons.mod_quiz') questionEl; if (!question) { - log.warn('Aborting quiz because of no question received.'); + log.warn('Aborting because of no question received.'); return self.showDirectiveError(scope); } questionEl = angular.element(question.html); // Extract question text. - question.text = $mmaModQuizHelper.getContentsOfElement(questionEl, '.qtext'); + question.text = $mmUtil.getContentsOfElement(questionEl, '.qtext'); if (!question.text) { - log.warn('Aborting quiz because of an error parsing question.', question.name); + log.warn('Aborting because of an error parsing question.', question.name); return self.showDirectiveError(scope); } return questionEl; }; + /** + * Removes the scripts from a question's HTML and adds it in a new 'scriptsCode' property. + * It will also search for init_question functions of the question type and add the object to an 'initObjects' property. + * + * @module mm.core.question + * @ngdoc method + * @name $mmQuestionHelper#extractQuestionScripts + * @param {Object} question Question. + * @return {Void} + */ + self.extractQuestionScripts = function(question) { + var matches; + + question.scriptsCode = ''; + question.initObjects = []; + + if (question.html) { + // Search the scripts. + matches = question.html.match(/]*>[\s\S]*?<\/script>/mg); + angular.forEach(matches, function(match) { + // Add the script to scriptsCode and remove it from html. + question.scriptsCode += match; + question.html = question.html.replace(match, ''); + + // Search init_question functions for this type. + var initMatches = match.match(new RegExp('M\.' + question.type + '\.init_question\\(.*?}\\);', 'mg')); + angular.forEach(initMatches, function(initMatch) { + // Remove start and end of the match, we only want the object. + initMatch = initMatch.replace('M.' + question.type + '.init_question(', ''); + initMatch = initMatch.substr(0, initMatch.length - 2); + + // Try to convert it to an object and add it to the question. + try { + initMatch = JSON.parse(initMatch); + question.initObjects.push(initMatch); + } catch(ex) {} + }); + }); + } + }; + + /** + * Get the sequence check from a question HTML. + * + * @module mm.core.question + * @ngdoc method + * @name $mmQuestionHelper#getQuestionSequenceCheckFromHtml + * @param {String} html Question's HTML. + * @return {Object} Object with the sequencecheck name and value. + */ + self.getQuestionSequenceCheckFromHtml = function(html) { + var el, + input; + + if (html) { + el = angular.element(html)[0]; + + // Search the input holding the sequencecheck. + input = el.querySelector('input[name*=sequencecheck]'); + if (input && typeof input.name != 'undefined' && typeof input.value != 'undefined') { + return { + name: input.name, + value: input.value + }; + } + } + }; + + /** + * Get the validation error message from a question HTML if it's there. + * + * @module mm.core.question + * @ngdoc method + * @name $mmQuestionHelper#getValidationErrorFromHtml + * @param {String} html Question's HTML. + * @return {Object} Validation error message if present. + */ + self.getValidationErrorFromHtml = function(html) { + return $mmUtil.getContentsOfElement(angular.element(html), '.validationerror'); + }; + /** * Generic link function for question directives with an input of type "text". * - * @module mm.addons.mod_quiz + * @module mm.core.question * @ngdoc method - * @name $mmaModQuestionHelper#inputTextDirective + * @name $mmQuestionHelper#inputTextDirective * @param {Object} scope Directive's scope. * @param {Object} log $log instance to log messages. * @return {Void} @@ -76,7 +157,7 @@ angular.module('mm.addons.mod_quiz') // Get the input element. input = questionEl.querySelector('input[type="text"][name*=answer]'); if (!input) { - log.warn('Aborting quiz because couldn\'t find input.', question.name); + log.warn('Aborting because couldn\'t find input.', question.name); return self.showDirectiveError(scope); } @@ -95,9 +176,9 @@ angular.module('mm.addons.mod_quiz') /** * Generic link function for question directives with a "matching" (selects). * - * @module mm.addons.mod_quiz + * @module mm.core.question * @ngdoc method - * @name $mmaModQuestionHelper#matchingDirective + * @name $mmQuestionHelper#matchingDirective * @param {Object} scope Directive's scope. * @param {Object} log $log instance to log messages. * @return {Void} @@ -113,7 +194,7 @@ angular.module('mm.addons.mod_quiz') // Find rows. rows = questionEl.querySelectorAll('tr'); if (!rows || !rows.length) { - log.warn('Aborting quiz because couldn\'t find any row.', question.name); + log.warn('Aborting because couldn\'t find any row.', question.name); return self.showDirectiveError(scope); } @@ -127,7 +208,7 @@ angular.module('mm.addons.mod_quiz') columns = row.querySelectorAll('td'); if (!columns || columns.length < 2) { - log.warn('Aborting quiz because couldn\'t find the right columns.', question.name); + log.warn('Aborting because couldn\'t find the right columns.', question.name); return self.showDirectiveError(scope); } @@ -139,7 +220,7 @@ angular.module('mm.addons.mod_quiz') options = columns[1].querySelectorAll('option'); if (!select || !options || !options.length) { - log.warn('Aborting quiz because couldn\'t find select or options.', question.name); + log.warn('Aborting because couldn\'t find select or options.', question.name); return self.showDirectiveError(scope); } @@ -150,7 +231,7 @@ angular.module('mm.addons.mod_quiz') // Treat each option. angular.forEach(options, function(option) { if (typeof option.value == 'undefined') { - log.warn('Aborting quiz because couldn\'t find option value.', question.name); + log.warn('Aborting because couldn\'t find option value.', question.name); return self.showDirectiveError(scope); } @@ -178,9 +259,9 @@ angular.module('mm.addons.mod_quiz') /** * Generic link function for question directives with a multi choice input. * - * @module mm.addons.mod_quiz + * @module mm.core.question * @ngdoc method - * @name $mmaModQuestionHelper#multiChoiceDirective + * @name $mmQuestionHelper#multiChoiceDirective * @param {Object} scope Directive's scope. * @param {Object} log $log instance to log messages. * @return {Void} @@ -193,7 +274,7 @@ angular.module('mm.addons.mod_quiz') questionEl = questionEl[0] || questionEl; // Convert from jqLite to plain JS if needed. // Get the prompt. - question.prompt = $mmaModQuizHelper.getContentsOfElement(questionEl, '.prompt'); + question.prompt = $mmUtil.getContentsOfElement(questionEl, '.prompt'); // Search radio buttons first (single choice). var options = questionEl.querySelectorAll('input[type="radio"]'); @@ -203,8 +284,8 @@ angular.module('mm.addons.mod_quiz') options = questionEl.querySelectorAll('input[type="checkbox"]'); if (!options || !options.length) { - // No checkbox found either. Abort the quiz. - log.warn('Aborting quiz because of no radio and checkbox found.', question.name); + // No checkbox found either. Abort. + log.warn('Aborting because of no radio and checkbox found.', question.name); return self.showDirectiveError(scope); } } @@ -239,18 +320,18 @@ angular.module('mm.addons.mod_quiz') } // Something went wrong when extracting the questions data. Abort. - log.warn('Aborting quiz because of an error parsing options.', question.name, option.name); + log.warn('Aborting because of an error parsing options.', question.name, option.name); return self.showDirectiveError(scope); }); } }; /** - * Convenience function to show a parsing error and abort a quiz. + * Convenience function to show a parsing error and abort. * - * @module mm.addons.mod_quiz + * @module mm.core.question * @ngdoc method - * @name $mmaModQuestionHelper#showDirectiveError + * @name $mmQuestionHelper#showDirectiveError * @param {Object} scope Directive scope. * @return {Void} */ @@ -259,9 +340,9 @@ angular.module('mm.addons.mod_quiz') var now = new Date().getTime(); if (now - lastErrorShown > 500) { lastErrorShown = now; - $mmUtil.showErrorModal('Error parsing question. Please make sure you don\'t have a custom theme that can affect this.'); + $mmUtil.showErrorModal('Error processing the question. This could be caused by custom modifications in your site.'); } - scope.abortQuiz(); + scope.abort(); }; return self; diff --git a/www/core/components/question/templates/question.html b/www/core/components/question/templates/question.html new file mode 100644 index 00000000000..6dd94abafb0 --- /dev/null +++ b/www/core/components/question/templates/question.html @@ -0,0 +1,9 @@ + +
    + +

    {{ 'mm.question.errorquestionnotsupported' | translate:{$a: question.type} }}

    +
    + +
    +

    {{ question.validationError }}

    +
    From 786bbeedeceafa744090ef85ff1a90f9779ea294 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Feb 2016 17:05:08 +0100 Subject: [PATCH 025/101] MOBILE-1420 quiz: Move questions outside of quiz --- gulpfile.js | 18 +++++++++++++---- www/addons/mod_quiz/lang/en.json | 1 - .../questions/gapselect/template.html | 5 ----- .../questions/randomsamatch/scss/styles.scss | 19 ------------------ www/addons/mod_quiz/scss/styles.scss | 4 ---- www/addons/qtype/base/lang/en.json | 3 +++ www/addons/qtype/base/scss/styles.scss | 6 ++++++ .../base => qtype/base/templates}/match.html | 6 +++--- .../base => qtype/base/templates}/multi.html | 4 ++-- .../base => qtype/base/templates}/text.html | 2 +- .../calculated/directive.js | 14 ++++++------- .../calculated/handlers.js | 20 +++++-------------- www/addons/qtype/calculated/main.js | 19 ++++++++++++++++++ .../calculatedmulti/directive.js | 14 ++++++------- .../calculatedmulti/handlers.js | 20 +++++-------------- www/addons/qtype/calculatedmulti/main.js | 20 +++++++++++++++++++ .../calculatedsimple/directive.js | 14 ++++++------- .../calculatedsimple/handlers.js | 20 +++++-------------- www/addons/qtype/calculatedsimple/main.js | 20 +++++++++++++++++++ .../description/directive.js | 14 ++++++------- .../description/handlers.js | 20 +++++-------------- www/addons/qtype/description/main.js | 19 ++++++++++++++++++ .../description/template.html | 0 .../questions => qtype}/essay/directive.js | 18 ++++++++--------- .../questions => qtype}/essay/handlers.js | 20 +++++-------------- www/addons/qtype/essay/main.js | 19 ++++++++++++++++++ .../questions => qtype}/essay/template.html | 2 +- .../gapselect/directive.js | 20 +++++++++---------- .../questions => qtype}/gapselect/handlers.js | 20 +++++-------------- www/addons/qtype/gapselect/main.js | 19 ++++++++++++++++++ .../gapselect/scss/styles.scss | 2 +- .../gapselect}/template.html | 2 +- .../questions => qtype}/match/directive.js | 14 ++++++------- .../questions => qtype}/match/handlers.js | 20 +++++-------------- www/addons/qtype/match/main.js | 19 ++++++++++++++++++ .../multianswer/directive.js | 20 +++++++++---------- .../multianswer/handlers.js | 20 +++++-------------- www/addons/qtype/multianswer/main.js | 19 ++++++++++++++++++ .../multianswer/scss/styles.scss | 2 +- .../multianswer}/template.html | 2 +- .../multichoice/directive.js | 14 ++++++------- .../multichoice/handlers.js | 20 +++++-------------- www/addons/qtype/multichoice/main.js | 19 ++++++++++++++++++ .../numerical/directive.js | 14 ++++++------- .../questions => qtype}/numerical/handlers.js | 20 +++++-------------- www/addons/qtype/numerical/main.js | 19 ++++++++++++++++++ .../randomsamatch/directive.js | 14 ++++++------- .../randomsamatch/handlers.js | 20 +++++-------------- www/addons/qtype/randomsamatch/main.js | 19 ++++++++++++++++++ .../shortanswer/directive.js | 14 ++++++------- .../shortanswer/handlers.js | 20 +++++-------------- www/addons/qtype/shortanswer/main.js | 19 ++++++++++++++++++ .../truefalse/directive.js | 14 ++++++------- .../questions => qtype}/truefalse/handlers.js | 20 +++++-------------- www/addons/qtype/truefalse/main.js | 19 ++++++++++++++++++ .../question/templates/question.html | 2 +- www/core/scss/styles.scss | 13 ++++++++++++ 57 files changed, 461 insertions(+), 339 deletions(-) delete mode 100644 www/addons/mod_quiz/questions/gapselect/template.html delete mode 100644 www/addons/mod_quiz/questions/randomsamatch/scss/styles.scss create mode 100644 www/addons/qtype/base/lang/en.json create mode 100644 www/addons/qtype/base/scss/styles.scss rename www/addons/{mod_quiz/questions/base => qtype/base/templates}/match.html (74%) rename www/addons/{mod_quiz/questions/base => qtype/base/templates}/multi.html (80%) rename www/addons/{mod_quiz/questions/base => qtype/base/templates}/text.html (63%) rename www/addons/{mod_quiz/questions => qtype}/calculated/directive.js (67%) rename www/addons/{mod_quiz/questions => qtype}/calculated/handlers.js (57%) create mode 100644 www/addons/qtype/calculated/main.js rename www/addons/{mod_quiz/questions => qtype}/calculatedmulti/directive.js (66%) rename www/addons/{mod_quiz/questions => qtype}/calculatedmulti/handlers.js (56%) create mode 100644 www/addons/qtype/calculatedmulti/main.js rename www/addons/{mod_quiz/questions => qtype}/calculatedsimple/directive.js (66%) rename www/addons/{mod_quiz/questions => qtype}/calculatedsimple/handlers.js (56%) create mode 100644 www/addons/qtype/calculatedsimple/main.js rename www/addons/{mod_quiz/questions => qtype}/description/directive.js (73%) rename www/addons/{mod_quiz/questions => qtype}/description/handlers.js (57%) create mode 100644 www/addons/qtype/description/main.js rename www/addons/{mod_quiz/questions => qtype}/description/template.html (100%) rename www/addons/{mod_quiz/questions => qtype}/essay/directive.js (75%) rename www/addons/{mod_quiz/questions => qtype}/essay/handlers.js (58%) create mode 100644 www/addons/qtype/essay/main.js rename www/addons/{mod_quiz/questions => qtype}/essay/template.html (57%) rename www/addons/{mod_quiz/questions => qtype}/gapselect/directive.js (74%) rename www/addons/{mod_quiz/questions => qtype}/gapselect/handlers.js (57%) create mode 100644 www/addons/qtype/gapselect/main.js rename www/addons/{mod_quiz/questions => qtype}/gapselect/scss/styles.scss (91%) rename www/addons/{mod_quiz/questions/multianswer => qtype/gapselect}/template.html (73%) rename www/addons/{mod_quiz/questions => qtype}/match/directive.js (68%) rename www/addons/{mod_quiz/questions => qtype}/match/handlers.js (58%) create mode 100644 www/addons/qtype/match/main.js rename www/addons/{mod_quiz/questions => qtype}/multianswer/directive.js (79%) rename www/addons/{mod_quiz/questions => qtype}/multianswer/handlers.js (57%) create mode 100644 www/addons/qtype/multianswer/main.js rename www/addons/{mod_quiz/questions => qtype}/multianswer/scss/styles.scss (94%) rename www/addons/{mod_quiz/questions/randomsamatch => qtype/multianswer}/template.html (68%) rename www/addons/{mod_quiz/questions => qtype}/multichoice/directive.js (67%) rename www/addons/{mod_quiz/questions => qtype}/multichoice/handlers.js (57%) create mode 100644 www/addons/qtype/multichoice/main.js rename www/addons/{mod_quiz/questions => qtype}/numerical/directive.js (67%) rename www/addons/{mod_quiz/questions => qtype}/numerical/handlers.js (57%) create mode 100644 www/addons/qtype/numerical/main.js rename www/addons/{mod_quiz/questions => qtype}/randomsamatch/directive.js (67%) rename www/addons/{mod_quiz/questions => qtype}/randomsamatch/handlers.js (57%) create mode 100644 www/addons/qtype/randomsamatch/main.js rename www/addons/{mod_quiz/questions => qtype}/shortanswer/directive.js (67%) rename www/addons/{mod_quiz/questions => qtype}/shortanswer/handlers.js (57%) create mode 100644 www/addons/qtype/shortanswer/main.js rename www/addons/{mod_quiz/questions => qtype}/truefalse/directive.js (67%) rename www/addons/{mod_quiz/questions => qtype}/truefalse/handlers.js (57%) create mode 100644 www/addons/qtype/truefalse/main.js diff --git a/gulpfile.js b/gulpfile.js index 699d93c95ca..972148b5375 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -103,7 +103,8 @@ gulp.task('watch', function() { gulp.task('build', function(done) { var dependencies = ["'mm.core'"], componentRegex = /core\/components\/([^\/]+)\/main.js/, - pluginRegex = /addons\/([^\/]+)\/main.js/; + pluginRegex = /addons\/([^\/]+)\/main.js/, + subpluginRegex = /addons\/([^\/]+)\/([^\/]+)\/main.js/; gulp.src(paths.js) .pipe(gulpSlash()) @@ -113,6 +114,10 @@ gulp.task('build', function(done) { dependencies.push("'mm.core." + file.path.match(componentRegex)[1] + "'"); } else if (pluginRegex.test(file.path)) { dependencies.push("'mm.addons." + file.path.match(pluginRegex)[1] + "'"); + } else if (subpluginRegex.test(file.path)) { + // It's a subplugin, use plugin_subplugin to identify it. + var matches = file.path.match(subpluginRegex); + dependencies.push("'mm.addons." + matches[1] + '_' + matches[2] + "'"); } })) @@ -144,7 +149,7 @@ gulp.task('lang', function() { return fs.readdirSync(dir) .filter(function(file) { return file.indexOf('.json') > -1; - }) + }); } /** @@ -181,8 +186,13 @@ gulp.task('lang', function() { } else if (filepath.indexOf('addons') == 0) { - var pluginName = filepath.replace('addons/', ''); - pluginName = pluginName.substr(0, pluginName.indexOf('/')); + var split = filepath.split('/'), + pluginName = split[1]; + + // Check if it's a subplugin. If so, we'll use plugin_subplugin. + if (split[2] != 'lang') { + pluginName = pluginName + '_' + split[2]; + } addProperties(merged, data[filepath], 'mma.'+pluginName+'.'); } else if (filepath.indexOf('core/assets/countries') == 0) { diff --git a/www/addons/mod_quiz/lang/en.json b/www/addons/mod_quiz/lang/en.json index e44a1254dee..d2383f5a0a6 100644 --- a/www/addons/mod_quiz/lang/en.json +++ b/www/addons/mod_quiz/lang/en.json @@ -1,5 +1,4 @@ { - "answer": "Answer", "attemptfirst": "First attempt", "attemptlast": "Last attempt", "attemptnumber": "Attempt", diff --git a/www/addons/mod_quiz/questions/gapselect/template.html b/www/addons/mod_quiz/questions/gapselect/template.html deleted file mode 100644 index 71f1830e77c..00000000000 --- a/www/addons/mod_quiz/questions/gapselect/template.html +++ /dev/null @@ -1,5 +0,0 @@ -
    -
    -

    {{ question.text }}

    -
    -
    diff --git a/www/addons/mod_quiz/questions/randomsamatch/scss/styles.scss b/www/addons/mod_quiz/questions/randomsamatch/scss/styles.scss deleted file mode 100644 index 1920bb8877d..00000000000 --- a/www/addons/mod_quiz/questions/randomsamatch/scss/styles.scss +++ /dev/null @@ -1,19 +0,0 @@ -// Style gapselect content a bit. All these styles are copied from Moodle. -.mm-quiz-gapselect-container { - p { - margin: 0 0 .5em; - } - - select { - height: 30px; - line-height: 30px; - display: inline-block; - border: 1px solid #ccc; - padding: 4px 6px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - margin-bottom: 10px; - } -} - diff --git a/www/addons/mod_quiz/scss/styles.scss b/www/addons/mod_quiz/scss/styles.scss index 18b7b64d51d..8bee3595ef4 100644 --- a/www/addons/mod_quiz/scss/styles.scss +++ b/www/addons/mod_quiz/scss/styles.scss @@ -25,7 +25,3 @@ p.mma-mod-quiz-warning { height: 250px; resize: none; } - -.item.row.mma-quiz-item-padding { - padding: $item-padding; // Prevent .row from overridding .item padding. -} diff --git a/www/addons/qtype/base/lang/en.json b/www/addons/qtype/base/lang/en.json new file mode 100644 index 00000000000..827e55cd71d --- /dev/null +++ b/www/addons/qtype/base/lang/en.json @@ -0,0 +1,3 @@ +{ + "answer": "Answer" +} diff --git a/www/addons/qtype/base/scss/styles.scss b/www/addons/qtype/base/scss/styles.scss new file mode 100644 index 00000000000..c4768ea7adf --- /dev/null +++ b/www/addons/qtype/base/scss/styles.scss @@ -0,0 +1,6 @@ + +.mma-qtype-textarea { + width: 100%; + height: 250px; + resize: none; +} diff --git a/www/addons/mod_quiz/questions/base/match.html b/www/addons/qtype/base/templates/match.html similarity index 74% rename from www/addons/mod_quiz/questions/base/match.html rename to www/addons/qtype/base/templates/match.html index 4b345c730a9..c94dc4f9d8d 100644 --- a/www/addons/mod_quiz/questions/base/match.html +++ b/www/addons/qtype/base/templates/match.html @@ -2,13 +2,13 @@

    {{ question.text }}

    -
    +
    -

    {{ row.text }}

    +

    {{ row.text }}

    - +
    diff --git a/www/addons/mod_quiz/questions/base/multi.html b/www/addons/qtype/base/templates/multi.html similarity index 80% rename from www/addons/mod_quiz/questions/base/multi.html rename to www/addons/qtype/base/templates/multi.html index 710db7946e9..ffa9090f41f 100644 --- a/www/addons/mod_quiz/questions/base/multi.html +++ b/www/addons/qtype/base/templates/multi.html @@ -6,9 +6,9 @@
  • -

    {{option.text}}

    +

    {{option.text}}