diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index fe583dd04..fb2243153 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -352,14 +352,13 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo } const modal = this.domUtils.showModalLoading('core.sending', true); - this.choiceProvider.submitResponse(this.choice.id, this.choice.name, this.courseId, responses).then(() => { + this.choiceProvider.submitResponse(this.choice.id, this.choice.name, this.courseId, responses).then((online) => { // Success! // Check completion since it could be configured to complete once the user answers the choice. this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); this.domUtils.scrollToTop(this.content); - // Let's refresh the data. - return this.refreshContent(false); + return this.dataUpdated(online); }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true); }).finally(() => { @@ -377,7 +376,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.choiceProvider.deleteResponses(this.choice.id, this.choice.name, this.courseId).then(() => { this.domUtils.scrollToTop(this.content); - // Success! Let's refresh the data. + // Refresh the data. Don't call dataUpdated because deleting an answer doesn't mark the choice as outdated. return this.refreshContent(false); }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true); @@ -389,6 +388,28 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo }); } + /** + * Function to call when some data has changed. It will refresh/prefetch data. + * + * @param {boolean} online Whether the data was sent to server or stored in offline. + * @return {Promise} Promise resolved when done. + */ + protected dataUpdated(online: boolean): Promise { + if (online && this.isPrefetched()) { + // The choice is downloaded, update the data. + return this.choiceSync.prefetchAfterUpdate(this.module, this.courseId).then(() => { + // Update the view. + this.showLoadingAndFetch(false, false); + }).catch(() => { + // Prefetch failed, refresh the data. + return this.refreshContent(false); + }); + } else { + // Not downloaded, refresh the data. + return this.refreshContent(false); + } + } + /** * Performs the sync of the activity. * diff --git a/src/addon/mod/choice/providers/choice.ts b/src/addon/mod/choice/providers/choice.ts index 8845aed49..b94e92b9c 100644 --- a/src/addon/mod/choice/providers/choice.ts +++ b/src/addon/mod/choice/providers/choice.ts @@ -19,6 +19,7 @@ import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModChoiceOfflineProvider } from './offline'; +import { CoreSiteWSPreSets } from '@classes/site'; /** * Service that provides some features for choices. @@ -68,9 +69,9 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID the choice belongs to. * @param {number[]} [responses] IDs of the answers. If not defined, delete all the answers of the current user. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when the options are deleted. + * @return {Promise} Promise resolved with boolean: true if response was sent to server, false if stored in device. */ - deleteResponses(choiceId: number, name: string, courseId: number, responses?: number[], siteId?: string): Promise { + deleteResponses(choiceId: number, name: string, courseId: number, responses?: number[], siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); responses = responses || []; @@ -173,20 +174,29 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {string} key Name of the property to check. * @param {any} value Value to search. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache: boolean = false) - : Promise { + protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache?: boolean, + ignoreCache?: boolean): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceDataCacheKey(courseId), omitExpires: forceCache }; + if (forceCache) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choices_by_courses', params, preSets).then((response) => { if (response && response.choices) { const currentChoice = response.choices.find((choice) => choice[key] == value); @@ -206,11 +216,12 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {number} cmId Course module ID. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - getChoice(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache); + getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache); } /** @@ -219,29 +230,36 @@ export class AddonModChoiceProvider { * @param {number} courseId Course ID. * @param {number} choiceId Choice ID. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the choice is retrieved. */ - getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache); + getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache); } /** * Get choice options. * - * @param {number} choiceId Choice ID. - * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} choiceId Choice ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with choice options. */ - getOptions(choiceId: number, siteId?: string): Promise { + getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceOptionsCacheKey(choiceId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choice_options', params, preSets).then((response) => { if (response.options) { return response.options; @@ -255,19 +273,25 @@ export class AddonModChoiceProvider { /** * Get choice results. * - * @param {number} choiceId Choice ID. - * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} choiceId Choice ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with choice results. */ - getResults(choiceId: number, siteId?: string): Promise { + getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets = { + const preSets: CoreSiteWSPreSets = { cacheKey: this.getChoiceResultsCacheKey(choiceId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_choice_get_choice_results', params, preSets).then((response) => { if (response.options) { return response.options; diff --git a/src/addon/mod/choice/providers/prefetch-handler.ts b/src/addon/mod/choice/providers/prefetch-handler.ts index fb3680e4c..0b2f74eba 100644 --- a/src/addon/mod/choice/providers/prefetch-handler.ts +++ b/src/addon/mod/choice/providers/prefetch-handler.ts @@ -23,7 +23,6 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonModChoiceProvider } from './choice'; -import { AddonModChoiceSyncProvider } from './sync'; /** * Handler to prefetch choices. @@ -38,7 +37,7 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan constructor(translate: TranslateService, appProvider: CoreAppProvider, utils: CoreUtilsProvider, courseProvider: CoreCourseProvider, filepoolProvider: CoreFilepoolProvider, sitesProvider: CoreSitesProvider, domUtils: CoreDomUtilsProvider, protected choiceProvider: AddonModChoiceProvider, - protected syncProvider: AddonModChoiceSyncProvider, protected userProvider: CoreUserProvider) { + protected userProvider: CoreUserProvider) { super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); } @@ -66,12 +65,12 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan * @return {Promise} Promise resolved when done. */ protected prefetchChoice(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.choiceProvider.getChoice(courseId, module.id, siteId).then((choice) => { + return this.choiceProvider.getChoice(courseId, module.id, siteId, false, true).then((choice) => { const promises = []; // Get the options and results. - promises.push(this.choiceProvider.getOptions(choice.id, siteId)); - promises.push(this.choiceProvider.getResults(choice.id, siteId).then((options) => { + promises.push(this.choiceProvider.getOptions(choice.id, true, siteId)); + promises.push(this.choiceProvider.getResults(choice.id, true, siteId).then((options) => { // If we can see the users that answered, prefetch their profile and avatar. const subPromises = []; options.forEach((option) => { diff --git a/src/addon/mod/choice/providers/sync.ts b/src/addon/mod/choice/providers/sync.ts index be612049a..606ffc385 100644 --- a/src/addon/mod/choice/providers/sync.ts +++ b/src/addon/mod/choice/providers/sync.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreSitesProvider } from '@providers/sites'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -26,13 +25,16 @@ import { CoreEventsProvider } from '@providers/events'; import { TranslateService } from '@ngx-translate/core'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { CoreSyncProvider } from '@providers/sync'; +import { AddonModChoicePrefetchHandler } from './prefetch-handler'; /** * Service to sync choices. */ @Injectable() -export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { +export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_choice_autom_synced'; protected componentTranslate: string; @@ -42,9 +44,11 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { private eventsProvider: CoreEventsProvider, private choiceProvider: AddonModChoiceProvider, translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider, timeUtils: CoreTimeUtilsProvider, - private logHelper: CoreCourseLogHelperProvider) { + private logHelper: CoreCourseLogHelperProvider, prefetchHandler: AddonModChoicePrefetchHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate) { + super('AddonModChoiceSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('choice'); } @@ -195,16 +199,8 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider { }); }).then(() => { if (courseId) { - const promises = [ - this.choiceProvider.invalidateChoiceData(courseId), - choiceId ? this.choiceProvider.invalidateOptions(choiceId) : Promise.resolve(), - choiceId ? this.choiceProvider.invalidateResults(choiceId) : Promise.resolve(), - ]; - - // Data has been sent to server, update choice data. - return Promise.all(promises).then(() => { - return this.choiceProvider.getChoiceById(courseId, choiceId, siteId); - }).catch(() => { + // Data has been sent to server, prefetch choice if needed. + return this.prefetchAfterUpdate(module, courseId, undefined, siteId).catch(() => { // Ignore errors. }); } diff --git a/src/addon/mod/feedback/components/index/index.ts b/src/addon/mod/feedback/components/index/index.ts index 6914e4b03..76610926d 100644 --- a/src/addon/mod/feedback/components/index/index.ts +++ b/src/addon/mod/feedback/components/index/index.ts @@ -77,15 +77,29 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity // Listen for form submit events. this.submitObserver = this.eventsProvider.on(AddonModFeedbackProvider.FORM_SUBMITTED, (data) => { if (this.feedback && data.feedbackId == this.feedback.id) { - // Go to review attempt if an attempt in this quiz was finished and synced. this.tabsLoaded['analysis'] = false; this.tabsLoaded['overview'] = false; this.loaded = false; - if (data.tab != this.tab) { - this.tabChanged(data.tab); + + let promise; + + // Prefetch data if needed. + if (!data.offline && this.isPrefetched()) { + promise = this.feedbackSync.prefetchAfterUpdate(this.module, this.courseId).catch(() => { + // Ignore errors. + }); } else { - this.loadContent(true); + promise = Promise.resolve(); } + + promise.then(() => { + // Load the right tab. + if (data.tab != this.tab) { + this.tabChanged(data.tab); + } else { + this.loadContent(true); + } + }); } }, this.siteId); } diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts index 214f12bd7..0d8bbf9fa 100644 --- a/src/addon/mod/feedback/pages/form/form.ts +++ b/src/addon/mod/feedback/pages/form/form.ts @@ -338,8 +338,13 @@ export class AddonModFeedbackFormPage implements OnDestroy { ngOnDestroy(): void { if (this.submitted) { const tab = this.submitted == 'analysis' ? 'analysis' : 'overview'; + // If form has been submitted, the info has been already invalidated but we should update index view. - this.eventsProvider.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, {feedbackId: this.feedback.id, tab: tab}); + this.eventsProvider.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, { + feedbackId: this.feedback.id, + tab: tab, + offline: this.completedOffline + }); } this.onlineObserver && this.onlineObserver.unsubscribe(); } diff --git a/src/addon/mod/feedback/providers/feedback.ts b/src/addon/mod/feedback/providers/feedback.ts index a113791c5..7d3ebd4d9 100644 --- a/src/addon/mod/feedback/providers/feedback.ts +++ b/src/addon/mod/feedback/providers/feedback.ts @@ -20,6 +20,7 @@ import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreAppProvider } from '@providers/app'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModFeedbackOfflineProvider } from './offline'; +import { CoreSiteWSPreSets } from '@classes/site'; /** * Service that provides some features for feedbacks. @@ -215,11 +216,14 @@ export class AddonModFeedbackProvider { * * @param {number} feedbackId Feedback ID. * @param {number} groupId Group id, 0 means that the function will determine the user group. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @param {any} [previous] Only for recurrent use. Object with the previous fetched info. * @return {Promise} Promise resolved when the info is retrieved. */ - getAllNonRespondents(feedbackId: number, groupId: number, siteId?: string, previous?: any): Promise { + getAllNonRespondents(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) + : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (typeof previous == 'undefined') { previous = { @@ -228,7 +232,7 @@ export class AddonModFeedbackProvider { }; } - return this.getNonRespondents(feedbackId, groupId, previous.page, siteId).then((response) => { + return this.getNonRespondents(feedbackId, groupId, previous.page, ignoreCache, siteId).then((response) => { if (previous.users.length < response.total) { previous.users = previous.users.concat(response.users); } @@ -237,7 +241,7 @@ export class AddonModFeedbackProvider { // Can load more. previous.page++; - return this.getAllNonRespondents(feedbackId, groupId, siteId, previous); + return this.getAllNonRespondents(feedbackId, groupId, ignoreCache, siteId, previous); } previous.total = response.total; @@ -250,11 +254,14 @@ export class AddonModFeedbackProvider { * * @param {number} feedbackId Feedback ID. * @param {number} groupId Group id, 0 means that the function will determine the user group. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @param {any} [previous] Only for recurrent use. Object with the previous fetched info. * @return {Promise} Promise resolved when the info is retrieved. */ - getAllResponsesAnalysis(feedbackId: number, groupId: number, siteId?: string, previous?: any): Promise { + getAllResponsesAnalysis(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) + : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (typeof previous == 'undefined') { previous = { @@ -264,7 +271,7 @@ export class AddonModFeedbackProvider { }; } - return this.getResponsesAnalysis(feedbackId, groupId, previous.page, siteId).then((responses) => { + return this.getResponsesAnalysis(feedbackId, groupId, previous.page, ignoreCache, siteId).then((responses) => { if (previous.anonattempts.length < responses.totalanonattempts) { previous.anonattempts = previous.anonattempts.concat(responses.anonattempts); } @@ -277,7 +284,7 @@ export class AddonModFeedbackProvider { // Can load more. previous.page++; - return this.getAllResponsesAnalysis(feedbackId, groupId, siteId, previous); + return this.getAllResponsesAnalysis(feedbackId, groupId, ignoreCache, siteId, previous); } previous.totalattempts = responses.totalattempts; @@ -292,15 +299,16 @@ export class AddonModFeedbackProvider { * * @param {number} feedbackId Feedback ID. * @param {number} [groupId] Group ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the feedback is retrieved. */ - getAnalysis(feedbackId: number, groupId?: number, siteId?: string): Promise { + getAnalysis(feedbackId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getAnalysisDataCacheKey(feedbackId, groupId) }; @@ -308,6 +316,11 @@ export class AddonModFeedbackProvider { params['groupid'] = groupId; } + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_analysis', params, preSets); }); } @@ -338,11 +351,12 @@ export class AddonModFeedbackProvider { * * @param {number} feedbackId Feedback ID. * @param {number} attemptId Attempt id to find. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @param {any} [previous] Only for recurrent use. Object with the previous fetched info. * @return {Promise} Promise resolved when the info is retrieved. */ - getAttempt(feedbackId: number, attemptId: number, siteId?: string, previous?: any): Promise { + getAttempt(feedbackId: number, attemptId: number, ignoreCache?: boolean, siteId?: string, previous?: any): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (typeof previous == 'undefined') { previous = { @@ -352,7 +366,7 @@ export class AddonModFeedbackProvider { }; } - return this.getResponsesAnalysis(feedbackId, 0, previous.page, siteId).then((responses) => { + return this.getResponsesAnalysis(feedbackId, 0, previous.page, ignoreCache, siteId).then((responses) => { let attempt; attempt = responses.attempts.find((attempt) => { @@ -383,7 +397,7 @@ export class AddonModFeedbackProvider { // Can load more. Check there. previous.page++; - return this.getAttempt(feedbackId, attemptId, siteId, previous); + return this.getAttempt(feedbackId, attemptId, ignoreCache, siteId, previous); } // Not found and all loaded. Reject. @@ -405,18 +419,24 @@ export class AddonModFeedbackProvider { * Returns the temporary completion timemodified for the current user. * * @param {number} feedbackId Feedback ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the info is retrieved. */ - getCurrentCompletedTimeModified(feedbackId: number, siteId?: string): Promise { + getCurrentCompletedTimeModified(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_current_completed_tmp', params, preSets).then((response) => { if (response && typeof response.feedback != 'undefined' && typeof response.feedback.timemodified != 'undefined') { return response.feedback.timemodified; @@ -552,20 +572,26 @@ export class AddonModFeedbackProvider { * @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. - * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the feedback is retrieved. */ - protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean): Promise { + protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean, + ignoreCache?: boolean): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getFeedbackCacheKey(courseId) }; if (forceCache) { - preSets['omitExpires'] = true; + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; } return site.read('mod_feedback_get_feedbacks_by_courses', params, preSets).then((response) => { @@ -589,11 +615,12 @@ export class AddonModFeedbackProvider { * @param {number} courseId Course ID. * @param {number} cmId Course module ID. * @param {string} [siteId] Site ID. If not defined, current site. - * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the feedback is retrieved. */ - getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean): Promise { - return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache); + getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache, ignoreCache); } /** @@ -603,28 +630,35 @@ export class AddonModFeedbackProvider { * @param {number} id Feedback ID. * @param {string} [siteId] Site ID. If not defined, current site. * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the feedback is retrieved. */ - getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean): Promise { - return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache); + getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { + return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache, ignoreCache); } /** * Returns the items (questions) in the given feedback. * * @param {number} feedbackId Feedback ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the info is retrieved. */ - getItems(feedbackId: number, siteId?: string): Promise { + getItems(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getItemsDataCacheKey(feedbackId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_items', params, preSets); }); } @@ -645,20 +679,28 @@ export class AddonModFeedbackProvider { * @param {number} feedbackId Feedback ID. * @param {number} [groupId=0] Group id, 0 means that the function will determine the user group. * @param {number} [page=0] The page of records to return. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the info is retrieved. */ - getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, siteId?: string): Promise { + getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, ignoreCache?: boolean, siteId?: string) + : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId, groupid: groupId, page: page }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, groupId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_non_respondents', params, preSets); }); } @@ -725,7 +767,7 @@ export class AddonModFeedbackProvider { }); }).catch(() => { // If getPageItems fail we should calculate it using getItems. - return this.getItems(feedbackId, siteId).then((response) => { + return this.getItems(feedbackId, false, siteId).then((response) => { return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { // Separate items by pages. let currentPage = 0; @@ -802,20 +844,26 @@ export class AddonModFeedbackProvider { * @param {number} feedbackId Feedback ID. * @param {number} groupId Group id, 0 means that the function will determine the user group. * @param {number} page The page of records to return. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the info is retrieved. */ - getResponsesAnalysis(feedbackId: number, groupId: number, page: number, siteId?: string): Promise { + getResponsesAnalysis(feedbackId: number, groupId: number, page: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId, groupid: groupId || 0, page: page || 0 }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, groupId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_feedback_get_responses_analysis', params, preSets); }); } @@ -1037,18 +1085,24 @@ export class AddonModFeedbackProvider { * Returns if feedback has been completed * * @param {number} feedbackId Feedback ID. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the info is retrieved. */ - isCompleted(feedbackId: number, siteId?: string): Promise { + isCompleted(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { feedbackid: feedbackId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getCompletedDataCacheKey(feedbackId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return this.utils.promiseWorks(site.read('mod_feedback_get_last_completed', params, preSets)); }); } diff --git a/src/addon/mod/feedback/providers/prefetch-handler.ts b/src/addon/mod/feedback/providers/prefetch-handler.ts index 73a9dfc6b..ab9528fc7 100644 --- a/src/addon/mod/feedback/providers/prefetch-handler.ts +++ b/src/addon/mod/feedback/providers/prefetch-handler.ts @@ -173,19 +173,15 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH */ protected prefetchFeedback(module: any, courseId: number, single: boolean, siteId: string): Promise { // Prefetch the feedback data. - return this.feedbackProvider.getFeedback(courseId, module.id).then((feedback) => { - const p1 = []; + return this.feedbackProvider.getFeedback(courseId, module.id, siteId, false, true).then((feedback) => { + let files = (feedback.pageaftersubmitfiles || []).concat(this.getIntroFilesFromInstance(module, feedback)); - p1.push(this.getFiles(module, courseId).then((files) => { - return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); - })); - - p1.push(this.feedbackProvider.getFeedbackAccessInformation(feedback.id, false, true, siteId).then((accessData) => { + return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, false, true, siteId).then((accessData) => { const p2 = []; if (accessData.canedititems || accessData.canviewreports) { // Get all groups analysis. - p2.push(this.feedbackProvider.getAnalysis(feedback.id, undefined, siteId)); - p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId) + p2.push(this.feedbackProvider.getAnalysis(feedback.id, undefined, true, siteId)); + p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId, true) .then((groupInfo) => { const p3 = [], userIds = []; @@ -194,8 +190,8 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH groupInfo.groups = [{id: 0}]; } groupInfo.groups.forEach((group) => { - p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, siteId)); - p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, siteId) + p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, true, siteId)); + p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, true, siteId) .then((responses) => { responses.attempts.forEach((attempt) => { userIds.push(attempt.userid); @@ -203,7 +199,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH })); if (!accessData.isanonymous) { - p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, group.id, siteId) + p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, group.id, true, siteId) .then((responses) => { responses.users.forEach((user) => { userIds.push(user.userid); @@ -219,7 +215,13 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH })); } - p2.push(this.feedbackProvider.getItems(feedback.id, siteId)); + p2.push(this.feedbackProvider.getItems(feedback.id, true, siteId).then((response) => { + response.items.forEach((item) => { + files = files.concat(item.itemfiles); + }); + + return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id); + })); if (accessData.cancomplete && accessData.cansubmit && !accessData.isempty) { // Send empty data, so it will recover last completed feedback attempt values. @@ -234,9 +236,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH } return Promise.all(p2); - })); - - return Promise.all(p1); + }); }); } } diff --git a/src/addon/mod/feedback/providers/show-entries-link-handler.ts b/src/addon/mod/feedback/providers/show-entries-link-handler.ts index fd32a42fe..d10c156da 100644 --- a/src/addon/mod/feedback/providers/show-entries-link-handler.ts +++ b/src/addon/mod/feedback/providers/show-entries-link-handler.ts @@ -65,7 +65,7 @@ export class AddonModFeedbackShowEntriesLinkHandler extends CoreContentLinksHand return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackRespondentsPage', stateParams, siteId); } - return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, siteId).then((attempt) => { + return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, true, siteId).then((attempt) => { stateParams = { moduleId: module.id, attempt: attempt, diff --git a/src/addon/mod/feedback/providers/sync.ts b/src/addon/mod/feedback/providers/sync.ts index 0a9364b48..dc447d944 100644 --- a/src/addon/mod/feedback/providers/sync.ts +++ b/src/addon/mod/feedback/providers/sync.ts @@ -15,7 +15,6 @@ 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'; @@ -25,14 +24,17 @@ import { AddonModFeedbackProvider } from './feedback'; import { CoreEventsProvider } from '@providers/events'; import { TranslateService } from '@ngx-translate/core'; import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreSyncProvider } from '@providers/sync'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreSyncProvider } from '@providers/sync'; +import { AddonModFeedbackPrefetchHandler } from './prefetch-handler'; /** * Service to sync feedbacks. */ @Injectable() -export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { +export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_feedback_autom_synced'; protected componentTranslate: string; @@ -42,13 +44,30 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { private eventsProvider: CoreEventsProvider, private feedbackProvider: AddonModFeedbackProvider, protected translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider, courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider, timeUtils: CoreTimeUtilsProvider, - private logHelper: CoreCourseLogHelperProvider) { + private logHelper: CoreCourseLogHelperProvider, prefetchDelegate: CoreCourseModulePrefetchDelegate, + prefetchHandler: AddonModFeedbackPrefetchHandler) { + super('AddonModFeedbackSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('feedback'); } + /** + * Conveniece function to prefetch data after an update. + * + * @param {any} module Module. + * @param {number} courseId Course ID. + * @param {RegExp} [regex] If regex matches, don't download the data. Defaults to check files and timers. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchAfterUpdate(module: any, courseId: number, regex?: RegExp, siteId?: string): Promise { + regex = regex || /^.*files$|^timers/; + + return super.prefetchAfterUpdate(module, courseId, regex, siteId); + } + /** * Try to synchronize all the feedbacks in a certain site or in all sites. * @@ -196,7 +215,7 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { return Promise.all(promises); } - return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, siteId).then((timemodified) => { + return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, true, siteId).then((timemodified) => { // Sort by page. responses.sort((a, b) => { return a.page - b.page; @@ -216,8 +235,8 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { }); }).then(() => { if (result.updated) { - // Data has been sent to server. Now invalidate the WS calls. - return this.feedbackProvider.invalidateAllFeedbackData(feedbackId, siteId).catch(() => { + // Data has been sent to server, update data. + return this.prefetchAfterUpdate(module, courseId, undefined, siteId).catch(() => { // Ignore errors. }); } diff --git a/src/addon/mod/lesson/components/index/index.ts b/src/addon/mod/lesson/components/index/index.ts index ada4eded7..0f98b9444 100644 --- a/src/addon/mod/lesson/components/index/index.ts +++ b/src/addon/mod/lesson/components/index/index.ts @@ -61,6 +61,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo protected accessInfo: any; // Lesson access info. protected password: string; // The password for the lesson. protected hasPlayed: boolean; // Whether the user has gone to the lesson player (attempted). + protected dataSentObserver; // To detect data sent to server. + protected dataSent = false; // Whether some data was sent to server while playing the lesson. constructor(injector: Injector, protected lessonProvider: AddonModLessonProvider, @Optional() content: Content, protected groupsProvider: CoreGroupsProvider, protected lessonOffline: AddonModLessonOfflineProvider, @@ -228,6 +230,13 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * @return {boolean} If suceed or not. */ protected hasSyncSucceed(result: any): boolean { + if (result.updated || this.dataSent) { + // Check completion status if something was sent. + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); + } + + this.dataSent = false; + return result.updated; } @@ -243,6 +252,10 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo if (this.hasPlayed) { this.hasPlayed = false; + this.dataSentObserver && this.dataSentObserver.off(); // Stop listening for changes. + this.dataSentObserver = undefined; + + // Refresh data. this.showLoadingAndRefresh(true, false); } } @@ -257,6 +270,16 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo if (this.navCtrl.getActive().component.name == 'AddonModLessonPlayerPage') { this.hasPlayed = true; + + // Detect if anything was sent to server. + this.dataSentObserver && this.dataSentObserver.off(); + + this.dataSentObserver = this.eventsProvider.on(AddonModLessonProvider.DATA_SENT_EVENT, (data) => { + // Ignore launch sending because it only affects timers. + if (data.lessonId === this.lesson.id && data.type != 'launch') { + this.dataSent = true; + } + }, this.siteId); } } @@ -556,7 +579,18 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * @return {Promise} Promise resolved when done. */ protected sync(): Promise { - return this.lessonSync.syncLesson(this.lesson.id, true); + return this.lessonSync.syncLesson(this.lesson.id, true).then((result) => { + if (!result.updated && this.dataSent && this.isPrefetched()) { + // The user sent data to server, but not in the sync process. Check if we need to fetch data. + return this.lessonSync.prefetchAfterUpdate(this.module, this.courseId).catch(() => { + // Ignore errors. + }).then(() => { + return result; + }); + } + + return result; + }); } /** @@ -575,4 +609,13 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo return Promise.reject(error); }); } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.dataSentObserver && this.dataSentObserver.off(); + } } diff --git a/src/addon/mod/lesson/providers/lesson-sync.ts b/src/addon/mod/lesson/providers/lesson-sync.ts index 68b741f94..d4c183285 100644 --- a/src/addon/mod/lesson/providers/lesson-sync.ts +++ b/src/addon/mod/lesson/providers/lesson-sync.ts @@ -25,7 +25,8 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { AddonModLessonProvider } from './lesson'; import { AddonModLessonOfflineProvider } from './lesson-offline'; import { AddonModLessonPrefetchHandler } from './prefetch-handler'; @@ -51,7 +52,7 @@ export interface AddonModLessonSyncResult { * Service to sync lesson. */ @Injectable() -export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { +export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_lesson_autom_synced'; @@ -92,12 +93,12 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, private lessonProvider: AddonModLessonProvider, private lessonOfflineProvider: AddonModLessonOfflineProvider, - private prefetchHandler: AddonModLessonPrefetchHandler, timeUtils: CoreTimeUtilsProvider, + protected prefetchHandler: AddonModLessonPrefetchHandler, timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, - private logHelper: CoreCourseLogHelperProvider) { + private logHelper: CoreCourseLogHelperProvider, prefetchDelegate: CoreCourseModulePrefetchDelegate) { super('AddonModLessonSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('lesson'); @@ -288,7 +289,7 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { courseId = attempts[0].courseid; // Get the info, access info and the lesson password if needed. - return this.lessonProvider.getLessonById(courseId, lessonId, false, siteId).then((lessonData) => { + return this.lessonProvider.getLessonById(courseId, lessonId, false, false, siteId).then((lessonData) => { lesson = lessonData; return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); @@ -364,7 +365,7 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { // Data already retrieved when syncing attempts. promise = Promise.resolve(); } else { - promise = this.lessonProvider.getLessonById(courseId, lessonId, false, siteId).then((lessonData) => { + promise = this.lessonProvider.getLessonById(courseId, lessonId, false, false, siteId).then((lessonData) => { lesson = lessonData; return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); @@ -429,31 +430,9 @@ export class AddonModLessonSyncProvider extends CoreSyncBaseProvider { }); }).then(() => { if (result.updated && courseId) { - // Data has been sent to server. Now invalidate the WS calls. - const promises = []; - - promises.push(this.lessonProvider.invalidateAccessInformation(lessonId, siteId)); - promises.push(this.lessonProvider.invalidateContentPagesViewed(lessonId, siteId)); - promises.push(this.lessonProvider.invalidateQuestionsAttempts(lessonId, siteId)); - promises.push(this.lessonProvider.invalidatePagesPossibleJumps(lessonId, siteId)); - promises.push(this.lessonProvider.invalidateTimers(lessonId, siteId)); - - return this.utils.allPromises(promises).catch(() => { + // Data has been sent to server, update data. + return this.prefetchAfterUpdate(module, courseId, undefined, siteId).catch(() => { // Ignore errors. - }).then(() => { - // Sync successful, update some data that might have been modified. - return this.lessonProvider.getAccessInformation(lessonId, false, false, siteId).then((info) => { - const promises = [], - retake = info.attemptscount; - - promises.push(this.lessonProvider.getContentPagesViewedOnline(lessonId, retake, false, false, siteId)); - promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lessonId, retake, false, undefined, false, - false, siteId)); - - return Promise.all(promises); - }).catch(() => { - // Ignore errors. - }); }); } }).then(() => { diff --git a/src/addon/mod/lesson/providers/lesson.ts b/src/addon/mod/lesson/providers/lesson.ts index e39b119cf..5441b3f3a 100644 --- a/src/addon/mod/lesson/providers/lesson.ts +++ b/src/addon/mod/lesson/providers/lesson.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -113,6 +114,7 @@ export interface AddonModLessonGrade { @Injectable() export class AddonModLessonProvider { static COMPONENT = 'mmaModLesson'; + static DATA_SENT_EVENT = 'addon_mod_lesson_data_sent'; // This page. static LESSON_THISPAGE = 0; @@ -186,7 +188,8 @@ export class AddonModLessonProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider, - private lessonOfflineProvider: AddonModLessonOfflineProvider, private logHelper: CoreCourseLogHelperProvider) { + private lessonOfflineProvider: AddonModLessonOfflineProvider, private logHelper: CoreCourseLogHelperProvider, + private eventsProvider: CoreEventsProvider) { this.logger = logger.getInstance('AddonModLessonProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); @@ -1087,7 +1090,17 @@ export class AddonModLessonProvider { }); } - return this.finishRetakeOnline(lesson.id, password, outOfTime, review, siteId); + return this.finishRetakeOnline(lesson.id, password, outOfTime, review, siteId).then((response) => { + this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { + lessonId: lesson.id, + type: 'finish', + courseId: courseId, + outOfTime: outOfTime, + review: review + }, this.sitesProvider.getCurrentSiteId()); + + return response; + }); } /** @@ -1363,11 +1376,12 @@ export class AddonModLessonProvider { * @param {number} courseId Course ID. * @param {number} cmid Course module ID. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the lesson is retrieved. */ - getLesson(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { - return this.getLessonByField(courseId, 'coursemodule', cmId, forceCache, siteId); + getLesson(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getLessonByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); } /** @@ -1377,10 +1391,12 @@ export class AddonModLessonProvider { * @param {string} key Name of the property to check. * @param {any} value Value to search. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the lesson is retrieved. */ - protected getLessonByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise { + protected getLessonByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { @@ -1392,6 +1408,9 @@ export class AddonModLessonProvider { if (forceCache) { preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; } return site.read('mod_lesson_get_lessons_by_courses', params, preSets).then((response) => { @@ -1416,11 +1435,12 @@ export class AddonModLessonProvider { * @param {number} courseId Course ID. * @param {number} id Lesson ID. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the lesson is retrieved. */ - getLessonById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { - return this.getLessonByField(courseId, 'id', id, forceCache, siteId); + getLessonById(courseId: number, id: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getLessonByField(courseId, 'id', id, forceCache, ignoreCache, siteId); } /** @@ -2758,7 +2778,14 @@ export class AddonModLessonProvider { params.pageid = pageId; } - return site.write('mod_lesson_launch_attempt', params); + return site.write('mod_lesson_launch_attempt', params).then((response) => { + this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { + lessonId: id, + type: 'launch' + }, this.sitesProvider.getCurrentSiteId()); + + return response; + }); }); } @@ -3028,7 +3055,17 @@ export class AddonModLessonProvider { }); } - return this.processPageOnline(lesson.id, pageId, data, password, review, siteId); + return this.processPageOnline(lesson.id, pageId, data, password, review, siteId).then((response) => { + this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { + lessonId: lesson.id, + type: 'process', + courseId: courseId, + pageId: pageId, + review: review + }, this.sitesProvider.getCurrentSiteId()); + + return response; + }); } /** diff --git a/src/addon/mod/lesson/providers/prefetch-handler.ts b/src/addon/mod/lesson/providers/prefetch-handler.ts index 17de0a1dd..3660a90cd 100644 --- a/src/addon/mod/lesson/providers/prefetch-handler.ts +++ b/src/addon/mod/lesson/providers/prefetch-handler.ts @@ -83,7 +83,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan password, result; - return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lessonData) => { + return this.lessonProvider.getLesson(courseId, module.id, false, false, siteId).then((lessonData) => { lesson = lessonData; // Get the lesson password if it's needed. @@ -190,7 +190,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan const siteId = this.sitesProvider.getCurrentSiteId(); // Invalidate data to determine if module is downloadable. - return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lesson) => { + return this.lessonProvider.getLesson(courseId, module.id, true, false, siteId).then((lesson) => { const promises = []; promises.push(this.lessonProvider.invalidateLessonData(courseId, siteId)); @@ -210,7 +210,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan isDownloadable(module: any, courseId: number): boolean | Promise { const siteId = this.sitesProvider.getCurrentSiteId(); - return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lesson) => { + return this.lessonProvider.getLesson(courseId, module.id, false, false, siteId).then((lesson) => { if (!this.lessonProvider.isLessonOffline(lesson)) { return false; } @@ -260,7 +260,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan password, accessInfo; - return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lessonData) => { + return this.lessonProvider.getLesson(courseId, module.id, false, true, siteId).then((lessonData) => { lesson = lessonData; // Get the lesson password if it's needed. @@ -360,7 +360,8 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan if (accessInfo.canviewreports) { // Prefetch reports data. - promises.push(this.groupsProvider.getActivityAllowedGroupsIfEnabled(module.id, undefined, siteId).then((groups) => { + promises.push(this.groupsProvider.getActivityAllowedGroupsIfEnabled(module.id, undefined, siteId, true) + .then((groups) => { const subPromises = []; groups.forEach((group) => { diff --git a/src/addon/mod/quiz/providers/prefetch-handler.ts b/src/addon/mod/quiz/providers/prefetch-handler.ts index 794cd9c5b..d292dc958 100644 --- a/src/addon/mod/quiz/providers/prefetch-handler.ts +++ b/src/addon/mod/quiz/providers/prefetch-handler.ts @@ -50,6 +50,21 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); } + /** + * Download the module. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {boolean} [canStart=true] If true, start a new attempt if needed. + * @return {Promise} Promise resolved when all content is downloaded. + */ + download(module: any, courseId: number, dirPath?: string, single?: boolean, canStart: boolean = true): Promise { + // Same implementation for download and prefetch. + return this.prefetch(module, courseId, single, dirPath, canStart); + } + /** * Get list of files. If not defined, we'll assume they're in module.contents. * @@ -190,7 +205,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl const siteId = this.sitesProvider.getCurrentSiteId(); - return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quiz) => { + return this.quizProvider.getQuiz(courseId, module.id, false, false, siteId).then((quiz) => { if (quiz.allowofflineattempts !== 1 || quiz.hasquestions === 0) { return false; } @@ -220,10 +235,11 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param {number} courseId Course ID the module belongs to. * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. * @param {string} [dirPath] Path of the directory where to store all the content files. + * @param {boolean} [canStart=true] If true, start a new attempt if needed. * @return {Promise} Promise resolved when done. */ - prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { - return this.prefetchPackage(module, courseId, single, this.prefetchQuiz.bind(this)); + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string, canStart: boolean = true): Promise { + return this.prefetchPackage(module, courseId, single, this.prefetchQuiz.bind(this), undefined, canStart); } /** @@ -233,9 +249,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param {number} courseId Course ID the module belongs to. * @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section. * @param {String} siteId Site ID. + * @param {boolean} canStart If true, start a new attempt if needed. * @return {Promise} Promise resolved when done. */ - protected prefetchQuiz(module: any, courseId: number, single: boolean, siteId: string): Promise { + protected prefetchQuiz(module: any, courseId: number, single: boolean, siteId: string, canStart: boolean): Promise { let attempts: any[], startAttempt = false, quiz, @@ -244,7 +261,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl preflightData; // Get quiz. - return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quizData) => { + return this.quizProvider.getQuiz(courseId, module.id, false, true, siteId).then((quizData) => { quiz = quizData; const promises = [], @@ -272,7 +289,13 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl }).then(() => { // Check if we need to start a new attempt. let attempt = attempts[attempts.length - 1]; - if (!attempt || this.quizProvider.isAttemptFinished(attempt.state)) { + + if (!canStart && !attempt) { + // No attempts and we won't start a new one, so we don't need preflight data. + return; + } + + if (canStart && (!attempt || this.quizProvider.isAttemptFinished(attempt.state))) { // Check if the user can attempt the quiz. if (attemptAccessInfo.preventnewattemptreasons.length) { return Promise.reject(this.textUtils.buildMessage(attemptAccessInfo.preventnewattemptreasons)); @@ -331,6 +354,11 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl return Promise.all(promises); }).then(() => { + if (!canStart) { + // Nothing else to do. + return; + } + // If there's nothing to send, mark the quiz as synchronized. // We don't return the promises because it should be fast and we don't want to block the user for this. if (!this.syncProvider) { @@ -477,14 +505,49 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl return this.prefetchAttempt(quiz, lastAttempt, preflightData, siteId); } }).then(() => { - // Prefetch finished, get current status to determine if we need to change it. + // Prefetch finished, set the right status. + return this.setStatusAfterPrefetch(quiz, attempts, true, false, siteId); + }); + } + + /** + * Set the right status to a quiz after prefetching. + * If the last attempt is finished or there isn't one, set it as not downloaded to show download icon. + * + * @param {any} quiz Quiz. + * @param {any[]} [attempts] List of attempts. If not provided, they will be calculated. + * @param {boolean} [forceCache] Whether it should always return cached data. Only if attempts is undefined. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). Only if + * attempts is undefined. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + setStatusAfterPrefetch(quiz: any, attempts?: any[], forceCache?: boolean, ignoreCache?: boolean, siteId?: string) + : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + let status; + + if (!attempts) { + // Get the attempts. + promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, forceCache, ignoreCache, siteId).then((atts) => { + attempts = atts; + })); + } + + // Check the current status of the quiz. + promises.push(this.filepoolProvider.getPackageStatus(siteId, this.component, quiz.coursemodule).then((stat) => { + status = stat; + })); + + return Promise.all(promises).then(() => { - return this.filepoolProvider.getPackageStatus(siteId, this.component, quiz.coursemodule); - }).then((status) => { if (status !== CoreConstants.NOT_DOWNLOADED) { // Quiz was downloaded, set the new status. // If no attempts or last is finished we'll mark it as not downloaded to show download icon. - const isLastFinished = !lastAttempt || this.quizProvider.isAttemptFinished(lastAttempt.state), + const lastAttempt = attempts[attempts.length - 1], + isLastFinished = !lastAttempt || this.quizProvider.isAttemptFinished(lastAttempt.state), newStatus = isLastFinished ? CoreConstants.NOT_DOWNLOADED : CoreConstants.DOWNLOADED; return this.filepoolProvider.storePackageStatus(siteId, newStatus, this.component, quiz.coursemodule); diff --git a/src/addon/mod/quiz/providers/quiz-sync.ts b/src/addon/mod/quiz/providers/quiz-sync.ts index f70b29109..854fb8a83 100644 --- a/src/addon/mod/quiz/providers/quiz-sync.ts +++ b/src/addon/mod/quiz/providers/quiz-sync.ts @@ -23,9 +23,10 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreQuestionProvider } from '@core/question/providers/question'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { AddonModQuizProvider } from './quiz'; import { AddonModQuizOfflineProvider } from './quiz-offline'; import { AddonModQuizPrefetchHandler } from './prefetch-handler'; @@ -51,7 +52,7 @@ export interface AddonModQuizSyncResult { * Service to sync quizzes. */ @Injectable() -export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { +export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_quiz_autom_synced'; @@ -59,13 +60,14 @@ export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider, syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, - courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider, timeUtils: CoreTimeUtilsProvider, + private eventsProvider: CoreEventsProvider, timeUtils: CoreTimeUtilsProvider, private quizProvider: AddonModQuizProvider, private quizOfflineProvider: AddonModQuizOfflineProvider, - private prefetchHandler: AddonModQuizPrefetchHandler, private questionProvider: CoreQuestionProvider, - private questionDelegate: CoreQuestionDelegate, private logHelper: CoreCourseLogHelperProvider) { + protected prefetchHandler: AddonModQuizPrefetchHandler, private questionProvider: CoreQuestionProvider, + private questionDelegate: CoreQuestionDelegate, private logHelper: CoreCourseLogHelperProvider, + prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseProvider: CoreCourseProvider) { super('AddonModQuizSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('quiz'); } @@ -97,7 +99,11 @@ export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { }).then(() => { if (updated) { // Data has been sent. Update prefetched data. - return this.prefetchHandler.prefetchQuizAndLastAttempt(quiz, false, siteId); + return this.courseProvider.getModuleBasicInfoByInstance(quiz.id, 'quiz', siteId).then((module) => { + return this.prefetchAfterUpdateQuiz(module, quiz, courseId, undefined, siteId); + }).catch(() => { + // Ignore errors. + }); } }).then(() => { return this.setSyncTime(quiz.id, siteId).catch(() => { @@ -145,6 +151,41 @@ export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { }); } + /** + * Conveniece function to prefetch data after an update. + * + * @param {any} module Module. + * @param {any} quiz Quiz. + * @param {number} courseId Course ID. + * @param {RegExp} [regex] If regex matches, don't download the data. Defaults to check files. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchAfterUpdateQuiz(module: any, quiz: any, courseId: number, regex?: RegExp, siteId?: string): Promise { + regex = regex || /^.*files$/; + + let shouldDownload; + + // Get the module updates to check if the data was updated or not. + return this.prefetchDelegate.getModuleUpdates(module, courseId, true, siteId).then((result) => { + + if (result && result.updates && result.updates.length > 0) { + // Only prefetch if files haven't changed. + shouldDownload = !result.updates.find((entry) => { + return entry.name.match(regex); + }); + + if (shouldDownload) { + return this.prefetchHandler.download(module, courseId, undefined, false, false); + } + } + + }).then(() => { + // Prefetch finished or not needed, set the right status. + return this.prefetchHandler.setStatusAfterPrefetch(quiz, undefined, shouldDownload, false, siteId); + }); + } + /** * Try to synchronize all the quizzes in a certain site or in all sites. * @@ -185,7 +226,7 @@ export class AddonModQuizSyncProvider extends CoreSyncBaseProvider { if (!this.syncProvider.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) { // Quiz not blocked, try to synchronize it. - promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, false, siteId).then((quiz) => { + promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, false, false, siteId).then((quiz) => { return this.syncQuizIfNeeded(quiz, false, siteId).then((data) => { if (data && data.warnings && data.warnings.length) { // Store the warnings to show them when the user opens the quiz. diff --git a/src/addon/mod/quiz/providers/quiz.ts b/src/addon/mod/quiz/providers/quiz.ts index 551675b22..8861537f6 100644 --- a/src/addon/mod/quiz/providers/quiz.ts +++ b/src/addon/mod/quiz/providers/quiz.ts @@ -672,10 +672,13 @@ export class AddonModQuizProvider { * @param {string} key Name of the property to check. * @param {any} value Value to search. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the Quiz is retrieved. */ - protected getQuizByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise { + protected getQuizByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] @@ -686,6 +689,9 @@ export class AddonModQuizProvider { if (forceCache) { preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; } return site.read('mod_quiz_get_quizzes_by_courses', params, preSets).then((response) => { @@ -710,11 +716,12 @@ export class AddonModQuizProvider { * @param {number} courseId Course ID. * @param {number} cmId Course module ID. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the quiz is retrieved. */ - getQuiz(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { - return this.getQuizByField(courseId, 'coursemodule', cmId, forceCache, siteId); + getQuiz(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getQuizByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); } /** @@ -723,11 +730,12 @@ export class AddonModQuizProvider { * @param {number} courseId Course ID. * @param {number} id Quiz ID. * @param {boolean} [forceCache] Whether it should always return cached data. + * @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the quiz is retrieved. */ - getQuizById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { - return this.getQuizByField(courseId, 'id', id, forceCache, siteId); + getQuizById(courseId: number, id: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getQuizByField(courseId, 'id', id, forceCache, ignoreCache, siteId); } /** @@ -1223,7 +1231,7 @@ export class AddonModQuizProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Get required data to call the invalidate functions. - return this.getQuiz(courseId, moduleId, false, siteId).then((quiz) => { + return this.getQuiz(courseId, moduleId, true, false, siteId).then((quiz) => { return this.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => { // Now invalidate it. const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined; diff --git a/src/addon/mod/scorm/providers/scorm-sync.ts b/src/addon/mod/scorm/providers/scorm-sync.ts index 861dce5dc..683cfd450 100644 --- a/src/addon/mod/scorm/providers/scorm-sync.ts +++ b/src/addon/mod/scorm/providers/scorm-sync.ts @@ -25,7 +25,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; -import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm'; import { AddonModScormOfflineProvider } from './scorm-offline'; import { AddonModScormPrefetchHandler } from './prefetch-handler'; @@ -57,7 +57,7 @@ export interface AddonModScormSyncResult { * Service to sync SCORMs. */ @Injectable() -export class AddonModScormSyncProvider extends CoreSyncBaseProvider { +export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_scorm_autom_synced'; @@ -67,12 +67,12 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, private eventsProvider: CoreEventsProvider, timeUtils: CoreTimeUtilsProvider, private scormProvider: AddonModScormProvider, private scormOfflineProvider: AddonModScormOfflineProvider, - private prefetchHandler: AddonModScormPrefetchHandler, private utils: CoreUtilsProvider, - private prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseProvider: CoreCourseProvider, + prefetchHandler: AddonModScormPrefetchHandler, private utils: CoreUtilsProvider, + prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseProvider: CoreCourseProvider, private logHelper: CoreCourseLogHelperProvider) { super('AddonModScormSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('scorm'); } @@ -196,7 +196,7 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { if (updated) { // Update downloaded data. promise = this.courseProvider.getModuleBasicInfoByInstance(scorm.id, 'scorm', siteId).then((module) => { - return this.prefetchAfterUpdate(module, scorm.course); + return this.prefetchAfterUpdate(module, scorm.course, undefined, siteId); }).catch(() => { // Ignore errors. }); @@ -361,31 +361,6 @@ export class AddonModScormSyncProvider extends CoreSyncBaseProvider { }); } - /** - * Prefetch data after an update. It won't prefetch the data if the package file was updated. - * - * @param {any} module Module. - * @param {number} courseId Course ID. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when done. - */ - prefetchAfterUpdate(module: any, courseId: number, siteId?: string): Promise { - // Get the module updates to check if the package was updated or not. - return this.prefetchDelegate.getModuleUpdates(module, courseId, true, siteId).then((result) => { - - if (result && result.updates) { - // Only prefetch if the package file hasn't changed. - const fileChanged = !!result.updates.find((entry) => { - return entry.name == 'packagefiles'; - }); - - if (!fileChanged) { - return this.prefetchHandler.download(module, courseId); - } - } - }); - } - /** * Save a snapshot from a synchronization. * diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index 581064e0e..3a7eb4151 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -188,8 +188,20 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo }); } - return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then(() => { - return this.showLoadingAndRefresh(false); + return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then((online) => { + if (online && this.isPrefetched()) { + // The survey is downloaded, update the data. + return this.surveySync.prefetchAfterUpdate(this.module, this.courseId).then(() => { + // Update the view. + this.showLoadingAndFetch(false, false); + }).catch((error) => { + // Prefetch failed, refresh the data. + return this.showLoadingAndRefresh(false); + }); + } else { + // Not downloaded, refresh the data. + return this.showLoadingAndRefresh(false); + } }).finally(() => { modal.dismiss(); }); diff --git a/src/addon/mod/survey/providers/prefetch-handler.ts b/src/addon/mod/survey/providers/prefetch-handler.ts index b3ac45ea6..e6b785486 100644 --- a/src/addon/mod/survey/providers/prefetch-handler.ts +++ b/src/addon/mod/survey/providers/prefetch-handler.ts @@ -111,7 +111,7 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan * @return {Promise} Promise resolved when done. */ protected prefetchSurvey(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.surveyProvider.getSurvey(courseId, module.id).then((survey) => { + return this.surveyProvider.getSurvey(courseId, module.id, true, siteId).then((survey) => { const promises = [], files = this.getIntroFilesFromInstance(module, survey); @@ -120,7 +120,7 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan // If survey isn't answered, prefetch the questions. if (!survey.surveydone) { - promises.push(this.surveyProvider.getQuestions(survey.id)); + promises.push(this.surveyProvider.getQuestions(survey.id, true, siteId)); } return Promise.all(promises); diff --git a/src/addon/mod/survey/providers/survey.ts b/src/addon/mod/survey/providers/survey.ts index 75bd970d0..ff376e2f8 100644 --- a/src/addon/mod/survey/providers/survey.ts +++ b/src/addon/mod/survey/providers/survey.ts @@ -20,6 +20,7 @@ import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModSurveyOfflineProvider } from './offline'; +import { CoreSiteWSPreSets } from '@classes/site'; /** * Service that provides some features for surveys. @@ -41,18 +42,24 @@ export class AddonModSurveyProvider { * Get a survey's questions. * * @param {number} surveyId Survey ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the questions are retrieved. */ - getQuestions(surveyId: number, siteId?: string): Promise { + getQuestions(surveyId: number, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { surveyid: surveyId }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getQuestionsCacheKey(surveyId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_survey_get_questions', params, preSets).then((response) => { if (response.questions) { return response.questions; @@ -87,20 +94,26 @@ export class AddonModSurveyProvider { * 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} key Name of the property to check. + * @param {any} value Value to search. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the survey is retrieved. */ - protected getSurveyDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + protected getSurveyDataByKey(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseids: [courseId] }, - preSets = { + preSets: CoreSiteWSPreSets = { cacheKey: this.getSurveyCacheKey(courseId) }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + return site.read('mod_survey_get_surveys_by_courses', params, preSets).then((response) => { if (response && response.surveys) { const currentSurvey = response.surveys.find((survey) => { @@ -120,24 +133,26 @@ export class AddonModSurveyProvider { * Get a survey by course module ID. * * @param {number} courseId Course ID. - * @param {number} cmId Course module ID. + * @param {number} cmId Course module ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the survey is retrieved. */ - getSurvey(courseId: number, cmId: number, siteId?: string): Promise { - return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, siteId); + getSurvey(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { + return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, ignoreCache, siteId); } /** * Get a survey by ID. * - * @param {number} courseId Course ID. - * @param {number} id Survey ID. + * @param {number} courseId Course ID. + * @param {number} id Survey ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the survey is retrieved. */ - getSurveyById(courseId: number, id: number, siteId?: string): Promise { - return this.getSurveyDataByKey(courseId, 'id', id, siteId); + getSurveyById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { + return this.getSurveyDataByKey(courseId, 'id', id, ignoreCache, siteId); } /** diff --git a/src/addon/mod/survey/providers/sync.ts b/src/addon/mod/survey/providers/sync.ts index 2e301f5fa..7ade2bb70 100644 --- a/src/addon/mod/survey/providers/sync.ts +++ b/src/addon/mod/survey/providers/sync.ts @@ -15,7 +15,6 @@ 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'; @@ -26,13 +25,16 @@ import { CoreEventsProvider } from '@providers/events'; import { TranslateService } from '@ngx-translate/core'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; import { CoreSyncProvider } from '@providers/sync'; +import { AddonModSurveyPrefetchHandler } from './prefetch-handler'; /** * Service to sync surveys. */ @Injectable() -export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { +export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvider { static AUTO_SYNCED = 'addon_mod_survey_autom_synced'; protected componentTranslate: string; @@ -41,10 +43,11 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService, courseProvider: CoreCourseProvider, private surveyOffline: AddonModSurveyOfflineProvider, private eventsProvider: CoreEventsProvider, private surveyProvider: AddonModSurveyProvider, - private utils: CoreUtilsProvider, timeUtils: CoreTimeUtilsProvider, private logHelper: CoreCourseLogHelperProvider) { + private utils: CoreUtilsProvider, timeUtils: CoreTimeUtilsProvider, private logHelper: CoreCourseLogHelperProvider, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModSurveyPrefetchHandler) { super('AddonModSurveySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, - timeUtils); + timeUtils, prefetchDelegate, prefetchHandler); this.componentTranslate = courseProvider.translateModuleName('survey'); } @@ -57,7 +60,7 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { * @return {string} Sync ID. * @protected */ - getSyncId (surveyId: number, userId: number): string { + getSyncId(surveyId: number, userId: number): string { return surveyId + '#' + userId; } @@ -192,9 +195,7 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider { }).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(() => { + return this.prefetchAfterUpdate(module, courseId, undefined, siteId).catch(() => { // Ignore errors. }); } diff --git a/src/addon/qtype/truefalse/providers/handler.ts b/src/addon/qtype/truefalse/providers/handler.ts index 59c47fdff..3be03abde 100644 --- a/src/addon/qtype/truefalse/providers/handler.ts +++ b/src/addon/qtype/truefalse/providers/handler.ts @@ -84,4 +84,20 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); } + + /** + * Prepare and add to answers the data to send to server based in the input. Return promise if async. + * + * @param {any} question Question. + * @param {any} answers The answers retrieved from the form. Prepared answers must be stored in this object. + * @param {boolean} [offline] Whether the data should be saved in offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Return a promise resolved when done if async, void if sync. + */ + prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + if (question && typeof answers[question.optionsName] != 'undefined' && !answers[question.optionsName]) { + // The user hasn't answered. Delete the answer to prevent marking one of the answers automatically. + delete answers[question.optionsName]; + } + } } diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index f4a2bb436..d247783da 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -46,9 +46,6 @@ export class CoreSyncBaseProvider { // Store sync promises. protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise } } = {}; - // 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. - constructor(component: string, loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService, diff --git a/src/core/course/classes/activity-sync.ts b/src/core/course/classes/activity-sync.ts new file mode 100644 index 000000000..8d7333963 --- /dev/null +++ b/src/core/course/classes/activity-sync.ts @@ -0,0 +1,67 @@ +// (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 { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreAppProvider } from '@providers/app'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler'; + +/** + * Base class to create activity sync providers. It provides some common functions. + */ +export class CoreCourseActivitySyncBaseProvider extends CoreSyncBaseProvider { + + constructor(component: string, loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, + protected appProvider: CoreAppProvider, protected syncProvider: CoreSyncProvider, + protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService, + protected timeUtils: CoreTimeUtilsProvider, protected prefetchDelegate: CoreCourseModulePrefetchDelegate, + protected prefetchHandler: CoreCourseModulePrefetchHandlerBase) { + + super(component, loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); + } + + /** + * Conveniece function to prefetch data after an update. + * + * @param {any} module Module. + * @param {number} courseId Course ID. + * @param {RegExp} [regex] If regex matches, don't download the data. Defaults to check files. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetchAfterUpdate(module: any, courseId: number, regex?: RegExp, siteId?: string): Promise { + regex = regex || /^.*files$/; + + // Get the module updates to check if the data was updated or not. + return this.prefetchDelegate.getModuleUpdates(module, courseId, true, siteId).then((result) => { + + if (result && result.updates && result.updates.length > 0) { + // Only prefetch if files haven't changed. + const shouldDownload = !result.updates.find((entry) => { + return entry.name.match(regex); + }); + + if (shouldDownload) { + return this.prefetchHandler.download(module, courseId); + } + } + }); + } +} diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts index 8bc986812..42727c13f 100644 --- a/src/core/course/classes/main-resource-component.ts +++ b/src/core/course/classes/main-resource-component.ts @@ -23,6 +23,7 @@ import { CoreCourseModuleMainComponent, CoreCourseModuleDelegate } from '@core/c import { CoreCourseSectionPage } from '@core/course/pages/section/section.ts'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { AddonBlogProvider } from '@addon/blog/providers/blog'; +import { CoreConstants } from '@core/constants'; /** * Template class to easily create CoreCourseModuleMainComponent of resources (or activities without syncing). @@ -42,6 +43,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, description: string; // Module description. refreshIcon: string; // Refresh icon, normally spinner or refresh. prefetchStatusIcon: string; // Used when calling fillContextMenu. + prefetchStatus: string; // Used when calling fillContextMenu. prefetchText: string; // Used when calling fillContextMenu. size: string; // Used when calling fillContextMenu. @@ -222,6 +224,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, this.courseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component); } + /** + * Check if the module is prefetched or being prefetched. To make it faster, just use the data calculated by fillContextMenu. + * This means that you need to call fillContextMenu to make this work. + */ + protected isPrefetched(): boolean { + return this.prefetchStatus != CoreConstants.NOT_DOWNLOADABLE && this.prefetchStatus != CoreConstants.NOT_DOWNLOADED; + } + /** * Expand the description. */ diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 3a6b746ac..448af929d 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -745,6 +745,7 @@ export class CoreCourseHelperProvider { return this.getModulePrefetchInfo(module, courseId, invalidateCache, component).then((moduleInfo) => { instance.size = moduleInfo.size > 0 ? moduleInfo.sizeReadable : 0; instance.prefetchStatusIcon = moduleInfo.statusIcon; + instance.prefetchStatus = moduleInfo.status; if (moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE) { // Module is downloadable, get the text to display to prefetch. diff --git a/src/providers/groups.ts b/src/providers/groups.ts index 437e6e03f..f2ce0b146 100644 --- a/src/providers/groups.ts +++ b/src/providers/groups.ts @@ -124,16 +124,17 @@ export class CoreGroupsProvider { * @param {number} cmId Course module ID. * @param {number} [userId] User ID. If not defined, use current user. * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved when the groups are retrieved. If not allowed, empty array will be returned. */ - getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string): Promise { + getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Get real groupmode, in case it's forced by the course. - return this.activityHasGroups(cmId, siteId).then((hasGroups) => { + return this.activityHasGroups(cmId, siteId, ignoreCache).then((hasGroups) => { if (hasGroups) { // Get the groups available for the user. - return this.getActivityAllowedGroups(cmId, userId, siteId); + return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); } return []; @@ -147,19 +148,22 @@ export class CoreGroupsProvider { * @param {boolean} [addAllParts=true] Whether to add the all participants option. Always true for visible groups. * @param {number} [userId] User ID. If not defined, use current user. * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved with the group info. */ - getActivityGroupInfo(cmId: number, addAllParts: boolean = true, userId?: number, siteId?: string): Promise { + getActivityGroupInfo(cmId: number, addAllParts: boolean = true, userId?: number, siteId?: string, ignoreCache?: boolean) + : Promise { + const groupInfo: CoreGroupInfo = { groups: [] }; - return this.getActivityGroupMode(cmId, siteId).then((groupMode) => { + return this.getActivityGroupMode(cmId, siteId, ignoreCache).then((groupMode) => { groupInfo.separateGroups = groupMode === CoreGroupsProvider.SEPARATEGROUPS; groupInfo.visibleGroups = groupMode === CoreGroupsProvider.VISIBLEGROUPS; if (groupInfo.separateGroups || groupInfo.visibleGroups) { - return this.getActivityAllowedGroups(cmId, userId, siteId); + return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); } return [];