From 978f69ea507387b29444f6af91e6e1ecc994e932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 9 Mar 2018 12:29:11 +0100 Subject: [PATCH 1/2] MOBILE-2351 survey: Implement the survey module --- .../mod/resource/components/index/index.ts | 1 - .../survey/components/components.module.ts | 45 +++ .../mod/survey/components/index/index.html | 105 +++++++ .../mod/survey/components/index/index.scss | 33 +++ .../mod/survey/components/index/index.ts | 234 +++++++++++++++ src/addon/mod/survey/lang/en.json | 9 + src/addon/mod/survey/pages/index/index.html | 16 + .../mod/survey/pages/index/index.module.ts | 33 +++ src/addon/mod/survey/pages/index/index.ts | 48 +++ src/addon/mod/survey/providers/helper.ts | 127 ++++++++ .../mod/survey/providers/link-handler.ts | 30 ++ .../mod/survey/providers/module-handler.ts | 69 +++++ src/addon/mod/survey/providers/offline.ts | 177 +++++++++++ .../mod/survey/providers/prefetch-handler.ts | 104 +++++++ src/addon/mod/survey/providers/survey.ts | 277 ++++++++++++++++++ .../mod/survey/providers/sync-cron-handler.ts | 47 +++ src/addon/mod/survey/providers/sync.ts | 181 ++++++++++++ src/addon/mod/survey/survey.module.ts | 57 ++++ src/app/app.ios.scss | 8 + src/app/app.md.scss | 8 + src/app/app.module.ts | 2 + src/app/app.scss | 16 + src/app/app.wp.scss | 8 + src/classes/base-sync.ts | 4 +- .../course/classes/main-activity-component.ts | 213 ++++++++++++++ 25 files changed, 1849 insertions(+), 3 deletions(-) create mode 100644 src/addon/mod/survey/components/components.module.ts create mode 100644 src/addon/mod/survey/components/index/index.html create mode 100644 src/addon/mod/survey/components/index/index.scss create mode 100644 src/addon/mod/survey/components/index/index.ts create mode 100644 src/addon/mod/survey/lang/en.json create mode 100644 src/addon/mod/survey/pages/index/index.html create mode 100644 src/addon/mod/survey/pages/index/index.module.ts create mode 100644 src/addon/mod/survey/pages/index/index.ts create mode 100644 src/addon/mod/survey/providers/helper.ts create mode 100644 src/addon/mod/survey/providers/link-handler.ts create mode 100644 src/addon/mod/survey/providers/module-handler.ts create mode 100644 src/addon/mod/survey/providers/offline.ts create mode 100644 src/addon/mod/survey/providers/prefetch-handler.ts create mode 100644 src/addon/mod/survey/providers/survey.ts create mode 100644 src/addon/mod/survey/providers/sync-cron-handler.ts create mode 100644 src/addon/mod/survey/providers/sync.ts create mode 100644 src/addon/mod/survey/survey.module.ts create mode 100644 src/core/course/classes/main-activity-component.ts diff --git a/src/addon/mod/resource/components/index/index.ts b/src/addon/mod/resource/components/index/index.ts index e52881649..85fb6a20f 100644 --- a/src/addon/mod/resource/components/index/index.ts +++ b/src/addon/mod/resource/components/index/index.ts @@ -45,7 +45,6 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource protected translate: TranslateService, private prefetchHandler: AddonModResourcePrefetchHandler, private resourceHelper: AddonModResourceHelperProvider) { super(textUtils, courseHelper, translate, domUtils); - } /** diff --git a/src/addon/mod/survey/components/components.module.ts b/src/addon/mod/survey/components/components.module.ts new file mode 100644 index 000000000..7760befed --- /dev/null +++ b/src/addon/mod/survey/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { AddonModSurveyIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModSurveyIndexComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule + ], + providers: [ + ], + exports: [ + AddonModSurveyIndexComponent + ], + entryComponents: [ + AddonModSurveyIndexComponent + ] +}) +export class AddonModSurveyComponentsModule {} diff --git a/src/addon/mod/survey/components/index/index.html b/src/addon/mod/survey/components/index/index.html new file mode 100644 index 000000000..b37a5ef9f --- /dev/null +++ b/src/addon/mod/survey/components/index/index.html @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + +

{{ 'addon.mod_survey.surveycompletednograph' | translate }}

+ + + {{ 'addon.mod_survey.results' | translate }} + +
+ + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ + +
+ + + +
+

{{ question.text }}

+ + + +
{{ 'addon.mod_survey.responses' | translate }}
+
+ +
{{ option }}
+
+
+
+ +

{{ question.intro }}

+
+
+ + + + + + {{question.num}}. {{ question.text }} + + + + + + + + + {{ 'core.choose' | translate }} + {{option}} + + + + + + + + + + + {{question.num}}. {{ question.text }} + + + + {{option}} + + + + + + + + {{question.num}}. {{ question.text }} + + + + + +
+ + + + +
+ +
diff --git a/src/addon/mod/survey/components/index/index.scss b/src/addon/mod/survey/components/index/index.scss new file mode 100644 index 000000000..d9cbd7ee7 --- /dev/null +++ b/src/addon/mod/survey/components/index/index.scss @@ -0,0 +1,33 @@ +addon-mod-survey-index { + + .label, .label[stacked] { + font-size: initial; + color: $text-color; + } + + .addon-mod_survey-question { + border-top: 1px solid $gray; + } + + ion-grid { + background-color: $white; + } + + ion-select { + float: right; + max-width: none; + .select-text { + white-space: normal; + text-align: right; + } + } + + .even { + background-color: $gray-light; + } + + .addon-mod_survey-textarea textarea { + height: 100px; + border: 1px solid $gray-dark; + } +} \ No newline at end of file diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts new file mode 100644 index 000000000..81a742e29 --- /dev/null +++ b/src/addon/mod/survey/components/index/index.ts @@ -0,0 +1,234 @@ +// (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, Optional } from '@angular/core'; +import { Content } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { Network } from '@ionic-native/network'; +import { CoreAppProvider } from '@providers/app'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { AddonModSurveyProvider } from '../../providers/survey'; +import { AddonModSurveyHelperProvider } from '../../providers/helper'; +import { AddonModSurveyOfflineProvider } from '../../providers/offline'; +import { AddonModSurveySyncProvider } from '../../providers/sync'; + +/** + * Component that displays a survey. + */ +@Component({ + selector: 'addon-mod-survey-index', + templateUrl: 'index.html', +}) +export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityComponent { + component = AddonModSurveyProvider.COMPONENT; + moduleName = 'survey'; + + survey: any; + questions: any; + answers = {}; + + protected userId: number; + protected syncEventName = AddonModSurveySyncProvider.AUTO_SYNCED; + + constructor(private surveyProvider: AddonModSurveyProvider, protected courseProvider: CoreCourseProvider, + protected domUtils: CoreDomUtilsProvider, protected appProvider: CoreAppProvider, + protected courseHelper: CoreCourseHelperProvider, protected translate: TranslateService, network: Network, + private surveyHelper: AddonModSurveyHelperProvider, protected sitesProvider: CoreSitesProvider, + protected eventsProvider: CoreEventsProvider, private surveyOffline: AddonModSurveyOfflineProvider, + private surveySync: AddonModSurveySyncProvider, @Optional() private content: Content, + protected textUtils: CoreTextUtilsProvider) { + super(textUtils, courseHelper, translate, domUtils, sitesProvider, courseProvider, network, appProvider, eventsProvider); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.userId = this.sitesProvider.getCurrentSiteUserId(); + + this.loadContent(false, true).then(() => { + this.surveyProvider.logView(this.survey.id).then(() => { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }); + }); + } + + /** + * Perform the invalidate content function. + * + * @return {Promise} Resolved when done. + */ + protected invalidateContent(): Promise { + const promises = []; + + promises.push(this.surveyProvider.invalidateSurveyData(this.courseId)); + if (this.survey) { + promises.push(this.surveyProvider.invalidateQuestions(this.survey.id)); + } + + return Promise.all(promises); + } + + /** + * Compares sync event data with current data to check if refresh content is needed. + * + * @param {any} syncEventData Data receiven on sync observer. + * @return {boolean} True if refresh is needed, false otherwise. + */ + protected isRefreshSyncNeeded(syncEventData: any): boolean { + if (this.survey && syncEventData.surveyId == this.survey.id && syncEventData.userId == this.userId) { + this.content.scrollToTop(); + + return true; + } + + return false; + } + + /** + * Download survey contents. + * + * @param {boolean} [refresh=false] If it's refreshing content. + * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + return this.surveyProvider.getSurvey(this.courseId, this.module.id).then((survey) => { + this.survey = survey; + + this.description = survey.intro || survey.description; + this.dataRetrieved.emit(survey); + + if (sync) { + // Try to synchronize the survey. + return this.syncActivity(showErrors).then((answersSent) => { + if (answersSent) { + // Answers were sent, update the survey. + return this.surveyProvider.getSurvey(this.courseId, this.module.id).then((survey) => { + this.survey = survey; + }); + } + }); + } + }).then(() => { + // Check if there are answers stored in offline. + return this.surveyOffline.hasAnswers(this.survey.id); + }).then((hasOffline) => { + this.hasOffline = this.survey.surveydone ? false : hasOffline; + + if (!this.survey.surveydone && !this.hasOffline) { + return this.fetchQuestions(); + } + }).then(() => { + // All data obtained, now fill the context menu. + this.fillContextMenu(refresh); + }); + } + + /** + * Convenience function to get survey questions. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchQuestions(): Promise { + return this.surveyProvider.getQuestions(this.survey.id).then((questions) => { + this.questions = this.surveyHelper.formatQuestions(questions); + + // Init answers object. + this.questions.forEach((q) => { + if (q.name) { + const isTextArea = q.multi && q.multi.length === 0 && q.type === 0; + this.answers[q.name] = q.required ? -1 : (isTextArea ? '' : '0'); + } + + if (q.multi && !q.multi.length && q.parent === 0 && q.type > 0) { + // Options shown in a select. Remove all HTML. + q.options = q.options.map((option) => { + return this.textUtils.cleanTags(option); + }); + } + }); + }); + } + + /** + * Check if answers are valid to be submitted. + * + * @return {boolean} If answers are valid + */ + isValidResponse(): boolean { + for (const x in this.answers) { + if (this.answers[x] === -1) { + return false; + } + } + + return true; + } + + /** + * Save options selected. + */ + submit(): void { + this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => { + const answers = [], + modal = this.domUtils.showModalLoading('core.sending', true); + + for (const x in this.answers) { + answers.push({ + key: x, + value: this.answers[x] + }); + } + + this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then(() => { + this.content.scrollToTop(); + + return this.refreshContent(false); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'addon.mod_survey.cannotsubmitsurvey', true); + }).finally(() => { + modal.dismiss(); + }); + }); + } + + /** + * Performs the sync of the activity. + * + * @return {Promise} Promise resolved when done. + */ + protected sync(): Promise { + return this.surveySync.syncSurvey(this.survey.id, this.userId); + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param {any} result Data returned on the sync function. + * @return {boolean} If suceed or not. + */ + protected hasSyncSucceed(result: any): boolean { + return result.answersSent; + } +} diff --git a/src/addon/mod/survey/lang/en.json b/src/addon/mod/survey/lang/en.json new file mode 100644 index 000000000..a8769f7cb --- /dev/null +++ b/src/addon/mod/survey/lang/en.json @@ -0,0 +1,9 @@ +{ + "cannotsubmitsurvey": "Sorry, there was a problem submitting your survey. Please try again.", + "errorgetsurvey": "Error getting survey data.", + "ifoundthat": "I found that", + "ipreferthat": "I prefer that", + "responses": "Responses", + "results": "Results", + "surveycompletednograph": "You have completed this survey." +} \ No newline at end of file diff --git a/src/addon/mod/survey/pages/index/index.html b/src/addon/mod/survey/pages/index/index.html new file mode 100644 index 000000000..21e4ff081 --- /dev/null +++ b/src/addon/mod/survey/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/survey/pages/index/index.module.ts b/src/addon/mod/survey/pages/index/index.module.ts new file mode 100644 index 000000000..5174d45a2 --- /dev/null +++ b/src/addon/mod/survey/pages/index/index.module.ts @@ -0,0 +1,33 @@ +// (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 { AddonModSurveyComponentsModule } from '../../components/components.module'; +import { AddonModSurveyIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModSurveyIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModSurveyComponentsModule, + IonicPageModule.forChild(AddonModSurveyIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModSurveyIndexPageModule {} diff --git a/src/addon/mod/survey/pages/index/index.ts b/src/addon/mod/survey/pages/index/index.ts new file mode 100644 index 000000000..3c18ae76f --- /dev/null +++ b/src/addon/mod/survey/pages/index/index.ts @@ -0,0 +1,48 @@ +// (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, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { AddonModSurveyIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a survey. + */ +@IonicPage({ segment: 'addon-mod-survey-index' }) +@Component({ + selector: 'page-addon-mod-survey-index', + templateUrl: 'index.html', +}) +export class AddonModSurveyIndexPage { + @ViewChild(AddonModSurveyIndexComponent) surveyComponent: AddonModSurveyIndexComponent; + + title: string; + module: any; + courseId: number; + + constructor(navParams: NavParams) { + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.title = this.module.name; + } + + /** + * Update some data based on the survey instance. + * + * @param {any} survey Survey instance. + */ + updateData(survey: any): void { + this.title = survey.name || this.title; + } +} diff --git a/src/addon/mod/survey/providers/helper.ts b/src/addon/mod/survey/providers/helper.ts new file mode 100644 index 000000000..45c30fb40 --- /dev/null +++ b/src/addon/mod/survey/providers/helper.ts @@ -0,0 +1,127 @@ +// (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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Service that provides helper functions for surveys. + */ +@Injectable() +export class AddonModSurveyHelperProvider { + + constructor(private translate: TranslateService) { } + + /** + * Turns a string with values separated by commas into an array. + * + * @param {string} value Value to convert. + * @return {string[]} Array. + */ + protected commaStringToArray(value: string): string[] { + if (typeof value == 'string') { + if (value.length > 0) { + return value.split(','); + } + + return []; + } + + return value; + } + + /** + * Gets the parent questions and puts them in an object: ID -> question. + * + * @param {Object[]} questions Questions. + * @return {any} Object with parent questions. + */ + protected getParentQuestions(questions: any[]): any { + const parents = {}; + + questions.forEach((question) => { + if (question.parent === 0) { + parents[question.id] = question; + } + }); + + return parents; + } + + /** + * Format a questions list, turning "multi" and "options" strings into arrays and adding the properties + * 'num' and 'name'. + * + * @param {any[]} questions Questions. + * @return {any[]} Promise resolved with the formatted questions. + */ + formatQuestions(questions: any[]): any[] { + + const strIPreferThat = this.translate.instant('addon.mod_survey.ipreferthat'), + strIFoundThat = this.translate.instant('addon.mod_survey.ifoundthat'), + strChoose = this.translate.instant('core.choose'), + formatted = [], + parents = this.getParentQuestions(questions); + + let num = 1; + + questions.forEach((question) => { + // Copy the object to prevent modifying the original. + const q1 = Object.assign({}, question), + parent = parents[q1.parent]; + + // Turn multi and options into arrays. + q1.multi = this.commaStringToArray(q1.multi); + q1.options = this.commaStringToArray(q1.options); + + if (parent) { + // It's a sub-question. + q1.required = true; + + if (parent.type === 1 || parent.type === 2) { + // One answer question. Set its name and add it to the returned array. + q1.name = 'q' + (parent.type == 2 ? 'P' : '') + q1.id; + q1.num = num++; + } else { + // Two answers per question (COLLES P&A). We'll add two questions. + const q2 = Object.assign({}, q1); + + q1.text = strIPreferThat + ' ' + q1.text; + q1.name = 'qP' + q1.id; + q1.num = num++; + formatted.push(q1); + + q2.text = strIFoundThat + ' ' + q2.text; + q2.name = 'q' + q1.id; + q2.num = num++; + formatted.push(q2); + + return; + } + } else if (q1.multi && q1.multi.length === 0) { + // It's a single question. + q1.name = 'q' + q1.id; + q1.num = num++; + if (q1.type > 0) { // Add "choose" option since this question is not required. + q1.options.unshift(strChoose); + } + } + + formatted.push(q1); + }); + + return formatted; + } + +} diff --git a/src/addon/mod/survey/providers/link-handler.ts b/src/addon/mod/survey/providers/link-handler.ts new file mode 100644 index 000000000..5db79d39b --- /dev/null +++ b/src/addon/mod/survey/providers/link-handler.ts @@ -0,0 +1,30 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModSurveyProvider } from './survey'; + +/** + * Handler to treat links to survey. + */ +@Injectable() +export class AddonModSurveyLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModSurveyLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider) { + super(courseHelper, AddonModSurveyProvider.COMPONENT, 'survey'); + } +} diff --git a/src/addon/mod/survey/providers/module-handler.ts b/src/addon/mod/survey/providers/module-handler.ts new file mode 100644 index 000000000..a2255adde --- /dev/null +++ b/src/addon/mod/survey/providers/module-handler.ts @@ -0,0 +1,69 @@ +// (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 { Injectable } from '@angular/core'; +import { NavController, NavOptions } from 'ionic-angular'; +import { AddonModSurveyIndexComponent } from '../components/index/index'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; + +/** + * Handler to support survey modules. + */ +@Injectable() +export class AddonModSurveyModuleHandler implements CoreCourseModuleHandler { + name = 'survey'; + + constructor(private courseProvider: CoreCourseProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean { + return true; + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @param {number} sectionId The section ID. + * @return {CoreCourseModuleHandlerData} Data to render the module. + */ + getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + return { + icon: this.courseProvider.getModuleIconSrc('survey'), + title: module.name, + class: 'addon-mod_survey-handler', + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + navCtrl.push('AddonModSurveyIndexPage', {module: module, courseId: courseId}, options); + } + }; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any} The component to use, undefined if not found. + */ + getMainComponent(course: any, module: any): any { + return AddonModSurveyIndexComponent; + } +} diff --git a/src/addon/mod/survey/providers/offline.ts b/src/addon/mod/survey/providers/offline.ts new file mode 100644 index 000000000..f1691c346 --- /dev/null +++ b/src/addon/mod/survey/providers/offline.ts @@ -0,0 +1,177 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; + +/** + * Service to handle Offline survey. + */ +@Injectable() +export class AddonModSurveyOfflineProvider { + + protected logger; + + // Variables for database. + protected SURVEY_TABLE = 'mma_mod_survey_answers'; + protected tablesSchema = [ + { + name: this.SURVEY_TABLE, + columns: [ + { + name: 'surveyid', + type: 'INTEGER' + }, + { + name: 'name', + type: 'TEXT' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'userid', + type: 'INTEGER' + }, + { + name: 'answers', + type: 'TEXT' + }, + { + name: 'timecreated', + type: 'INTEGER' + } + ], + primaryKeys: ['surveyid', 'userid'] + } + ]; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider) { + this.logger = logger.getInstance('AddonModSurveyOfflineProvider'); + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Delete a survey answers. + * + * @param {number} surveyId Survey ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User the answers belong to. If not defined, current user in site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + deleteSurveyAnswers(surveyId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.getDb().deleteRecords(this.SURVEY_TABLE, {surveyid: surveyId, userid: userId}); + }); + } + + /** + * Get all the stored data from all the surveys. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with answers. + */ + getAllData(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getAllRecords(this.SURVEY_TABLE).then((entries) => { + return entries.map((entry) => { + entry.answers = this.textUtils.parseJSON(entry.answers); + }); + }); + }); + } + + /** + * Get a survey stored answers. + * + * @param {number} surveyId Survey ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User the answers belong to. If not defined, current user in site. + * @return {Promise} Promise resolved with the answers. + */ + getSurveyAnswers(surveyId: number, siteId?: string, userId?: number): Promise { + return this.getSurveyData(surveyId, siteId, userId).then((entry) => { + return entry.answers || []; + }).catch(() => { + return []; + }); + } + + /** + * Get a survey stored data. + * + * @param {number} surveyId Survey ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User the answers belong to. If not defined, current user in site. + * @return {Promise} Promise resolved with the data. + */ + getSurveyData(surveyId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.getDb().getRecord(this.SURVEY_TABLE, {surveyid: surveyId, userid: userId}).then((entry) => { + entry.answers = this.textUtils.parseJSON(entry.answers); + + return entry; + }); + }); + } + + /** + * Check if there are offline answers to send. + * + * @param {number} surveyId Survey ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User the answers belong to. If not defined, current user in site. + * @return {Promise} Promise resolved with boolean: true if has offline answers, false otherwise. + */ + hasAnswers(surveyId: number, siteId?: string, userId?: number): Promise { + return this.getSurveyAnswers(surveyId, siteId, userId).then((answers) => { + return !!answers.length; + }); + } + + /** + * Save answers to be sent later. + * + * @param {number} surveyId Survey ID. + * @param {string} name Survey name. + * @param {number} courseId Course ID the survey belongs to. + * @param {any[]} answers Answers. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User the answers belong to. If not defined, current user in site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + saveAnswers(surveyId: number, name: string, courseId: number, answers: any[], siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const entry = { + surveyid: surveyId, + name: name, + courseid: courseId, + userid: userId, + answers: JSON.stringify(answers), + timecreated: new Date().getTime() + }; + + return site.getDb().insertOrUpdateRecord(this.SURVEY_TABLE, entry, {surveyid: surveyId, userid: userId}); + }); + } +} diff --git a/src/addon/mod/survey/providers/prefetch-handler.ts b/src/addon/mod/survey/providers/prefetch-handler.ts new file mode 100644 index 000000000..1dce26b38 --- /dev/null +++ b/src/addon/mod/survey/providers/prefetch-handler.ts @@ -0,0 +1,104 @@ +// (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 { Injectable, Injector } from '@angular/core'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; +import { AddonModSurveyProvider } from './survey'; +import { AddonModSurveyHelperProvider } from './helper'; + +/** + * Handler to prefetch surveys. + */ +@Injectable() +export class AddonModSurveyPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + name = 'survey'; + component = AddonModSurveyProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^answers$/; + + constructor(injector: Injector, protected surveyProvider: AddonModSurveyProvider, + protected surveyHelper: AddonModSurveyHelperProvider) { + super(injector); + } + + /** + * Download or prefetch the content. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files + * relative paths and make the package work in an iframe. Undefined to download the files + * in the filepool root survey. + * @return {Promise} Promise resolved when all content is downloaded. Data returned is not reliable. + */ + downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise { + const promises = []; + + promises.push(super.downloadOrPrefetch(module, courseId, prefetch)); + promises.push(this.surveyProvider.getSurvey(courseId, module.id).then((survey) => { + // If survey isn't answered, prefetch the questions. + if (!survey.surveydone) { + promises.push(this.surveyProvider.getQuestions(survey.id)); + } + })); + + return Promise.all(promises); + } + + /** + * Returns survey intro files. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved with list of intro files. + */ + getIntroFiles(module: any, courseId: number): Promise { + return this.surveyProvider.getSurvey(courseId, module.id).catch(() => { + // Not found, return undefined so module description is used. + }).then((survey) => { + return this.getIntroFilesFromInstance(module, survey); + }); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return this.surveyProvider.invalidateContent(moduleId, courseId); + } + + /** + * Invalidate WS calls needed to determine module status. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when invalidated. + */ + invalidateModule(module: any, courseId: number): Promise { + return this.surveyProvider.invalidateSurveyData(courseId); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/survey/providers/survey.ts b/src/addon/mod/survey/providers/survey.ts new file mode 100644 index 000000000..dc286b793 --- /dev/null +++ b/src/addon/mod/survey/providers/survey.ts @@ -0,0 +1,277 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { AddonModSurveyOfflineProvider } from './offline'; + +/** + * Service that provides some features for surveys. + */ +@Injectable() +export class AddonModSurveyProvider { + static COMPONENT = 'mmaModSurvey'; + + protected ROOT_CACHE_KEY = 'mmaModSurvey:'; + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, + private filepoolProvider: CoreFilepoolProvider, private utils: CoreUtilsProvider, + private surveyOffline: AddonModSurveyOfflineProvider) { + this.logger = logger.getInstance('AddonModSurveyProvider'); + } + + /** + * Get a survey's questions. + * + * @param {number} surveyId Survey ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the questions are retrieved. + */ + getQuestions(surveyId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + surveyid: surveyId + }, + preSets = { + cacheKey: this.getQuestionsCacheKey(surveyId) + }; + + return site.read('mod_survey_get_questions', params, preSets).then((response) => { + if (response.questions) { + return response.questions; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get cache key for survey questions WS calls. + * + * @param {number} surveyId Survey ID. + * @return {string} Cache key. + */ + protected getQuestionsCacheKey(surveyId: number): string { + return this.ROOT_CACHE_KEY + 'questions:' + surveyId; + } + + /** + * Get cache key for survey data WS calls. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getSurveyCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'survey:' + courseId; + } + + /** + * Get a survey data. + * + * @param {number} courseId Course ID. + * @param {string} key Name of the property to check. + * @param {any} value Value to search. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the survey is retrieved. + */ + protected getSurveyDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseids: [courseId] + }, + preSets = { + cacheKey: this.getSurveyCacheKey(courseId) + }; + + return site.read('mod_survey_get_surveys_by_courses', params, preSets).then((response) => { + if (response && response.surveys) { + const currentSurvey = response.surveys.find((survey) => { + return survey[key] == value; + }); + if (currentSurvey) { + return currentSurvey; + } + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get a survey by course module ID. + * + * @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 survey is retrieved. + */ + getSurvey(courseId: number, cmId: number, siteId?: string): Promise { + return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, siteId); + } + + /** + * Get a survey by ID. + * + * @param {number} courseId Course ID. + * @param {number} id Survey ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the survey is retrieved. + */ + getSurveyById(courseId: number, id: number, siteId?: string): Promise { + return this.getSurveyDataByKey(courseId, 'id', id, siteId); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID of the module. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + + promises.push(this.getSurvey(courseId, moduleId).then((survey) => { + const ps = []; + + // Do not invalidate wiki data before getting wiki info, we need it! + ps.push(this.invalidateSurveyData(courseId, siteId)); + ps.push(this.invalidateQuestions(survey.id, siteId)); + + return Promise.all(ps); + })); + + promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModSurveyProvider.COMPONENT, moduleId)); + + return this.utils.allPromises(promises); + } + + /** + * Invalidates survey questions. + * + * @param {number} surveyId Survey ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateQuestions(surveyId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getQuestionsCacheKey(surveyId)); + }); + } + + /** + * Invalidates survey data. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateSurveyData(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getSurveyCacheKey(courseId)); + }); + } + + /** + * Report the survey as being viewed. + * + * @param {number} id Module ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logView(id: number): Promise { + const params = { + surveyid: id + }; + + return this.sitesProvider.getCurrentSite().write('mod_survey_view_survey', params); + } + + /** + * Send survey answers. If cannot send them to Moodle, they'll be stored in offline to be sent later. + * + * @param {number} surveyId Survey ID. + * @param {string} name Survey name. + * @param {number} courseId Course ID the survey belongs to. + * @param {any[]} answers Answers. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean if success: true if answers were sent to server, + * false if stored in device. + */ + submitAnswers(surveyId: number, name: string, courseId: number, answers: any[], siteId?: string): Promise { + // Convenience function to store a survey to be synchronized later. + const storeOffline = (): Promise => { + return this.surveyOffline.saveAnswers(surveyId, name, courseId, answers, siteId).then(() => { + return false; + }); + }; + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!this.appProvider.isOnline()) { + // App is offline, store the message. + return storeOffline(); + } + + // If there's already answers to be sent to the server, discard it first. + return this.surveyOffline.deleteSurveyAnswers(surveyId, siteId).then(() => { + // Device is online, try to send them to server. + return this.submitAnswersOnline(surveyId, answers, siteId).then(() => { + return true; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the message so don't store it. + return Promise.reject(error); + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + }); + }); + } + + /** + * Send survey answers to Moodle. + * + * @param {number} surveyId Survey ID. + * @param {any[]} answers Answers. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when answers are successfully submitted. Rejected with object containing + * the error message (if any) and a boolean indicating if the error was returned by WS. + */ + submitAnswersOnline(surveyId: number, answers: any[], siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + surveyid: surveyId, + answers: answers + }; + + return site.write('mod_survey_submit_answers', params).then((response) => { + if (!response.status) { + // There was an error, and it should be translated already. + return this.utils.createFakeWSError(''); + } + }); + }); + } +} diff --git a/src/addon/mod/survey/providers/sync-cron-handler.ts b/src/addon/mod/survey/providers/sync-cron-handler.ts new file mode 100644 index 000000000..d26a67561 --- /dev/null +++ b/src/addon/mod/survey/providers/sync-cron-handler.ts @@ -0,0 +1,47 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@providers/cron'; +import { AddonModSurveySyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonModSurveySyncCronHandler implements CoreCronHandler { + name = 'AddonModSurveySyncCronHandler'; + + constructor(private surveySync: AddonModSurveySyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.surveySync.syncAllSurveys(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 600000; // 10 minutes. + } +} diff --git a/src/addon/mod/survey/providers/sync.ts b/src/addon/mod/survey/providers/sync.ts new file mode 100644 index 000000000..7c3aaa35e --- /dev/null +++ b/src/addon/mod/survey/providers/sync.ts @@ -0,0 +1,181 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModSurveyOfflineProvider } from './offline'; +import { AddonModSurveyProvider } from './survey'; +import { CoreEventsProvider } from '@providers/events'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync surveys. + */ +@Injectable() +export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_mod_survey_autom_synced'; + protected componentTranslate: string; + + constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, + protected appProvider: CoreAppProvider, private surveyOffline: AddonModSurveyOfflineProvider, + private eventsProvider: CoreEventsProvider, private surveyProvider: AddonModSurveyProvider, + private translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, + courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider) { + super('AddonModSurveySyncProvider', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils); + this.componentTranslate = courseProvider.translateModuleName('survey'); + } + + /** + * Get the ID of a survey sync. + * + * @param {number} surveyId Survey ID. + * @param {number} userId User the answers belong to. + * @return {string} Sync ID. + * @protected + */ + getSyncId (surveyId: number, userId: number): string { + return surveyId + '#' + userId; + } + + /** + * Try to synchronize all the surveys in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllSurveys(siteId?: string): Promise { + return this.syncOnSites('all surveys', this.syncAllSurveysFunc.bind(this), undefined, siteId); + } + + /** + * Sync all pending surveys on a site. + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllSurveysFunc(siteId?: string): Promise { + // Get all survey answers pending to be sent in the site. + return this.surveyOffline.getAllData(siteId).then((entries) => { + // Sync all surveys. + const promises = entries.map((entry) => { + return this.syncSurvey(entry.surveyid, entry.userid, siteId).then((result) => { + if (result && result.answersSent) { + // Sync successful, send event. + this.eventsProvider.trigger(AddonModSurveySyncProvider.AUTO_SYNCED, { + surveyId: entry.surveyid, + userId: entry.userid, + warnings: result.warnings + }, siteId); + } + }); + }); + + return Promise.all(promises); + }); + } + + /** + * Synchronize a survey. + * + * @param {number} surveyId Survey ID. + * @param {number} userId User the answers belong to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncSurvey(surveyId: number, userId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const syncId = this.getSyncId(surveyId, userId); + if (this.isSyncing(syncId, siteId)) { + // There's already a sync ongoing for this survey and user, return the promise. + return this.getOngoingSync(syncId, siteId); + } + + let courseId; + const result = { + warnings: [], + answersSent: false + }; + + this.logger.debug(`Try to sync survey '${surveyId}' for user '${userId}'`); + + // Get answers to be sent. + const syncPromise = this.surveyOffline.getSurveyData(surveyId, siteId, userId).catch(() => { + // No offline data found, return empty object. + return {}; + }).then((data) => { + if (!data.answers || !data.answers.length) { + // Nothing to sync. + return; + } + + if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + courseId = data.courseid; + + // Send the answers. + return this.surveyProvider.submitAnswersOnline(surveyId, data.answers, siteId).then(() => { + result.answersSent = true; + + // Answers sent, delete them. + return this.surveyOffline.deleteSurveyAnswers(surveyId, siteId, userId); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + + // The WebService has thrown an error, this means that answers cannot be submitted. Delete them. + result.answersSent = true; + + return this.surveyOffline.deleteSurveyAnswers(surveyId, siteId, userId).then(() => { + // Answers deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: data.name, + error: error.error + })); + }); + } + + // Couldn't connect to server, reject. + return Promise.reject(error && error.error); + }); + }).then(() => { + if (courseId) { + // Data has been sent to server, update survey data. + return this.surveyProvider.invalidateSurveyData(courseId, siteId).then(() => { + return this.surveyProvider.getSurveyById(courseId, surveyId, siteId); + }).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + // Sync finished, set sync time. + return this.setSyncTime(syncId, siteId); + }).then(() => { + return result; + }); + + return this.addOngoingSync(syncId, syncPromise, siteId); + } + +} diff --git a/src/addon/mod/survey/survey.module.ts b/src/addon/mod/survey/survey.module.ts new file mode 100644 index 000000000..beaea6d43 --- /dev/null +++ b/src/addon/mod/survey/survey.module.ts @@ -0,0 +1,57 @@ +// (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 { CoreCronDelegate } from '@providers/cron'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { AddonModSurveyComponentsModule } from './components/components.module'; +import { AddonModSurveyModuleHandler } from './providers/module-handler'; +import { AddonModSurveyProvider } from './providers/survey'; +import { AddonModSurveyLinkHandler } from './providers/link-handler'; +import { AddonModSurveyHelperProvider } from './providers/helper'; +import { AddonModSurveyPrefetchHandler } from './providers/prefetch-handler'; +import { AddonModSurveySyncProvider } from './providers/sync'; +import { AddonModSurveySyncCronHandler } from './providers/sync-cron-handler'; +import { AddonModSurveyOfflineProvider } from './providers/offline'; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonModSurveyComponentsModule + ], + providers: [ + AddonModSurveyProvider, + AddonModSurveyModuleHandler, + AddonModSurveyPrefetchHandler, + AddonModSurveyHelperProvider, + AddonModSurveyLinkHandler, + AddonModSurveySyncCronHandler, + AddonModSurveySyncProvider, + AddonModSurveyOfflineProvider + ] +}) +export class AddonModSurveyModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModSurveyModuleHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModSurveyPrefetchHandler, + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModSurveyLinkHandler, + cronDelegate: CoreCronDelegate, syncHandler: AddonModSurveySyncCronHandler) { + moduleDelegate.registerHandler(moduleHandler); + prefetchDelegate.registerHandler(prefetchHandler); + contentLinksDelegate.registerHandler(linkHandler); + cronDelegate.register(syncHandler); + } +} diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index 93f5fcfbf..6ec16b1d5 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -25,6 +25,14 @@ .ios .core-#{$color-name}-card { @extend .card-ios ; @extend .card-content-ios; + + &[icon-start] { + padding-left: $card-ios-padding-left * 2 + 20; + + ion-icon { + left: $card-ios-padding-left; + } + } } } diff --git a/src/app/app.md.scss b/src/app/app.md.scss index e9029a1dc..f96fff139 100644 --- a/src/app/app.md.scss +++ b/src/app/app.md.scss @@ -25,6 +25,14 @@ .md .core-#{$color-name}-card { @extend .card-md; @extend .card-content-md; + + &[icon-start] { + padding-left: $card-md-padding-left * 2 + 20; + + ion-icon { + left: $card-md-padding-left; + } + } } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4ac415d9d..1239b0b7b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -80,6 +80,7 @@ import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; import { AddonModFolderModule } from '@addon/mod/folder/folder.module'; import { AddonModPageModule } from '@addon/mod/page/page.module'; import { AddonModUrlModule } from '@addon/mod/url/url.module'; +import { AddonModSurveyModule } from '@addon/mod/survey/survey.module'; import { AddonMessagesModule } from '@addon/messages/messages.module'; import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module'; import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module'; @@ -164,6 +165,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModFolderModule, AddonModPageModule, AddonModUrlModule, + AddonModSurveyModule, AddonMessagesModule, AddonPushNotificationsModule, AddonRemoteThemesModule diff --git a/src/app/app.scss b/src/app/app.scss index 84557fa0e..c42c14c87 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -478,6 +478,22 @@ textarea { .core-#{$color-name}-card { @extend ion-card; border-bottom: 3px solid $color-base; + + &[icon-start] { + padding-left: 52px; + position: relative; + + ion-icon { + color: $color-base; + position: absolute; + top: 0; + left: 16px; + height: 100%; + font-size: 24px; + display: flex; + align-items: center; + } + } } } diff --git a/src/app/app.wp.scss b/src/app/app.wp.scss index 4d22d076f..431dfdb08 100644 --- a/src/app/app.wp.scss +++ b/src/app/app.wp.scss @@ -25,6 +25,14 @@ .wp .core-#{$color-name}-card { @extend .card-wp ; @extend .card-content-wp; + + &[icon-start] { + padding-left: $card-wp-padding-left * 2 + 20; + + ion-icon { + left: $card-wp-padding-left; + } + } } } diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index 654b3bba8..6a0416daf 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -151,11 +151,11 @@ export class CoreSyncBaseProvider { /** * Check if a sync is needed: if a certain time has passed since the last time. * - * @param {string} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with boolean: whether sync is needed. */ - isSyncNeeded(id: string, siteId?: string): Promise { + isSyncNeeded(id: string | number, siteId?: string): Promise { return this.getSyncTime(id, siteId).then((time) => { return Date.now() - this.syncInterval >= time; }); diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts new file mode 100644 index 000000000..0b23760b2 --- /dev/null +++ b/src/core/course/classes/main-activity-component.ts @@ -0,0 +1,213 @@ +// (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 { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreEventsProvider } from '@providers/events'; +import { Network } from '@ionic-native/network'; +import { CoreAppProvider } from '@providers/app'; +import { CoreCourseModuleMainResourceComponent } from './main-resource-component'; + +/** + * Template class to easily create CoreCourseModuleMainComponent of activities. + */ +export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainResourceComponent { + moduleName: string; // Raw module name to be translated. It will be translated on init. + + // Data for context menu. + syncIcon: string; // Sync icon. + hasOffline: boolean; // If it has offline data to be synced. + isOnline: boolean; // If the app is online or not. + + protected siteId: string; // Current Site ID. + protected syncObserver: any; // It will observe the sync auto event. + protected onlineObserver: any; // It will observe the status of the network connection. + protected syncEventName: string; // Auto sync event name. + + constructor(protected textUtils: CoreTextUtilsProvider, protected courseHelper: CoreCourseHelperProvider, + protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider, + protected sitesProvider: CoreSitesProvider, protected courseProvider: CoreCourseProvider, network: Network, + protected appProvider: CoreAppProvider, protected eventsProvider: CoreEventsProvider) { + super(textUtils, courseHelper, translate, domUtils); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe((online) => { + this.isOnline = this.appProvider.isOnline(); + }); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + if (this.syncEventName) { + // Refresh data if this discussion is synchronized automatically. + this.syncObserver = this.eventsProvider.on(this.syncEventName, (data) => { + if (this.isRefreshSyncNeeded(data)) { + // Refresh the data. + this.refreshContent(false); + } + }, this.siteId); + } + + this.hasOffline = false; + this.syncIcon = 'spinner'; + this.siteId = this.sitesProvider.getCurrentSiteId(); + this.moduleName = this.courseProvider.translateModuleName(this.moduleName); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any, done?: () => void, showErrors: boolean = false): Promise { + if (this.loaded) { + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + + return this.refreshContent(true, showErrors).finally(() => { + this.refreshIcon = 'refresh'; + this.syncIcon = 'sync'; + refresher && refresher.complete(); + done && done(); + }); + } + + return Promise.resolve(); + } + + /** + * Compares sync event data with current data to check if refresh content is needed. + * + * @param {any} syncEventData Data receiven on sync observer. + * @return {boolean} True if refresh is needed, false otherwise. + */ + protected isRefreshSyncNeeded(syncEventData: any): boolean { + return false; + } + + /** + * Perform the refresh content function. + * + * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Resolved when done. + */ + protected refreshContent(sync: boolean = false, showErrors: boolean = false): Promise { + return this.invalidateContent().catch(() => { + // Ignore errors. + }).then(() => { + return this.loadContent(true, sync, showErrors); + }); + } + + /** + * Download the component contents. + * + * @param {boolean} [refresh=false] Whether we're refreshing data. + * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + return Promise.resolve(); + } + + /** + * Loads the component contents and shows the corresponding error. + * + * @param {boolean} [refresh=false] Whether we're refreshing data. + * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved when done. + */ + protected loadContent(refresh?: boolean, sync: boolean = false, showErrors: boolean = false): Promise { + this.isOnline = this.appProvider.isOnline(); + + return this.fetchContent(refresh, sync, showErrors).catch((error) => { + if (!refresh) { + // Some call failed, retry without using cache since it might be a new activity. + return this.refreshContent(sync); + } + + // Error getting data, fail. + this.domUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true); + }).finally(() => { + this.loaded = true; + this.refreshIcon = 'refresh'; + this.syncIcon = 'sync'; + }); + } + + /** + * Performs the sync of the activity. + * + * @return {Promise} Promise resolved when done. + */ + protected sync(): Promise { + return Promise.resolve(true); + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param {any} result Data returned on the sync function. + * @return {boolean} If suceed or not. + */ + protected hasSyncSucceed(result: any): boolean { + return true; + } + + /** + * Tries to synchronize the activity. + * + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved with true if sync succeed, or false if failed. + */ + protected syncActivity(showErrors: boolean = false): Promise { + return this.sync().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); + } + + return this.hasSyncSucceed(result); + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + + return false; + }); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.onlineObserver && this.onlineObserver.unsubscribe(); + this.syncObserver && this.syncObserver.off(); + } +} From 5f107f604bea3c99100a25af77af33dcfec4ca55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 22 Mar 2018 15:17:12 +0100 Subject: [PATCH 2/2] MOBILE-2351 survey: Fix PR problems --- src/addon/mod/book/components/index/index.ts | 16 ++---- .../mod/folder/components/index/index.ts | 14 ++---- src/addon/mod/page/components/index/index.ts | 14 ++---- .../mod/resource/components/index/index.ts | 14 ++---- .../mod/survey/components/index/index.html | 2 +- .../mod/survey/components/index/index.ts | 23 ++------- src/addon/mod/survey/providers/helper.ts | 4 +- src/addon/mod/survey/providers/offline.ts | 4 +- src/addon/mod/survey/providers/survey.ts | 3 +- src/addon/mod/url/components/index/index.ts | 14 ++---- .../course/classes/main-activity-component.ts | 50 +++++++++++-------- .../course/classes/main-resource-component.ts | 16 ++++-- 12 files changed, 74 insertions(+), 100 deletions(-) diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index c4b4f815e..92f43bc98 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -12,14 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Optional } from '@angular/core'; +import { Component, Optional, Injector } from '@angular/core'; import { Content, PopoverController } from 'ionic-angular'; -import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book'; import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler'; @@ -42,12 +38,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp protected currentChapter: string; protected contentsMap: AddonModBookContentsMap; - constructor(private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider, - protected domUtils: CoreDomUtilsProvider, private appProvider: CoreAppProvider, - protected textUtils: CoreTextUtilsProvider, protected courseHelper: CoreCourseHelperProvider, - private prefetchDelegate: AddonModBookPrefetchHandler, private popoverCtrl: PopoverController, - protected translate: TranslateService, @Optional() private content: Content) { - super(textUtils, courseHelper, translate, domUtils); + constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider, + private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler, + private popoverCtrl: PopoverController, @Optional() private content: Content) { + super(injector); } /** diff --git a/src/addon/mod/folder/components/index/index.ts b/src/addon/mod/folder/components/index/index.ts index e109b87c0..269f25641 100644 --- a/src/addon/mod/folder/components/index/index.ts +++ b/src/addon/mod/folder/components/index/index.ts @@ -12,13 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { Component, Input, Injector } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModFolderProvider } from '../../providers/folder'; import { AddonModFolderHelperProvider } from '../../providers/helper'; @@ -39,11 +35,9 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo canGetFolder: boolean; contents: any; - constructor(private folderProvider: AddonModFolderProvider, private courseProvider: CoreCourseProvider, - protected domUtils: CoreDomUtilsProvider, private appProvider: CoreAppProvider, - protected textUtils: CoreTextUtilsProvider, protected courseHelper: CoreCourseHelperProvider, - protected translate: TranslateService, private folderHelper: AddonModFolderHelperProvider) { - super(textUtils, courseHelper, translate, domUtils); + constructor(injector: Injector, private folderProvider: AddonModFolderProvider, private courseProvider: CoreCourseProvider, + private appProvider: CoreAppProvider, private folderHelper: AddonModFolderHelperProvider) { + super(injector); } /** diff --git a/src/addon/mod/page/components/index/index.ts b/src/addon/mod/page/components/index/index.ts index f4cfcf92f..78a5b2788 100644 --- a/src/addon/mod/page/components/index/index.ts +++ b/src/addon/mod/page/components/index/index.ts @@ -12,13 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { Component, Injector } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModPageProvider } from '../../providers/page'; import { AddonModPageHelperProvider } from '../../providers/helper'; @@ -37,12 +33,10 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp contents: any; protected fetchContentDefaultError = 'addon.mod_page.errorwhileloadingthepage'; - constructor(private pageProvider: AddonModPageProvider, private courseProvider: CoreCourseProvider, - protected domUtils: CoreDomUtilsProvider, private appProvider: CoreAppProvider, - protected textUtils: CoreTextUtilsProvider, protected courseHelper: CoreCourseHelperProvider, - protected translate: TranslateService, private pageHelper: AddonModPageHelperProvider, + constructor(injector: Injector, private pageProvider: AddonModPageProvider, private courseProvider: CoreCourseProvider, + private appProvider: CoreAppProvider, private pageHelper: AddonModPageHelperProvider, private pagePrefetch: AddonModPagePrefetchHandler) { - super(textUtils, courseHelper, translate, domUtils); + super(injector); } /** diff --git a/src/addon/mod/resource/components/index/index.ts b/src/addon/mod/resource/components/index/index.ts index 85fb6a20f..1e8f6a2da 100644 --- a/src/addon/mod/resource/components/index/index.ts +++ b/src/addon/mod/resource/components/index/index.ts @@ -12,13 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { Component, Injector } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModResourceProvider } from '../../providers/resource'; import { AddonModResourcePrefetchHandler } from '../../providers/prefetch-handler'; @@ -39,12 +35,10 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource src: string; contentText: string; - constructor(private resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider, - protected domUtils: CoreDomUtilsProvider, private appProvider: CoreAppProvider, - protected textUtils: CoreTextUtilsProvider, protected courseHelper: CoreCourseHelperProvider, - protected translate: TranslateService, private prefetchHandler: AddonModResourcePrefetchHandler, + constructor(injector: Injector, private resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider, + private appProvider: CoreAppProvider, private prefetchHandler: AddonModResourcePrefetchHandler, private resourceHelper: AddonModResourceHelperProvider) { - super(textUtils, courseHelper, translate, domUtils); + super(injector); } /** diff --git a/src/addon/mod/survey/components/index/index.html b/src/addon/mod/survey/components/index/index.html index b37a5ef9f..094b44471 100644 --- a/src/addon/mod/survey/components/index/index.html +++ b/src/addon/mod/survey/components/index/index.html @@ -13,7 +13,7 @@ - + diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index 81a742e29..a5e8e234b 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -12,17 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Optional } from '@angular/core'; +import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; -import { TranslateService } from '@ngx-translate/core'; -import { Network } from '@ionic-native/network'; -import { CoreAppProvider } from '@providers/app'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreSitesProvider } from '@providers/sites'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreEventsProvider } from '@providers/events'; -import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { AddonModSurveyProvider } from '../../providers/survey'; import { AddonModSurveyHelperProvider } from '../../providers/helper'; @@ -47,14 +38,10 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo protected userId: number; protected syncEventName = AddonModSurveySyncProvider.AUTO_SYNCED; - constructor(private surveyProvider: AddonModSurveyProvider, protected courseProvider: CoreCourseProvider, - protected domUtils: CoreDomUtilsProvider, protected appProvider: CoreAppProvider, - protected courseHelper: CoreCourseHelperProvider, protected translate: TranslateService, network: Network, - private surveyHelper: AddonModSurveyHelperProvider, protected sitesProvider: CoreSitesProvider, - protected eventsProvider: CoreEventsProvider, private surveyOffline: AddonModSurveyOfflineProvider, - private surveySync: AddonModSurveySyncProvider, @Optional() private content: Content, - protected textUtils: CoreTextUtilsProvider) { - super(textUtils, courseHelper, translate, domUtils, sitesProvider, courseProvider, network, appProvider, eventsProvider); + constructor(injector: Injector, private surveyProvider: AddonModSurveyProvider, @Optional() private content: Content, + private surveyHelper: AddonModSurveyHelperProvider, private surveyOffline: AddonModSurveyOfflineProvider, + private surveySync: AddonModSurveySyncProvider) { + super(injector); } /** diff --git a/src/addon/mod/survey/providers/helper.ts b/src/addon/mod/survey/providers/helper.ts index 45c30fb40..cc3d48e31 100644 --- a/src/addon/mod/survey/providers/helper.ts +++ b/src/addon/mod/survey/providers/helper.ts @@ -26,10 +26,10 @@ export class AddonModSurveyHelperProvider { /** * Turns a string with values separated by commas into an array. * - * @param {string} value Value to convert. + * @param {any} value Value to convert. * @return {string[]} Array. */ - protected commaStringToArray(value: string): string[] { + protected commaStringToArray(value: any): string[] { if (typeof value == 'string') { if (value.length > 0) { return value.split(','); diff --git a/src/addon/mod/survey/providers/offline.ts b/src/addon/mod/survey/providers/offline.ts index f1691c346..07b2065fb 100644 --- a/src/addon/mod/survey/providers/offline.ts +++ b/src/addon/mod/survey/providers/offline.ts @@ -26,7 +26,7 @@ export class AddonModSurveyOfflineProvider { protected logger; // Variables for database. - protected SURVEY_TABLE = 'mma_mod_survey_answers'; + protected SURVEY_TABLE = 'addon_mod_survey_answers'; protected tablesSchema = [ { name: this.SURVEY_TABLE, @@ -171,7 +171,7 @@ export class AddonModSurveyOfflineProvider { timecreated: new Date().getTime() }; - return site.getDb().insertOrUpdateRecord(this.SURVEY_TABLE, entry, {surveyid: surveyId, userid: userId}); + return site.getDb().insertRecord(this.SURVEY_TABLE, entry); }); } } diff --git a/src/addon/mod/survey/providers/survey.ts b/src/addon/mod/survey/providers/survey.ts index dc286b793..70c92dd03 100644 --- a/src/addon/mod/survey/providers/survey.ts +++ b/src/addon/mod/survey/providers/survey.ts @@ -155,7 +155,7 @@ export class AddonModSurveyProvider { promises.push(this.getSurvey(courseId, moduleId).then((survey) => { const ps = []; - // Do not invalidate wiki data before getting wiki info, we need it! + // Do not invalidate activity data before getting activity info, we need it! ps.push(this.invalidateSurveyData(courseId, siteId)); ps.push(this.invalidateQuestions(survey.id, siteId)); @@ -268,7 +268,6 @@ export class AddonModSurveyProvider { return site.write('mod_survey_submit_answers', params).then((response) => { if (!response.status) { - // There was an error, and it should be translated already. return this.utils.createFakeWSError(''); } }); diff --git a/src/addon/mod/url/components/index/index.ts b/src/addon/mod/url/components/index/index.ts index df2cca224..d774c55fc 100644 --- a/src/addon/mod/url/components/index/index.ts +++ b/src/addon/mod/url/components/index/index.ts @@ -12,15 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, Injector } from '@angular/core'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModUrlProvider } from '../../providers/url'; import { AddonModUrlHelperProvider } from '../../providers/helper'; -import { CoreCourseHelperProvider } from '@core/course/providers/helper'; /** * Component that displays a url. @@ -35,11 +31,9 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo canGetUrl: boolean; url: string; - constructor(private urlProvider: AddonModUrlProvider, private courseProvider: CoreCourseProvider, - protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - protected translate: TranslateService, private urlHelper: AddonModUrlHelperProvider, - protected courseHelper: CoreCourseHelperProvider) { - super(textUtils, courseHelper, translate, domUtils); + constructor(injector: Injector, private urlProvider: AddonModUrlProvider, private courseProvider: CoreCourseProvider, + private urlHelper: AddonModUrlHelperProvider) { + super(injector); } /** diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts index 0b23760b2..8af38dfbc 100644 --- a/src/core/course/classes/main-activity-component.ts +++ b/src/core/course/classes/main-activity-component.ts @@ -12,10 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TranslateService } from '@ngx-translate/core'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { Injector } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreEventsProvider } from '@providers/events'; @@ -39,11 +36,22 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR protected onlineObserver: any; // It will observe the status of the network connection. protected syncEventName: string; // Auto sync event name. - constructor(protected textUtils: CoreTextUtilsProvider, protected courseHelper: CoreCourseHelperProvider, - protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider, - protected sitesProvider: CoreSitesProvider, protected courseProvider: CoreCourseProvider, network: Network, - protected appProvider: CoreAppProvider, protected eventsProvider: CoreEventsProvider) { - super(textUtils, courseHelper, translate, domUtils); + // List of services that will be injected using injector. + // It's done like this so subclasses don't have to send all the services to the parent in the constructor. + protected sitesProvider: CoreSitesProvider; + protected courseProvider: CoreCourseProvider; + protected appProvider: CoreAppProvider; + protected eventsProvider: CoreEventsProvider; + + constructor(injector: Injector) { + super(injector); + + this.sitesProvider = injector.get(CoreSitesProvider); + this.courseProvider = injector.get(CoreCourseProvider); + this.appProvider = injector.get(CoreAppProvider); + this.eventsProvider = injector.get(CoreEventsProvider); + + const network = injector.get(Network); // Refresh online status when changes. this.onlineObserver = network.onchange().subscribe((online) => { @@ -57,6 +65,11 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR ngOnInit(): void { super.ngOnInit(); + this.hasOffline = false; + this.syncIcon = 'spinner'; + this.siteId = this.sitesProvider.getCurrentSiteId(); + this.moduleName = this.courseProvider.translateModuleName(this.moduleName); + if (this.syncEventName) { // Refresh data if this discussion is synchronized automatically. this.syncObserver = this.eventsProvider.on(this.syncEventName, (data) => { @@ -66,11 +79,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR } }, this.siteId); } - - this.hasOffline = false; - this.syncIcon = 'spinner'; - this.siteId = this.sitesProvider.getCurrentSiteId(); - this.moduleName = this.courseProvider.translateModuleName(this.moduleName); } /** @@ -100,7 +108,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR /** * Compares sync event data with current data to check if refresh content is needed. * - * @param {any} syncEventData Data receiven on sync observer. + * @param {any} syncEventData Data received on sync observer. * @return {boolean} True if refresh is needed, false otherwise. */ protected isRefreshSyncNeeded(syncEventData: any): boolean { @@ -110,8 +118,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR /** * Perform the refresh content function. * - * @param {boolean} [sync=false] If the refresh is needs syncing. - * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @param {boolean} [sync=false] If the refresh needs syncing. + * @param {boolean} [showErrors=false] Wether to show errors to the user or hide them. * @return {Promise} Resolved when done. */ protected refreshContent(sync: boolean = false, showErrors: boolean = false): Promise { @@ -126,8 +134,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR * Download the component contents. * * @param {boolean} [refresh=false] Whether we're refreshing data. - * @param {boolean} [sync=false] If the refresh is needs syncing. - * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @param {boolean} [sync=false] If the refresh needs syncing. + * @param {boolean} [showErrors=false] Wether to show errors to the user or hide them. * @return {Promise} Promise resolved when done. */ protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { @@ -138,8 +146,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR * Loads the component contents and shows the corresponding error. * * @param {boolean} [refresh=false] Whether we're refreshing data. - * @param {boolean} [sync=false] If the refresh is needs syncing. - * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @param {boolean} [sync=false] If the refresh needs syncing. + * @param {boolean} [showErrors=false] Wether to show errors to the user or hide them. * @return {Promise} Promise resolved when done. */ protected loadContent(refresh?: boolean, sync: boolean = false, showErrors: boolean = false): Promise { diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts index 370720736..be9c687e7 100644 --- a/src/core/course/classes/main-resource-component.ts +++ b/src/core/course/classes/main-resource-component.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { OnInit, OnDestroy, Input, Output, EventEmitter, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -43,8 +43,18 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected statusObserver; // Observer of package status changed, used when calling fillContextMenu. protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents. - constructor(protected textUtils: CoreTextUtilsProvider, protected courseHelper: CoreCourseHelperProvider, - protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider) { + // List of services that will be injected using injector. + // It's done like this so subclasses don't have to send all the services to the parent in the constructor. + protected textUtils: CoreTextUtilsProvider; + protected courseHelper: CoreCourseHelperProvider; + protected translate: TranslateService; + protected domUtils: CoreDomUtilsProvider; + + constructor(injector: Injector) { + this.textUtils = injector.get(CoreTextUtilsProvider); + this.courseHelper = injector.get(CoreCourseHelperProvider); + this.translate = injector.get(TranslateService); + this.domUtils = injector.get(CoreDomUtilsProvider); this.dataRetrieved = new EventEmitter(); }