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 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ '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 }}
+
+
{{ attempt.readableMark }}
+
{{ attempt.readableGrade }}
+
+ {{ 'mma.mod_quiz.review' | translate }}
+
+
+
+
+
+
+
+
+
+
{{ gradeResult }}
+
{{ 'mm.course.overriddennotice' | translate }}
+
+
{{ 'mma.mod_quiz.comment' | translate }}
+
{{ gradebookFeedback }}
+
+
+
{{ 'mma.mod_quiz.overallfeedback' | translate }}
+
{{ overallFeedback }}
+
+
+
+
+
+
+
{{ 'mma.mod_quiz.noquestions' | translate }}
+
+
+ {{ buttonText | 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 }}
+
+
+ {{ 'mma.mod_quiz.review' | translate }}
+
+
+
+
+
\ 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 @@
{{ 'mma.mod_quiz.review' | translate }}
-
+
@@ -61,7 +61,7 @@
-
+
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 }}
+
{{ buttonText | translate }}
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 }}
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 @@
+
+
+
+
+
+
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 }}
+
+
+
+ {{ 'mma.mod_quiz.startattempt' | translate }}
+
+
+ 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 @@
+
+
+
+
+
+
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 @@
-
-
-
-
-
-
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 @@
{{ 'mma.mod_quiz.startattempt' | translate }}
-
- TODO
+
+
+
+
{{ 'mma.mod_quiz.questionno' | translate:{$a: question.number} }}
+
{{ 'mma.mod_quiz.information' | translate }}
+
+
+
+
{{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(/