From 4e7c9eb6e80f7e6ae6dc7c68267f5daa6b86723d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 29 Mar 2021 16:06:33 +0200 Subject: [PATCH] MOBILE-3639 choice: Implement services --- src/addons/mod/choice/choice.module.ts | 61 ++ .../mod/choice/services/choice-offline.ts | 150 +++++ src/addons/mod/choice/services/choice-sync.ts | 236 +++++++ src/addons/mod/choice/services/choice.ts | 603 ++++++++++++++++++ .../mod/choice/services/database/choice.ts | 73 +++ .../choice/services/handlers/index-link.ts | 33 + .../mod/choice/services/handlers/list-link.ts | 33 + .../mod/choice/services/handlers/module.ts | 83 +++ .../mod/choice/services/handlers/prefetch.ts | 157 +++++ .../mod/choice/services/handlers/sync-cron.ts | 51 ++ src/addons/mod/mod.module.ts | 2 + src/core/features/compile/services/compile.ts | 4 +- 12 files changed, 1484 insertions(+), 2 deletions(-) create mode 100644 src/addons/mod/choice/choice.module.ts create mode 100644 src/addons/mod/choice/services/choice-offline.ts create mode 100644 src/addons/mod/choice/services/choice-sync.ts create mode 100644 src/addons/mod/choice/services/choice.ts create mode 100644 src/addons/mod/choice/services/database/choice.ts create mode 100644 src/addons/mod/choice/services/handlers/index-link.ts create mode 100644 src/addons/mod/choice/services/handlers/list-link.ts create mode 100644 src/addons/mod/choice/services/handlers/module.ts create mode 100644 src/addons/mod/choice/services/handlers/prefetch.ts create mode 100644 src/addons/mod/choice/services/handlers/sync-cron.ts diff --git a/src/addons/mod/choice/choice.module.ts b/src/addons/mod/choice/choice.module.ts new file mode 100644 index 000000000..fd2ca332a --- /dev/null +++ b/src/addons/mod/choice/choice.module.ts @@ -0,0 +1,61 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { APP_INITIALIZER, NgModule, Type } from '@angular/core'; + +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreCronDelegate } from '@services/cron'; +import { CORE_SITE_SCHEMAS } from '@services/sites'; +import { AddonModChoiceProvider } from './services/choice'; +import { AddonModChoiceOfflineProvider } from './services/choice-offline'; +import { AddonModChoiceSyncProvider } from './services/choice-sync'; +import { OFFLINE_SITE_SCHEMA } from './services/database/choice'; +import { AddonModChoiceIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModChoiceListLinkHandler } from './services/handlers/list-link'; +import { AddonModChoiceModuleHandler } from './services/handlers/module'; +import { AddonModChoicePrefetchHandler } from './services/handlers/prefetch'; +import { AddonModChoiceSyncCronHandler } from './services/handlers/sync-cron'; + +export const ADDON_MOD_CHOICE_SERVICES: Type[] = [ + AddonModChoiceProvider, + AddonModChoiceOfflineProvider, + AddonModChoiceSyncProvider, +]; + +@NgModule({ + imports: [ + ], + providers: [ + { + provide: CORE_SITE_SCHEMAS, + useValue: [OFFLINE_SITE_SCHEMA], + multi: true, + }, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModChoiceModuleHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModChoicePrefetchHandler.instance); + CoreCronDelegate.register(AddonModChoiceSyncCronHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModChoiceIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModChoiceListLinkHandler.instance); + }, + }, + ], +}) +export class AddonModChoiceModule {} diff --git a/src/addons/mod/choice/services/choice-offline.ts b/src/addons/mod/choice/services/choice-offline.ts new file mode 100644 index 000000000..9d7738935 --- /dev/null +++ b/src/addons/mod/choice/services/choice-offline.ts @@ -0,0 +1,150 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { makeSingleton } from '@singletons'; +import { AddonModChoiceResponsesDBRecord, RESPONSES_TABLE_NAME } from './database/choice'; + +/** + * Service to handle offline choices. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChoiceOfflineProvider { + + /** + * Delete a response. + * + * @param choiceId Choice ID to remove. + * @param siteId Site ID. If not defined, current site. + * @param userId User the responses belong to. If not defined, current user in site. + * @return Promise resolved if stored, rejected if failure. + */ + async deleteResponse(choiceId: number, siteId?: string, userId?: number): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + await site.getDb().deleteRecords(RESPONSES_TABLE_NAME, { choiceid: choiceId, userid: userId }); + } + + /** + * Get all offline responses. + * + * @param siteId Site ID. If not defined, current site. + * @return Promi[se resolved with responses. + */ + async getResponses(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const records = await site.getDb().getRecords(RESPONSES_TABLE_NAME); + + return records.map((record) => this.parseResponse(record)); + } + + /** + * Check if there are offline responses to send. + * + * @param choiceId Choice ID. + * @param siteId Site ID. If not defined, current site. + * @param userId User the responses belong to. If not defined, current user in site. + * @return Promise resolved with boolean: true if has offline answers, false otherwise. + */ + async hasResponse(choiceId: number, siteId?: string, userId?: number): Promise { + try { + const response = await this.getResponse(choiceId, siteId, userId); + + return !!response.choiceid; + } catch (error) { + // No offline data found, return false. + return false; + } + } + + /** + * Get response to be synced. + * + * @param choiceId Choice ID to get. + * @param siteId Site ID. If not defined, current site. + * @param userId User the responses belong to. If not defined, current user in site. + * @return Promise resolved with the object to be synced. + */ + async getResponse(choiceId: number, siteId?: string, userId?: number): Promise { + const site = await CoreSites.getSite(siteId); + userId = userId || site.getUserId(); + + const record = await site.getDb().getRecord(RESPONSES_TABLE_NAME, { + choiceid: choiceId, + userid: userId, + }); + + return this.parseResponse(record); + } + + /** + * Parse responses. + * + * @param entry Entry to parse. + * @return Parsed entry. + */ + protected parseResponse(entry: AddonModChoiceResponsesDBRecord): AddonModChoiceOfflineResponses { + return { + ...entry, + responses: CoreTextUtils.parseJSON(entry.responses, []), + }; + } + + /** + * Offline version for sending a response to a choice to Moodle. + * + * @param choiceId Choice ID. + * @param name Choice name. + * @param courseId Course ID the choice belongs to. + * @param responses IDs of selected options. + * @param deleting If true, the user is deleting responses, if false, submitting. + * @param siteId Site ID. If not defined, current site. + * @param userId User the responses belong to. If not defined, current user in site. + * @return Promise resolved when results are successfully submitted. + */ + async saveResponse( + choiceId: number, + name: string, + courseId: number, + responses: number[], + deleting: boolean, + siteId?: string, + userId?: number, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const entry: AddonModChoiceResponsesDBRecord = { + choiceid: choiceId, + name: name, + courseid: courseId, + userid: userId || site.getUserId(), + responses: JSON.stringify(responses), + deleting: deleting ? 1 : 0, + timecreated: Date.now(), + }; + + await site.getDb().insertRecord(RESPONSES_TABLE_NAME, entry); + } + +} + +export const AddonModChoiceOffline = makeSingleton(AddonModChoiceOfflineProvider); + +export type AddonModChoiceOfflineResponses = Omit & { + responses: number[]; +}; diff --git a/src/addons/mod/choice/services/choice-sync.ts b/src/addons/mod/choice/services/choice-sync.ts new file mode 100644 index 000000000..d933c29c2 --- /dev/null +++ b/src/addons/mod/choice/services/choice-sync.ts @@ -0,0 +1,236 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreNetworkError } from '@classes/errors/network-error'; +import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { AddonModChoice, AddonModChoiceProvider } from './choice'; +import { AddonModChoiceOffline } from './choice-offline'; +import { AddonModChoicePrefetchHandler } from './handlers/prefetch'; + +/** + * Service to sync choices. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvider { + + static readonly AUTO_SYNCED = 'addon_mod_choice_autom_synced'; + + protected componentTranslatableString = 'choice'; + + constructor() { + super('AddonModChoiceSyncProvider'); + } + + /** + * Get the ID of a choice sync. + * + * @param choiceId Choice ID. + * @param userId User the responses belong to. + * @return Sync ID. + */ + protected getSyncId(choiceId: number, userId: number): string { + return choiceId + '#' + userId; + } + + /** + * Try to synchronize all the choices in a certain site or in all sites. + * + * @param siteId Site ID to sync. If not defined, sync all sites. + * @param force Wether to force sync not depending on last execution. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllChoices(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all choices', this.syncAllChoicesFunc.bind(this, !!force), siteId); + } + + /** + * Sync all pending choices on a site. + * + * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID to sync. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + protected async syncAllChoicesFunc(force: boolean, siteId: string): Promise { + const responses = await AddonModChoiceOffline.getResponses(siteId); + + // Sync all responses. + await Promise.all(responses.map(async (response) => { + const result = force ? + await this.syncChoice(response.choiceid, response.userid, siteId) : + await this.syncChoiceIfNeeded(response.choiceid, response.userid, siteId); + + if (result?.updated) { + // Sync successful, send event. + CoreEvents.trigger(AddonModChoiceSyncProvider.AUTO_SYNCED, { + choiceId: response.choiceid, + userId: response.userid, + warnings: result.warnings, + }, siteId); + } + })); + } + + /** + * Sync an choice only if a certain time has passed since the last time. + * + * @param choiceId Choice ID to be synced. + * @param userId User the answers belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the choice is synced or it doesn't need to be synced. + */ + async syncChoiceIfNeeded(choiceId: number, userId: number, siteId?: string): Promise { + const syncId = this.getSyncId(choiceId, userId); + + const needed = await this.isSyncNeeded(syncId, siteId); + + if (needed) { + return this.syncChoice(choiceId, userId, siteId); + } + } + + /** + * Synchronize a choice. + * + * @param choiceId Choice ID to be synced. + * @param userId User the answers belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + async syncChoice(choiceId: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + userId = userId || site.getUserId(); + siteId = site.getId(); + + const syncId = this.getSyncId(choiceId, userId); + if (this.isSyncing(syncId, siteId)) { + // There's already a sync ongoing for this discussion, return the promise. + return this.getOngoingSync(syncId, siteId)!; + } + + this.logger.debug(`Try to sync choice '${choiceId}' for user '${userId}'`); + + return this.addOngoingSync(syncId, this.performSync(choiceId, userId, siteId), siteId); + } + + /** + * Synchronize a choice. + * + * @param choiceId Choice ID to be synced. + * @param userId User the answers belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + protected async performSync(choiceId: number, userId: number, siteId?: string): Promise { + const syncId = this.getSyncId(choiceId, userId); + const result: AddonModChoiceSyncResult = { + warnings: [], + updated: false, + }; + + // Sync offline logs. + await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModChoiceProvider.COMPONENT, choiceId, siteId)); + + const data = await CoreUtils.ignoreErrors(AddonModChoiceOffline.getResponse(choiceId, siteId, userId)); + + if (!data || !data.choiceid) { + // Nothing to sync. Set sync time. + await this.setSyncTime(syncId, siteId); + + return result; + } + + if (!CoreApp.isOnline()) { + // Cannot sync in offline. + throw new CoreNetworkError(); + } + + const courseId = data.courseid; + + try { + // Send the responses. + if (data.deleting) { + // A user has deleted some responses. + await AddonModChoice.deleteResponsesOnline(choiceId, data.responses, siteId); + } else { + // A user has added some responses. + await AddonModChoice.submitResponseOnline(choiceId, data.responses, siteId); + } + + result.updated = true; + + await AddonModChoiceOffline.deleteResponse(choiceId, siteId, userId); + } catch (error) { + if (!CoreUtils.isWebServiceError(error)) { + // Couldn't connect to server, reject. + throw error; + } + + // The WebService has thrown an error, this means that responses cannot be submitted. Delete them. + result.updated = true; + + await AddonModChoiceOffline.deleteResponse(choiceId, siteId, userId); + + // Responses deleted, add a warning. + result.warnings.push(Translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: data.name, + error: CoreTextUtils.getErrorMessageFromError(error), + })); + } + + // Data has been sent to server, prefetch choice if needed. + try { + const module = await CoreCourse.getModuleBasicInfoByInstance(choiceId, 'choice', siteId); + + await this.prefetchAfterUpdate(AddonModChoicePrefetchHandler.instance, module, courseId, undefined, siteId); + } catch { + // Ignore errors. + } + + // Sync finished, set sync time. + await this.setSyncTime(syncId, siteId); + + return result; + } + +} + +export const AddonModChoiceSync = makeSingleton(AddonModChoiceSyncProvider); + +/** + * Data returned by a choice sync. + */ +export type AddonModChoiceSyncResult = { + warnings: string[]; // List of warnings. + updated: boolean; // Whether some data was sent to the server or offline data was updated. +}; + +/** + * Data passed to AUTO_SYNCED event. + */ +export type AddonModChoiceAutoSyncData = { + choiceId: number; + userId: number; + warnings: string[]; +}; diff --git a/src/addons/mod/choice/services/choice.ts b/src/addons/mod/choice/services/choice.ts new file mode 100644 index 000000000..97570f95b --- /dev/null +++ b/src/addons/mod/choice/services/choice.ts @@ -0,0 +1,603 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreError } from '@classes/errors/error'; +import { CoreWSError } from '@classes/errors/wserror'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreApp } from '@services/app'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonModChoiceOffline } from './choice-offline'; +import { AddonModChoiceAutoSyncData, AddonModChoiceSyncProvider } from './choice-sync'; + +const ROOT_CACHE_KEY = 'mmaModChoice:'; + +/** + * Service that provides some features for choices. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChoiceProvider { + + static readonly COMPONENT = 'mmaModChoice'; + + static readonly RESULTS_NOT = 0; + static readonly RESULTS_AFTER_ANSWER = 1; + static readonly RESULTS_AFTER_CLOSE = 2; + static readonly RESULTS_ALWAYS = 3; + + static readonly PUBLISH_ANONYMOUS = false; + static readonly PUBLISH_NAMES = true; + + /** + * Check if results can be seen by a student. The student can see the results if: + * - they're always published, OR + * - they're published after the choice is closed and it's closed, OR + * - they're published after answering and the user has answered. + * + * @param choice Choice to check. + * @param hasAnswered True if user has answered the choice, false otherwise. + * @return True if the students can see the results. + */ + canStudentSeeResults(choice: AddonModChoiceChoice, hasAnswered: boolean): boolean { + const now = Date.now(); + + return choice.showresults === AddonModChoiceProvider.RESULTS_ALWAYS || + choice.showresults === AddonModChoiceProvider.RESULTS_AFTER_CLOSE && choice.timeclose && choice.timeclose <= now || + choice.showresults === AddonModChoiceProvider.RESULTS_AFTER_ANSWER && hasAnswered; + } + + /** + * Delete responses from a choice. + * + * @param choiceId Choice ID. + * @param name Choice name. + * @param courseId Course ID the choice belongs to. + * @param responses IDs of the answers. If not defined, delete all the answers of the current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: true if response was sent to server, false if stored in device. + */ + async deleteResponses( + choiceId: number, + name: string, + courseId: number, + responses?: number[], + siteId?: string, + ): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + responses = responses || []; + + // Convenience function to store a message to be synchronized later. + const storeOffline = async (): Promise => { + await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses!, true, siteId); + + return false; + }; + + if (!CoreApp.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + // If there's already a response to be sent to the server, discard it first. + await AddonModChoiceOffline.deleteResponse(choiceId, siteId); + + try { + await this.deleteResponsesOnline(choiceId, responses, siteId); + + return true; + } catch (error) { + if (CoreUtils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + } + } + + /** + * Delete responses from a choice. It will fail if offline or cannot connect. + * + * @param choiceId Choice ID. + * @param responses IDs of the answers. If not defined, delete all the answers of the current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when responses are successfully deleted. + */ + async deleteResponsesOnline(choiceId: number, responses?: number[], siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModChoiceDeleteChoiceResponsesWSParams = { + choiceid: choiceId, + responses: responses, + }; + + const response = await site.write('mod_choice_delete_choice_responses', params); + + // Other errors ocurring. + if (response.status === false) { + if (response.warnings?.[0]) { + throw new CoreWSError(response.warnings[0]); + } + + throw new CoreError('Cannot delete responses.'); + } + + // Invalidate related data. + await CoreUtils.ignoreErrors(Promise.all([ + this.invalidateOptions(choiceId, site.id), + this.invalidateResults(choiceId, site.id), + ])); + } + + /** + * Get cache key for choice data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getChoiceDataCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'choice:' + courseId; + } + + /** + * Get cache key for choice options WS calls. + * + * @param choiceId Choice ID. + * @return Cache key. + */ + protected getChoiceOptionsCacheKey(choiceId: number): string { + return ROOT_CACHE_KEY + 'options:' + choiceId; + } + + /** + * Get cache key for choice results WS calls. + * + * @param choiceId Choice ID. + * @return Cache key. + */ + protected getChoiceResultsCacheKey(choiceId: number): string { + return ROOT_CACHE_KEY + 'results:' + choiceId; + } + + /** + * Get a choice with key=value. If more than one is found, only the first will be returned. + * + * @param courseId Course ID. + * @param key Name of the property to check. + * @param value Value to search. + * @param options Other options. + * @return Promise resolved when the choice is retrieved. + */ + protected async getChoiceByDataKey( + courseId: number, + key: string, + value: unknown, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModChoiceGetChoicesByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getChoiceDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChoiceProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read( + 'mod_choice_get_choices_by_courses', + params, + preSets, + ); + + const currentChoice = response.choices.find((choice) => choice[key] == value); + if (currentChoice) { + return currentChoice; + } + + throw new CoreError('Choice not found.'); + } + + /** + * Get a choice by course module ID. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the choice is retrieved. + */ + getChoice(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getChoiceByDataKey(courseId, 'coursemodule', cmId, options); + } + + /** + * Get a choice by ID. + * + * @param courseId Course ID. + * @param choiceId Choice ID. + * @param options Other options. + * @return Promise resolved when the choice is retrieved. + */ + getChoiceById(courseId: number, choiceId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getChoiceByDataKey(courseId, 'id', choiceId, options); + } + + /** + * Get choice options. + * + * @param choiceId Choice ID. + * @param options Other options. + * @return Promise resolved with choice options. + */ + async getOptions(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModChoiceGetChoiceOptionsWSParams = { + choiceid: choiceId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getChoiceOptionsCacheKey(choiceId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChoiceProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read( + 'mod_choice_get_choice_options', + params, + preSets, + ); + + return response.options; + } + + /** + * Get choice results. + * + * @param choiceId Choice ID. + * @param options Other options. + * @return Promise resolved with choice results. + */ + async getResults(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModChoiceGetChoiceResultsWSParams = { + choiceid: choiceId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getChoiceOptionsCacheKey(choiceId), + component: AddonModChoiceProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read( + 'mod_choice_get_choice_results', + params, + preSets, + ); + + return response.options; + } + + /** + * Invalidate choice data. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateChoiceData(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getChoiceDataCacheKey(courseId)); + } + + /** + * Invalidate the prefetched content. + * + * @param moduleId The module ID. + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when data is invalidated. + */ + async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const choice = await this.getChoice(courseId, moduleId); + + await Promise.all([ + this.invalidateChoiceData(courseId), + this.invalidateOptions(choice.id), + this.invalidateResults(choice.id), + CoreFilepool.invalidateFilesByComponent(siteId, AddonModChoiceProvider.COMPONENT, moduleId), + ]); + } + + /** + * Invalidate choice options. + * + * @param choiceId Choice ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateOptions(choiceId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getChoiceOptionsCacheKey(choiceId)); + } + + /** + * Invalidate choice results. + * + * @param choiceId Choice ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateResults(choiceId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getChoiceResultsCacheKey(choiceId)); + } + + /** + * Report the choice as being viewed. + * + * @param id Choice ID. + * @param name Name of the choice. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logView(id: number, name?: string, siteId?: string): Promise { + const params: AddonModChoiceViewChoiceWSParams = { + choiceid: id, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_choice_view_choice', + params, + AddonModChoiceProvider.COMPONENT, + id, + name, + 'choice', + {}, + siteId, + ); + } + + /** + * Send a response to a choice to Moodle. + * + * @param choiceId Choice ID. + * @param name Choice name. + * @param courseId Course ID the choice belongs to. + * @param responses IDs of selected options. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: true if response was sent to server, false if stored in device. + */ + async submitResponse(choiceId: number, name: string, courseId: number, responses: number[], siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = async (): Promise => { + await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses, false, siteId); + + return false; + }; + + if (!CoreApp.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + // If there's already a response to be sent to the server, discard it first. + await AddonModChoiceOffline.deleteResponse(choiceId, siteId); + + try { + await this.submitResponseOnline(choiceId, responses, siteId); + + return true; + } catch (error) { + if (CoreUtils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; + } + + // Couldn't connect to server, store it offline. + return storeOffline(); + } + } + + /** + * Send a response to a choice to Moodle. It will fail if offline or cannot connect. + * + * @param choiceId Choice ID. + * @param responses IDs of selected options. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when responses are successfully submitted. + */ + async submitResponseOnline(choiceId: number, responses: number[], siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModChoiceSubmitChoiceResponseWSParams = { + choiceid: choiceId, + responses: responses, + }; + + await site.write('mod_choice_submit_choice_response', params); + + // Invalidate related data. + await CoreUtils.ignoreErrors(Promise.all([ + this.invalidateOptions(choiceId, siteId), + this.invalidateResults(choiceId, siteId), + ])); + } + +} + +export const AddonModChoice = makeSingleton(AddonModChoiceProvider); + +/** + * Params of mod_choice_get_choices_by_courses WS. + */ +export type AddonModChoiceGetChoicesByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; + +/** + * Data returned by mod_choice_get_choices_by_courses WS. + */ +export type AddonModChoiceGetChoicesByCoursesWSResponse = { + choices: AddonModChoiceChoice[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Choice returned by mod_choice_get_choices_by_courses. + */ +export type AddonModChoiceChoice = { + id: number; // Choice instance id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Choice name. + intro: string; // The choice intro. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; // @since 3.2. + publish?: boolean; // If choice is published. + showresults?: number; // 0 never, 1 after answer, 2 after close, 3 always. + display?: number; // Display mode (vertical, horizontal). + allowupdate?: boolean; // Allow update. + allowmultiple?: boolean; // Allow multiple choices. + showunanswered?: boolean; // Show users who not answered yet. + includeinactive?: boolean; // Include inactive users. + limitanswers?: boolean; // Limit unswers. + timeopen?: number; // Date of opening validity. + timeclose?: number; // Date of closing validity. + showpreview?: boolean; // Show preview before timeopen. + timemodified?: number; // Time of last modification. + completionsubmit?: boolean; // Completion on user submission. + showavailable?: boolean; // Show available spaces. @since 3.10 + section?: number; // Course section id. + visible?: boolean; // Visible. + groupmode?: number; // Group mode. + groupingid?: number; // Group id. +}; + +/** + * Params of mod_choice_delete_choice_responses WS. + */ +export type AddonModChoiceDeleteChoiceResponsesWSParams = { + choiceid: number; // Choice instance id. + responses?: number[]; // Array of response ids, empty for deleting all the current user responses. +}; + +/** + * Params of mod_choice_get_choice_options WS. + */ +export type AddonModChoiceGetChoiceOptionsWSParams = { + choiceid: number; // Choice instance id. +}; + +/** + * Data returned by mod_choice_get_choice_options WS. + */ +export type AddonModChoiceGetChoiceOptionsWSResponse = { + options: AddonModChoiceOption[]; // Options. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Option returned by mod_choice_get_choice_options. + */ +export type AddonModChoiceOption = { + id: number; // Option id. + text: string; // Text of the choice. + maxanswers: number; // Maximum number of answers. + displaylayout: boolean; // True for orizontal, otherwise vertical. + countanswers: number; // Number of answers. + checked: boolean; // We already answered. + disabled: boolean; // Option disabled. +}; + +/** + * Params of mod_choice_get_choice_results WS. + */ +export type AddonModChoiceGetChoiceResultsWSParams = { + choiceid: number; // Choice instance id. +}; + +/** + * Data returned by mod_choice_get_choice_results WS. + */ +export type AddonModChoiceGetChoiceResultsWSResponse = { + options: AddonModChoiceResult[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result returned by mod_choice_get_choice_results. + */ +export type AddonModChoiceResult = { + id: number; // Choice instance id. + text: string; // Text of the choice. + maxanswer: number; // Maximum number of answers. + userresponses: { + userid: number; // User id. + fullname: string; // User full name. + profileimageurl: string; // Profile user image url. + answerid?: number; // Answer id. + timemodified?: number; // Time of modification. + }[]; + numberofuser: number; // Number of users answers. + percentageamount: number; // Percentage of users answers. +}; + +/** + * Params of mod_choice_view_choice WS. + */ +export type AddonModChoiceViewChoiceWSParams = { + choiceid: number; // Choice instance id. +}; + +/** + * Params of mod_choice_submit_choice_response WS. + */ +export type AddonModChoiceSubmitChoiceResponseWSParams = { + choiceid: number; // Choice instance id. + responses: number[]; // Array of response ids. +}; + +declare module '@singletons/events' { + + /** + * Augment CoreEventsData interface with events specific to this service. + * + * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + */ + export interface CoreEventsData { + [AddonModChoiceSyncProvider.AUTO_SYNCED]: AddonModChoiceAutoSyncData; + } + +} diff --git a/src/addons/mod/choice/services/database/choice.ts b/src/addons/mod/choice/services/database/choice.ts new file mode 100644 index 000000000..973c5a6b6 --- /dev/null +++ b/src/addons/mod/choice/services/database/choice.ts @@ -0,0 +1,73 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreSiteSchema } from '@services/sites'; + +/** + * Database variables for AddonModChoiceOfflineProvider. + */ +export const RESPONSES_TABLE_NAME = 'addon_mod_choice_responses'; +export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { + name: 'AddonModChoiceOfflineProvider', + version: 1, + tables: [ + { + name: RESPONSES_TABLE_NAME, + columns: [ + { + name: 'choiceid', + type: 'INTEGER', + }, + { + name: 'name', + type: 'TEXT', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'userid', + type: 'INTEGER', + }, + { + name: 'responses', + type: 'TEXT', + }, + { + name: 'deleting', + type: 'INTEGER', + }, + { + name: 'timecreated', + type: 'INTEGER', + }, + ], + primaryKeys: ['choiceid', 'userid'], + }, + ], +}; + +/** + * Response data. + */ +export type AddonModChoiceResponsesDBRecord = { + choiceid: number; + userid: number; + courseid: number; + name: string; + responses: string; + deleting: number; + timecreated: number; +}; diff --git a/src/addons/mod/choice/services/handlers/index-link.ts b/src/addons/mod/choice/services/handlers/index-link.ts new file mode 100644 index 000000000..d7d32ff52 --- /dev/null +++ b/src/addons/mod/choice/services/handlers/index-link.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to choice index. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChoiceIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModChoiceIndexLinkHandler'; + + constructor() { + super('AddonModChoice', 'choice'); + } + +} + +export const AddonModChoiceIndexLinkHandler = makeSingleton(AddonModChoiceIndexLinkHandlerService); diff --git a/src/addons/mod/choice/services/handlers/list-link.ts b/src/addons/mod/choice/services/handlers/list-link.ts new file mode 100644 index 000000000..2c0cf955e --- /dev/null +++ b/src/addons/mod/choice/services/handlers/list-link.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to choice list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChoiceListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModChoiceListLinkHandler'; + + constructor() { + super('AddonModChoice', 'choice'); + } + +} + +export const AddonModChoiceListLinkHandler = makeSingleton(AddonModChoiceListLinkHandlerService); diff --git a/src/addons/mod/choice/services/handlers/module.ts b/src/addons/mod/choice/services/handlers/module.ts new file mode 100644 index 000000000..5f6d333cb --- /dev/null +++ b/src/addons/mod/choice/services/handlers/module.ts @@ -0,0 +1,83 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonModChoiceIndexComponent } from '../../components/index'; + +/** + * Handler to support choice modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChoiceModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_choice'; + + name = 'AddonModChoice'; + modName = 'choice'; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: true, + [CoreConstants.FEATURE_GROUPINGS]: true, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData { + return { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_choice-handler', + showDownloadButton: true, + action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.navigateToSitePath(AddonModChoiceModuleHandlerService.PAGE_NAME + routeParams, options); + }, + }; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise> { + return AddonModChoiceIndexComponent; + } + +} + +export const AddonModChoiceModuleHandler = makeSingleton(AddonModChoiceModuleHandlerService); diff --git a/src/addons/mod/choice/services/handlers/prefetch.ts b/src/addons/mod/choice/services/handlers/prefetch.ts new file mode 100644 index 000000000..0ead84f9a --- /dev/null +++ b/src/addons/mod/choice/services/handlers/prefetch.ts @@ -0,0 +1,157 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreUser } from '@features/user/services/user'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonModChoice, AddonModChoiceProvider } from '../choice'; +import { AddonModChoiceSync, AddonModChoiceSyncResult } from '../choice-sync'; + +/** + * Handler to prefetch choices. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChoicePrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { + + name = 'AddonModChoice'; + modName = 'choice'; + component = AddonModChoiceProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^answers$/; + + /** + * @inheritdoc + */ + prefetch(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise { + return this.prefetchPackage(module, courseId, this.prefetchChoice.bind(this, module, courseId, !!single)); + } + + /** + * Prefetch a choice. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @param single True if we're downloading a single module, false if we're downloading a whole section. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchChoice( + module: CoreCourseAnyModuleData, + courseId: number, + single: boolean, + siteId?: string, + ): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + + const choice = await AddonModChoice.getChoice(courseId, module.id, commonOptions); + + // Get the intro files. + const introFiles = this.getIntroFilesFromInstance(module, choice); + + await Promise.all([ + AddonModChoice.getOptions(choice.id, modOptions), + this.prefetchResults(choice.id, courseId, modOptions), + CoreFilepool.addFilesToQueue(siteId, introFiles, AddonModChoiceProvider.COMPONENT, module.id), + ]); + } + + /** + * Prefetch choice results. + * + * @param choiceId Choice ID. + * @param modOptions Options. + * @return Promise resolved when done. + */ + protected async prefetchResults( + choiceId: number, + courseId: number, + modOptions: CoreCourseCommonModWSOptions, + ): Promise { + const options = await AddonModChoice.getResults(choiceId, modOptions); + + // If we can see the users that answered, prefetch their profile and avatar. + const promises: Promise[] = []; + + options.forEach((option) => { + option.userresponses.forEach((response) => { + if (response.userid) { + promises.push(CoreUser.getProfile(response.userid, courseId, false, modOptions.siteId)); + } + if (response.profileimageurl) { + promises.push(CoreFilepool.addToQueueByUrl(modOptions.siteId!, response.profileimageurl).catch(() => { + // Ignore failures. + })); + } + }); + }); + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + const choice = await CoreUtils.ignoreErrors(AddonModChoice.getChoice(courseId, module.id)); + + return this.getIntroFilesFromInstance(module, choice); + } + + /** + * @inheritdoc + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return AddonModChoice.invalidateContent(moduleId, courseId); + } + + /** + * Invalidate WS calls needed to determine module status. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @return Promise resolved when invalidated. + */ + invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise { + return AddonModChoice.invalidateChoiceData(courseId); + } + + /** + * Sync a module. + * + * @param module Module. + * @param courseId Course ID the module belongs to + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise { + return AddonModChoiceSync.syncChoice(module.instance!, undefined, siteId); + } + +} + +export const AddonModChoicePrefetchHandler = makeSingleton(AddonModChoicePrefetchHandlerService); diff --git a/src/addons/mod/choice/services/handlers/sync-cron.ts b/src/addons/mod/choice/services/handlers/sync-cron.ts new file mode 100644 index 000000000..d59dc5aed --- /dev/null +++ b/src/addons/mod/choice/services/handlers/sync-cron.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 '@services/cron'; +import { makeSingleton } from '@singletons'; +import { AddonModChoiceSync } from '../choice-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModChoiceSyncCronHandlerService implements CoreCronHandler { + + name = 'AddonModChoiceSyncCronHandler'; + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param siteId ID of the site affected, undefined for all sites. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return AddonModChoiceSync.syncAllChoices(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return Time between consecutive executions (in ms). + */ + getInterval(): number { + return AddonModChoiceSync.syncInterval; + } + +} + +export const AddonModChoiceSyncCronHandler = makeSingleton(AddonModChoiceSyncCronHandlerService); diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 0ee59c1c3..f16e25ea1 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -29,6 +29,7 @@ import { AddonModLtiModule } from './lti/lti.module'; import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module'; import { AddonModSurveyModule } from './survey/survey.module'; import { AddonModScormModule } from './scorm/scorm.module'; +import { AddonModChoiceModule } from './choice/choice.module'; @NgModule({ declarations: [], @@ -48,6 +49,7 @@ import { AddonModScormModule } from './scorm/scorm.module'; AddonModH5PActivityModule, AddonModSurveyModule, AddonModScormModule, + AddonModChoiceModule, ], providers: [], exports: [], diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 173c46c4f..4738f4538 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -124,7 +124,7 @@ import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module'; import { ADDON_MOD_ASSIGN_SERVICES } from '@addons/mod/assign/assign.module'; import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module'; // @todo import { ADDON_MOD_CHAT_SERVICES } from '@addons/mod/chat/chat.module'; -// @todo import { ADDON_MOD_CHOICE_SERVICES } from '@addons/mod/choice/choice.module'; +import { ADDON_MOD_CHOICE_SERVICES } from '@addons/mod/choice/choice.module'; // @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module'; import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module'; import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module'; @@ -289,7 +289,7 @@ export class CoreCompileProvider { ...ADDON_MOD_ASSIGN_SERVICES, ...ADDON_MOD_BOOK_SERVICES, // @todo ...ADDON_MOD_CHAT_SERVICES, - // @todo ...ADDON_MOD_CHOICE_SERVICES, + ...ADDON_MOD_CHOICE_SERVICES, // @todo ...ADDON_MOD_FEEDBACK_SERVICES, ...ADDON_MOD_FOLDER_SERVICES, ...ADDON_MOD_FORUM_SERVICES,