From fca428843e05ec62b3def5cb94e8f04fd57a9008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 22 Mar 2018 16:49:13 +0100 Subject: [PATCH] MOBILE-2339 feedback: Add Attempt review page --- src/addon/mod/feedback/lang/en.json | 1 + .../mod/feedback/pages/attempt/attempt.html | 34 +++ .../feedback/pages/attempt/attempt.module.ts | 37 +++ .../mod/feedback/pages/attempt/attempt.ts | 89 ++++++++ src/addon/mod/feedback/providers/feedback.ts | 35 +++ src/addon/mod/feedback/providers/helper.ts | 212 +++++++++++++++++- 6 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 src/addon/mod/feedback/pages/attempt/attempt.html create mode 100644 src/addon/mod/feedback/pages/attempt/attempt.module.ts create mode 100644 src/addon/mod/feedback/pages/attempt/attempt.ts diff --git a/src/addon/mod/feedback/lang/en.json b/src/addon/mod/feedback/lang/en.json index f9a3b44a2..1e4de3090 100644 --- a/src/addon/mod/feedback/lang/en.json +++ b/src/addon/mod/feedback/lang/en.json @@ -13,6 +13,7 @@ "non_anonymous": "User's name will be logged and shown with answers", "non_anonymous_entries": "Non anonymous entries ({{$a}})", "non_respondents_students": "Non respondents students ({{$a}})", + "not_selected": "Not selected", "not_started": "Not started", "overview": "Overview", "page_after_submit": "Completion message", diff --git a/src/addon/mod/feedback/pages/attempt/attempt.html b/src/addon/mod/feedback/pages/attempt/attempt.html new file mode 100644 index 000000000..f3e10d3ff --- /dev/null +++ b/src/addon/mod/feedback/pages/attempt/attempt.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + +

{{attempt.fullname}}

+

{{attempt.timemodified * 1000 | coreFormatDate:"LLL"}}

+
+ + +

{{ 'addon.mod_feedback.response_nr' |translate }}: {{attempt.number}} ({{ 'addon.mod_feedback.anonymous' |translate }})

+

{{attempt.timemodified * 1000 | coreFormatDate:"LLL"}}

+
+ + + + +

+ {{item.itemnumber}}. {{ item.name }} +

+

+
+
+
+
+
+
diff --git a/src/addon/mod/feedback/pages/attempt/attempt.module.ts b/src/addon/mod/feedback/pages/attempt/attempt.module.ts new file mode 100644 index 000000000..df162f477 --- /dev/null +++ b/src/addon/mod/feedback/pages/attempt/attempt.module.ts @@ -0,0 +1,37 @@ +// (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. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreComponentsModule } from '@components/components.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonModFeedbackComponentsModule } from '../../components/components.module'; +import { AddonModFeedbackAttemptPage } from './attempt'; + +@NgModule({ + declarations: [ + AddonModFeedbackAttemptPage, + ], + imports: [ + CoreDirectivesModule, + CoreComponentsModule, + CorePipesModule, + AddonModFeedbackComponentsModule, + IonicPageModule.forChild(AddonModFeedbackAttemptPage), + TranslateModule.forChild() + ], +}) +export class AddonModFeedbackAttemptPageModule {} diff --git a/src/addon/mod/feedback/pages/attempt/attempt.ts b/src/addon/mod/feedback/pages/attempt/attempt.ts new file mode 100644 index 000000000..f53f04ead --- /dev/null +++ b/src/addon/mod/feedback/pages/attempt/attempt.ts @@ -0,0 +1,89 @@ +// (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. + +import { Component } from '@angular/core'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; +import { AddonModFeedbackProvider } from '../../providers/feedback'; +import { AddonModFeedbackHelperProvider } from '../../providers/helper'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; + +/** + * Page that displays a feedback attempt review. + */ +@IonicPage({ segment: 'addon-mod-feedback-attempt' }) +@Component({ + selector: 'page-addon-mod-feedback-attempt', + templateUrl: 'attempt.html', +}) +export class AddonModFeedbackAttemptPage { + + protected feedbackId: number; + + attempt: any; + items: any; + componentId: number; + component = AddonModFeedbackProvider.COMPONENT; + feedbackLoaded = false; + + constructor(navParams: NavParams, protected feedbackProvider: AddonModFeedbackProvider, protected navCtrl: NavController, + protected domUtils: CoreDomUtilsProvider, protected feedbackHelper: AddonModFeedbackHelperProvider, + protected textUtils: CoreTextUtilsProvider) { + this.feedbackId = navParams.get('feedbackId') || 0; + this.attempt = navParams.get('attempt') || false; + this.componentId = navParams.get('moduleId'); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchData(); + } + + /** + * Fetch all the data required for the view. + * + * @return {Promise} Promise resolved when done. + */ + fetchData(): Promise { + return this.feedbackProvider.getItems(this.feedbackId).then((items) => { + // Add responses and format items. + this.items = items.items.map((item) => { + if (item.typ == 'label') { + item.submittedValue = this.textUtils.replacePluginfileUrls(item.presentation, item.itemfiles); + } else { + for (const x in this.attempt.responses) { + if (this.attempt.responses[x].id == item.id) { + item.submittedValue = this.attempt.responses[x].printval; + delete this.attempt.responses[x]; + break; + } + } + } + + return this.feedbackHelper.getItemForm(item, true); + }); + + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + // Some call failed on first fetch, go back. + this.navCtrl.pop(); + + return Promise.reject(null); + }).finally(() => { + this.feedbackLoaded = true; + }); + } +} diff --git a/src/addon/mod/feedback/providers/feedback.ts b/src/addon/mod/feedback/providers/feedback.ts index bc85041c1..8a358a9b5 100644 --- a/src/addon/mod/feedback/providers/feedback.ts +++ b/src/addon/mod/feedback/providers/feedback.ts @@ -25,6 +25,11 @@ import { CoreFilepoolProvider } from '@providers/filepool'; export class AddonModFeedbackProvider { static COMPONENT = 'mmaModFeedback'; static FORM_SUBMITTED = 'addon_mod_feedback_form_submitted'; + static LINE_SEP = '|'; + static MULTICHOICE_TYPE_SEP = '>>>>>'; + static MULTICHOICE_ADJUST_SEP = '<<<<<'; + static MULTICHOICE_HIDENOSELECT = 'h'; + static MULTICHOICERATED_VALUE_SEP = '####'; protected ROOT_CACHE_KEY = this.ROOT_CACHE_KEY + ''; protected logger; @@ -356,6 +361,36 @@ export class AddonModFeedbackProvider { return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache); } + /** + * Returns the items (questions) in the given feedback. + * + * @param {number} feedbackId Feedback ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the info is retrieved. + */ + getItems(feedbackId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + feedbackid: feedbackId + }, + preSets = { + cacheKey: this.getItemsDataCacheKey(feedbackId) + }; + + return site.read('mod_feedback_get_items', params, preSets); + }); + } + + /** + * Get cache key for get items feedback data WS calls. + * + * @param {number} feedbackId Feedback ID. + * @return {string} Cache key. + */ + protected getItemsDataCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':items'; + } + /** * Retrieves a list of students who didn't submit the feedback. * diff --git a/src/addon/mod/feedback/providers/helper.ts b/src/addon/mod/feedback/providers/helper.ts index 81a04a919..a6c9b48c3 100644 --- a/src/addon/mod/feedback/providers/helper.ts +++ b/src/addon/mod/feedback/providers/helper.ts @@ -16,6 +16,9 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; import { AddonModFeedbackProvider } from './feedback'; import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { TranslateService } from '@ngx-translate/core'; +import * as moment from 'moment'; /** * Service that provides helper functions for feedbacks. @@ -23,7 +26,12 @@ import { CoreUserProvider } from '@core/user/providers/user'; @Injectable() export class AddonModFeedbackHelperProvider { - constructor(protected feedbackProvider: AddonModFeedbackProvider, protected userProvider: CoreUserProvider) { + protected MODE_RESPONSETIME = 1; + protected MODE_COURSE = 2; + protected MODE_CATEGORY = 3; + + constructor(protected feedbackProvider: AddonModFeedbackProvider, protected userProvider: CoreUserProvider, + protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService) { } /** @@ -161,4 +169,206 @@ export class AddonModFeedbackHelperProvider { return navCtrl.push(pageName, stateParams); } + /** + * Helper funtion for item type Label. + * + * @param {any} item Item to process. + * @return {any} Item processed to show form. + */ + protected getItemFormLabel(item: any): any { + item.template = 'label'; + item.name = ''; + item.presentation = this.textUtils.replacePluginfileUrls(item.presentation, item.itemfiles); + + return item; + } + + /** + * Helper funtion for item type Info. + * + * @param {any} item Item to process. + * @return {any} Item processed to show form. + */ + protected getItemFormInfo(item: any): any { + item.template = 'label'; + + const type = parseInt(item.presentation, 10); + + if (type == this.MODE_COURSE || type == this.MODE_CATEGORY) { + item.presentation = item.otherdata; + item.value = typeof item.rawValue != 'undefined' ? item.rawValue : item.otherdata; + } else if (type == this.MODE_RESPONSETIME) { + item.value = '__CURRENT__TIMESTAMP__'; + const tempValue = typeof item.rawValue != 'undefined' ? item.rawValue * 1000 : new Date().getTime(); + item.presentation = moment(tempValue).format('LLL'); + } else { + // Errors on item, return false. + return false; + } + + return item; + } + + /** + * Helper funtion for item type Numeric. + * + * @param {any} item Item to process. + * @return {any} Item processed to show form. + */ + protected getItemFormNumeric(item: any): any { + item.template = 'numeric'; + + const range = item.presentation.split(AddonModFeedbackProvider.LINE_SEP) || []; + item.rangefrom = range.length > 0 ? parseInt(range[0], 10) || '' : ''; + item.rangeto = range.length > 1 ? parseInt(range[1], 10) || '' : ''; + item.value = typeof item.rawValue != 'undefined' ? parseFloat(item.rawValue) : ''; + + return item; + } + + /** + * Helper funtion for item type Text field. + * + * @param {any} item Item to process. + * @return {any} Item processed to show form. + */ + protected getItemFormTextfield(item: any): any { + item.template = 'textfield'; + item.length = item.presentation.split(AddonModFeedbackProvider.LINE_SEP)[1] || 255; + item.value = typeof item.rawValue != 'undefined' ? item.rawValue : ''; + + return item; + } + + /** + * Helper funtion for item type Textarea. + * + * @param {any} item Item to process. + * @return {any} Item processed to show form. + */ + protected getItemFormTextarea(item: any): any { + item.template = 'textarea'; + item.value = typeof item.rawValue != 'undefined' ? item.rawValue : ''; + + return item; + } + + /** + * Helper funtion for item type Multichoice. + * + * @param {any} item Item to process. + * @return {any} Item processed to show form. + */ + protected getItemFormMultichoice(item: any): any { + let parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP) || []; + item.subtype = parts.length > 0 && parts[0] ? parts[0] : 'r'; + item.template = 'multichoice-' + item.subtype; + + item.presentation = parts.length > 1 ? parts[1] : ''; + if (item.subtype != 'd') { + parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP) || []; + item.presentation = parts.length > 0 ? parts[0] : ''; + // Horizontal are not supported right now. item.horizontal = parts.length > 1 && !!parts[1]; + } else { + item.class = 'item-select'; + } + + item.choices = item.presentation.split(AddonModFeedbackProvider.LINE_SEP) || []; + item.choices = item.choices.map((choice, index) => { + const weightValue = choice.split(AddonModFeedbackProvider.MULTICHOICERATED_VALUE_SEP) || ['']; + choice = weightValue.length == 1 ? weightValue[0] : '(' + weightValue[0] + ') ' + weightValue[1]; + + return {value: index + 1, label: choice}; + }); + + if (item.subtype === 'r' && item.options.search(AddonModFeedbackProvider.MULTICHOICE_HIDENOSELECT) == -1) { + item.choices.unshift({value: 0, label: this.translate.instant('addon.mod_feedback.not_selected')}); + item.value = typeof item.rawValue != 'undefined' ? parseInt(item.rawValue, 10) : 0; + } else if (item.subtype === 'd') { + item.choices.unshift({value: 0, label: ''}); + item.value = typeof item.rawValue != 'undefined' ? parseInt(item.rawValue, 10) : 0; + } else if (item.subtype === 'c') { + if (typeof item.rawValue == 'undefined') { + item.value = ''; + } else { + item.rawValue = '' + item.rawValue; + const values = item.rawValue.split(AddonModFeedbackProvider.LINE_SEP); + item.choices.forEach((choice) => { + for (const x in values) { + if (choice.value == values[x]) { + choice.checked = true; + + return; + } + } + }); + } + } else { + item.value = typeof item.rawValue != 'undefined' ? parseInt(item.rawValue, 10) : ''; + } + + return item; + } + + /** + * Helper funtion for item type Captcha. + * + * @param {any} item Item to process. + * @return {any} Item processed to show form. + */ + protected getItemFormCaptcha(item: any): any { + const data = this.textUtils.parseJSON(item.otherdata); + if (data && data.length > 3) { + item.captcha = { + challengehash: data[0], + imageurl: data[1], + jsurl: data[2], + recaptchapublickey: data[3] + }; + } + item.template = 'captcha'; + item.value = ''; + + return item; + } + + /** + * Process and returns item to print form. + * + * @param {any} item Item to process. + * @param {boolean} preview Previewing options. + * @return {any} Item processed to show form. + */ + getItemForm(item: any, preview: boolean): any { + switch (item.typ) { + case 'label': + return this.getItemFormLabel(item); + case 'info': + return this.getItemFormInfo(item); + case 'numeric': + return this.getItemFormNumeric(item); + case 'textfield': + return this.getItemFormTextfield(item); + case 'textarea': + return this.getItemFormTextarea(item); + case 'multichoice': + return this.getItemFormMultichoice(item); + case 'multichoicerated': + return this.getItemFormMultichoice(item); + case 'pagebreak': + if (!preview) { + // Pagebreaks are only used on preview. + return false; + } + break; + case 'captcha': + // Captcha is not supported right now. However label will be shown. + return this.getItemFormCaptcha(item); + default: + return false; + } + + return item; + } + }