From 8be8ac89cb27dccd24e34f97a7992a0dd79d76eb Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 14 Jul 2020 10:05:23 +0200 Subject: [PATCH 001/182] MOBILE-3451 airnotifier: Display more data of devices --- src/addon/messageoutput/airnotifier/pages/devices/devices.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/messageoutput/airnotifier/pages/devices/devices.html b/src/addon/messageoutput/airnotifier/pages/devices/devices.html index 82f688482..24434141b 100644 --- a/src/addon/messageoutput/airnotifier/pages/devices/devices.html +++ b/src/addon/messageoutput/airnotifier/pages/devices/devices.html @@ -11,7 +11,7 @@ - {{ device.model }} + {{ device.name }} {{ device.model }} {{ device.platform }} {{ device.version }} ({{ 'core.currentdevice' | translate }}) From 312a81f3ee19d45fd8e3590bab79037bad6f35c5 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 14 Jul 2020 12:32:45 +0200 Subject: [PATCH 002/182] MOBILE-3445 siteplugins: Let plugins disable PTR --- .../siteplugins/classes/handlers/course-option-handler.ts | 3 ++- src/core/siteplugins/classes/handlers/main-menu-handler.ts | 3 ++- .../siteplugins/classes/handlers/message-output-handler.ts | 3 ++- src/core/siteplugins/classes/handlers/settings-handler.ts | 3 ++- src/core/siteplugins/classes/handlers/user-handler.ts | 3 ++- .../course-option/core-siteplugins-course-option.html | 2 +- .../siteplugins/components/course-option/course-option.ts | 3 +++ .../siteplugins/components/module-index/module-index.ts | 2 ++ .../components/only-title-block/only-title-block.ts | 1 + .../components/plugin-content/plugin-content.ts | 7 +++++-- src/core/siteplugins/directives/call-ws-new-content.ts | 4 +++- src/core/siteplugins/directives/new-content.ts | 4 +++- src/core/siteplugins/pages/module-index/module-index.html | 2 +- src/core/siteplugins/pages/plugin-page/plugin-page.html | 2 +- src/core/siteplugins/pages/plugin-page/plugin-page.ts | 3 +++ 15 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/core/siteplugins/classes/handlers/course-option-handler.ts b/src/core/siteplugins/classes/handlers/course-option-handler.ts index 29800bbbf..e1a572172 100644 --- a/src/core/siteplugins/classes/handlers/course-option-handler.ts +++ b/src/core/siteplugins/classes/handlers/course-option-handler.ts @@ -98,7 +98,8 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl args: { courseid: course.id }, - initResult: this.initResult + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, } }; } diff --git a/src/core/siteplugins/classes/handlers/main-menu-handler.ts b/src/core/siteplugins/classes/handlers/main-menu-handler.ts index 431164e07..5ff904abf 100644 --- a/src/core/siteplugins/classes/handlers/main-menu-handler.ts +++ b/src/core/siteplugins/classes/handlers/main-menu-handler.ts @@ -43,7 +43,8 @@ export class CoreSitePluginsMainMenuHandler extends CoreSitePluginsBaseHandler i title: this.title, component: this.plugin.component, method: this.handlerSchema.method, - initResult: this.initResult + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, }, onlyInMore: true }; diff --git a/src/core/siteplugins/classes/handlers/message-output-handler.ts b/src/core/siteplugins/classes/handlers/message-output-handler.ts index 3c8d9b996..1e0985ccc 100644 --- a/src/core/siteplugins/classes/handlers/message-output-handler.ts +++ b/src/core/siteplugins/classes/handlers/message-output-handler.ts @@ -40,7 +40,8 @@ export class CoreSitePluginsMessageOutputHandler extends CoreSitePluginsBaseHand title: this.title, component: this.plugin.component, method: this.handlerSchema.method, - initResult: this.initResult + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, } }; } diff --git a/src/core/siteplugins/classes/handlers/settings-handler.ts b/src/core/siteplugins/classes/handlers/settings-handler.ts index d3de29f45..1267faa07 100644 --- a/src/core/siteplugins/classes/handlers/settings-handler.ts +++ b/src/core/siteplugins/classes/handlers/settings-handler.ts @@ -43,7 +43,8 @@ export class CoreSitePluginsSettingsHandler extends CoreSitePluginsBaseHandler i title: this.title, component: this.plugin.component, method: this.handlerSchema.method, - initResult: this.initResult + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, } }; } diff --git a/src/core/siteplugins/classes/handlers/user-handler.ts b/src/core/siteplugins/classes/handlers/user-handler.ts index 29266d048..ae381c477 100644 --- a/src/core/siteplugins/classes/handlers/user-handler.ts +++ b/src/core/siteplugins/classes/handlers/user-handler.ts @@ -94,7 +94,8 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle courseid: courseId, userid: user.id }, - initResult: this.initResult + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, }); } }; diff --git a/src/core/siteplugins/components/course-option/core-siteplugins-course-option.html b/src/core/siteplugins/components/course-option/core-siteplugins-course-option.html index b2191c305..782649125 100644 --- a/src/core/siteplugins/components/course-option/core-siteplugins-course-option.html +++ b/src/core/siteplugins/components/course-option/core-siteplugins-course-option.html @@ -1,5 +1,5 @@ - + diff --git a/src/core/siteplugins/components/course-option/course-option.ts b/src/core/siteplugins/components/course-option/course-option.ts index 26adaf219..22753f553 100644 --- a/src/core/siteplugins/components/course-option/course-option.ts +++ b/src/core/siteplugins/components/course-option/course-option.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Component, OnInit, Input, ViewChild } from '@angular/core'; +import { CoreUtils } from '@providers/utils/utils'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; @@ -33,6 +34,7 @@ export class CoreSitePluginsCourseOptionComponent implements OnInit { method: string; args: any; initResult: any; + ptrEnabled = true; constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } @@ -49,6 +51,7 @@ export class CoreSitePluginsCourseOptionComponent implements OnInit { courseid: this.courseId, }; this.initResult = handler.initResult; + this.ptrEnabled = !CoreUtils.instance.isFalseOrZero(handler.handlerSchema.ptrenabled); } } } diff --git a/src/core/siteplugins/components/module-index/module-index.ts b/src/core/siteplugins/components/module-index/module-index.ts index 94f1dcc75..48a6621e8 100644 --- a/src/core/siteplugins/components/module-index/module-index.ts +++ b/src/core/siteplugins/components/module-index/module-index.ts @@ -53,6 +53,7 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C displayRefresh = true; displayPrefetch = true; displaySize = true; + ptrEnabled = true; jsData: any; // Data to pass to the component. @@ -92,6 +93,7 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C this.displayRefresh = !this.utils.isFalseOrZero(handler.handlerSchema.displayrefresh); this.displayPrefetch = !this.utils.isFalseOrZero(handler.handlerSchema.displayprefetch); this.displaySize = !this.utils.isFalseOrZero(handler.handlerSchema.displaysize); + this.ptrEnabled = !this.utils.isFalseOrZero(handler.handlerSchema.ptrenabled); } // Get the data for the context menu. diff --git a/src/core/siteplugins/components/only-title-block/only-title-block.ts b/src/core/siteplugins/components/only-title-block/only-title-block.ts index 8b1a3987f..fc89a6440 100644 --- a/src/core/siteplugins/components/only-title-block/only-title-block.ts +++ b/src/core/siteplugins/components/only-title-block/only-title-block.ts @@ -63,6 +63,7 @@ export class CoreSitePluginsOnlyTitleBlockComponent extends CoreBlockBaseCompon contextlevel: this.contextLevel, instanceid: this.instanceId, }, + ptrEnabled: handler.handlerSchema.ptrenabled, }); } } diff --git a/src/core/siteplugins/components/plugin-content/plugin-content.ts b/src/core/siteplugins/components/plugin-content/plugin-content.ts index 3a0b491c7..632a64222 100644 --- a/src/core/siteplugins/components/plugin-content/plugin-content.ts +++ b/src/core/siteplugins/components/plugin-content/plugin-content.ts @@ -129,8 +129,10 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { * @param jsData JS variables to pass to the new view so they can be used in the template or JS. * If true is supplied instead of an object, all initial variables from current page will be copied. * @param preSets The preSets for the WS call of the new content. + * @param ptrEnabled Whether PTR should be enabled in the new page. Defaults to true. */ - openContent(title: string, args: any, component?: string, method?: string, jsData?: any, preSets?: any): void { + openContent(title: string, args: any, component?: string, method?: string, jsData?: any, preSets?: any, + ptrEnabled?: boolean): void { if (jsData === true) { jsData = this.data; } @@ -142,7 +144,8 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { args: args, initResult: this.initResult, jsData: jsData, - preSets: preSets + preSets: preSets, + ptrEnabled: ptrEnabled, }); } diff --git a/src/core/siteplugins/directives/call-ws-new-content.ts b/src/core/siteplugins/directives/call-ws-new-content.ts index b6c1bdea0..03c7d2ee4 100644 --- a/src/core/siteplugins/directives/call-ws-new-content.ts +++ b/src/core/siteplugins/directives/call-ws-new-content.ts @@ -62,6 +62,7 @@ export class CoreSitePluginsCallWSNewContentDirective extends CoreSitePluginsCal @Input() jsData: any; // JS variables to pass to the new page so they can be used in the template or JS. // If true is supplied instead of an object, all initial variables from current page will be copied. @Input() newContentPreSets: any; // The preSets for the WS call of the new content. + @Input() ptrEnabled: boolean | string; // Whether PTR should be enabled in the new page. Defaults to true. constructor(element: ElementRef, translate: TranslateService, domUtils: CoreDomUtilsProvider, sitePluginsProvider: CoreSitePluginsProvider, @Optional() parentContent: CoreSitePluginsPluginContentComponent, @@ -102,7 +103,8 @@ export class CoreSitePluginsCallWSNewContentDirective extends CoreSitePluginsCal args: args, initResult: this.parentContent && this.parentContent.initResult, jsData: jsData, - preSets: this.newContentPreSets + preSets: this.newContentPreSets, + ptrEnabled: this.ptrEnabled, }); } } diff --git a/src/core/siteplugins/directives/new-content.ts b/src/core/siteplugins/directives/new-content.ts index a373a55ee..cf9a183a5 100644 --- a/src/core/siteplugins/directives/new-content.ts +++ b/src/core/siteplugins/directives/new-content.ts @@ -54,6 +54,7 @@ export class CoreSitePluginsNewContentDirective implements OnInit { @Input() jsData: any; // JS variables to pass to the new page so they can be used in the template or JS. // If true is supplied instead of an object, all initial variables from current page will be copied. @Input() preSets: any; // The preSets for the WS call of the new content. + @Input() ptrEnabled: boolean | string; // Whether PTR should be enabled in the new page. Defaults to true. protected element: HTMLElement; @@ -99,7 +100,8 @@ export class CoreSitePluginsNewContentDirective implements OnInit { args: args, initResult: this.parentContent && this.parentContent.initResult, jsData: jsData, - preSets: this.preSets + preSets: this.preSets, + ptrEnabled: this.ptrEnabled, }); } }); diff --git a/src/core/siteplugins/pages/module-index/module-index.html b/src/core/siteplugins/pages/module-index/module-index.html index 8d83a46b7..f82c0f72e 100644 --- a/src/core/siteplugins/pages/module-index/module-index.html +++ b/src/core/siteplugins/pages/module-index/module-index.html @@ -8,7 +8,7 @@ - + diff --git a/src/core/siteplugins/pages/plugin-page/plugin-page.html b/src/core/siteplugins/pages/plugin-page/plugin-page.html index be1566304..35ea3950a 100644 --- a/src/core/siteplugins/pages/plugin-page/plugin-page.html +++ b/src/core/siteplugins/pages/plugin-page/plugin-page.html @@ -8,7 +8,7 @@ - + diff --git a/src/core/siteplugins/pages/plugin-page/plugin-page.ts b/src/core/siteplugins/pages/plugin-page/plugin-page.ts index 75c81bc53..ec5a97ad4 100644 --- a/src/core/siteplugins/pages/plugin-page/plugin-page.ts +++ b/src/core/siteplugins/pages/plugin-page/plugin-page.ts @@ -14,6 +14,7 @@ import { Component, ViewChild } from '@angular/core'; import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreUtils } from '@providers/utils/utils'; import { CoreSitePluginsPluginContentComponent } from '../../components/plugin-content/plugin-content'; /** @@ -35,6 +36,7 @@ export class CoreSitePluginsPluginPage { initResult: any; jsData: any; // JS variables to pass to the plugin so they can be used in the template or JS. preSets: any; // The preSets for the WS call. + ptrEnabled: boolean; constructor(params: NavParams) { this.title = params.get('title'); @@ -44,6 +46,7 @@ export class CoreSitePluginsPluginPage { this.initResult = params.get('initResult'); this.jsData = params.get('jsData'); this.preSets = params.get('preSets'); + this.ptrEnabled = !CoreUtils.instance.isFalseOrZero(params.get('ptrEnabled')); } /** From edba91d5f49bcf5ce744f0d2a097f0fc397c8eda Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 15 Jul 2020 13:09:47 +0200 Subject: [PATCH 003/182] MOBILE-3487 filepool: Fix invalidate unknown files query --- src/providers/filepool.ts | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index 3171fbf5a..8b9072962 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -279,6 +279,8 @@ export class CoreFilepoolProvider { protected ERR_QUEUE_IS_EMPTY = 'CoreFilepoolError:ERR_QUEUE_IS_EMPTY'; protected ERR_FS_OR_NETWORK_UNAVAILABLE = 'CoreFilepoolError:ERR_FS_OR_NETWORK_UNAVAILABLE'; protected ERR_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE'; + protected FILE_UPDATE_UNKNOWN_WHERE_CLAUSE = + 'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))'; // Variables for database. protected QUEUE_TABLE = 'filepool_files_queue'; // Queue of files to download. @@ -2439,17 +2441,12 @@ export class CoreFilepoolProvider { * It is advised to set it to true to reduce the performance and data usage of the app. * @return Resolved on success. */ - invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - let where, - whereParams; - if (onlyUnknown) { - where = 'isexternalfile = ? OR (revision < ? AND timemodified = ?)'; - whereParams = [0, 1, 0]; - } + async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise { + const db = await this.sitesProvider.getSiteDb(siteId); - return db.updateRecordsWhere(this.FILES_TABLE, { stale: 1 }, where, whereParams); - }); + const where = onlyUnknown ? this.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : null; + + await db.updateRecordsWhere(this.FILES_TABLE, { stale: 1 }, where); } /** @@ -2484,25 +2481,28 @@ export class CoreFilepoolProvider { * It is advised to set it to true to reduce the performance and data usage of the app. * @return Resolved when done. */ - invalidateFilesByComponent(siteId: string, component: string, componentId?: string | number, onlyUnknown: boolean = true) - : Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return this.getComponentFiles(db, component, componentId).then((items) => { - const fileIds = items.map((item) => { - return item.fileId; - }), - whereAndParams = db.getInOrEqual(fileIds); + async invalidateFilesByComponent(siteId: string, component: string, componentId?: string | number, onlyUnknown: boolean = true) + : Promise { - whereAndParams[0] = 'fileId ' + whereAndParams[0]; + const db = await this.sitesProvider.getSiteDb(siteId); - if (onlyUnknown) { - whereAndParams[0] += ' AND (isexternalfile = ? OR (revision < ? AND timemodified = ?))'; - whereAndParams[1] = whereAndParams[1].concat([0, 1, 0]); - } + const items = await this.getComponentFiles(db, component, componentId); - return db.updateRecordsWhere(this.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]); - }); - }); + if (!items.length) { + // Nothing to invalidate. + return; + } + + const fileIds = items.map((item) => item.fileId); + const whereAndParams = db.getInOrEqual(fileIds); + + whereAndParams[0] = 'fileId ' + whereAndParams[0]; + + if (onlyUnknown) { + whereAndParams[0] += ' AND (' + this.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')'; + } + + await db.updateRecordsWhere(this.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]); } /** @@ -2567,7 +2567,7 @@ export class CoreFilepoolProvider { * @return Whether it cannot determine updates. */ protected isFileUpdateUnknown(entry: CoreFilepoolFileEntry): boolean { - return !!entry.isexternalfile || (entry.revision < 1 && !entry.timemodified); + return !!entry.isexternalfile || (!entry.revision && !entry.timemodified); } /** From 8b4c693bec110c036481f9866a8b409bab8dd741 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 16 Jul 2020 11:49:19 +0200 Subject: [PATCH 004/182] MOBILE-3488 android: Open HTML files in InAppBrowser --- src/providers/utils/utils.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 75a50620b..8ee4a2798 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -897,13 +897,20 @@ export class CoreUtilsProvider { * @param path The local path of the file to be open. * @return Promise resolved when done. */ - openFile(path: string): Promise { + async openFile(path: string): Promise { // Convert the path to a native path if needed. path = CoreFile.instance.unconvertFileSrc(path); const extension = this.mimetypeUtils.getFileExtension(path); const mimetype = this.mimetypeUtils.getMimeType(extension); + if (mimetype == 'text/html' && this.platform.is('android')) { + // Open HTML local files in InAppBrowser, in system browser some embedded files aren't loaded. + this.openInApp(path); + + return; + } + // Path needs to be decoded, the file won't be opened if the path has %20 instead of spaces and so. try { path = decodeURIComponent(path); @@ -911,7 +918,9 @@ export class CoreUtilsProvider { // Error, use the original path. } - return this.fileOpener.open(path, mimetype).catch((error) => { + try { + await this.fileOpener.open(path, mimetype); + } catch (error) { this.logger.error('Error opening file ' + path + ' with mimetype ' + mimetype); this.logger.error('Error: ', JSON.stringify(error)); @@ -922,8 +931,8 @@ export class CoreUtilsProvider { error = this.translate.instant('core.erroropenfilenoapp'); } - return Promise.reject(error); - }); + throw error; + } } /** From 6b1e7b70d1e1b228c3862cb1519a579bd3474942 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 20 Jul 2020 10:03:19 +0200 Subject: [PATCH 005/182] MOBILE-3493 course: Don't display selector if course only has one section --- .../course/components/format/core-course-format.html | 2 +- src/core/course/components/format/format.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index 453928f72..1956e9ad6 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -11,7 +11,7 @@ -
+
+ +
@@ -37,7 +46,7 @@ - diff --git a/src/core/settings/pages/space-usage/space-usage.html b/src/core/settings/pages/space-usage/space-usage.html index 29dac4705..914cb1f9b 100644 --- a/src/core/settings/pages/space-usage/space-usage.html +++ b/src/core/settings/pages/space-usage/space-usage.html @@ -20,7 +20,6 @@

{{ site.fullName }}

{{ site.spaceUsage | coreBytesToSize }}

-

{{ 'core.settings.entriesincache' | translate: { $a: site.cacheEntries } }}

+ + +
- - - - - + - - - {{ 'core.login.selectsite' | translate }} - - {{site.name}} - - - - + - +

{{ 'core.login.selectsite' | translate }}

- -

{{site.name}}

-

{{site.url}}

+ + + + + +

{{site.title}}

+

{{site.noProtocolUrl}}

+

{{site.location}}

{{ 'core.login.selectsite' | translate }}

-
{{site.name}} + {{site.title}}
diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index 68b1c6142..4c734d1e6 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -35,7 +35,17 @@ import { TranslateService } from '@ngx-translate/core'; */ type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & { noProtocolUrl?: string; // Url wihtout protocol. - country?: string; // Based on countrycode. + location?: string; // City + country. + title?: string; // Name + alias. +}; + +type SiteFinderSettings = { + displayalias: boolean, + displaycity: boolean, + displaycountry: boolean, + displayimage: boolean, + displaysitename: boolean, + displayurl: boolean }; /** @@ -51,8 +61,8 @@ export class CoreLoginSitePage { @ViewChild('siteFormEl') formElement: ElementRef; siteForm: FormGroup; - fixedSites: CoreLoginSiteInfo[]; - filteredSites: CoreLoginSiteInfo[]; + fixedSites: CoreLoginSiteInfoExtended[]; + filteredSites: CoreLoginSiteInfoExtended[]; siteSelector = 'sitefinder'; showKeyboard = false; filter = ''; @@ -62,6 +72,7 @@ export class CoreLoginSitePage { searchFnc: Function; showScanQR: boolean; enteredSiteUrl: CoreLoginSiteInfoExtended; + siteFinderSettings: SiteFinderSettings; constructor(navParams: NavParams, protected navCtrl: NavController, @@ -84,13 +95,37 @@ export class CoreLoginSitePage { let url = ''; this.siteSelector = CoreConfigConstants.multisitesdisplay; + const siteFinderSettings: Partial = CoreConfigConstants['sitefindersettings'] || {}; + this.siteFinderSettings = { + displaysitename: true, + displayimage: true, + displayalias: true, + displaycity: true, + displaycountry: true, + displayurl: true, + ...siteFinderSettings + }; + // Load fixed sites if they're set. if (this.loginHelper.hasSeveralFixedSites()) { - this.fixedSites = this.loginHelper.getFixedSites(); - // Autoselect if not defined. - if (['list', 'listnourl', 'select', 'buttons'].indexOf(this.siteSelector) < 0) { - this.siteSelector = this.fixedSites.length > 8 ? 'list' : (this.fixedSites.length > 3 ? 'select' : 'buttons'); + // Deprecate listnourl on 3.9.3, remove this block on the following release. + if (this.siteSelector == 'listnourl') { + this.siteSelector = 'list'; + this.siteFinderSettings.displayurl = false; } + + this.fixedSites = this.extendCoreLoginSiteInfo( this.loginHelper.getFixedSites()); + + // Do not show images if none are set. + if (!this.fixedSites.some((site) => !!site.imageurl)) { + this.siteFinderSettings.displayimage = false; + } + + // Autoselect if not defined. + if (this.siteSelector != 'list' && this.siteSelector != 'buttons') { + this.siteSelector = this.fixedSites.length > 3 ? 'list' : 'buttons'; + } + this.filteredSites = this.fixedSites; url = this.fixedSites[0].url; } else if (CoreConfigConstants.enableonboarding && !this.appProvider.isIOS() && !this.appProvider.isMac()) { @@ -116,11 +151,8 @@ export class CoreLoginSitePage { // Update the sites list. this.sites = await this.sitesProvider.findSites(search); - // UI tweaks. - this.sites.forEach((site) => { - site.noProtocolUrl = CoreUrl.removeProtocol(site.url); - site.country = this.utils.getCountryName(site.countrycode); - }); + // Add UI tweaks. + this.sites = this.extendCoreLoginSiteInfo(this.sites); this.hasSites = !!this.sites.length; } else { @@ -132,6 +164,34 @@ export class CoreLoginSitePage { }, 1000); } + /** + * Extend info of Login Site Info to get UI tweaks. + * + * @param sites Sites list. + * @return Sites list with extended info. + */ + protected extendCoreLoginSiteInfo(sites: CoreLoginSiteInfoExtended[]): CoreLoginSiteInfoExtended[] { + return sites.map((site) => { + site.noProtocolUrl = this.siteFinderSettings.displayurl && site.url ? CoreUrl.removeProtocol(site.url) : ''; + + const name = this.siteFinderSettings.displaysitename ? site.name : ''; + const alias = this.siteFinderSettings.displayalias && site.alias ? site.alias : ''; + + // Set title with parenthesis if both name and alias are present. + site.title = name && alias ? name + ' (' + alias + ')' : name + alias; + + const country = this.siteFinderSettings.displaycountry && site.countrycode ? + this.utils.getCountryName(site.countrycode) : ''; + const city = this.siteFinderSettings.displaycity && site.city ? + site.city : ''; + + // Separate location with hiphen if both country and city are present. + site.location = city && country ? city + ' - ' + country : city + country; + + return site; + }); + } + /** * Try to connect to a site. * @@ -224,7 +284,8 @@ export class CoreLoginSitePage { this.filteredSites = this.fixedSites; } else { this.filteredSites = this.fixedSites.filter((site) => { - return site.name.toLowerCase().indexOf(newValue) > -1 || site.url.toLowerCase().indexOf(newValue) > -1; + return site.title.toLowerCase().indexOf(newValue) > -1 || site.noProtocolUrl.toLowerCase().indexOf(newValue) > -1 || + site.location.toLowerCase().indexOf(newValue) > -1; }); } } diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 9e5ea3630..7c1857924 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -21,7 +21,7 @@ import { CoreConfigProvider } from '@providers/config'; import { CoreEventsProvider } from '@providers/events'; import { CoreInitDelegate } from '@providers/init'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreLoginSiteInfo } from '@providers/sites'; import { CoreWSProvider } from '@providers/ws'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -464,7 +464,7 @@ export class CoreLoginHelperProvider { * * @return Fixed site or list of fixed sites. */ - getFixedSites(): string | any[] { + getFixedSites(): string | CoreLoginSiteInfo[] { return CoreConfigConstants.siteurl; } From fe36b3d8986a9e875ec8f19b45edb1ab91e10c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 10 Sep 2020 17:15:32 +0200 Subject: [PATCH 072/182] MOBILE-3526 cordova: Reverse cordova-android version --- package-lock.json | 239 ++++++++-------------------------------------- package.json | 2 +- 2 files changed, 39 insertions(+), 202 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e147c117..402606118 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2167,6 +2167,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=" + }, "array-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", @@ -3465,6 +3470,30 @@ "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true }, + "compare-func": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.4.tgz", + "integrity": "sha512-sq2sWtrqKPkEXAC8tEJA1+BqAH9GbFkGBtUOqrUX57VSfwp8xyktctk+uLoRy5eccTdxzDcVIztlYDpKs3Jv1Q==", + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^3.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "requires": { + "is-obj": "^1.0.0" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + } + } + }, "compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", @@ -3912,140 +3941,19 @@ } }, "cordova-android": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cordova-android/-/cordova-android-9.0.0.tgz", - "integrity": "sha512-2ZEgApK4LPMYW0zh/mLAH3CabzCaKE0yxQTzA2wTf0Eo2HHTJnRtDCf9spGf3nPOkubyXS6+pvzz5QzNHpVTqQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cordova-android/-/cordova-android-8.1.0.tgz", + "integrity": "sha512-eAY6g9q3raJ4P03wNdSWC5MOW1EfxoomWNXsPhi7T6Q9yAqmxqn0sLEUjLL1Ib0LCH3nKQWBXdxapQ5LgbHu+g==", "requires": { - "android-versions": "^1.5.0", - "cordova-common": "^4.0.1", - "execa": "^4.0.2", - "fs-extra": "^9.0.1", - "nopt": "^4.0.3", + "android-versions": "^1.4.0", + "compare-func": "^1.3.2", + "cordova-common": "^3.2.0", + "nopt": "^4.0.1", "properties-parser": "^0.3.1", - "which": "^2.0.2" + "q": "^1.5.1", + "shelljs": "^0.5.3" }, "dependencies": { - "bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "requires": { - "big-integer": "^1.6.44" - } - }, - "cordova-common": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cordova-common/-/cordova-common-4.0.2.tgz", - "integrity": "sha512-od7aNShyuBajzPY83mUEO8tERwwWdFklXETHiXP5Ft87CWeo/tSuwNPFztyTy8XYc74yXdogXKPTJeUHuVzB8Q==", - "requires": { - "@netflix/nerror": "^1.1.3", - "ansi": "^0.3.1", - "bplist-parser": "^0.2.0", - "cross-spawn": "^7.0.1", - "elementtree": "^0.1.7", - "endent": "^1.4.1", - "fast-glob": "^3.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "plist": "^3.0.1", - "q": "^1.5.1", - "read-chunk": "^3.2.0", - "strip-bom": "^4.0.0", - "underscore": "^1.9.2" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "endent": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/endent/-/endent-1.4.1.tgz", - "integrity": "sha512-buHTb5c8AC9NshtP6dgmNLYkiT+olskbq1z6cEGvfGCF3Qphbu/1zz5Xu+yjTDln8RbxNhPoUyJ5H8MSrp1olQ==", - "requires": { - "dedent": "^0.7.0", - "fast-json-parse": "^1.0.3", - "objectorarray": "^1.0.4" - } - }, - "execa": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", - "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - }, - "jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, "nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", @@ -4054,77 +3962,6 @@ "abbrev": "1", "osenv": "^0.1.4" } - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "requires": { - "path-key": "^3.0.0" - } - }, - "objectorarray": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/objectorarray/-/objectorarray-1.0.4.tgz", - "integrity": "sha512-91k8bjcldstRz1bG6zJo8lWD7c6QXcB4nTDUqiEvIL1xAsLoZlOOZZG+nd6YPz+V7zY1580J4Xxh1vZtyv4i/w==" - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" - }, - "underscore": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", - "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } } } }, diff --git a/package.json b/package.json index 09d90bf24..1e2674500 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "chart.js": "^2.9.3", "com-darryncampbell-cordova-plugin-intent": "^1.3.0", "cordova": "^10.0.0", - "cordova-android": "^9.0.0", + "cordova-android": "^8.1.0", "cordova-android-support-gradle-release": "^3.0.1", "cordova-clipboard": "^1.3.0", "cordova-ios": "^5.1.1", From 7e25dcf46b6b37ed024dc6283771e7fde8c1253b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 10 Sep 2020 15:25:58 +0200 Subject: [PATCH 073/182] MOBILE-3549 siteplugins: Allow any HTML tag --- src/core/compile/providers/compile.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index e62e8ba70..ef3868ccf 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable, Injector, Component, NgModule, Compiler, ComponentFactory, ComponentRef, NgModuleRef } from '@angular/core'; +import { + Injectable, Injector, Component, NgModule, Compiler, ComponentFactory, ComponentRef, NgModuleRef, NO_ERRORS_SCHEMA +} from '@angular/core'; import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; import { Platform, ActionSheetController, AlertController, LoadingController, ModalController, PopoverController, ToastController, @@ -179,7 +181,7 @@ export class CoreCompileProvider { const imports = this.IMPORTS.concat(extraImports); // Now create the module containing the component. - const module = NgModule({imports: imports, declarations: [component]})(class {}); + const module = NgModule({imports: imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA]})(class {}); try { // Compile the module and the component. @@ -193,6 +195,9 @@ export class CoreCompileProvider { } }); } catch (ex) { + this.logger.error('Error compiling template', template); + this.logger.error(ex); + return Promise.reject({message: 'Template has some errors and cannot be displayed.', debuginfo: ex}); } } From 8844abd857cbbc4ea182bcd83ef21e34d71aa292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 14 Sep 2020 12:58:17 +0200 Subject: [PATCH 074/182] MOBILE-3534 ionic: Fix check device during loading --- src/core/sharedfiles/sharedfiles.module.ts | 50 +++++++++++----------- src/providers/app.ts | 6 ++- src/providers/file.ts | 41 ++++++++++-------- src/providers/utils/iframe.ts | 11 ++--- 4 files changed, 58 insertions(+), 50 deletions(-) diff --git a/src/core/sharedfiles/sharedfiles.module.ts b/src/core/sharedfiles/sharedfiles.module.ts index c31accbad..185a9a5bf 100644 --- a/src/core/sharedfiles/sharedfiles.module.ts +++ b/src/core/sharedfiles/sharedfiles.module.ts @@ -49,31 +49,33 @@ export class CoreSharedFilesModule { // Register the handler. delegate.registerHandler(handler); - if (appsProvider.isIOS()) { - let lastCheck = 0; + platform.ready().then(() => { + if (appsProvider.isIOS()) { + let lastCheck = 0; - // Check if there are new files at app start and when the app is resumed. - helper.searchIOSNewSharedFiles(); - platform.resume.subscribe(() => { - // Wait a bit to make sure that APP_LAUNCHED_URL is treated before this callback. - setTimeout(() => { - if (Date.now() - lastCheck < 1000) { - // Last check less than 1s ago, don't do anything. - return; + // Check if there are new files at app start and when the app is resumed. + helper.searchIOSNewSharedFiles(); + platform.resume.subscribe(() => { + // Wait a bit to make sure that APP_LAUNCHED_URL is treated before this callback. + setTimeout(() => { + if (Date.now() - lastCheck < 1000) { + // Last check less than 1s ago, don't do anything. + return; + } + + lastCheck = Date.now(); + helper.searchIOSNewSharedFiles(); + }, 200); + }); + + eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, (url) => { + if (url && url.indexOf('file://') === 0) { + // We received a file in iOS, it's probably a shared file. Treat it. + lastCheck = Date.now(); + helper.searchIOSNewSharedFiles(url); } - - lastCheck = Date.now(); - helper.searchIOSNewSharedFiles(); - }, 200); - }); - - eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, (url) => { - if (url && url.indexOf('file://') === 0) { - // We received a file in iOS, it's probably a shared file. Treat it. - lastCheck = Date.now(); - helper.searchIOSNewSharedFiles(url); - } - }); - } + }); + } + }); } } diff --git a/src/providers/app.ts b/src/providers/app.ts index 8ad693772..02c1279ae 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -392,7 +392,8 @@ export class CoreAppProvider { * @return Whether the app is running in an Android mobile or tablet device. */ isAndroid(): boolean { - return this.isMobile() && this.device.platform.toLowerCase() == 'android'; + return this.isMobile() && + ((this.device.platform && this.device.platform.toLowerCase() == 'android') || this.platform.is('android')); } /** @@ -412,7 +413,8 @@ export class CoreAppProvider { * @return Whether the app is running in an iOS mobile or tablet device. */ isIOS(): boolean { - return this.isMobile() && this.device.platform.toLowerCase() == 'ios'; + return this.isMobile() && + ((this.device.platform && this.device.platform.toLowerCase() == 'ios') || this.platform.is('ios')); } /** diff --git a/src/providers/file.ts b/src/providers/file.ts index 98df61923..d0a86cc78 100644 --- a/src/providers/file.ts +++ b/src/providers/file.ts @@ -81,28 +81,31 @@ export class CoreFileProvider { this.logger = logger.getInstance('CoreFileProvider'); - if (appProvider.isAndroid() && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) { - // Cordova File plugin creates some getters and setter for FileReader, but Ionic's polyfills override them in Android. - // Create the getters and setters again. This code comes from FileReader.js in cordova-plugin-file. - this.defineGetterSetter(FileReader.prototype, 'readyState', function(): any { - return this._localURL ? this._readyState : this._realReader.readyState; - }); + platform.ready().then(() => { + if (appProvider.isAndroid() && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) { + // Cordova File plugin creates some getters and setter for FileReader, but + // Ionic's polyfills override them in Android. + // Create the getters and setters again. This code comes from FileReader.js in cordova-plugin-file. + this.defineGetterSetter(FileReader.prototype, 'readyState', function(): any { + return this._localURL ? this._readyState : this._realReader.readyState; + }); - this.defineGetterSetter(FileReader.prototype, 'error', function(): any { - return this._localURL ? this._error : this._realReader.error; - }); + this.defineGetterSetter(FileReader.prototype, 'error', function(): any { + return this._localURL ? this._error : this._realReader.error; + }); - this.defineGetterSetter(FileReader.prototype, 'result', function(): any { - return this._localURL ? this._result : this._realReader.result; - }); + this.defineGetterSetter(FileReader.prototype, 'result', function(): any { + return this._localURL ? this._result : this._realReader.result; + }); - this.defineEvent('onloadstart'); - this.defineEvent('onprogress'); - this.defineEvent('onload'); - this.defineEvent('onerror'); - this.defineEvent('onloadend'); - this.defineEvent('onabort'); - } + this.defineEvent('onloadstart'); + this.defineEvent('onprogress'); + this.defineEvent('onload'); + this.defineEvent('onerror'); + this.defineEvent('onloadend'); + this.defineEvent('onabort'); + } + }); } /** diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index 7eb0edf2f..efcbda6b7 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -16,7 +16,7 @@ import { Injectable, NgZone } from '@angular/core'; import { Config, Platform, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { Network } from '@ionic-native/network'; -import { CoreApp } from '../app'; +import { CoreApp, CoreAppProvider } from '../app'; import { CoreFileProvider } from '../file'; import { CoreLoggerProvider } from '../logger'; import { CoreSitesProvider } from '../sites'; @@ -47,6 +47,7 @@ export class CoreIframeUtilsProvider { private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider, platform: Platform, + appProvider: CoreAppProvider, private translate: TranslateService, private network: Network, private zone: NgZone, private config: Config, @@ -56,8 +57,8 @@ export class CoreIframeUtilsProvider { const win = window; - if (CoreApp.instance.isIOS() && win.WKUserScript) { - platform.ready().then(() => { + platform.ready().then(() => { + if (appProvider.isIOS() && win.WKUserScript) { // Inject code to the iframes because we cannot access the online ones. const wwwPath = fileProvider.getWWWAbsolutePath(); const linksPath = textUtils.concatenatePaths(wwwPath, 'assets/js/iframe-treat-links.js'); @@ -72,8 +73,8 @@ export class CoreIframeUtilsProvider { // Handle post messages received by iframes. window.addEventListener('message', this.handleIframeMessage.bind(this)); - }); - } + } + }); } /** From 4d8697a6015734520049997f4e8067b086d444a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 26 Aug 2020 12:59:45 +0200 Subject: [PATCH 075/182] MOBILE-3519 forum: Hide advanced section when cannot attach files --- .../components/post/addon-mod-forum-post.html | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html index 10c1f36fc..71f4aaddc 100644 --- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html +++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html @@ -70,13 +70,15 @@ {{ 'addon.mod_forum.privatereply' | translate }} - - - - {{ 'addon.mod_forum.advanced' | translate }} - - - + + + + + {{ 'addon.mod_forum.advanced' | translate }} + + + + From 6c894a8abee4530f7268272c0df9fe2d16d28aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 31 Aug 2020 12:28:03 +0200 Subject: [PATCH 076/182] MOBILE-3519 forum: Use forumid and courseid from WS --- src/addon/mod/forum/pages/discussion/discussion.ts | 12 +++++++----- src/addon/mod/forum/providers/forum.ts | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts index bc5b23206..c8d18d494 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.ts +++ b/src/addon/mod/forum/pages/discussion/discussion.ts @@ -331,6 +331,8 @@ export class AddonModForumDiscussionPage implements OnDestroy { return this.forumProvider.getDiscussionPosts(this.discussionId, this.cmId).then((response) => { onlinePosts = response.posts; ratingInfo = response.ratinginfo; + this.courseId = response.courseid; + this.forumId = response.forumid; }).then(() => { // Check if there are responses stored in offline. return this.forumOffline.getDiscussionReplies(this.discussionId).then((replies) => { @@ -371,17 +373,13 @@ export class AddonModForumDiscussionPage implements OnDestroy { let posts = offlineReplies.concat(onlinePosts); const startingPost = this.forumProvider.extractStartingPost(posts); - if (startingPost) { - // Update discussion data from first post. - this.discussion = Object.assign(this.discussion || {}, startingPost); - } // If sort type is nested, normal sorting is disabled and nested posts will be displayed. if (this.sort == 'nested') { // Sort first by creation date to make format tree work. this.forumProvider.sortDiscussionPosts(posts, 'ASC'); - posts = this.utils.formatTree(posts, 'parent', 'id', this.discussion.id); + posts = this.utils.formatTree(posts, 'parent', 'id', this.discussion ? this.discussion.id : startingPost.id); } else { // Set default reply subject. const direction = this.sort == 'flat-newest' ? 'DESC' : 'ASC'; @@ -424,6 +422,10 @@ export class AddonModForumDiscussionPage implements OnDestroy { }).catch(() => { // Ignore errors. }).then(() => { + if (startingPost) { + // Update discussion data from first post. + this.discussion = Object.assign(this.discussion || {}, startingPost); + } if (!this.discussion) { // The discussion object was not passed as parameter and there is no starting post. Should not happen. diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index fa6cebdfc..ff91b9935 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -496,7 +496,8 @@ export class AddonModForumProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with forum posts and rating info. */ - getDiscussionPosts(discussionId: number, cmId: number, siteId?: string): Promise<{posts: any[], ratinginfo?: CoreRatingInfo}> { + getDiscussionPosts(discussionId: number, cmId: number, siteId?: string): Promise<{posts: any[], courseid?: number, + forumid?: number, ratinginfo?: CoreRatingInfo}> { const params = { discussionid: discussionId }; From 0e5c448a82552b99ad0a7179314c4c6c65048749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 14 Sep 2020 14:06:50 +0200 Subject: [PATCH 077/182] MOBILE-3554 login: Add forceLoginLogo setting to avoid showing web logo --- src/config.json | 1 + src/core/login/pages/credentials/credentials.ts | 2 +- src/core/login/providers/helper.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/config.json b/src/config.json index 5d4d3f040..581f210f1 100644 --- a/src/config.json +++ b/src/config.json @@ -97,6 +97,7 @@ "enableanalytics": false, "enableonboarding": true, "forceColorScheme": "", + "forceLoginLogo": false, "ioswebviewscheme": "moodleappfs", "appstores": { "android": "com.moodle.moodlemobile", diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index 57541ce34..c40ad2703 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -70,7 +70,7 @@ export class CoreLoginCredentialsPage { this.siteUrl = navParams.get('siteUrl'); this.siteName = navParams.get('siteName') || null; - this.logoUrl = navParams.get('logoUrl') || null; + this.logoUrl = !CoreConfigConstants.forceLoginLogo && navParams.get('logoUrl') || null; this.siteConfig = navParams.get('siteConfig'); this.urlToOpen = navParams.get('urlToOpen'); diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 9e5ea3630..6a598d7c3 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -412,7 +412,7 @@ export class CoreLoginHelperProvider { * @return Logo URL. */ getLogoUrl(config: any): string { - return config ? (config.logourl || config.compactlogourl) : null; + return !CoreConfigConstants.forceLoginLogo && config ? (config.logourl || config.compactlogourl) : null; } /** From afce5d28b16a025293da9ba732f4774e65e86d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 1 Sep 2020 19:20:39 +0200 Subject: [PATCH 078/182] MOBILE-3519 forum: Use WS mod_forum_get_discussion_posts when available --- src/addon/mod/forum/components/index/index.ts | 2 +- .../post-options-menu/post-options-menu.ts | 50 ++++++++------ .../components/post/addon-mod-forum-post.html | 26 +++---- src/addon/mod/forum/components/post/post.ts | 13 ++-- .../forum/pages/discussion/discussion.html | 10 +-- .../mod/forum/pages/discussion/discussion.ts | 35 +++++----- src/addon/mod/forum/providers/forum.ts | 68 ++++++++++++++++++- src/components/user-avatar/user-avatar.ts | 2 +- src/core/editor/providers/editor-offline.ts | 2 +- src/providers/utils/mimetype.ts | 2 +- 10 files changed, 141 insertions(+), 69 deletions(-) diff --git a/src/addon/mod/forum/components/index/index.ts b/src/addon/mod/forum/components/index/index.ts index 34ea7ba6c..5c3147867 100644 --- a/src/addon/mod/forum/components/index/index.ts +++ b/src/addon/mod/forum/components/index/index.ts @@ -133,7 +133,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } if (typeof data.deleted != 'undefined' && data.deleted) { - if (data.post.parent == 0 && this.splitviewCtrl && this.splitviewCtrl.isOn()) { + if (data.post.parentid == 0 && this.splitviewCtrl && this.splitviewCtrl.isOn()) { // Discussion deleted, clear details page. this.splitviewCtrl.emptyDetails(); } diff --git a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts index cd0fb7fdf..3479d7154 100644 --- a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts +++ b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts @@ -47,30 +47,38 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { /** * Component being initialized. */ - ngOnInit(): void { - if (this.forumId) { - if (this.post.id) { - const site: CoreSite = this.sitesProvider.getCurrentSite(); - this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussion}, 'p' + this.post.id); - - this.forumProvider.getDiscussionPost(this.forumId, this.post.discussion, this.post.id, true).then((post) => { - this.canDelete = post.capabilities.delete && this.forumProvider.isDeletePostAvailable(); - this.canEdit = post.capabilities.edit && this.forumProvider.isUpdatePostAvailable(); - this.wordCount = post.wordcount; - }).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.'); - }).finally(() => { - this.loaded = true; - }); - } else { - // Offline post, you can edit or discard the post. - this.canEdit = true; - this.canDelete = true; - this.loaded = true; - } + async ngOnInit(): Promise { + if (this.post.id) { + const site: CoreSite = this.sitesProvider.getCurrentSite(); + this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussionid}, 'p' + this.post.id); } else { + // Offline post, you can edit or discard the post. + this.canEdit = true; + this.canDelete = true; this.loaded = true; + + return; } + + if (typeof this.post.capabilities.delete == 'undefined') { + if (this.forumId) { + try { + this.post = + await this.forumProvider.getDiscussionPost(this.forumId, this.post.discussionid, this.post.id, true); + } catch (error) { + this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.'); + } + } else { + this.loaded = true; + + return; + } + } + + this.canDelete = this.post.capabilities.delete && this.forumProvider.isDeletePostAvailable(); + this.canEdit = this.post.capabilities.edit && this.forumProvider.isUpdatePostAvailable(); + this.wordCount = this.post.haswordcount && this.post.wordcount; + this.loaded = true; } /** diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html index 71f4aaddc..c1a08da81 100644 --- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html +++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html @@ -3,11 +3,11 @@

- - + +

- +
- +
{{ 'addon.mod_forum.postisprivatereply' | translate }}
@@ -47,17 +47,17 @@
{{ 'core.tag.tags' | translate }}:
- + - + -
+ {{ 'addon.mod_forum.subject' | translate }} diff --git a/src/addon/mod/forum/components/post/post.ts b/src/addon/mod/forum/components/post/post.ts index cf4354222..b55449ef5 100644 --- a/src/addon/mod/forum/components/post/post.ts +++ b/src/addon/mod/forum/components/post/post.ts @@ -43,6 +43,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges @Input() post: any; // Post. @Input() courseId: number; // Post's course ID. @Input() discussionId: number; // Post's' discussion ID. + @Input() discussion?: any; // Post's' discussion, only for starting posts. @Input() component: string; // Component this post belong to. @Input() componentId: number; // Component ID. @Input() replyData: any; // Object with the new post data. Usually shared between posts. @@ -92,14 +93,14 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges * Component being initialized. */ ngOnInit(): void { - this.uniqueId = this.post.id ? 'reply' + this.post.id : 'edit' + this.post.parent; + this.uniqueId = this.post.id ? 'reply' + this.post.id : 'edit' + this.post.parentid; const reTranslated = this.translate.instant('addon.mod_forum.re'); this.displaySubject = !this.parentSubject || (this.post.subject != this.parentSubject && this.post.subject != `Re: ${this.parentSubject}` && this.post.subject != `${reTranslated} ${this.parentSubject}`); - this.defaultReplySubject = (this.post.subject.startsWith('Re: ') || this.post.subject.startsWith(reTranslated)) - ? this.post.subject : `${reTranslated} ${this.post.subject}`; + this.defaultReplySubject = this.post.replysubject || ((this.post.subject.startsWith('Re: ') || + this.post.subject.startsWith(reTranslated)) ? this.post.subject : `${reTranslated} ${this.post.subject}`); this.optionsMenuEnabled = !this.post.id || (this.forumProvider.isGetDiscussionPostAvailable() && (this.forumProvider.isDeletePostAvailable() || this.forumProvider.isUpdatePostAvailable())); @@ -328,7 +329,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges this.syncId = this.forumSync.getDiscussionSyncId(this.discussionId); this.syncProvider.blockOperation(AddonModForumProvider.COMPONENT, this.syncId); - this.setReplyFormData(this.post.parent, true, this.post.subject, this.post.message, this.post.attachments, + this.setReplyFormData(this.post.parentid, true, this.post.subject, this.post.message, this.post.attachments, this.post.isprivatereply); }).catch(() => { // Cancelled. @@ -460,9 +461,9 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges this.domUtils.showDeleteConfirm().then(() => { const promises = []; - promises.push(this.forumOffline.deleteReply(this.post.parent)); + promises.push(this.forumOffline.deleteReply(this.post.parentid)); if (this.forum.id) { - promises.push(this.forumHelper.deleteReplyStoredFiles(this.forum.id, this.post.parent).catch(() => { + promises.push(this.forumHelper.deleteReplyStoredFiles(this.forum.id, this.post.parentid).catch(() => { // Ignore errors, maybe there are no files. })); } diff --git a/src/addon/mod/forum/pages/discussion/discussion.html b/src/addon/mod/forum/pages/discussion/discussion.html index 2cb0c0e84..bc403cdd9 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.html +++ b/src/addon/mod/forum/pages/discussion/discussion.html @@ -1,6 +1,6 @@ - + @@ -41,14 +41,14 @@ {{ 'addon.mod_forum.discussionlocked' | translate }} -
- +
+
- + @@ -60,7 +60,7 @@ - +
diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts index c8d18d494..65fac0147 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.ts +++ b/src/addon/mod/forum/pages/discussion/discussion.ts @@ -52,6 +52,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { forum: any = {}; accessInfo: any = {}; discussion: any; + startingPost: any; posts: any[]; discussionLoaded = false; postSubjects: { [id: string]: string }; @@ -253,7 +254,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { } if (typeof data.deleted != 'undefined' && data.deleted) { - if (data.post.parent == 0) { + if (!data.post.parentid) { if (this.svComponent && this.svComponent.isOn()) { this.svComponent.emptyDetails(); } else { @@ -331,8 +332,8 @@ export class AddonModForumDiscussionPage implements OnDestroy { return this.forumProvider.getDiscussionPosts(this.discussionId, this.cmId).then((response) => { onlinePosts = response.posts; ratingInfo = response.ratinginfo; - this.courseId = response.courseid; - this.forumId = response.forumid; + this.courseId = response.courseid || this.courseId; + this.forumId = response.forumid || this.forumId; }).then(() => { // Check if there are responses stored in offline. return this.forumOffline.getDiscussionReplies(this.discussionId).then((replies) => { @@ -343,7 +344,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { const posts = {}; onlinePosts.forEach((post) => { posts[post.id] = post; - hasUnreadPosts = hasUnreadPosts || !post.postread; + hasUnreadPosts = hasUnreadPosts || !!post.unread; }); replies.forEach((offlineReply) => { @@ -372,14 +373,15 @@ export class AddonModForumDiscussionPage implements OnDestroy { }).then(() => { let posts = offlineReplies.concat(onlinePosts); - const startingPost = this.forumProvider.extractStartingPost(posts); + this.startingPost = this.forumProvider.extractStartingPost(posts); // If sort type is nested, normal sorting is disabled and nested posts will be displayed. if (this.sort == 'nested') { // Sort first by creation date to make format tree work. this.forumProvider.sortDiscussionPosts(posts, 'ASC'); - posts = this.utils.formatTree(posts, 'parent', 'id', this.discussion ? this.discussion.id : startingPost.id); + const rootId = this.startingPost ? this.startingPost.id : (this.discussion ? this.discussion.id : 0); + posts = this.utils.formatTree(posts, 'parentid', 'id', rootId); } else { // Set default reply subject. const direction = this.sort == 'flat-newest' ? 'DESC' : 'ASC'; @@ -408,7 +410,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { // Just in case the posts were fetched from WS when the cut-off date was not reached but it is now. if (this.forumHelper.isCutoffDateReached(forum) && !accessInfo.cancanoverridecutoff) { posts.forEach((post) => { - post.canreply = false; + post.capabilities.reply = false; }); } })); @@ -422,28 +424,26 @@ export class AddonModForumDiscussionPage implements OnDestroy { }).catch(() => { // Ignore errors. }).then(() => { - if (startingPost) { - // Update discussion data from first post. - this.discussion = Object.assign(this.discussion || {}, startingPost); - } - - if (!this.discussion) { + if (!this.discussion && !this.startingPost) { // The discussion object was not passed as parameter and there is no starting post. Should not happen. return Promise.reject('Invalid forum discussion.'); } - if (this.discussion.userfullname && this.discussion.parent == 0 && this.forum.type == 'single') { - // Hide author for first post and type single. - this.discussion.userfullname = null; + if (this.startingPost.author && this.forum.type == 'single') { + // Hide author and groups for first post and type single. + this.startingPost.author.fullname = null; + this.startingPost.author.groups = null; + } this.posts = posts; this.ratingInfo = ratingInfo; + this.postSubjects = this.getAllPosts().reduce((postSubjects, post) => { postSubjects[post.id] = post.subject; return postSubjects; - }, { [this.discussion.id]: this.discussion.subject }); + }, { [this.startingPost.id]: this.startingPost.subject }); }); }).then(() => { if (this.forumProvider.isSetPinStateAvailableForSite()) { @@ -748,5 +748,4 @@ export class AddonModForumDiscussionPage implements OnDestroy { return posts; } - } diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index ff91b9935..1ac3fdaf7 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -280,7 +280,7 @@ export class AddonModForumProvider { * @return Starting post or undefined if not found. */ extractStartingPost(posts: any[]): any { - const index = posts.findIndex((post) => post.parent == 0); + const index = posts.findIndex((post) => !post.parentid); return index >= 0 ? posts.splice(index, 1).pop() : undefined; } @@ -305,6 +305,18 @@ export class AddonModForumProvider { return this.sitesProvider.wsAvailableInCurrentSite('mod_forum_get_discussion_post'); } + /** + * Returns whether or not getDiscussionPost WS available or not. + * + * @param site Site. If not defined, current site. + * @return If WS is avalaible. + * @since 3.7 + */ + isGetDiscussionPostsAvailable(site?: CoreSite): boolean { + return site ? site.wsAvailable('mod_forum_get_discussion_posts') : + this.sitesProvider.wsAvailableInCurrentSite('mod_forum_get_discussion_posts'); + } + /** * Returns whether or not deletePost WS available or not. * @@ -498,6 +510,41 @@ export class AddonModForumProvider { */ getDiscussionPosts(discussionId: number, cmId: number, siteId?: string): Promise<{posts: any[], courseid?: number, forumid?: number, ratinginfo?: CoreRatingInfo}> { + + // Convenience function to translate legacy data to new format. + const translateLegacyPostsFormat = (posts: any[]): any[] => { + return posts.map((post) => { + const newPost = { + id: post.id , + discussionid: post.discussion, + parentid: post.parent, + hasparent: !!post.parent, + author: { + id: post.userid, + fullname: post.userfullname, + urls: { profileimage: post.userpictureurl }, + }, + timecreated: post.created, + subject: post.subject, + message: post.message, + attachments : post.attachments, + capabilities: { + reply: !!post.canreply, + }, + + unread: !post.postread, + isprivatereply: !!post.isprivatereply, + tags: post.tags + }; + + if (post.groupname) { + newPost.author['groups'] = [{name: post.groupname}]; + } + + return newPost; + }); + }; + const params = { discussionid: discussionId }; @@ -508,8 +555,15 @@ export class AddonModForumProvider { }; return this.sitesProvider.getSite(siteId).then((site) => { - return site.read('mod_forum_get_forum_discussion_posts', params, preSets).then((response) => { + const wsName = this.isGetDiscussionPostsAvailable(site) ? 'mod_forum_get_discussion_posts' : + 'mod_forum_get_forum_discussion_posts'; + + return site.read(wsName, params, preSets).then((response) => { if (response) { + + if (wsName == 'mod_forum_get_forum_discussion_posts') { + response.posts = translateLegacyPostsFormat(response.posts); + } this.storeUserData(response.posts); return response; @@ -1054,6 +1108,16 @@ export class AddonModForumProvider { const users = {}; list.forEach((entry) => { + if (entry.author) { + const authorId = parseInt(entry.author.id); + if (!isNaN(authorId) && !users[authorId]) { + users[authorId] = { + id: entry.author.id, + fullname: entry.author.fullname, + profileimageurl: entry.author.urls.profileimage + }; + } + } const userId = parseInt(entry.userid); if (!isNaN(userId) && !users[userId]) { users[userId] = { diff --git a/src/components/user-avatar/user-avatar.ts b/src/components/user-avatar/user-avatar.ts index d537765df..dc25a4bd3 100644 --- a/src/components/user-avatar/user-avatar.ts +++ b/src/components/user-avatar/user-avatar.ts @@ -87,7 +87,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { */ protected setFields(): void { const profileUrl = this.profileUrl || (this.user && (this.user.profileimageurl || this.user.userprofileimageurl || - this.user.userpictureurl || this.user.profileimageurlsmall)); + this.user.userpictureurl || this.user.profileimageurlsmall || (this.user.urls && this.user.urls.profileimage)); if (typeof profileUrl == 'string') { this.avatarUrl = profileUrl; diff --git a/src/core/editor/providers/editor-offline.ts b/src/core/editor/providers/editor-offline.ts index 17d8b44ce..c8decce7d 100644 --- a/src/core/editor/providers/editor-offline.ts +++ b/src/core/editor/providers/editor-offline.ts @@ -228,7 +228,7 @@ export class CoreEditorOfflineProvider { if (entry) { if (entry.pageinstance != pageInstance) { - this.logger.warning(`Discarding draft because of pageinstance. Context '${contextLevel}' '${contextInstanceId}', ` + + this.logger.warn(`Discarding draft because of pageinstance. Context '${contextLevel}' '${contextInstanceId}', ` + `element '${elementId}'`); throw null; } diff --git a/src/providers/utils/mimetype.ts b/src/providers/utils/mimetype.ts index eae580a46..7decdd627 100644 --- a/src/providers/utils/mimetype.ts +++ b/src/providers/utils/mimetype.ts @@ -166,7 +166,7 @@ export class CoreMimetypeUtilsProvider { if (this.canBeEmbedded(ext)) { file.embedType = this.getExtensionType(ext); - path = CoreFile.instance.convertFileSrc(path || file.fileurl || (file.toURL && file.toURL())); + path = CoreFile.instance.convertFileSrc(path || file.fileurl || file.url || (file.toURL && file.toURL())); if (file.embedType == 'image') { return ''; From dbae55bff29b475992791a5e07aeac773dcc8e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 15 Sep 2020 14:36:06 +0200 Subject: [PATCH 079/182] MOBILE-3532 ionic: Half Revert Fix check device during loading This reverts commit 8844abd857cbbc4ea182bcd83ef21e34d71aa292. --- src/core/sharedfiles/sharedfiles.module.ts | 50 +++++++++++----------- src/providers/app.ts | 10 ++--- src/providers/file.ts | 41 ++++++++---------- src/providers/utils/iframe.ts | 8 ++-- 4 files changed, 50 insertions(+), 59 deletions(-) diff --git a/src/core/sharedfiles/sharedfiles.module.ts b/src/core/sharedfiles/sharedfiles.module.ts index 185a9a5bf..c31accbad 100644 --- a/src/core/sharedfiles/sharedfiles.module.ts +++ b/src/core/sharedfiles/sharedfiles.module.ts @@ -49,33 +49,31 @@ export class CoreSharedFilesModule { // Register the handler. delegate.registerHandler(handler); - platform.ready().then(() => { - if (appsProvider.isIOS()) { - let lastCheck = 0; + if (appsProvider.isIOS()) { + let lastCheck = 0; - // Check if there are new files at app start and when the app is resumed. - helper.searchIOSNewSharedFiles(); - platform.resume.subscribe(() => { - // Wait a bit to make sure that APP_LAUNCHED_URL is treated before this callback. - setTimeout(() => { - if (Date.now() - lastCheck < 1000) { - // Last check less than 1s ago, don't do anything. - return; - } - - lastCheck = Date.now(); - helper.searchIOSNewSharedFiles(); - }, 200); - }); - - eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, (url) => { - if (url && url.indexOf('file://') === 0) { - // We received a file in iOS, it's probably a shared file. Treat it. - lastCheck = Date.now(); - helper.searchIOSNewSharedFiles(url); + // Check if there are new files at app start and when the app is resumed. + helper.searchIOSNewSharedFiles(); + platform.resume.subscribe(() => { + // Wait a bit to make sure that APP_LAUNCHED_URL is treated before this callback. + setTimeout(() => { + if (Date.now() - lastCheck < 1000) { + // Last check less than 1s ago, don't do anything. + return; } - }); - } - }); + + lastCheck = Date.now(); + helper.searchIOSNewSharedFiles(); + }, 200); + }); + + eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, (url) => { + if (url && url.indexOf('file://') === 0) { + // We received a file in iOS, it's probably a shared file. Treat it. + lastCheck = Date.now(); + helper.searchIOSNewSharedFiles(url); + } + }); + } } } diff --git a/src/providers/app.ts b/src/providers/app.ts index 02c1279ae..206528dc7 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -17,7 +17,6 @@ import { Platform, App, NavController, MenuController } from 'ionic-angular'; import { Keyboard } from '@ionic-native/keyboard'; import { Network } from '@ionic-native/network'; import { StatusBar } from '@ionic-native/status-bar'; -import { Device } from '@ionic-native/device'; import { CoreDbProvider } from './db'; import { CoreLoggerProvider } from './logger'; @@ -179,7 +178,6 @@ export class CoreAppProvider { zone: NgZone, private menuCtrl: MenuController, private statusBar: StatusBar, - private device: Device, appRef: ApplicationRef) { this.logger = logger.getInstance('CoreAppProvider'); @@ -392,8 +390,7 @@ export class CoreAppProvider { * @return Whether the app is running in an Android mobile or tablet device. */ isAndroid(): boolean { - return this.isMobile() && - ((this.device.platform && this.device.platform.toLowerCase() == 'android') || this.platform.is('android')); + return this.isMobile() && this.platform.is('android'); } /** @@ -413,8 +410,7 @@ export class CoreAppProvider { * @return Whether the app is running in an iOS mobile or tablet device. */ isIOS(): boolean { - return this.isMobile() && - ((this.device.platform && this.device.platform.toLowerCase() == 'ios') || this.platform.is('ios')); + return this.isMobile() && !this.platform.is('android'); } /** @@ -572,7 +568,7 @@ export class CoreAppProvider { */ openKeyboard(): void { // Open keyboard is not supported in desktop and in iOS. - if (this.isMobile() && !this.isIOS()) { + if (this.isAndroid()) { this.keyboard.show(); } } diff --git a/src/providers/file.ts b/src/providers/file.ts index d0a86cc78..98df61923 100644 --- a/src/providers/file.ts +++ b/src/providers/file.ts @@ -81,31 +81,28 @@ export class CoreFileProvider { this.logger = logger.getInstance('CoreFileProvider'); - platform.ready().then(() => { - if (appProvider.isAndroid() && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) { - // Cordova File plugin creates some getters and setter for FileReader, but - // Ionic's polyfills override them in Android. - // Create the getters and setters again. This code comes from FileReader.js in cordova-plugin-file. - this.defineGetterSetter(FileReader.prototype, 'readyState', function(): any { - return this._localURL ? this._readyState : this._realReader.readyState; - }); + if (appProvider.isAndroid() && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) { + // Cordova File plugin creates some getters and setter for FileReader, but Ionic's polyfills override them in Android. + // Create the getters and setters again. This code comes from FileReader.js in cordova-plugin-file. + this.defineGetterSetter(FileReader.prototype, 'readyState', function(): any { + return this._localURL ? this._readyState : this._realReader.readyState; + }); - this.defineGetterSetter(FileReader.prototype, 'error', function(): any { - return this._localURL ? this._error : this._realReader.error; - }); + this.defineGetterSetter(FileReader.prototype, 'error', function(): any { + return this._localURL ? this._error : this._realReader.error; + }); - this.defineGetterSetter(FileReader.prototype, 'result', function(): any { - return this._localURL ? this._result : this._realReader.result; - }); + this.defineGetterSetter(FileReader.prototype, 'result', function(): any { + return this._localURL ? this._result : this._realReader.result; + }); - this.defineEvent('onloadstart'); - this.defineEvent('onprogress'); - this.defineEvent('onload'); - this.defineEvent('onerror'); - this.defineEvent('onloadend'); - this.defineEvent('onabort'); - } - }); + this.defineEvent('onloadstart'); + this.defineEvent('onprogress'); + this.defineEvent('onload'); + this.defineEvent('onerror'); + this.defineEvent('onloadend'); + this.defineEvent('onabort'); + } } /** diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index efcbda6b7..fc9d3afb9 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -57,8 +57,8 @@ export class CoreIframeUtilsProvider { const win = window; - platform.ready().then(() => { - if (appProvider.isIOS() && win.WKUserScript) { + if (appProvider.isIOS() && win.WKUserScript) { + platform.ready().then(() => { // Inject code to the iframes because we cannot access the online ones. const wwwPath = fileProvider.getWWWAbsolutePath(); const linksPath = textUtils.concatenatePaths(wwwPath, 'assets/js/iframe-treat-links.js'); @@ -73,8 +73,8 @@ export class CoreIframeUtilsProvider { // Handle post messages received by iframes. window.addEventListener('message', this.handleIframeMessage.bind(this)); - } - }); + }); + } } /** From c69d108c11b0bc0559fc40fd4672f2f6f25f5b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 15 Sep 2020 15:56:38 +0200 Subject: [PATCH 080/182] MOBILE-3532 ionic: Apply ios mode on iPadOS --- src/app/app.component.ts | 62 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c87872880..1dc6627bb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, NgZone } from '@angular/core'; -import { Platform, IonicApp } from 'ionic-angular'; +import { Config, Platform, IonicApp } from 'ionic-angular'; import { Network } from '@ionic-native/network'; import { CoreApp, CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; @@ -28,6 +28,7 @@ import { Keyboard } from '@ionic-native/keyboard'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { CoreLoginSitesPage } from '@core/login/pages/sites/sites'; import { CoreWindow } from '@singletons/window'; +import { Device } from '@ionic-native/device'; @Component({ templateUrl: 'app.html' @@ -40,13 +41,62 @@ export class MoodleMobileApp implements OnInit { protected lastUrls = {}; protected lastInAppUrl: string; - constructor(private platform: Platform, logger: CoreLoggerProvider, keyboard: Keyboard, private app: IonicApp, - private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider, private zone: NgZone, - private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider, - private screenOrientation: ScreenOrientation, private urlSchemesProvider: CoreCustomURLSchemesProvider, - private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private network: Network) { + constructor( + private platform: Platform, + logger: CoreLoggerProvider, + keyboard: Keyboard, + config: Config, + device: Device, + private app: IonicApp, + private eventsProvider: CoreEventsProvider, + private loginHelper: CoreLoginHelperProvider, + private zone: NgZone, + private appProvider: CoreAppProvider, + private langProvider: CoreLangProvider, + private sitesProvider: CoreSitesProvider, + private screenOrientation: ScreenOrientation, + private urlSchemesProvider: CoreCustomURLSchemesProvider, + private utils: CoreUtilsProvider, + private urlUtils: CoreUrlUtilsProvider, + private network: Network + ) { this.logger = logger.getInstance('AppComponent'); + if (this.appProvider.isIOS() && !platform.is('ios')) { + // Solve problem with wrong detected iPadOS. + const platforms = platform.platforms(); + const index = platforms.indexOf('core'); + if (index > -1) { + platforms.splice(index, 1); + } + platforms.push('mobile'); + platforms.push('ios'); + platforms.push('ipad'); + platforms.push('tablet'); + + app.setElementClass('app-root-ios', true); + platform.ready().then(() => { + if (device.version) { + const [major, minor]: string[] = device.version.split('.', 2); + app.setElementClass('platform-ios' + major, true); + app.setElementClass('platform-ios' + major + '_' + minor, true); + } + }); + + app._elementRef.nativeElement.classList.remove('app-root-md'); + + const iosConfig = config.getModeConfig('ios'); + + config.set('mode', 'ios'); + + Object.keys(iosConfig).forEach((key) => { + // Already overriden: pageTransition, do not change. + if (key != 'pageTransition') { + config.set('ios', key, iosConfig[key]); + } + }); + } + platform.ready().then(() => { // Okay, so the platform is ready and our plugins are available. // Here you can do any higher level native things you might need. From 5d3e75e22091a4084e4c7d9c1e71b4c6bc97e20c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 15 Sep 2020 08:27:16 +0200 Subject: [PATCH 081/182] MOBILE-3507 notifications: Support notifications with big pictures --- src/core/pushnotifications/providers/pushnotifications.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/pushnotifications/providers/pushnotifications.ts b/src/core/pushnotifications/providers/pushnotifications.ts index 76b2faa73..7c8449406 100644 --- a/src/core/pushnotifications/providers/pushnotifications.ts +++ b/src/core/pushnotifications/providers/pushnotifications.ts @@ -527,6 +527,12 @@ export class CorePushNotificationsProvider { localNotif.icon = notification.image; // This feature isn't supported by the official plugin, we use a fork. ( localNotif).iconType = data['image-type']; + + localNotif.summary = data.summaryText; + + if (data.picture) { + localNotif.attachments = [data.picture]; + } } Promise.all(promises).then(() => { From 06ce5e765c2339666b9b399302af719036c2bb4c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 15 Sep 2020 12:03:31 +0200 Subject: [PATCH 082/182] MOBILE-3507 notifications: Support view rich text when clicked --- .../providers/push-click-handler.ts | 48 +++++++++---------- src/providers/utils/text.ts | 5 +- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/addon/notifications/providers/push-click-handler.ts b/src/addon/notifications/providers/push-click-handler.ts index 425437ddb..4797819ae 100644 --- a/src/addon/notifications/providers/push-click-handler.ts +++ b/src/addon/notifications/providers/push-click-handler.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; +import { CoreTextUtils } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CorePushNotificationsClickHandler } from '@core/pushnotifications/providers/delegate'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; @@ -60,38 +61,35 @@ export class AddonNotificationsPushClickHandler implements CorePushNotifications * @param notification The notification to check. * @return Promise resolved when done. */ - handleClick(notification: any): Promise { - let promise; + async handleClick(notification: any): Promise { + + if (notification.customdata.extendedtext) { + // Display the text in a modal. + return CoreTextUtils.instance.viewText(notification.title, notification.customdata.extendedtext, { + displayCopyButton: true, + modalOptions: { cssClass: 'core-modal-fullscreen' }, + }); + } // Try to handle the appurl first. if (notification.customdata && notification.customdata.appurl) { - promise = this.linkHelper.handleLink(notification.customdata.appurl, undefined, undefined, true); - } else { - promise = Promise.resolve(false); + if (this.linkHelper.handleLink(notification.customdata.appurl, undefined, undefined, true)) { + // Link treated, stop. + return; + } } - return promise.then((treated) => { - - if (!treated) { - // No link or cannot be handled by the app. Try to handle the contexturl now. - if (notification.contexturl) { - return this.linkHelper.handleLink(notification.contexturl); - } else { - return false; - } + // No appurl or cannot be handled by the app. Try to handle the contexturl now. + if (notification.contexturl) { + if (this.linkHelper.handleLink(notification.contexturl)) { + // Link treated, stop. + return; } + } - return true; - }).then((treated) => { + // No contexturl or cannot be handled by the app. Open the notifications page. + await this.utils.ignoreErrors(this.notificationsProvider.invalidateNotificationsList(notification.site)); - if (!treated) { - // No link or cannot be handled by the app. Open the notifications page. - return this.notificationsProvider.invalidateNotificationsList(notification.site).catch(() => { - // Ignore errors. - }).then(() => { - return this.linkHelper.goInSite(undefined, 'AddonNotificationsListPage', undefined, notification.site); - }); - } - }); + await this.linkHelper.goInSite(undefined, 'AddonNotificationsListPage', undefined, notification.site); } } diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index af83ced3b..ddac24c1b 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { ModalController } from 'ionic-angular'; +import { ModalController, ModalOptions } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreLangProvider } from '../lang'; import { makeSingleton } from '@singletons/core.singletons'; @@ -1163,7 +1163,7 @@ export class CoreTextUtilsProvider { Object.assign(params, options); - const modal = this.modalCtrl.create('CoreViewerTextPage', params); + const modal = this.modalCtrl.create('CoreViewerTextPage', params, options.modalOptions); modal.present(); } } @@ -1181,6 +1181,7 @@ export type CoreTextUtilsViewTextOptions = { instanceId?: number; // The instance ID related to the context. courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters. displayCopyButton?: boolean; // Whether to display a button to copy the text. + modalOptions?: ModalOptions; // Modal options. }; export class CoreTextUtils extends makeSingleton(CoreTextUtilsProvider) {} From 4c32239b54cc08eb2fe100c282fde0a6d25bf715 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 16 Sep 2020 11:34:23 +0200 Subject: [PATCH 083/182] MOBILE-3507 notifications: Support open appurl in browser/iab --- .../providers/push-click-handler.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/addon/notifications/providers/push-click-handler.ts b/src/addon/notifications/providers/push-click-handler.ts index 4797819ae..b6504debd 100644 --- a/src/addon/notifications/providers/push-click-handler.ts +++ b/src/addon/notifications/providers/push-click-handler.ts @@ -71,11 +71,22 @@ export class AddonNotificationsPushClickHandler implements CorePushNotifications }); } - // Try to handle the appurl first. + // Try to handle the appurl. if (notification.customdata && notification.customdata.appurl) { - if (this.linkHelper.handleLink(notification.customdata.appurl, undefined, undefined, true)) { - // Link treated, stop. - return; + switch (notification.customdata.appurlopenin) { + case 'inappbrowser': + this.utils.openInApp(notification.customdata.appurl); + + return; + + case 'browser': + return this.utils.openInBrowser(notification.customdata.appurl); + + default: + if (this.linkHelper.handleLink(notification.customdata.appurl, undefined, undefined, true)) { + // Link treated, stop. + return; + } } } From d7b2c06ae3581db7b36536e902d2de3351165eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 16 Sep 2020 09:55:46 +0200 Subject: [PATCH 084/182] MOBILE-3421 ios: Fix log subtitles on ios 12 and 13 --- src/app/app.ios.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index 76b7d379d..72ee007a5 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -120,4 +120,15 @@ ion-app.app-root.ios { cursor: pointer; } } + + video { + &::cue { + white-space: pre-line; + } + + &::-webkit-media-text-track-display-backdrop { + margin-left: 1.5%; + margin-right: 1.5%; + } + } } From 39c4fc516dce4918e1b1c2c0d4c902b16ae61f31 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 16 Sep 2020 13:14:19 +0200 Subject: [PATCH 085/182] MOBILE-3511 message: Fix mark all messages as read --- src/addon/messages/providers/messages.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index ce0b4ae5d..6033ad8fd 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -1961,7 +1961,8 @@ export class AddonMessagesProvider { * @since 3.2 */ isMarkAllMessagesReadEnabled(): boolean { - return this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_messages_as_read'); + return this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_conversation_messages_as_read') || + this.sitesProvider.wsAvailableInCurrentSite('core_message_mark_all_messages_as_read'); } /** From 03cea0a2e00628126a0aab2f8b0709d78bf7a3ad Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 16 Sep 2020 14:29:28 +0200 Subject: [PATCH 086/182] MOBILE-3553 h5p: Remove jQuery version number --- src/core/h5p/assets/js/jquery.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/h5p/assets/js/jquery.js b/src/core/h5p/assets/js/jquery.js index 2057286b5..9583951e4 100644 --- a/src/core/h5p/assets/js/jquery.js +++ b/src/core/h5p/assets/js/jquery.js @@ -1,5 +1,5 @@ -/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */ -!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){var n=[],r=e.document,i=n.slice,o=n.concat,a=n.push,s=n.indexOf,u={},l=u.toString,c=u.hasOwnProperty,f={},d=function(e,t){return new d.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,h=/^-ms-/,g=/-([\da-z])/gi,m=function(e,t){return t.toUpperCase()};function v(e){var t=!!e&&"length"in e&&e.length,n=d.type(e);return"function"!==n&&!d.isWindow(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}d.fn=d.prototype={jquery:"1.12.4",constructor:d,selector:"",length:0,toArray:function(){return i.call(this)},get:function(e){return null!=e?e<0?this[e+this.length]:this[e]:i.call(this)},pushStack:function(e){var t=d.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e){return d.each(this,e)},map:function(e){return this.pushStack(d.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==d.type(e)||e.nodeType||d.isWindow(e))return!1;try{if(e.constructor&&!c.call(e,"constructor")&&!c.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(e){return!1}if(!f.ownFirst)for(t in e)return c.call(e,t);for(t in e);return void 0===t||c.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?u[l.call(e)]||"object":typeof e},globalEval:function(t){t&&d.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(h,"ms-").replace(g,m)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t){var n,r=0;if(v(e))for(n=e.length;r+~]|"+O+")"+O+"*"),X=new RegExp("="+O+"*([^\\]'\"]*?)"+O+"*\\]","g"),U=new RegExp(B),V=new RegExp("^"+R+"$"),Y={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+B),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),bool:new RegExp("^(?:"+M+")$","i"),needsContext:new RegExp("^"+O+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)","i")},J=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/[+~]/,ee=/'|\\/g,te=new RegExp("\\\\([\\da-f]{1,6}"+O+"?|("+O+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=function(){d()};try{q.apply(j=_.call(w.childNodes),w.childNodes),j[w.childNodes.length].nodeType}catch(e){q={apply:j.length?function(e,t){H.apply(e,_.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function ie(e,t,r,i){var o,s,l,c,f,h,v,y,T=t&&t.ownerDocument,C=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==C&&9!==C&&11!==C)return r;if(!i&&((t?t.ownerDocument||t:w)!==p&&d(t),t=t||p,g)){if(11!==C&&(h=K.exec(e)))if(o=h[1]){if(9===C){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(T&&(l=T.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(h[2])return q.apply(r,t.getElementsByTagName(e)),r;if((o=h[3])&&n.getElementsByClassName&&t.getElementsByClassName)return q.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!k[e+" "]&&(!m||!m.test(e))){if(1!==C)T=t,y=e;else if("object"!==t.nodeName.toLowerCase()){for((c=t.getAttribute("id"))?c=c.replace(ee,"\\$&"):t.setAttribute("id",c=b),s=(v=a(e)).length,f=V.test(c)?"#"+c:"[id='"+c+"']";s--;)v[s]=f+" "+ge(v[s]);y=v.join(","),T=Z.test(e)&&pe(t.parentNode)||t}if(y)try{return q.apply(r,T.querySelectorAll(y)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(I,"$1"),t,r,i)}function oe(){var e=[];return function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}}function ae(e){return e[b]=!0,e}function se(e){var t=p.createElement("div");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ue(e,t){for(var n=e.split("|"),i=n.length;i--;)r.attrHandle[n[i]]=t}function le(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||A)-(~e.sourceIndex||A);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function ce(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function fe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return ae(function(t){return t=+t,ae(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function pe(e){return e&&void 0!==e.getElementsByTagName&&e}for(t in n=ie.support={},o=ie.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},d=ie.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==p&&9===a.nodeType&&a.documentElement?(h=(p=a).documentElement,g=!o(p),(i=p.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=se(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=se(function(e){return e.appendChild(p.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(p.getElementsByClassName),n.getById=se(function(e){return h.appendChild(e).id=b,!p.getElementsByName||!p.getElementsByName(b).length}),n.getById?(r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}},r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}}):(delete r.find.ID,r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}}),r.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],m=[],(n.qsa=Q.test(p.querySelectorAll))&&(se(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+O+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+O+"*(?:value|"+M+")"),e.querySelectorAll("[id~="+b+"-]").length||m.push("~="),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||m.push(".#.+[+~]")}),se(function(e){var t=p.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+O+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(n.matchesSelector=Q.test(y=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&se(function(e){n.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),v.push("!=",B)}),m=m.length&&new RegExp(m.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},S=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===p||e.ownerDocument===w&&x(w,e)?-1:t===p||t.ownerDocument===w&&x(w,t)?1:c?F(c,e)-F(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===p?-1:t===p?1:i?-1:o?1:c?F(c,e)-F(c,t):0;if(i===o)return le(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?le(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},p):p},ie.matches=function(e,t){return ie(e,null,null,t)},ie.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&d(e),t=t.replace(X,"='$1']"),n.matchesSelector&&g&&!k[t+" "]&&(!v||!v.test(t))&&(!m||!m.test(t)))try{var r=y.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return ie(t,p,null,[e]).length>0},ie.contains=function(e,t){return(e.ownerDocument||e)!==p&&d(e),x(e,t)},ie.attr=function(e,t){(e.ownerDocument||e)!==p&&d(e);var i=r.attrHandle[t.toLowerCase()],o=i&&D.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},ie.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},ie.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(S),f){for(;t=e[o++];)t===e[o]&&(i=r.push(o));for(;i--;)e.splice(r[i],1)}return c=null,e},i=ie.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r++];)n+=i(t);return n},(r=ie.selectors={cacheLength:50,createPseudo:ae,match:Y,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ie.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ie.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&U.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ie.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(W," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,d,p,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!u&&!s,x=!1;if(m){if(o){for(;g;){for(d=t;d=d[g];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&y){for(x=(p=(l=(c=(f=(d=m)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],d=p&&m.childNodes[p];d=++p&&d&&d[g]||(x=p=0)||h.pop();)if(1===d.nodeType&&++x&&d===t){c[e]=[T,p,x];break}}else if(y&&(x=p=(l=(c=(f=(d=t)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)for(;(d=++p&&d&&d[g]||(x=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++x||(y&&((c=(f=d[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]=[T,x]),d!==t)););return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||ie.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?ae(function(e,n){for(var r,o=i(e,t),a=o.length;a--;)e[r=F(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:ae(function(e){var t=[],n=[],r=s(e.replace(I,"$1"));return r[b]?ae(function(e,t,n,i){for(var o,a=r(e,null,i,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:ae(function(e){return function(t){return ie(e,t).length>0}}),contains:ae(function(e){return e=e.replace(te,ne),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:ae(function(e){return V.test(e||"")||ie.error("unsupported lang: "+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return!1===e.disabled},disabled:function(e){return!0===e.disabled},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return G.test(e.nodeName)},input:function(e){return J.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:de(function(){return[0]}),last:de(function(e,t){return[t-1]}),eq:de(function(e,t,n){return[n<0?n+t:n]}),even:de(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:de(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function ye(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s-1&&(o[l]=!(a[l]=f))}}else v=ye(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):q.apply(a,v)})}function be(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return F(t,e)>-1},s,!0),d=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&ve(d),u>1&&ge(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(I,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,m,v=0,y="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,N=C.length;for(c&&(l=a===p||a||c);y!==N&&null!=(f=C[y]);y++){if(i&&f){for(h=0,a||f.ownerDocument===p||(d(f),s=!g);m=e[h++];)if(m(f,a||p,s)){u.push(f);break}c&&(T=E)}n&&((f=!m&&f)&&v--,o&&x.push(f))}if(v+=y,n&&y!==v){for(h=0;m=t[h++];)m(x,b,a,s);if(o){if(v>0)for(;y--;)x[y]||b[y]||(b[y]=L.call(u));b=ye(b)}q.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&ie.uniqueSort(u)}return c&&(T=E,l=w),x};return n?ae(o):o}(o,i))).selector=e}return s},u=ie.select=function(e,t,i,o){var u,l,c,f,d,p="function"==typeof e&&e,h=!o&&a(e=p.selector||e);if(i=i||[],1===h.length){if((l=h[0]=h[0].slice(0)).length>2&&"ID"===(c=l[0]).type&&n.getById&&9===t.nodeType&&g&&r.relative[l[1].type]){if(!(t=(r.find.ID(c.matches[0].replace(te,ne),t)||[])[0]))return i;p&&(t=t.parentNode),e=e.slice(l.shift().value.length)}for(u=Y.needsContext.test(e)?0:l.length;u--&&(c=l[u],!r.relative[f=c.type]);)if((d=r.find[f])&&(o=d(c.matches[0].replace(te,ne),Z.test(l[0].type)&&pe(t.parentNode)||t))){if(l.splice(u,1),!(e=o.length&&ge(l)))return q.apply(i,o),i;break}}return(p||s(e,h))(o,t,!g,i,!t||Z.test(e)&&pe(t.parentNode)||t),i},n.sortStable=b.split("").sort(S).join("")===b,n.detectDuplicates=!!f,d(),n.sortDetached=se(function(e){return 1&e.compareDocumentPosition(p.createElement("div"))}),se(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ue("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&se(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ue("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),se(function(e){return null==e.getAttribute("disabled")})||ue(M,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),ie}(e);d.find=y,d.expr=y.selectors,d.expr[":"]=d.expr.pseudos,d.uniqueSort=d.unique=y.uniqueSort,d.text=y.getText,d.isXMLDoc=y.isXML,d.contains=y.contains;var x=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&d(e).is(n))break;r.push(e)}return r},b=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},w=d.expr.match.needsContext,T=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,C=/^.[^:#\[\.,]*$/;function E(e,t,n){if(d.isFunction(t))return d.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return d.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(C.test(t))return d.filter(t,e,n);t=d.filter(t,e)}return d.grep(e,function(e){return d.inArray(e,t)>-1!==n})}d.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?d.find.matchesSelector(r,e)?[r]:[]:d.find.matches(e,d.grep(t,function(e){return 1===e.nodeType}))},d.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(d(e).filter(function(){for(t=0;t1?d.unique(n):n)).selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(E(this,e||[],!1))},not:function(e){return this.pushStack(E(this,e||[],!0))},is:function(e){return!!E(this,"string"==typeof e&&w.test(e)?d(e):e||[],!1).length}});var N,k=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/;(d.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||N,"string"==typeof e){if(!(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:k.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof d?t[0]:t,d.merge(this,d.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),T.test(i[1])&&d.isPlainObject(t))for(i in t)d.isFunction(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}if((o=r.getElementById(i[2]))&&o.parentNode){if(o.id!==i[2])return N.find(e);this.length=1,this[0]=o}return this.context=r,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):d.isFunction(e)?void 0!==n.ready?n.ready(e):e(d):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),d.makeArray(e,this))}).prototype=d.fn,N=d(r);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){do{e=e[t]}while(e&&1!==e.nodeType);return e}d.fn.extend({has:function(e){var t,n=d(e,this),r=n.length;return this.filter(function(){for(t=0;t-1:1===n.nodeType&&d.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?d.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?d.inArray(this[0],d(e)):d.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(d.uniqueSort(d.merge(this.get(),d(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),d.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x(e,"parentNode")},parentsUntil:function(e,t,n){return x(e,"parentNode",n)},next:function(e){return D(e,"nextSibling")},prev:function(e){return D(e,"previousSibling")},nextAll:function(e){return x(e,"nextSibling")},prevAll:function(e){return x(e,"previousSibling")},nextUntil:function(e,t,n){return x(e,"nextSibling",n)},prevUntil:function(e,t,n){return x(e,"previousSibling",n)},siblings:function(e){return b((e.parentNode||{}).firstChild,e)},children:function(e){return b(e.firstChild)},contents:function(e){return d.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:d.merge([],e.childNodes)}},function(e,t){d.fn[e]=function(n,r){var i=d.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=d.filter(r,i)),this.length>1&&(A[e]||(i=d.uniqueSort(i)),S.test(e)&&(i=i.reverse())),this.pushStack(i)}});var j,L,H=/\S+/g;function q(){r.addEventListener?(r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_)):(r.detachEvent("onreadystatechange",_),e.detachEvent("onload",_))}function _(){(r.addEventListener||"load"===e.event.type||"complete"===r.readyState)&&(q(),d.ready())}for(L in d.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return d.each(e.match(H)||[],function(e,n){t[n]=!0}),t}(e):d.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=e.once,r=t=!0;a.length;s=-1)for(n=a.shift();++s-1;)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?d.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=!0,n||l.disable(),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l},d.extend({Deferred:function(e){var t=[["resolve","done",d.Callbacks("once memory"),"resolved"],["reject","fail",d.Callbacks("once memory"),"rejected"],["notify","progress",d.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return d.Deferred(function(n){d.each(t,function(t,o){var a=d.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&d.isFunction(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?d.extend(e,r):r}},i={};return r.pipe=r.then,d.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,o=0,a=i.call(arguments),s=a.length,u=1!==s||e&&d.isFunction(e.promise)?s:0,l=1===u?e:d.Deferred(),c=function(e,n,r){return function(o){n[e]=this,r[e]=arguments.length>1?i.call(arguments):o,r===t?l.notifyWith(n,r):--u||l.resolveWith(n,r)}};if(s>1)for(t=new Array(s),n=new Array(s),r=new Array(s);o0||(j.resolveWith(r,[d]),d.fn.triggerHandler&&(d(r).triggerHandler("ready"),d(r).off("ready"))))}}),d.ready.promise=function(t){if(!j)if(j=d.Deferred(),"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll)e.setTimeout(d.ready);else if(r.addEventListener)r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_);else{r.attachEvent("onreadystatechange",_),e.attachEvent("onload",_);var n=!1;try{n=null==e.frameElement&&r.documentElement}catch(e){}n&&n.doScroll&&function t(){if(!d.isReady){try{n.doScroll("left")}catch(n){return e.setTimeout(t,50)}q(),d.ready()}}()}return j.promise(t)},d.ready.promise(),d(f))break;f.ownFirst="0"===L,f.inlineBlockNeedsLayout=!1,d(function(){var e,t,n,i;(n=r.getElementsByTagName("body")[0])&&n.style&&(t=r.createElement("div"),(i=r.createElement("div")).style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(i).appendChild(t),void 0!==t.style.zoom&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",f.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(i))}),function(){var e=r.createElement("div");f.deleteExpando=!0;try{delete e.test}catch(e){f.deleteExpando=!1}e=null}();var F,M=function(e){var t=d.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return(1===n||9===n)&&(!t||!0!==t&&e.getAttribute("classid")===t)},O=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,R=/([A-Z])/g;function P(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(R,"-$1").toLowerCase();if("string"==typeof(n=e.getAttribute(r))){try{n="true"===n||"false"!==n&&("null"===n?null:+n+""===n?+n:O.test(n)?d.parseJSON(n):n)}catch(e){}d.data(e,t,n)}else n=void 0}return n}function B(e){var t;for(t in e)if(("data"!==t||!d.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function W(e,t,r,i){if(M(e)){var o,a,s=d.expando,u=e.nodeType,l=u?d.cache:e,c=u?e[s]:e[s]&&s;if(c&&l[c]&&(i||l[c].data)||void 0!==r||"string"!=typeof t)return c||(c=u?e[s]=n.pop()||d.guid++:s),l[c]||(l[c]=u?{}:{toJSON:d.noop}),"object"!=typeof t&&"function"!=typeof t||(i?l[c]=d.extend(l[c],t):l[c].data=d.extend(l[c].data,t)),a=l[c],i||(a.data||(a.data={}),a=a.data),void 0!==r&&(a[d.camelCase(t)]=r),"string"==typeof t?null==(o=a[t])&&(o=a[d.camelCase(t)]):o=a,o}}function I(e,t,n){if(M(e)){var r,i,o=e.nodeType,a=o?d.cache:e,s=o?e[d.expando]:d.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){i=(t=d.isArray(t)?t.concat(d.map(t,d.camelCase)):t in r?[t]:(t=d.camelCase(t))in r?[t]:t.split(" ")).length;for(;i--;)delete r[t[i]];if(n?!B(r):!d.isEmptyObject(r))return}(n||(delete a[s].data,B(a[s])))&&(o?d.cleanData([e],!0):f.deleteExpando||a!=a.window?delete a[s]:a[s]=void 0)}}}d.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return!!(e=e.nodeType?d.cache[e[d.expando]]:e[d.expando])&&!B(e)},data:function(e,t,n){return W(e,t,n)},removeData:function(e,t){return I(e,t)},_data:function(e,t,n){return W(e,t,n,!0)},_removeData:function(e,t){return I(e,t,!0)}}),d.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=d.data(o),1===o.nodeType&&!d._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&0===(r=a[n].name).indexOf("data-")&&P(o,r=d.camelCase(r.slice(5)),i[r]);d._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){d.data(this,e)}):arguments.length>1?this.each(function(){d.data(this,e,t)}):o?P(o,e,d.data(o,e)):void 0},removeData:function(e){return this.each(function(){d.removeData(this,e)})}}),d.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=d._data(e,t),n&&(!r||d.isArray(n)?r=d._data(e,t,d.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=d.queue(e,t),r=n.length,i=n.shift(),o=d._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){d.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return d._data(e,n)||d._data(e,n,{empty:d.Callbacks("once memory").add(function(){d._removeData(e,t+"queue"),d._removeData(e,n)})})}}),d.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length
a",f.leadingWhitespace=3===Y.firstChild.nodeType,f.tbody=!Y.getElementsByTagName("tbody").length,f.htmlSerialize=!!Y.getElementsByTagName("link").length,f.html5Clone="<:nav>"!==r.createElement("nav").cloneNode(!0).outerHTML,G.type="checkbox",G.checked=!0,J.appendChild(G),f.appendChecked=G.checked,Y.innerHTML="",f.noCloneChecked=!!Y.cloneNode(!0).lastChild.defaultValue,J.appendChild(Y),(G=r.createElement("input")).setAttribute("type","radio"),G.setAttribute("checked","checked"),G.setAttribute("name","t"),Y.appendChild(G),f.checkClone=Y.cloneNode(!0).cloneNode(!0).lastChild.checked,f.noCloneEvent=!!Y.addEventListener,Y[d.expando]=1,f.attributes=!Y.getAttribute(d.expando);var ie={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:f.htmlSerialize?[0,"",""]:[1,"X
","
"]};function oe(e,t){var n,r,i=0,o=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||d.nodeName(r,t)?o.push(r):d.merge(o,oe(r,t));return void 0===t||t&&d.nodeName(e,t)?d.merge([e],o):o}function ae(e,t){for(var n,r=0;null!=(n=e[r]);r++)d._data(n,"globalEval",!t||d._data(t[r],"globalEval"))}ie.optgroup=ie.option,ie.tbody=ie.tfoot=ie.colgroup=ie.caption=ie.thead,ie.th=ie.td;var se=/<|&#?\w+;/,ue=/"!==p[1]||ue.test(a)?0:u:u.firstChild)&&a.childNodes.length;o--;)d.nodeName(c=a.childNodes[o],"tbody")&&!c.childNodes.length&&a.removeChild(c);for(d.merge(m,u.childNodes),u.textContent="";u.firstChild;)u.removeChild(u.firstChild);u=g.lastChild}else m.push(t.createTextNode(a));for(u&&g.removeChild(u),f.appendChecked||d.grep(oe(m,"input"),le),v=0;a=m[v++];)if(r&&d.inArray(a,r)>-1)i&&i.push(a);else if(s=d.contains(a.ownerDocument,a),u=oe(g.appendChild(a),"script"),s&&ae(u),n)for(o=0;a=u[o++];)ee.test(a.type||"")&&n.push(a);return u=null,g}!function(){var t,n,i=r.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(f[t]=n in e)||(i.setAttribute(n,"t"),f[t]=!1===i.attributes[n].expando);i=null}();var fe=/^(?:input|select|textarea)$/i,de=/^key/,pe=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,he=/^(?:focusinfocus|focusoutblur)$/,ge=/^([^.]*)(?:\.(.+)|)/;function me(){return!0}function ve(){return!1}function ye(){try{return r.activeElement}catch(e){}}function xe(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)xe(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ve;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return d().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=d.guid++)),e.each(function(){d.event.add(this,t,i,r,n)})}d.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,h,g,m,v=d._data(e);if(v){for(n.handler&&(n=(u=n).handler,i=u.selector),n.guid||(n.guid=d.guid++),(a=v.events)||(a=v.events={}),(c=v.handle)||((c=v.handle=function(e){return void 0===d||e&&d.event.triggered===e.type?void 0:d.event.dispatch.apply(c.elem,arguments)}).elem=e),s=(t=(t||"").match(H)||[""]).length;s--;)h=m=(o=ge.exec(t[s])||[])[1],g=(o[2]||"").split(".").sort(),h&&(l=d.event.special[h]||{},h=(i?l.delegateType:l.bindType)||h,l=d.event.special[h]||{},f=d.extend({type:h,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&d.expr.match.needsContext.test(i),namespace:g.join(".")},u),(p=a[h])||((p=a[h]=[]).delegateCount=0,l.setup&&!1!==l.setup.call(e,r,g,c)||(e.addEventListener?e.addEventListener(h,c,!1):e.attachEvent&&e.attachEvent("on"+h,c))),l.add&&(l.add.call(e,f),f.handler.guid||(f.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,f):p.push(f),d.event.global[h]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,h,g,m,v=d.hasData(e)&&d._data(e);if(v&&(c=v.events)){for(l=(t=(t||"").match(H)||[""]).length;l--;)if(h=m=(s=ge.exec(t[l])||[])[1],g=(s[2]||"").split(".").sort(),h){for(f=d.event.special[h]||{},p=c[h=(r?f.delegateType:f.bindType)||h]||[],s=s[2]&&new RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"),u=o=p.length;o--;)a=p[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(p.splice(o,1),a.selector&&p.delegateCount--,f.remove&&f.remove.call(e,a));u&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,g,v.handle)||d.removeEvent(e,h,v.handle),delete c[h])}else for(h in c)d.event.remove(e,h+t[l],n,r,!0);d.isEmptyObject(c)&&(delete v.handle,d._removeData(e,"events"))}},trigger:function(t,n,i,o){var a,s,u,l,f,p,h,g=[i||r],m=c.call(t,"type")?t.type:t,v=c.call(t,"namespace")?t.namespace.split("."):[];if(u=p=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!he.test(m+d.event.triggered)&&(m.indexOf(".")>-1&&(v=m.split("."),m=v.shift(),v.sort()),s=m.indexOf(":")<0&&"on"+m,(t=t[d.expando]?t:new d.Event(m,"object"==typeof t&&t)).isTrigger=o?2:3,t.namespace=v.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+v.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:d.makeArray(n,[t]),f=d.event.special[m]||{},o||!f.trigger||!1!==f.trigger.apply(i,n))){if(!o&&!f.noBubble&&!d.isWindow(i)){for(l=f.delegateType||m,he.test(l+m)||(u=u.parentNode);u;u=u.parentNode)g.push(u),p=u;p===(i.ownerDocument||r)&&g.push(p.defaultView||p.parentWindow||e)}for(h=0;(u=g[h++])&&!t.isPropagationStopped();)t.type=h>1?l:f.bindType||m,(a=(d._data(u,"events")||{})[t.type]&&d._data(u,"handle"))&&a.apply(u,n),(a=s&&u[s])&&a.apply&&M(u)&&(t.result=a.apply(u,n),!1===t.result&&t.preventDefault());if(t.type=m,!o&&!t.isDefaultPrevented()&&(!f._default||!1===f._default.apply(g.pop(),n))&&M(i)&&s&&i[m]&&!d.isWindow(i)){(p=i[s])&&(i[s]=null),d.event.triggered=m;try{i[m]()}catch(e){}d.event.triggered=void 0,p&&(i[s]=p)}return t.result}},dispatch:function(e){e=d.event.fix(e);var t,n,r,o,a,s,u=i.call(arguments),l=(d._data(this,"events")||{})[e.type]||[],c=d.event.special[e.type]||{};if(u[0]=e,e.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,e)){for(s=d.event.handlers.call(this,e,l),t=0;(o=s[t++])&&!e.isPropagationStopped();)for(e.currentTarget=o.elem,n=0;(a=o.handlers[n++])&&!e.isImmediatePropagationStopped();)e.rnamespace&&!e.rnamespace.test(a.namespace)||(e.handleObj=a,e.data=a.data,void 0!==(r=((d.event.special[a.origType]||{}).handle||a.handler).apply(o.elem,u))&&!1===(e.result=r)&&(e.preventDefault(),e.stopPropagation()));return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,u=e.target;if(s&&u.nodeType&&("click"!==e.type||isNaN(e.button)||e.button<1))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(!0!==u.disabled||"click"!==e.type)){for(r=[],n=0;n-1:d.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&a.push({elem:u,handlers:r})}return s]","i"),Te=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,Ce=/\s*$/g,Se=re(r).appendChild(r.createElement("div"));function Ae(e,t){return d.nodeName(e,"table")&&d.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function De(e){return e.type=(null!==d.find.attr(e,"type"))+"/"+e.type,e}function je(e){var t=Ne.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Le(e,t){if(1===t.nodeType&&d.hasData(e)){var n,r,i,o=d._data(e),a=d._data(t,o),s=o.events;if(s)for(n in delete a.handle,a.events={},s)for(r=0,i=s[n].length;r1&&"string"==typeof m&&!f.checkClone&&Ee.test(m))return e.each(function(i){var o=e.eq(i);v&&(t[0]=m.call(this,i,o.html())),qe(o,t,n,r)});if(h&&(i=(c=ce(t,e[0].ownerDocument,!1,e,r)).firstChild,1===c.childNodes.length&&(c=i),i||r)){for(s=(u=d.map(oe(c,"script"),De)).length;p")},clone:function(e,t,n){var r,i,o,a,s,u=d.contains(e.ownerDocument,e);if(f.html5Clone||d.isXMLDoc(e)||!we.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Se.innerHTML=e.outerHTML,Se.removeChild(o=Se.firstChild)),!(f.noCloneEvent&&f.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||d.isXMLDoc(e)))for(r=oe(o),s=oe(e),a=0;null!=(i=s[a]);++a)r[a]&&He(i,r[a]);if(t)if(n)for(s=s||oe(e),r=r||oe(o),a=0;null!=(i=s[a]);a++)Le(i,r[a]);else Le(e,o);return(r=oe(o,"script")).length>0&&ae(r,!u&&oe(e,"script")),r=s=i=null,o},cleanData:function(e,t){for(var r,i,o,a,s=0,u=d.expando,l=d.cache,c=f.attributes,p=d.event.special;null!=(r=e[s]);s++)if((t||M(r))&&(a=(o=r[u])&&l[o])){if(a.events)for(i in a.events)p[i]?d.event.remove(r,i):d.removeEvent(r,i,a.handle);l[o]&&(delete l[o],c||void 0===r.removeAttribute?r[u]=void 0:r.removeAttribute(u),n.push(o))}}}),d.fn.extend({domManip:qe,detach:function(e){return _e(this,e,!0)},remove:function(e){return _e(this,e)},text:function(e){return Q(this,function(e){return void 0===e?d.text(this):this.empty().append((this[0]&&this[0].ownerDocument||r).createTextNode(e))},null,e,arguments.length)},append:function(){return qe(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Ae(this,e).appendChild(e)})},prepend:function(){return qe(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Ae(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return qe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return qe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&d.cleanData(oe(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&d.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return d.clone(this,e,t)})},html:function(e){return Q(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(be,""):void 0;if("string"==typeof e&&!Ce.test(e)&&(f.htmlSerialize||!we.test(e))&&(f.leadingWhitespace||!te.test(e))&&!ie[(Z.exec(e)||["",""])[1].toLowerCase()]){e=d.htmlPrefilter(e);try{for(;n")).appendTo(t.documentElement))[0].contentWindow||Fe[0].contentDocument).document).write(),t.close(),n=Oe(e,t),Fe.detach()),Me[e]=n),n}var Pe=/^margin/,Be=new RegExp("^("+$+")(?!px)[a-z%]+$","i"),We=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];for(o in i=n.apply(e,r||[]),t)e.style[o]=a[o];return i},Ie=r.documentElement;!function(){var t,n,i,o,a,s,u=r.createElement("div"),l=r.createElement("div");function c(){var c,f,d=r.documentElement;d.appendChild(u),l.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",t=i=s=!1,n=a=!0,e.getComputedStyle&&(f=e.getComputedStyle(l),t="1%"!==(f||{}).top,s="2px"===(f||{}).marginLeft,i="4px"===(f||{width:"4px"}).width,l.style.marginRight="50%",n="4px"===(f||{marginRight:"4px"}).marginRight,(c=l.appendChild(r.createElement("div"))).style.cssText=l.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",l.style.width="1px",a=!parseFloat((e.getComputedStyle(c)||{}).marginRight),l.removeChild(c)),l.style.display="none",(o=0===l.getClientRects().length)&&(l.style.display="",l.innerHTML="
t
",l.childNodes[0].style.borderCollapse="separate",(c=l.getElementsByTagName("td"))[0].style.cssText="margin:0;border:0;padding:0;display:none",(o=0===c[0].offsetHeight)&&(c[0].style.display="",c[1].style.display="none",o=0===c[0].offsetHeight)),d.removeChild(u)}l.style&&(l.style.cssText="float:left;opacity:.5",f.opacity="0.5"===l.style.opacity,f.cssFloat=!!l.style.cssFloat,l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",f.clearCloneStyle="content-box"===l.style.backgroundClip,(u=r.createElement("div")).style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",l.innerHTML="",u.appendChild(l),f.boxSizing=""===l.style.boxSizing||""===l.style.MozBoxSizing||""===l.style.WebkitBoxSizing,d.extend(f,{reliableHiddenOffsets:function(){return null==t&&c(),o},boxSizingReliable:function(){return null==t&&c(),i},pixelMarginRight:function(){return null==t&&c(),n},pixelPosition:function(){return null==t&&c(),t},reliableMarginRight:function(){return null==t&&c(),a},reliableMarginLeft:function(){return null==t&&c(),s}}))}();var $e,ze,Xe=/^(top|right|bottom|left)$/;function Ue(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}e.getComputedStyle?($e=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},ze=function(e,t,n){var r,i,o,a,s=e.style;return""!==(a=(n=n||$e(e))?n.getPropertyValue(t)||n[t]:void 0)&&void 0!==a||d.contains(e.ownerDocument,e)||(a=d.style(e,t)),n&&!f.pixelMarginRight()&&Be.test(a)&&Pe.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o),void 0===a?a:a+""}):Ie.currentStyle&&($e=function(e){return e.currentStyle},ze=function(e,t,n){var r,i,o,a,s=e.style;return null==(a=(n=n||$e(e))?n[t]:void 0)&&s&&s[t]&&(a=s[t]),Be.test(a)&&!Xe.test(t)&&(r=s.left,(o=(i=e.runtimeStyle)&&i.left)&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"});var Ve=/alpha\([^)]*\)/i,Ye=/opacity\s*=\s*([^)]*)/i,Je=/^(none|table(?!-c[ea]).+)/,Ge=new RegExp("^("+$+")(.*)$","i"),Qe={position:"absolute",visibility:"hidden",display:"block"},Ke={letterSpacing:"0",fontWeight:"400"},Ze=["Webkit","O","Moz","ms"],et=r.createElement("div").style;function tt(e){if(e in et)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=Ze.length;n--;)if((e=Ze[n]+t)in et)return e}function nt(e,t){for(var n,r,i,o=[],a=0,s=e.length;a=1||""===t)&&""===d.trim(o.replace(Ve,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=Ve.test(o)?o.replace(Ve,i):o+" "+i)}}),d.cssHooks.marginRight=Ue(f.reliableMarginRight,function(e,t){if(t)return We(e,{display:"inline-block"},ze,[e,"marginRight"])}),d.cssHooks.marginLeft=Ue(f.reliableMarginLeft,function(e,t){if(t)return(parseFloat(ze(e,"marginLeft"))||(d.contains(e.ownerDocument,e)?e.getBoundingClientRect().left-We(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}):0))+"px"}),d.each({margin:"",padding:"",border:"Width"},function(e,t){d.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+X[r]+t]=o[r]||o[r-2]||o[0];return i}},Pe.test(e)||(d.cssHooks[e+t].set=rt)}),d.fn.extend({css:function(e,t){return Q(this,function(e,t,n){var r,i,o={},a=0;if(d.isArray(t)){for(r=$e(e),i=t.length;a1)},show:function(){return nt(this,!0)},hide:function(){return nt(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){U(this)?d(this).show():d(this).hide()})}}),d.Tween=at,at.prototype={constructor:at,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||d.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(d.cssNumber[n]?"":"px")},cur:function(){var e=at.propHooks[this.prop];return e&&e.get?e.get(this):at.propHooks._default.get(this)},run:function(e){var t,n=at.propHooks[this.prop];return this.options.duration?this.pos=t=d.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):at.propHooks._default.set(this),this}},at.prototype.init.prototype=at.prototype,at.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=d.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){d.fx.step[e.prop]?d.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[d.cssProps[e.prop]]&&!d.cssHooks[e.prop]?e.elem[e.prop]=e.now:d.style(e.elem,e.prop,e.now+e.unit)}}},at.propHooks.scrollTop=at.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},d.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},d.fx=at.prototype.init,d.fx.step={};var st,ut,lt=/^(?:toggle|show|hide)$/,ct=/queueHooks$/;function ft(){return e.setTimeout(function(){st=void 0}),st=d.now()}function dt(e,t){var n,r={height:e},i=0;for(t=t?1:0;i<4;i+=2-t)r["margin"+(n=X[i])]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function pt(e,t,n){for(var r,i=(ht.tweeners[t]||[]).concat(ht.tweeners["*"]),o=0,a=i.length;o
a",e=n.getElementsByTagName("a")[0],t.setAttribute("type","checkbox"),n.appendChild(t),(e=n.getElementsByTagName("a")[0]).style.cssText="top:1px",f.getSetAttribute="t"!==n.className,f.style=/top/.test(e.getAttribute("style")),f.hrefNormalized="/a"===e.getAttribute("href"),f.checkOn=!!t.value,f.optSelected=o.selected,f.enctype=!!r.createElement("form").enctype,i.disabled=!0,f.optDisabled=!o.disabled,(t=r.createElement("input")).setAttribute("value",""),f.input=""===t.getAttribute("value"),t.value="t",t.setAttribute("type","radio"),f.radioValue="t"===t.value}();var gt=/\r/g,mt=/[\x20\t\r\n\f]+/g;d.fn.extend({val:function(e){var t,n,r,i=this[0];return arguments.length?(r=d.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,d(this).val()):e)?i="":"number"==typeof i?i+="":d.isArray(i)&&(i=d.map(i,function(e){return null==e?"":e+""})),(t=d.valHooks[this.type]||d.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))})):i?(t=d.valHooks[i.type]||d.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(gt,""):null==n?"":n:void 0}}),d.extend({valHooks:{option:{get:function(e){var t=d.find.attr(e,"value");return null!=t?t:d.trim(d.text(e)).replace(mt," ")}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||i<0,a=o?null:[],s=o?i+1:r.length,u=i<0?s:o?i:0;u-1)try{r.selected=n=!0}catch(e){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),d.each(["radio","checkbox"],function(){d.valHooks[this]={set:function(e,t){if(d.isArray(t))return e.checked=d.inArray(d(e).val(),t)>-1}},f.checkOn||(d.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var vt,yt,xt=d.expr.attrHandle,bt=/^(?:checked|selected)$/i,wt=f.getSetAttribute,Tt=f.input;d.fn.extend({attr:function(e,t){return Q(this,d.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){d.removeAttr(this,e)})}}),d.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===e.getAttribute?d.prop(e,t,n):(1===o&&d.isXMLDoc(e)||(t=t.toLowerCase(),i=d.attrHooks[t]||(d.expr.match.bool.test(t)?yt:vt)),void 0!==n?null===n?void d.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=d.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!f.radioValue&&"radio"===t&&d.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(H);if(o&&1===e.nodeType)for(;n=o[i++];)r=d.propFix[n]||n,d.expr.match.bool.test(n)?Tt&&wt||!bt.test(n)?e[r]=!1:e[d.camelCase("default-"+n)]=e[r]=!1:d.attr(e,n,""),e.removeAttribute(wt?n:r)}}),yt={set:function(e,t,n){return!1===t?d.removeAttr(e,n):Tt&&wt||!bt.test(n)?e.setAttribute(!wt&&d.propFix[n]||n,n):e[d.camelCase("default-"+n)]=e[n]=!0,n}},d.each(d.expr.match.bool.source.match(/\w+/g),function(e,t){var n=xt[t]||d.find.attr;Tt&&wt||!bt.test(t)?xt[t]=function(e,t,r){var i,o;return r||(o=xt[t],xt[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,xt[t]=o),i}:xt[t]=function(e,t,n){if(!n)return e[d.camelCase("default-"+t)]?t.toLowerCase():null}}),Tt&&wt||(d.attrHooks.value={set:function(e,t,n){if(!d.nodeName(e,"input"))return vt&&vt.set(e,t,n);e.defaultValue=t}}),wt||(vt={set:function(e,t,n){var r=e.getAttributeNode(n);if(r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n))return t}},xt.id=xt.name=xt.coords=function(e,t,n){var r;if(!n)return(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},d.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);if(n&&n.specified)return n.value},set:vt.set},d.attrHooks.contenteditable={set:function(e,t,n){vt.set(e,""!==t&&t,n)}},d.each(["width","height"],function(e,t){d.attrHooks[t]={set:function(e,n){if(""===n)return e.setAttribute(t,"auto"),n}}})),f.style||(d.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var Ct=/^(?:input|select|textarea|button|object)$/i,Et=/^(?:a|area)$/i;d.fn.extend({prop:function(e,t){return Q(this,d.prop,e,t,arguments.length>1)},removeProp:function(e){return e=d.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(e){}})}}),d.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&d.isXMLDoc(e)||(t=d.propFix[t]||t,i=d.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=d.find.attr(e,"tabindex");return t?parseInt(t,10):Ct.test(e.nodeName)||Et.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),f.hrefNormalized||d.each(["href","src"],function(e,t){d.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),f.optSelected||(d.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),d.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){d.propFix[this.toLowerCase()]=this}),f.enctype||(d.propFix.enctype="encoding");var Nt=/[\t\r\n\f]/g;function kt(e){return d.attr(e,"class")||""}d.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(d.isFunction(e))return this.each(function(t){d(this).addClass(e.call(this,t,kt(this)))});if("string"==typeof e&&e)for(t=e.match(H)||[];n=this[u++];)if(i=kt(n),r=1===n.nodeType&&(" "+i+" ").replace(Nt," ")){for(a=0;o=t[a++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=d.trim(r))&&d.attr(n,"class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(d.isFunction(e))return this.each(function(t){d(this).removeClass(e.call(this,t,kt(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof e&&e)for(t=e.match(H)||[];n=this[u++];)if(i=kt(n),r=1===n.nodeType&&(" "+i+" ").replace(Nt," ")){for(a=0;o=t[a++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");i!==(s=d.trim(r))&&d.attr(n,"class",s)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):d.isFunction(e)?this.each(function(n){d(this).toggleClass(e.call(this,n,kt(this),t),t)}):this.each(function(){var t,r,i,o;if("string"===n)for(r=0,i=d(this),o=e.match(H)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else void 0!==e&&"boolean"!==n||((t=kt(this))&&d._data(this,"__className__",t),d.attr(this,"class",t||!1===e?"":d._data(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+kt(n)+" ").replace(Nt," ").indexOf(t)>-1)return!0;return!1}}),d.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){d.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),d.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}});var St=e.location,At=d.now(),Dt=/\?/,jt=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;d.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=d.trim(t+"");return i&&!d.trim(i.replace(jt,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():d.error("Invalid JSON: "+t)},d.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{e.DOMParser?n=(new e.DOMParser).parseFromString(t,"text/xml"):((n=new e.ActiveXObject("Microsoft.XMLDOM")).async="false",n.loadXML(t))}catch(e){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||d.error("Invalid XML: "+t),n};var Lt=/#.*$/,Ht=/([?&])_=[^&]*/,qt=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,_t=/^(?:GET|HEAD)$/,Ft=/^\/\//,Mt=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Ot={},Rt={},Pt="*/".concat("*"),Bt=St.href,Wt=Mt.exec(Bt.toLowerCase())||[];function It(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(H)||[];if(d.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function $t(e,t,n,r){var i={},o=e===Rt;function a(s){var u;return i[s]=!0,d.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=d.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&d.extend(!0,e,n),e}function Xt(e){return e.style&&e.style.display||d.css(e,"display")}d.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Bt,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Wt[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Pt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":d.parseJSON,"text xml":d.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,d.ajaxSettings),t):zt(d.ajaxSettings,e)},ajaxPrefilter:It(Ot),ajaxTransport:It(Rt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var r,i,o,a,s,u,l,c,f=d.ajaxSetup({},n),p=f.context||f,h=f.context&&(p.nodeType||p.jquery)?d(p):d.event,g=d.Deferred(),m=d.Callbacks("once memory"),v=f.statusCode||{},y={},x={},b=0,w="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=qt.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=x[n]=x[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(f.mimeType=e),this},statusCode:function(e){var t;if(e)if(b<2)for(t in e)v[t]=[v[t],e[t]];else T.always(e[T.status]);return this},abort:function(e){var t=e||w;return l&&l.abort(t),C(0,t),this}};if(g.promise(T).complete=m.add,T.success=T.done,T.error=T.fail,f.url=((t||f.url||Bt)+"").replace(Lt,"").replace(Ft,Wt[1]+"//"),f.type=n.method||n.type||f.method||f.type,f.dataTypes=d.trim(f.dataType||"*").toLowerCase().match(H)||[""],null==f.crossDomain&&(r=Mt.exec(f.url.toLowerCase()),f.crossDomain=!(!r||r[1]===Wt[1]&&r[2]===Wt[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(Wt[3]||("http:"===Wt[1]?"80":"443")))),f.data&&f.processData&&"string"!=typeof f.data&&(f.data=d.param(f.data,f.traditional)),$t(Ot,f,n,T),2===b)return T;for(i in(u=d.event&&f.global)&&0==d.active++&&d.event.trigger("ajaxStart"),f.type=f.type.toUpperCase(),f.hasContent=!_t.test(f.type),o=f.url,f.hasContent||(f.data&&(o=f.url+=(Dt.test(o)?"&":"?")+f.data,delete f.data),!1===f.cache&&(f.url=Ht.test(o)?o.replace(Ht,"$1_="+At++):o+(Dt.test(o)?"&":"?")+"_="+At++)),f.ifModified&&(d.lastModified[o]&&T.setRequestHeader("If-Modified-Since",d.lastModified[o]),d.etag[o]&&T.setRequestHeader("If-None-Match",d.etag[o])),(f.data&&f.hasContent&&!1!==f.contentType||n.contentType)&&T.setRequestHeader("Content-Type",f.contentType),T.setRequestHeader("Accept",f.dataTypes[0]&&f.accepts[f.dataTypes[0]]?f.accepts[f.dataTypes[0]]+("*"!==f.dataTypes[0]?", "+Pt+"; q=0.01":""):f.accepts["*"]),f.headers)T.setRequestHeader(i,f.headers[i]);if(f.beforeSend&&(!1===f.beforeSend.call(p,T,f)||2===b))return T.abort();for(i in w="abort",{success:1,error:1,complete:1})T[i](f[i]);if(l=$t(Rt,f,n,T)){if(T.readyState=1,u&&h.trigger("ajaxSend",[T,f]),2===b)return T;f.async&&f.timeout>0&&(s=e.setTimeout(function(){T.abort("timeout")},f.timeout));try{b=1,l.send(y,C)}catch(e){if(!(b<2))throw e;C(-1,e)}}else C(-1,"No Transport");function C(t,n,r,i){var c,y,x,w,C,E=n;2!==b&&(b=2,s&&e.clearTimeout(s),l=void 0,a=i||"",T.readyState=t>0?4:0,c=t>=200&&t<300||304===t,r&&(w=function(e,t,n){for(var r,i,o,a,s=e.contents,u=e.dataTypes;"*"===u[0];)u.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){u.unshift(a);break}if(u[0]in n)o=u[0];else{for(a in n){if(!u[0]||e.converters[a+" "+u[0]]){o=a;break}r||(r=a)}o=o||r}if(o)return o!==u[0]&&u.unshift(o),n[o]}(f,T,r)),w=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e.throws)t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(f,w,T,c),c?(f.ifModified&&((C=T.getResponseHeader("Last-Modified"))&&(d.lastModified[o]=C),(C=T.getResponseHeader("etag"))&&(d.etag[o]=C)),204===t||"HEAD"===f.type?E="nocontent":304===t?E="notmodified":(E=w.state,y=w.data,c=!(x=w.error))):(x=E,!t&&E||(E="error",t<0&&(t=0))),T.status=t,T.statusText=(n||E)+"",c?g.resolveWith(p,[y,E,T]):g.rejectWith(p,[T,E,x]),T.statusCode(v),v=void 0,u&&h.trigger(c?"ajaxSuccess":"ajaxError",[T,f,c?y:x]),m.fireWith(p,[T,E]),u&&(h.trigger("ajaxComplete",[T,f]),--d.active||d.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return d.get(e,t,n,"json")},getScript:function(e,t){return d.get(e,void 0,t,"script")}}),d.each(["get","post"],function(e,t){d[t]=function(e,n,r,i){return d.isFunction(n)&&(i=i||r,r=n,n=void 0),d.ajax(d.extend({url:e,type:t,dataType:i,data:n,success:r},d.isPlainObject(e)&&e))}}),d._evalUrl=function(e){return d.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},d.fn.extend({wrapAll:function(e){if(d.isFunction(e))return this.each(function(t){d(this).wrapAll(e.call(this,t))});if(this[0]){var t=d(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return d.isFunction(e)?this.each(function(t){d(this).wrapInner(e.call(this,t))}):this.each(function(){var t=d(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=d.isFunction(e);return this.each(function(n){d(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){d.nodeName(this,"body")||d(this).replaceWith(this.childNodes)}).end()}}),d.expr.filters.hidden=function(e){return f.reliableHiddenOffsets()?e.offsetWidth<=0&&e.offsetHeight<=0&&!e.getClientRects().length:function(e){if(!d.contains(e.ownerDocument||r,e))return!0;for(;e&&1===e.nodeType;){if("none"===Xt(e)||"hidden"===e.type)return!0;e=e.parentNode}return!1}(e)},d.expr.filters.visible=function(e){return!d.expr.filters.hidden(e)};var Ut=/%20/g,Vt=/\[\]$/,Yt=/\r?\n/g,Jt=/^(?:submit|button|image|reset|file)$/i,Gt=/^(?:input|select|textarea|keygen)/i;function Qt(e,t,n,r){var i;if(d.isArray(t))d.each(t,function(t,i){n||Vt.test(e)?r(e,i):Qt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==d.type(t))r(e,t);else for(i in t)Qt(e+"["+i+"]",t[i],n,r)}d.param=function(e,t){var n,r=[],i=function(e,t){t=d.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=d.ajaxSettings&&d.ajaxSettings.traditional),d.isArray(e)||e.jquery&&!d.isPlainObject(e))d.each(e,function(){i(this.name,this.value)});else for(n in e)Qt(n,e[n],t,i);return r.join("&").replace(Ut,"+")},d.fn.extend({serialize:function(){return d.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=d.prop(this,"elements");return e?d.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!d(this).is(":disabled")&&Gt.test(this.nodeName)&&!Jt.test(e)&&(this.checked||!K.test(e))}).map(function(e,t){var n=d(this).val();return null==n?null:d.isArray(n)?d.map(n,function(e){return{name:t.name,value:e.replace(Yt,"\r\n")}}):{name:t.name,value:n.replace(Yt,"\r\n")}}).get()}}),d.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return this.isLocal?nn():r.documentMode>8?tn():/^(get|post|head|put|delete|options)$/i.test(this.type)&&tn()||nn()}:tn;var Kt=0,Zt={},en=d.ajaxSettings.xhr();function tn(){try{return new e.XMLHttpRequest}catch(e){}}function nn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(e){}}e.attachEvent&&e.attachEvent("onunload",function(){for(var e in Zt)Zt[e](void 0,!0)}),f.cors=!!en&&"withCredentials"in en,(en=f.ajax=!!en)&&d.ajaxTransport(function(t){var n;if(!t.crossDomain||f.cors)return{send:function(r,i){var o,a=t.xhr(),s=++Kt;if(a.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(o in t.xhrFields)a[o]=t.xhrFields[o];for(o in t.mimeType&&a.overrideMimeType&&a.overrideMimeType(t.mimeType),t.crossDomain||r["X-Requested-With"]||(r["X-Requested-With"]="XMLHttpRequest"),r)void 0!==r[o]&&a.setRequestHeader(o,r[o]+"");a.send(t.hasContent&&t.data||null),n=function(e,r){var o,u,l;if(n&&(r||4===a.readyState))if(delete Zt[s],n=void 0,a.onreadystatechange=d.noop,r)4!==a.readyState&&a.abort();else{l={},o=a.status,"string"==typeof a.responseText&&(l.text=a.responseText);try{u=a.statusText}catch(e){u=""}o||!t.isLocal||t.crossDomain?1223===o&&(o=204):o=l.text?200:404}l&&i(o,u,l,a.getAllResponseHeaders())},t.async?4===a.readyState?e.setTimeout(n):a.onreadystatechange=Zt[s]=n:n()},abort:function(){n&&n(void 0,!0)}}}),d.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return d.globalEval(e),e}}}),d.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),d.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=r.head||d("head")[0]||r.documentElement;return{send:function(i,o){(t=r.createElement("script")).async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||o(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var rn=[],on=/(=)\?(?=&|$)|\?\?/;d.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=rn.pop()||d.expando+"_"+At++;return this[e]=!0,e}}),d.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=!1!==t.jsonp&&(on.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&on.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return i=t.jsonpCallback=d.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(on,"$1"+i):!1!==t.jsonp&&(t.url+=(Dt.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||d.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){void 0===o?d(e).removeProp(i):e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,rn.push(i)),a&&d.isFunction(o)&&o(a[0]),a=o=void 0}),"script"}),d.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||r;var i=T.exec(e),o=!n&&[];return i?[t.createElement(i[1])]:(i=ce([e],t,o),o&&o.length&&d(o).remove(),d.merge([],i.childNodes))};var an=d.fn.load;function sn(e){return d.isWindow(e)?e:9===e.nodeType&&(e.defaultView||e.parentWindow)}d.fn.load=function(e,t,n){if("string"!=typeof e&&an)return an.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=d.trim(e.slice(s,e.length)),e=e.slice(0,s)),d.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),a.length>0&&d.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?d("
").append(d.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},d.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){d.fn[t]=function(e){return this.on(t,e)}}),d.expr.filters.animated=function(e){return d.grep(d.timers,function(t){return e===t.elem}).length},d.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=d.css(e,"position"),c=d(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=d.css(e,"top"),u=d.css(e,"left"),("absolute"===l||"fixed"===l)&&d.inArray("auto",[o,u])>-1?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),d.isFunction(t)&&(t=t.call(e,n,d.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},d.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){d.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;return o?(t=o.documentElement,d.contains(t,i)?(void 0!==i.getBoundingClientRect&&(r=i.getBoundingClientRect()),n=sn(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r):void 0},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===d.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),d.nodeName(e[0],"html")||(n=e.offset()),n.top+=d.css(e[0],"borderTopWidth",!0),n.left+=d.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-d.css(r,"marginTop",!0),left:t.left-n.left-d.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&!d.nodeName(e,"html")&&"static"===d.css(e,"position");)e=e.offsetParent;return e||Ie})}}),d.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);d.fn[e]=function(r){return Q(this,function(e,r,i){var o=sn(e);if(void 0===i)return o?t in o?o[t]:o.document.documentElement[r]:e[r];o?o.scrollTo(n?d(o).scrollLeft():i,n?i:d(o).scrollTop()):e[r]=i},e,r,arguments.length,null)}}),d.each(["top","left"],function(e,t){d.cssHooks[t]=Ue(f.pixelPosition,function(e,n){if(n)return n=ze(e,t),Be.test(n)?d(e).position()[t]+"px":n})}),d.each({Height:"height",Width:"width"},function(e,t){d.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){d.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(!0===r||!0===i?"margin":"border");return Q(this,function(t,n,r){var i;return d.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?d.css(t,n,a):d.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),d.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),d.fn.size=function(){return this.length},d.fn.andSelf=d.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return d});var un=e.jQuery,ln=e.$;return d.noConflict=function(t){return e.$===d&&(e.$=ln),t&&e.jQuery===d&&(e.jQuery=un),d},t||(e.jQuery=e.$=d),d}); +/* The jQuery version has been modified to prevent warnings in the stores. This version has been patched to fix some security issues. */ +!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){var n=[],r=e.document,i=n.slice,o=n.concat,a=n.push,s=n.indexOf,u={},l=u.toString,c=u.hasOwnProperty,f={},d=function(e,t){return new d.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,h=/^-ms-/,g=/-([\da-z])/gi,m=function(e,t){return t.toUpperCase()};function v(e){var t=!!e&&"length"in e&&e.length,n=d.type(e);return"function"!==n&&!d.isWindow(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}d.fn=d.prototype={jquery:"123.4.5",constructor:d,selector:"",length:0,toArray:function(){return i.call(this)},get:function(e){return null!=e?e<0?this[e+this.length]:this[e]:i.call(this)},pushStack:function(e){var t=d.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e){return d.each(this,e)},map:function(e){return this.pushStack(d.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==d.type(e)||e.nodeType||d.isWindow(e))return!1;try{if(e.constructor&&!c.call(e,"constructor")&&!c.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(e){return!1}if(!f.ownFirst)for(t in e)return c.call(e,t);for(t in e);return void 0===t||c.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?u[l.call(e)]||"object":typeof e},globalEval:function(t){t&&d.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(h,"ms-").replace(g,m)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t){var n,r=0;if(v(e))for(n=e.length;r+~]|"+O+")"+O+"*"),X=new RegExp("="+O+"*([^\\]'\"]*?)"+O+"*\\]","g"),U=new RegExp(B),V=new RegExp("^"+R+"$"),Y={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+B),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),bool:new RegExp("^(?:"+M+")$","i"),needsContext:new RegExp("^"+O+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)","i")},J=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/[+~]/,ee=/'|\\/g,te=new RegExp("\\\\([\\da-f]{1,6}"+O+"?|("+O+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=function(){d()};try{q.apply(j=_.call(w.childNodes),w.childNodes),j[w.childNodes.length].nodeType}catch(e){q={apply:j.length?function(e,t){H.apply(e,_.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function ie(e,t,r,i){var o,s,l,c,f,h,v,y,T=t&&t.ownerDocument,C=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==C&&9!==C&&11!==C)return r;if(!i&&((t?t.ownerDocument||t:w)!==p&&d(t),t=t||p,g)){if(11!==C&&(h=K.exec(e)))if(o=h[1]){if(9===C){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(T&&(l=T.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(h[2])return q.apply(r,t.getElementsByTagName(e)),r;if((o=h[3])&&n.getElementsByClassName&&t.getElementsByClassName)return q.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!k[e+" "]&&(!m||!m.test(e))){if(1!==C)T=t,y=e;else if("object"!==t.nodeName.toLowerCase()){for((c=t.getAttribute("id"))?c=c.replace(ee,"\\$&"):t.setAttribute("id",c=b),s=(v=a(e)).length,f=V.test(c)?"#"+c:"[id='"+c+"']";s--;)v[s]=f+" "+ge(v[s]);y=v.join(","),T=Z.test(e)&&pe(t.parentNode)||t}if(y)try{return q.apply(r,T.querySelectorAll(y)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(I,"$1"),t,r,i)}function oe(){var e=[];return function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}}function ae(e){return e[b]=!0,e}function se(e){var t=p.createElement("div");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ue(e,t){for(var n=e.split("|"),i=n.length;i--;)r.attrHandle[n[i]]=t}function le(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||A)-(~e.sourceIndex||A);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function ce(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function fe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return ae(function(t){return t=+t,ae(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function pe(e){return e&&void 0!==e.getElementsByTagName&&e}for(t in n=ie.support={},o=ie.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},d=ie.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==p&&9===a.nodeType&&a.documentElement?(h=(p=a).documentElement,g=!o(p),(i=p.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=se(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=se(function(e){return e.appendChild(p.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(p.getElementsByClassName),n.getById=se(function(e){return h.appendChild(e).id=b,!p.getElementsByName||!p.getElementsByName(b).length}),n.getById?(r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}},r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}}):(delete r.find.ID,r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}}),r.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],m=[],(n.qsa=Q.test(p.querySelectorAll))&&(se(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+O+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+O+"*(?:value|"+M+")"),e.querySelectorAll("[id~="+b+"-]").length||m.push("~="),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||m.push(".#.+[+~]")}),se(function(e){var t=p.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+O+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(n.matchesSelector=Q.test(y=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&se(function(e){n.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),v.push("!=",B)}),m=m.length&&new RegExp(m.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},S=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===p||e.ownerDocument===w&&x(w,e)?-1:t===p||t.ownerDocument===w&&x(w,t)?1:c?F(c,e)-F(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===p?-1:t===p?1:i?-1:o?1:c?F(c,e)-F(c,t):0;if(i===o)return le(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?le(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},p):p},ie.matches=function(e,t){return ie(e,null,null,t)},ie.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&d(e),t=t.replace(X,"='$1']"),n.matchesSelector&&g&&!k[t+" "]&&(!v||!v.test(t))&&(!m||!m.test(t)))try{var r=y.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return ie(t,p,null,[e]).length>0},ie.contains=function(e,t){return(e.ownerDocument||e)!==p&&d(e),x(e,t)},ie.attr=function(e,t){(e.ownerDocument||e)!==p&&d(e);var i=r.attrHandle[t.toLowerCase()],o=i&&D.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},ie.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},ie.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(S),f){for(;t=e[o++];)t===e[o]&&(i=r.push(o));for(;i--;)e.splice(r[i],1)}return c=null,e},i=ie.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r++];)n+=i(t);return n},(r=ie.selectors={cacheLength:50,createPseudo:ae,match:Y,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ie.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ie.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&U.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ie.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(W," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,d,p,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!u&&!s,x=!1;if(m){if(o){for(;g;){for(d=t;d=d[g];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&y){for(x=(p=(l=(c=(f=(d=m)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],d=p&&m.childNodes[p];d=++p&&d&&d[g]||(x=p=0)||h.pop();)if(1===d.nodeType&&++x&&d===t){c[e]=[T,p,x];break}}else if(y&&(x=p=(l=(c=(f=(d=t)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)for(;(d=++p&&d&&d[g]||(x=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++x||(y&&((c=(f=d[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]=[T,x]),d!==t)););return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||ie.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?ae(function(e,n){for(var r,o=i(e,t),a=o.length;a--;)e[r=F(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:ae(function(e){var t=[],n=[],r=s(e.replace(I,"$1"));return r[b]?ae(function(e,t,n,i){for(var o,a=r(e,null,i,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:ae(function(e){return function(t){return ie(e,t).length>0}}),contains:ae(function(e){return e=e.replace(te,ne),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:ae(function(e){return V.test(e||"")||ie.error("unsupported lang: "+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return!1===e.disabled},disabled:function(e){return!0===e.disabled},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return G.test(e.nodeName)},input:function(e){return J.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:de(function(){return[0]}),last:de(function(e,t){return[t-1]}),eq:de(function(e,t,n){return[n<0?n+t:n]}),even:de(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:de(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function ye(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s-1&&(o[l]=!(a[l]=f))}}else v=ye(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):q.apply(a,v)})}function be(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return F(t,e)>-1},s,!0),d=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&ve(d),u>1&&ge(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(I,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,m,v=0,y="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,N=C.length;for(c&&(l=a===p||a||c);y!==N&&null!=(f=C[y]);y++){if(i&&f){for(h=0,a||f.ownerDocument===p||(d(f),s=!g);m=e[h++];)if(m(f,a||p,s)){u.push(f);break}c&&(T=E)}n&&((f=!m&&f)&&v--,o&&x.push(f))}if(v+=y,n&&y!==v){for(h=0;m=t[h++];)m(x,b,a,s);if(o){if(v>0)for(;y--;)x[y]||b[y]||(b[y]=L.call(u));b=ye(b)}q.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&ie.uniqueSort(u)}return c&&(T=E,l=w),x};return n?ae(o):o}(o,i))).selector=e}return s},u=ie.select=function(e,t,i,o){var u,l,c,f,d,p="function"==typeof e&&e,h=!o&&a(e=p.selector||e);if(i=i||[],1===h.length){if((l=h[0]=h[0].slice(0)).length>2&&"ID"===(c=l[0]).type&&n.getById&&9===t.nodeType&&g&&r.relative[l[1].type]){if(!(t=(r.find.ID(c.matches[0].replace(te,ne),t)||[])[0]))return i;p&&(t=t.parentNode),e=e.slice(l.shift().value.length)}for(u=Y.needsContext.test(e)?0:l.length;u--&&(c=l[u],!r.relative[f=c.type]);)if((d=r.find[f])&&(o=d(c.matches[0].replace(te,ne),Z.test(l[0].type)&&pe(t.parentNode)||t))){if(l.splice(u,1),!(e=o.length&&ge(l)))return q.apply(i,o),i;break}}return(p||s(e,h))(o,t,!g,i,!t||Z.test(e)&&pe(t.parentNode)||t),i},n.sortStable=b.split("").sort(S).join("")===b,n.detectDuplicates=!!f,d(),n.sortDetached=se(function(e){return 1&e.compareDocumentPosition(p.createElement("div"))}),se(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ue("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&se(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ue("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),se(function(e){return null==e.getAttribute("disabled")})||ue(M,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),ie}(e);d.find=y,d.expr=y.selectors,d.expr[":"]=d.expr.pseudos,d.uniqueSort=d.unique=y.uniqueSort,d.text=y.getText,d.isXMLDoc=y.isXML,d.contains=y.contains;var x=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&d(e).is(n))break;r.push(e)}return r},b=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},w=d.expr.match.needsContext,T=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,C=/^.[^:#\[\.,]*$/;function E(e,t,n){if(d.isFunction(t))return d.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return d.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(C.test(t))return d.filter(t,e,n);t=d.filter(t,e)}return d.grep(e,function(e){return d.inArray(e,t)>-1!==n})}d.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?d.find.matchesSelector(r,e)?[r]:[]:d.find.matches(e,d.grep(t,function(e){return 1===e.nodeType}))},d.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(d(e).filter(function(){for(t=0;t1?d.unique(n):n)).selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(E(this,e||[],!1))},not:function(e){return this.pushStack(E(this,e||[],!0))},is:function(e){return!!E(this,"string"==typeof e&&w.test(e)?d(e):e||[],!1).length}});var N,k=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/;(d.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||N,"string"==typeof e){if(!(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:k.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof d?t[0]:t,d.merge(this,d.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),T.test(i[1])&&d.isPlainObject(t))for(i in t)d.isFunction(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}if((o=r.getElementById(i[2]))&&o.parentNode){if(o.id!==i[2])return N.find(e);this.length=1,this[0]=o}return this.context=r,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):d.isFunction(e)?void 0!==n.ready?n.ready(e):e(d):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),d.makeArray(e,this))}).prototype=d.fn,N=d(r);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){do{e=e[t]}while(e&&1!==e.nodeType);return e}d.fn.extend({has:function(e){var t,n=d(e,this),r=n.length;return this.filter(function(){for(t=0;t-1:1===n.nodeType&&d.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?d.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?d.inArray(this[0],d(e)):d.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(d.uniqueSort(d.merge(this.get(),d(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),d.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x(e,"parentNode")},parentsUntil:function(e,t,n){return x(e,"parentNode",n)},next:function(e){return D(e,"nextSibling")},prev:function(e){return D(e,"previousSibling")},nextAll:function(e){return x(e,"nextSibling")},prevAll:function(e){return x(e,"previousSibling")},nextUntil:function(e,t,n){return x(e,"nextSibling",n)},prevUntil:function(e,t,n){return x(e,"previousSibling",n)},siblings:function(e){return b((e.parentNode||{}).firstChild,e)},children:function(e){return b(e.firstChild)},contents:function(e){return d.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:d.merge([],e.childNodes)}},function(e,t){d.fn[e]=function(n,r){var i=d.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=d.filter(r,i)),this.length>1&&(A[e]||(i=d.uniqueSort(i)),S.test(e)&&(i=i.reverse())),this.pushStack(i)}});var j,L,H=/\S+/g;function q(){r.addEventListener?(r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_)):(r.detachEvent("onreadystatechange",_),e.detachEvent("onload",_))}function _(){(r.addEventListener||"load"===e.event.type||"complete"===r.readyState)&&(q(),d.ready())}for(L in d.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return d.each(e.match(H)||[],function(e,n){t[n]=!0}),t}(e):d.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=e.once,r=t=!0;a.length;s=-1)for(n=a.shift();++s-1;)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?d.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=!0,n||l.disable(),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l},d.extend({Deferred:function(e){var t=[["resolve","done",d.Callbacks("once memory"),"resolved"],["reject","fail",d.Callbacks("once memory"),"rejected"],["notify","progress",d.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return d.Deferred(function(n){d.each(t,function(t,o){var a=d.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&d.isFunction(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?d.extend(e,r):r}},i={};return r.pipe=r.then,d.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,o=0,a=i.call(arguments),s=a.length,u=1!==s||e&&d.isFunction(e.promise)?s:0,l=1===u?e:d.Deferred(),c=function(e,n,r){return function(o){n[e]=this,r[e]=arguments.length>1?i.call(arguments):o,r===t?l.notifyWith(n,r):--u||l.resolveWith(n,r)}};if(s>1)for(t=new Array(s),n=new Array(s),r=new Array(s);o0||(j.resolveWith(r,[d]),d.fn.triggerHandler&&(d(r).triggerHandler("ready"),d(r).off("ready"))))}}),d.ready.promise=function(t){if(!j)if(j=d.Deferred(),"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll)e.setTimeout(d.ready);else if(r.addEventListener)r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_);else{r.attachEvent("onreadystatechange",_),e.attachEvent("onload",_);var n=!1;try{n=null==e.frameElement&&r.documentElement}catch(e){}n&&n.doScroll&&function t(){if(!d.isReady){try{n.doScroll("left")}catch(n){return e.setTimeout(t,50)}q(),d.ready()}}()}return j.promise(t)},d.ready.promise(),d(f))break;f.ownFirst="0"===L,f.inlineBlockNeedsLayout=!1,d(function(){var e,t,n,i;(n=r.getElementsByTagName("body")[0])&&n.style&&(t=r.createElement("div"),(i=r.createElement("div")).style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(i).appendChild(t),void 0!==t.style.zoom&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",f.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(i))}),function(){var e=r.createElement("div");f.deleteExpando=!0;try{delete e.test}catch(e){f.deleteExpando=!1}e=null}();var F,M=function(e){var t=d.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return(1===n||9===n)&&(!t||!0!==t&&e.getAttribute("classid")===t)},O=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,R=/([A-Z])/g;function P(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(R,"-$1").toLowerCase();if("string"==typeof(n=e.getAttribute(r))){try{n="true"===n||"false"!==n&&("null"===n?null:+n+""===n?+n:O.test(n)?d.parseJSON(n):n)}catch(e){}d.data(e,t,n)}else n=void 0}return n}function B(e){var t;for(t in e)if(("data"!==t||!d.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function W(e,t,r,i){if(M(e)){var o,a,s=d.expando,u=e.nodeType,l=u?d.cache:e,c=u?e[s]:e[s]&&s;if(c&&l[c]&&(i||l[c].data)||void 0!==r||"string"!=typeof t)return c||(c=u?e[s]=n.pop()||d.guid++:s),l[c]||(l[c]=u?{}:{toJSON:d.noop}),"object"!=typeof t&&"function"!=typeof t||(i?l[c]=d.extend(l[c],t):l[c].data=d.extend(l[c].data,t)),a=l[c],i||(a.data||(a.data={}),a=a.data),void 0!==r&&(a[d.camelCase(t)]=r),"string"==typeof t?null==(o=a[t])&&(o=a[d.camelCase(t)]):o=a,o}}function I(e,t,n){if(M(e)){var r,i,o=e.nodeType,a=o?d.cache:e,s=o?e[d.expando]:d.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){i=(t=d.isArray(t)?t.concat(d.map(t,d.camelCase)):t in r?[t]:(t=d.camelCase(t))in r?[t]:t.split(" ")).length;for(;i--;)delete r[t[i]];if(n?!B(r):!d.isEmptyObject(r))return}(n||(delete a[s].data,B(a[s])))&&(o?d.cleanData([e],!0):f.deleteExpando||a!=a.window?delete a[s]:a[s]=void 0)}}}d.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return!!(e=e.nodeType?d.cache[e[d.expando]]:e[d.expando])&&!B(e)},data:function(e,t,n){return W(e,t,n)},removeData:function(e,t){return I(e,t)},_data:function(e,t,n){return W(e,t,n,!0)},_removeData:function(e,t){return I(e,t,!0)}}),d.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=d.data(o),1===o.nodeType&&!d._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&0===(r=a[n].name).indexOf("data-")&&P(o,r=d.camelCase(r.slice(5)),i[r]);d._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){d.data(this,e)}):arguments.length>1?this.each(function(){d.data(this,e,t)}):o?P(o,e,d.data(o,e)):void 0},removeData:function(e){return this.each(function(){d.removeData(this,e)})}}),d.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=d._data(e,t),n&&(!r||d.isArray(n)?r=d._data(e,t,d.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=d.queue(e,t),r=n.length,i=n.shift(),o=d._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){d.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return d._data(e,n)||d._data(e,n,{empty:d.Callbacks("once memory").add(function(){d._removeData(e,t+"queue"),d._removeData(e,n)})})}}),d.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length
a",f.leadingWhitespace=3===Y.firstChild.nodeType,f.tbody=!Y.getElementsByTagName("tbody").length,f.htmlSerialize=!!Y.getElementsByTagName("link").length,f.html5Clone="<:nav>"!==r.createElement("nav").cloneNode(!0).outerHTML,G.type="checkbox",G.checked=!0,J.appendChild(G),f.appendChecked=G.checked,Y.innerHTML="",f.noCloneChecked=!!Y.cloneNode(!0).lastChild.defaultValue,J.appendChild(Y),(G=r.createElement("input")).setAttribute("type","radio"),G.setAttribute("checked","checked"),G.setAttribute("name","t"),Y.appendChild(G),f.checkClone=Y.cloneNode(!0).cloneNode(!0).lastChild.checked,f.noCloneEvent=!!Y.addEventListener,Y[d.expando]=1,f.attributes=!Y.getAttribute(d.expando);var ie={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:f.htmlSerialize?[0,"",""]:[1,"X
","
"]};function oe(e,t){var n,r,i=0,o=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||d.nodeName(r,t)?o.push(r):d.merge(o,oe(r,t));return void 0===t||t&&d.nodeName(e,t)?d.merge([e],o):o}function ae(e,t){for(var n,r=0;null!=(n=e[r]);r++)d._data(n,"globalEval",!t||d._data(t[r],"globalEval"))}ie.optgroup=ie.option,ie.tbody=ie.tfoot=ie.colgroup=ie.caption=ie.thead,ie.th=ie.td;var se=/<|&#?\w+;/,ue=/"!==p[1]||ue.test(a)?0:u:u.firstChild)&&a.childNodes.length;o--;)d.nodeName(c=a.childNodes[o],"tbody")&&!c.childNodes.length&&a.removeChild(c);for(d.merge(m,u.childNodes),u.textContent="";u.firstChild;)u.removeChild(u.firstChild);u=g.lastChild}else m.push(t.createTextNode(a));for(u&&g.removeChild(u),f.appendChecked||d.grep(oe(m,"input"),le),v=0;a=m[v++];)if(r&&d.inArray(a,r)>-1)i&&i.push(a);else if(s=d.contains(a.ownerDocument,a),u=oe(g.appendChild(a),"script"),s&&ae(u),n)for(o=0;a=u[o++];)ee.test(a.type||"")&&n.push(a);return u=null,g}!function(){var t,n,i=r.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(f[t]=n in e)||(i.setAttribute(n,"t"),f[t]=!1===i.attributes[n].expando);i=null}();var fe=/^(?:input|select|textarea)$/i,de=/^key/,pe=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,he=/^(?:focusinfocus|focusoutblur)$/,ge=/^([^.]*)(?:\.(.+)|)/;function me(){return!0}function ve(){return!1}function ye(){try{return r.activeElement}catch(e){}}function xe(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)xe(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ve;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return d().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=d.guid++)),e.each(function(){d.event.add(this,t,i,r,n)})}d.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,h,g,m,v=d._data(e);if(v){for(n.handler&&(n=(u=n).handler,i=u.selector),n.guid||(n.guid=d.guid++),(a=v.events)||(a=v.events={}),(c=v.handle)||((c=v.handle=function(e){return void 0===d||e&&d.event.triggered===e.type?void 0:d.event.dispatch.apply(c.elem,arguments)}).elem=e),s=(t=(t||"").match(H)||[""]).length;s--;)h=m=(o=ge.exec(t[s])||[])[1],g=(o[2]||"").split(".").sort(),h&&(l=d.event.special[h]||{},h=(i?l.delegateType:l.bindType)||h,l=d.event.special[h]||{},f=d.extend({type:h,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&d.expr.match.needsContext.test(i),namespace:g.join(".")},u),(p=a[h])||((p=a[h]=[]).delegateCount=0,l.setup&&!1!==l.setup.call(e,r,g,c)||(e.addEventListener?e.addEventListener(h,c,!1):e.attachEvent&&e.attachEvent("on"+h,c))),l.add&&(l.add.call(e,f),f.handler.guid||(f.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,f):p.push(f),d.event.global[h]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,h,g,m,v=d.hasData(e)&&d._data(e);if(v&&(c=v.events)){for(l=(t=(t||"").match(H)||[""]).length;l--;)if(h=m=(s=ge.exec(t[l])||[])[1],g=(s[2]||"").split(".").sort(),h){for(f=d.event.special[h]||{},p=c[h=(r?f.delegateType:f.bindType)||h]||[],s=s[2]&&new RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"),u=o=p.length;o--;)a=p[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(p.splice(o,1),a.selector&&p.delegateCount--,f.remove&&f.remove.call(e,a));u&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,g,v.handle)||d.removeEvent(e,h,v.handle),delete c[h])}else for(h in c)d.event.remove(e,h+t[l],n,r,!0);d.isEmptyObject(c)&&(delete v.handle,d._removeData(e,"events"))}},trigger:function(t,n,i,o){var a,s,u,l,f,p,h,g=[i||r],m=c.call(t,"type")?t.type:t,v=c.call(t,"namespace")?t.namespace.split("."):[];if(u=p=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!he.test(m+d.event.triggered)&&(m.indexOf(".")>-1&&(v=m.split("."),m=v.shift(),v.sort()),s=m.indexOf(":")<0&&"on"+m,(t=t[d.expando]?t:new d.Event(m,"object"==typeof t&&t)).isTrigger=o?2:3,t.namespace=v.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+v.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:d.makeArray(n,[t]),f=d.event.special[m]||{},o||!f.trigger||!1!==f.trigger.apply(i,n))){if(!o&&!f.noBubble&&!d.isWindow(i)){for(l=f.delegateType||m,he.test(l+m)||(u=u.parentNode);u;u=u.parentNode)g.push(u),p=u;p===(i.ownerDocument||r)&&g.push(p.defaultView||p.parentWindow||e)}for(h=0;(u=g[h++])&&!t.isPropagationStopped();)t.type=h>1?l:f.bindType||m,(a=(d._data(u,"events")||{})[t.type]&&d._data(u,"handle"))&&a.apply(u,n),(a=s&&u[s])&&a.apply&&M(u)&&(t.result=a.apply(u,n),!1===t.result&&t.preventDefault());if(t.type=m,!o&&!t.isDefaultPrevented()&&(!f._default||!1===f._default.apply(g.pop(),n))&&M(i)&&s&&i[m]&&!d.isWindow(i)){(p=i[s])&&(i[s]=null),d.event.triggered=m;try{i[m]()}catch(e){}d.event.triggered=void 0,p&&(i[s]=p)}return t.result}},dispatch:function(e){e=d.event.fix(e);var t,n,r,o,a,s,u=i.call(arguments),l=(d._data(this,"events")||{})[e.type]||[],c=d.event.special[e.type]||{};if(u[0]=e,e.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,e)){for(s=d.event.handlers.call(this,e,l),t=0;(o=s[t++])&&!e.isPropagationStopped();)for(e.currentTarget=o.elem,n=0;(a=o.handlers[n++])&&!e.isImmediatePropagationStopped();)e.rnamespace&&!e.rnamespace.test(a.namespace)||(e.handleObj=a,e.data=a.data,void 0!==(r=((d.event.special[a.origType]||{}).handle||a.handler).apply(o.elem,u))&&!1===(e.result=r)&&(e.preventDefault(),e.stopPropagation()));return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,u=e.target;if(s&&u.nodeType&&("click"!==e.type||isNaN(e.button)||e.button<1))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(!0!==u.disabled||"click"!==e.type)){for(r=[],n=0;n-1:d.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&a.push({elem:u,handlers:r})}return s]","i"),Te=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,Ce=/\s*$/g,Se=re(r).appendChild(r.createElement("div"));function Ae(e,t){return d.nodeName(e,"table")&&d.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function De(e){return e.type=(null!==d.find.attr(e,"type"))+"/"+e.type,e}function je(e){var t=Ne.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Le(e,t){if(1===t.nodeType&&d.hasData(e)){var n,r,i,o=d._data(e),a=d._data(t,o),s=o.events;if(s)for(n in delete a.handle,a.events={},s)for(r=0,i=s[n].length;r1&&"string"==typeof m&&!f.checkClone&&Ee.test(m))return e.each(function(i){var o=e.eq(i);v&&(t[0]=m.call(this,i,o.html())),qe(o,t,n,r)});if(h&&(i=(c=ce(t,e[0].ownerDocument,!1,e,r)).firstChild,1===c.childNodes.length&&(c=i),i||r)){for(s=(u=d.map(oe(c,"script"),De)).length;p")},clone:function(e,t,n){var r,i,o,a,s,u=d.contains(e.ownerDocument,e);if(f.html5Clone||d.isXMLDoc(e)||!we.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Se.innerHTML=e.outerHTML,Se.removeChild(o=Se.firstChild)),!(f.noCloneEvent&&f.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||d.isXMLDoc(e)))for(r=oe(o),s=oe(e),a=0;null!=(i=s[a]);++a)r[a]&&He(i,r[a]);if(t)if(n)for(s=s||oe(e),r=r||oe(o),a=0;null!=(i=s[a]);a++)Le(i,r[a]);else Le(e,o);return(r=oe(o,"script")).length>0&&ae(r,!u&&oe(e,"script")),r=s=i=null,o},cleanData:function(e,t){for(var r,i,o,a,s=0,u=d.expando,l=d.cache,c=f.attributes,p=d.event.special;null!=(r=e[s]);s++)if((t||M(r))&&(a=(o=r[u])&&l[o])){if(a.events)for(i in a.events)p[i]?d.event.remove(r,i):d.removeEvent(r,i,a.handle);l[o]&&(delete l[o],c||void 0===r.removeAttribute?r[u]=void 0:r.removeAttribute(u),n.push(o))}}}),d.fn.extend({domManip:qe,detach:function(e){return _e(this,e,!0)},remove:function(e){return _e(this,e)},text:function(e){return Q(this,function(e){return void 0===e?d.text(this):this.empty().append((this[0]&&this[0].ownerDocument||r).createTextNode(e))},null,e,arguments.length)},append:function(){return qe(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Ae(this,e).appendChild(e)})},prepend:function(){return qe(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Ae(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return qe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return qe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&d.cleanData(oe(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&d.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return d.clone(this,e,t)})},html:function(e){return Q(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(be,""):void 0;if("string"==typeof e&&!Ce.test(e)&&(f.htmlSerialize||!we.test(e))&&(f.leadingWhitespace||!te.test(e))&&!ie[(Z.exec(e)||["",""])[1].toLowerCase()]){e=d.htmlPrefilter(e);try{for(;n")).appendTo(t.documentElement))[0].contentWindow||Fe[0].contentDocument).document).write(),t.close(),n=Oe(e,t),Fe.detach()),Me[e]=n),n}var Pe=/^margin/,Be=new RegExp("^("+$+")(?!px)[a-z%]+$","i"),We=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];for(o in i=n.apply(e,r||[]),t)e.style[o]=a[o];return i},Ie=r.documentElement;!function(){var t,n,i,o,a,s,u=r.createElement("div"),l=r.createElement("div");function c(){var c,f,d=r.documentElement;d.appendChild(u),l.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",t=i=s=!1,n=a=!0,e.getComputedStyle&&(f=e.getComputedStyle(l),t="1%"!==(f||{}).top,s="2px"===(f||{}).marginLeft,i="4px"===(f||{width:"4px"}).width,l.style.marginRight="50%",n="4px"===(f||{marginRight:"4px"}).marginRight,(c=l.appendChild(r.createElement("div"))).style.cssText=l.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",l.style.width="1px",a=!parseFloat((e.getComputedStyle(c)||{}).marginRight),l.removeChild(c)),l.style.display="none",(o=0===l.getClientRects().length)&&(l.style.display="",l.innerHTML="
t
",l.childNodes[0].style.borderCollapse="separate",(c=l.getElementsByTagName("td"))[0].style.cssText="margin:0;border:0;padding:0;display:none",(o=0===c[0].offsetHeight)&&(c[0].style.display="",c[1].style.display="none",o=0===c[0].offsetHeight)),d.removeChild(u)}l.style&&(l.style.cssText="float:left;opacity:.5",f.opacity="0.5"===l.style.opacity,f.cssFloat=!!l.style.cssFloat,l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",f.clearCloneStyle="content-box"===l.style.backgroundClip,(u=r.createElement("div")).style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",l.innerHTML="",u.appendChild(l),f.boxSizing=""===l.style.boxSizing||""===l.style.MozBoxSizing||""===l.style.WebkitBoxSizing,d.extend(f,{reliableHiddenOffsets:function(){return null==t&&c(),o},boxSizingReliable:function(){return null==t&&c(),i},pixelMarginRight:function(){return null==t&&c(),n},pixelPosition:function(){return null==t&&c(),t},reliableMarginRight:function(){return null==t&&c(),a},reliableMarginLeft:function(){return null==t&&c(),s}}))}();var $e,ze,Xe=/^(top|right|bottom|left)$/;function Ue(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}e.getComputedStyle?($e=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},ze=function(e,t,n){var r,i,o,a,s=e.style;return""!==(a=(n=n||$e(e))?n.getPropertyValue(t)||n[t]:void 0)&&void 0!==a||d.contains(e.ownerDocument,e)||(a=d.style(e,t)),n&&!f.pixelMarginRight()&&Be.test(a)&&Pe.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o),void 0===a?a:a+""}):Ie.currentStyle&&($e=function(e){return e.currentStyle},ze=function(e,t,n){var r,i,o,a,s=e.style;return null==(a=(n=n||$e(e))?n[t]:void 0)&&s&&s[t]&&(a=s[t]),Be.test(a)&&!Xe.test(t)&&(r=s.left,(o=(i=e.runtimeStyle)&&i.left)&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"});var Ve=/alpha\([^)]*\)/i,Ye=/opacity\s*=\s*([^)]*)/i,Je=/^(none|table(?!-c[ea]).+)/,Ge=new RegExp("^("+$+")(.*)$","i"),Qe={position:"absolute",visibility:"hidden",display:"block"},Ke={letterSpacing:"0",fontWeight:"400"},Ze=["Webkit","O","Moz","ms"],et=r.createElement("div").style;function tt(e){if(e in et)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=Ze.length;n--;)if((e=Ze[n]+t)in et)return e}function nt(e,t){for(var n,r,i,o=[],a=0,s=e.length;a=1||""===t)&&""===d.trim(o.replace(Ve,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=Ve.test(o)?o.replace(Ve,i):o+" "+i)}}),d.cssHooks.marginRight=Ue(f.reliableMarginRight,function(e,t){if(t)return We(e,{display:"inline-block"},ze,[e,"marginRight"])}),d.cssHooks.marginLeft=Ue(f.reliableMarginLeft,function(e,t){if(t)return(parseFloat(ze(e,"marginLeft"))||(d.contains(e.ownerDocument,e)?e.getBoundingClientRect().left-We(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}):0))+"px"}),d.each({margin:"",padding:"",border:"Width"},function(e,t){d.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+X[r]+t]=o[r]||o[r-2]||o[0];return i}},Pe.test(e)||(d.cssHooks[e+t].set=rt)}),d.fn.extend({css:function(e,t){return Q(this,function(e,t,n){var r,i,o={},a=0;if(d.isArray(t)){for(r=$e(e),i=t.length;a1)},show:function(){return nt(this,!0)},hide:function(){return nt(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){U(this)?d(this).show():d(this).hide()})}}),d.Tween=at,at.prototype={constructor:at,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||d.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(d.cssNumber[n]?"":"px")},cur:function(){var e=at.propHooks[this.prop];return e&&e.get?e.get(this):at.propHooks._default.get(this)},run:function(e){var t,n=at.propHooks[this.prop];return this.options.duration?this.pos=t=d.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):at.propHooks._default.set(this),this}},at.prototype.init.prototype=at.prototype,at.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=d.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){d.fx.step[e.prop]?d.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[d.cssProps[e.prop]]&&!d.cssHooks[e.prop]?e.elem[e.prop]=e.now:d.style(e.elem,e.prop,e.now+e.unit)}}},at.propHooks.scrollTop=at.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},d.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},d.fx=at.prototype.init,d.fx.step={};var st,ut,lt=/^(?:toggle|show|hide)$/,ct=/queueHooks$/;function ft(){return e.setTimeout(function(){st=void 0}),st=d.now()}function dt(e,t){var n,r={height:e},i=0;for(t=t?1:0;i<4;i+=2-t)r["margin"+(n=X[i])]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function pt(e,t,n){for(var r,i=(ht.tweeners[t]||[]).concat(ht.tweeners["*"]),o=0,a=i.length;o
a",e=n.getElementsByTagName("a")[0],t.setAttribute("type","checkbox"),n.appendChild(t),(e=n.getElementsByTagName("a")[0]).style.cssText="top:1px",f.getSetAttribute="t"!==n.className,f.style=/top/.test(e.getAttribute("style")),f.hrefNormalized="/a"===e.getAttribute("href"),f.checkOn=!!t.value,f.optSelected=o.selected,f.enctype=!!r.createElement("form").enctype,i.disabled=!0,f.optDisabled=!o.disabled,(t=r.createElement("input")).setAttribute("value",""),f.input=""===t.getAttribute("value"),t.value="t",t.setAttribute("type","radio"),f.radioValue="t"===t.value}();var gt=/\r/g,mt=/[\x20\t\r\n\f]+/g;d.fn.extend({val:function(e){var t,n,r,i=this[0];return arguments.length?(r=d.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,d(this).val()):e)?i="":"number"==typeof i?i+="":d.isArray(i)&&(i=d.map(i,function(e){return null==e?"":e+""})),(t=d.valHooks[this.type]||d.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))})):i?(t=d.valHooks[i.type]||d.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(gt,""):null==n?"":n:void 0}}),d.extend({valHooks:{option:{get:function(e){var t=d.find.attr(e,"value");return null!=t?t:d.trim(d.text(e)).replace(mt," ")}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||i<0,a=o?null:[],s=o?i+1:r.length,u=i<0?s:o?i:0;u-1)try{r.selected=n=!0}catch(e){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),d.each(["radio","checkbox"],function(){d.valHooks[this]={set:function(e,t){if(d.isArray(t))return e.checked=d.inArray(d(e).val(),t)>-1}},f.checkOn||(d.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var vt,yt,xt=d.expr.attrHandle,bt=/^(?:checked|selected)$/i,wt=f.getSetAttribute,Tt=f.input;d.fn.extend({attr:function(e,t){return Q(this,d.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){d.removeAttr(this,e)})}}),d.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===e.getAttribute?d.prop(e,t,n):(1===o&&d.isXMLDoc(e)||(t=t.toLowerCase(),i=d.attrHooks[t]||(d.expr.match.bool.test(t)?yt:vt)),void 0!==n?null===n?void d.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=d.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!f.radioValue&&"radio"===t&&d.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(H);if(o&&1===e.nodeType)for(;n=o[i++];)r=d.propFix[n]||n,d.expr.match.bool.test(n)?Tt&&wt||!bt.test(n)?e[r]=!1:e[d.camelCase("default-"+n)]=e[r]=!1:d.attr(e,n,""),e.removeAttribute(wt?n:r)}}),yt={set:function(e,t,n){return!1===t?d.removeAttr(e,n):Tt&&wt||!bt.test(n)?e.setAttribute(!wt&&d.propFix[n]||n,n):e[d.camelCase("default-"+n)]=e[n]=!0,n}},d.each(d.expr.match.bool.source.match(/\w+/g),function(e,t){var n=xt[t]||d.find.attr;Tt&&wt||!bt.test(t)?xt[t]=function(e,t,r){var i,o;return r||(o=xt[t],xt[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,xt[t]=o),i}:xt[t]=function(e,t,n){if(!n)return e[d.camelCase("default-"+t)]?t.toLowerCase():null}}),Tt&&wt||(d.attrHooks.value={set:function(e,t,n){if(!d.nodeName(e,"input"))return vt&&vt.set(e,t,n);e.defaultValue=t}}),wt||(vt={set:function(e,t,n){var r=e.getAttributeNode(n);if(r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n))return t}},xt.id=xt.name=xt.coords=function(e,t,n){var r;if(!n)return(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},d.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);if(n&&n.specified)return n.value},set:vt.set},d.attrHooks.contenteditable={set:function(e,t,n){vt.set(e,""!==t&&t,n)}},d.each(["width","height"],function(e,t){d.attrHooks[t]={set:function(e,n){if(""===n)return e.setAttribute(t,"auto"),n}}})),f.style||(d.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var Ct=/^(?:input|select|textarea|button|object)$/i,Et=/^(?:a|area)$/i;d.fn.extend({prop:function(e,t){return Q(this,d.prop,e,t,arguments.length>1)},removeProp:function(e){return e=d.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(e){}})}}),d.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&d.isXMLDoc(e)||(t=d.propFix[t]||t,i=d.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=d.find.attr(e,"tabindex");return t?parseInt(t,10):Ct.test(e.nodeName)||Et.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),f.hrefNormalized||d.each(["href","src"],function(e,t){d.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),f.optSelected||(d.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),d.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){d.propFix[this.toLowerCase()]=this}),f.enctype||(d.propFix.enctype="encoding");var Nt=/[\t\r\n\f]/g;function kt(e){return d.attr(e,"class")||""}d.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(d.isFunction(e))return this.each(function(t){d(this).addClass(e.call(this,t,kt(this)))});if("string"==typeof e&&e)for(t=e.match(H)||[];n=this[u++];)if(i=kt(n),r=1===n.nodeType&&(" "+i+" ").replace(Nt," ")){for(a=0;o=t[a++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=d.trim(r))&&d.attr(n,"class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(d.isFunction(e))return this.each(function(t){d(this).removeClass(e.call(this,t,kt(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof e&&e)for(t=e.match(H)||[];n=this[u++];)if(i=kt(n),r=1===n.nodeType&&(" "+i+" ").replace(Nt," ")){for(a=0;o=t[a++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");i!==(s=d.trim(r))&&d.attr(n,"class",s)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):d.isFunction(e)?this.each(function(n){d(this).toggleClass(e.call(this,n,kt(this),t),t)}):this.each(function(){var t,r,i,o;if("string"===n)for(r=0,i=d(this),o=e.match(H)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else void 0!==e&&"boolean"!==n||((t=kt(this))&&d._data(this,"__className__",t),d.attr(this,"class",t||!1===e?"":d._data(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+kt(n)+" ").replace(Nt," ").indexOf(t)>-1)return!0;return!1}}),d.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){d.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),d.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}});var St=e.location,At=d.now(),Dt=/\?/,jt=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;d.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=d.trim(t+"");return i&&!d.trim(i.replace(jt,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():d.error("Invalid JSON: "+t)},d.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{e.DOMParser?n=(new e.DOMParser).parseFromString(t,"text/xml"):((n=new e.ActiveXObject("Microsoft.XMLDOM")).async="false",n.loadXML(t))}catch(e){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||d.error("Invalid XML: "+t),n};var Lt=/#.*$/,Ht=/([?&])_=[^&]*/,qt=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,_t=/^(?:GET|HEAD)$/,Ft=/^\/\//,Mt=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Ot={},Rt={},Pt="*/".concat("*"),Bt=St.href,Wt=Mt.exec(Bt.toLowerCase())||[];function It(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(H)||[];if(d.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function $t(e,t,n,r){var i={},o=e===Rt;function a(s){var u;return i[s]=!0,d.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=d.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&d.extend(!0,e,n),e}function Xt(e){return e.style&&e.style.display||d.css(e,"display")}d.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Bt,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Wt[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Pt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":d.parseJSON,"text xml":d.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,d.ajaxSettings),t):zt(d.ajaxSettings,e)},ajaxPrefilter:It(Ot),ajaxTransport:It(Rt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var r,i,o,a,s,u,l,c,f=d.ajaxSetup({},n),p=f.context||f,h=f.context&&(p.nodeType||p.jquery)?d(p):d.event,g=d.Deferred(),m=d.Callbacks("once memory"),v=f.statusCode||{},y={},x={},b=0,w="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=qt.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=x[n]=x[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(f.mimeType=e),this},statusCode:function(e){var t;if(e)if(b<2)for(t in e)v[t]=[v[t],e[t]];else T.always(e[T.status]);return this},abort:function(e){var t=e||w;return l&&l.abort(t),C(0,t),this}};if(g.promise(T).complete=m.add,T.success=T.done,T.error=T.fail,f.url=((t||f.url||Bt)+"").replace(Lt,"").replace(Ft,Wt[1]+"//"),f.type=n.method||n.type||f.method||f.type,f.dataTypes=d.trim(f.dataType||"*").toLowerCase().match(H)||[""],null==f.crossDomain&&(r=Mt.exec(f.url.toLowerCase()),f.crossDomain=!(!r||r[1]===Wt[1]&&r[2]===Wt[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(Wt[3]||("http:"===Wt[1]?"80":"443")))),f.data&&f.processData&&"string"!=typeof f.data&&(f.data=d.param(f.data,f.traditional)),$t(Ot,f,n,T),2===b)return T;for(i in(u=d.event&&f.global)&&0==d.active++&&d.event.trigger("ajaxStart"),f.type=f.type.toUpperCase(),f.hasContent=!_t.test(f.type),o=f.url,f.hasContent||(f.data&&(o=f.url+=(Dt.test(o)?"&":"?")+f.data,delete f.data),!1===f.cache&&(f.url=Ht.test(o)?o.replace(Ht,"$1_="+At++):o+(Dt.test(o)?"&":"?")+"_="+At++)),f.ifModified&&(d.lastModified[o]&&T.setRequestHeader("If-Modified-Since",d.lastModified[o]),d.etag[o]&&T.setRequestHeader("If-None-Match",d.etag[o])),(f.data&&f.hasContent&&!1!==f.contentType||n.contentType)&&T.setRequestHeader("Content-Type",f.contentType),T.setRequestHeader("Accept",f.dataTypes[0]&&f.accepts[f.dataTypes[0]]?f.accepts[f.dataTypes[0]]+("*"!==f.dataTypes[0]?", "+Pt+"; q=0.01":""):f.accepts["*"]),f.headers)T.setRequestHeader(i,f.headers[i]);if(f.beforeSend&&(!1===f.beforeSend.call(p,T,f)||2===b))return T.abort();for(i in w="abort",{success:1,error:1,complete:1})T[i](f[i]);if(l=$t(Rt,f,n,T)){if(T.readyState=1,u&&h.trigger("ajaxSend",[T,f]),2===b)return T;f.async&&f.timeout>0&&(s=e.setTimeout(function(){T.abort("timeout")},f.timeout));try{b=1,l.send(y,C)}catch(e){if(!(b<2))throw e;C(-1,e)}}else C(-1,"No Transport");function C(t,n,r,i){var c,y,x,w,C,E=n;2!==b&&(b=2,s&&e.clearTimeout(s),l=void 0,a=i||"",T.readyState=t>0?4:0,c=t>=200&&t<300||304===t,r&&(w=function(e,t,n){for(var r,i,o,a,s=e.contents,u=e.dataTypes;"*"===u[0];)u.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){u.unshift(a);break}if(u[0]in n)o=u[0];else{for(a in n){if(!u[0]||e.converters[a+" "+u[0]]){o=a;break}r||(r=a)}o=o||r}if(o)return o!==u[0]&&u.unshift(o),n[o]}(f,T,r)),w=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e.throws)t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(f,w,T,c),c?(f.ifModified&&((C=T.getResponseHeader("Last-Modified"))&&(d.lastModified[o]=C),(C=T.getResponseHeader("etag"))&&(d.etag[o]=C)),204===t||"HEAD"===f.type?E="nocontent":304===t?E="notmodified":(E=w.state,y=w.data,c=!(x=w.error))):(x=E,!t&&E||(E="error",t<0&&(t=0))),T.status=t,T.statusText=(n||E)+"",c?g.resolveWith(p,[y,E,T]):g.rejectWith(p,[T,E,x]),T.statusCode(v),v=void 0,u&&h.trigger(c?"ajaxSuccess":"ajaxError",[T,f,c?y:x]),m.fireWith(p,[T,E]),u&&(h.trigger("ajaxComplete",[T,f]),--d.active||d.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return d.get(e,t,n,"json")},getScript:function(e,t){return d.get(e,void 0,t,"script")}}),d.each(["get","post"],function(e,t){d[t]=function(e,n,r,i){return d.isFunction(n)&&(i=i||r,r=n,n=void 0),d.ajax(d.extend({url:e,type:t,dataType:i,data:n,success:r},d.isPlainObject(e)&&e))}}),d._evalUrl=function(e){return d.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},d.fn.extend({wrapAll:function(e){if(d.isFunction(e))return this.each(function(t){d(this).wrapAll(e.call(this,t))});if(this[0]){var t=d(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return d.isFunction(e)?this.each(function(t){d(this).wrapInner(e.call(this,t))}):this.each(function(){var t=d(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=d.isFunction(e);return this.each(function(n){d(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){d.nodeName(this,"body")||d(this).replaceWith(this.childNodes)}).end()}}),d.expr.filters.hidden=function(e){return f.reliableHiddenOffsets()?e.offsetWidth<=0&&e.offsetHeight<=0&&!e.getClientRects().length:function(e){if(!d.contains(e.ownerDocument||r,e))return!0;for(;e&&1===e.nodeType;){if("none"===Xt(e)||"hidden"===e.type)return!0;e=e.parentNode}return!1}(e)},d.expr.filters.visible=function(e){return!d.expr.filters.hidden(e)};var Ut=/%20/g,Vt=/\[\]$/,Yt=/\r?\n/g,Jt=/^(?:submit|button|image|reset|file)$/i,Gt=/^(?:input|select|textarea|keygen)/i;function Qt(e,t,n,r){var i;if(d.isArray(t))d.each(t,function(t,i){n||Vt.test(e)?r(e,i):Qt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==d.type(t))r(e,t);else for(i in t)Qt(e+"["+i+"]",t[i],n,r)}d.param=function(e,t){var n,r=[],i=function(e,t){t=d.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=d.ajaxSettings&&d.ajaxSettings.traditional),d.isArray(e)||e.jquery&&!d.isPlainObject(e))d.each(e,function(){i(this.name,this.value)});else for(n in e)Qt(n,e[n],t,i);return r.join("&").replace(Ut,"+")},d.fn.extend({serialize:function(){return d.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=d.prop(this,"elements");return e?d.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!d(this).is(":disabled")&&Gt.test(this.nodeName)&&!Jt.test(e)&&(this.checked||!K.test(e))}).map(function(e,t){var n=d(this).val();return null==n?null:d.isArray(n)?d.map(n,function(e){return{name:t.name,value:e.replace(Yt,"\r\n")}}):{name:t.name,value:n.replace(Yt,"\r\n")}}).get()}}),d.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return this.isLocal?nn():r.documentMode>8?tn():/^(get|post|head|put|delete|options)$/i.test(this.type)&&tn()||nn()}:tn;var Kt=0,Zt={},en=d.ajaxSettings.xhr();function tn(){try{return new e.XMLHttpRequest}catch(e){}}function nn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(e){}}e.attachEvent&&e.attachEvent("onunload",function(){for(var e in Zt)Zt[e](void 0,!0)}),f.cors=!!en&&"withCredentials"in en,(en=f.ajax=!!en)&&d.ajaxTransport(function(t){var n;if(!t.crossDomain||f.cors)return{send:function(r,i){var o,a=t.xhr(),s=++Kt;if(a.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(o in t.xhrFields)a[o]=t.xhrFields[o];for(o in t.mimeType&&a.overrideMimeType&&a.overrideMimeType(t.mimeType),t.crossDomain||r["X-Requested-With"]||(r["X-Requested-With"]="XMLHttpRequest"),r)void 0!==r[o]&&a.setRequestHeader(o,r[o]+"");a.send(t.hasContent&&t.data||null),n=function(e,r){var o,u,l;if(n&&(r||4===a.readyState))if(delete Zt[s],n=void 0,a.onreadystatechange=d.noop,r)4!==a.readyState&&a.abort();else{l={},o=a.status,"string"==typeof a.responseText&&(l.text=a.responseText);try{u=a.statusText}catch(e){u=""}o||!t.isLocal||t.crossDomain?1223===o&&(o=204):o=l.text?200:404}l&&i(o,u,l,a.getAllResponseHeaders())},t.async?4===a.readyState?e.setTimeout(n):a.onreadystatechange=Zt[s]=n:n()},abort:function(){n&&n(void 0,!0)}}}),d.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return d.globalEval(e),e}}}),d.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),d.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=r.head||d("head")[0]||r.documentElement;return{send:function(i,o){(t=r.createElement("script")).async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||o(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var rn=[],on=/(=)\?(?=&|$)|\?\?/;d.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=rn.pop()||d.expando+"_"+At++;return this[e]=!0,e}}),d.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=!1!==t.jsonp&&(on.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&on.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return i=t.jsonpCallback=d.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(on,"$1"+i):!1!==t.jsonp&&(t.url+=(Dt.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||d.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){void 0===o?d(e).removeProp(i):e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,rn.push(i)),a&&d.isFunction(o)&&o(a[0]),a=o=void 0}),"script"}),d.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||r;var i=T.exec(e),o=!n&&[];return i?[t.createElement(i[1])]:(i=ce([e],t,o),o&&o.length&&d(o).remove(),d.merge([],i.childNodes))};var an=d.fn.load;function sn(e){return d.isWindow(e)?e:9===e.nodeType&&(e.defaultView||e.parentWindow)}d.fn.load=function(e,t,n){if("string"!=typeof e&&an)return an.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=d.trim(e.slice(s,e.length)),e=e.slice(0,s)),d.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),a.length>0&&d.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?d("
").append(d.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},d.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){d.fn[t]=function(e){return this.on(t,e)}}),d.expr.filters.animated=function(e){return d.grep(d.timers,function(t){return e===t.elem}).length},d.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=d.css(e,"position"),c=d(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=d.css(e,"top"),u=d.css(e,"left"),("absolute"===l||"fixed"===l)&&d.inArray("auto",[o,u])>-1?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),d.isFunction(t)&&(t=t.call(e,n,d.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},d.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){d.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;return o?(t=o.documentElement,d.contains(t,i)?(void 0!==i.getBoundingClientRect&&(r=i.getBoundingClientRect()),n=sn(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r):void 0},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===d.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),d.nodeName(e[0],"html")||(n=e.offset()),n.top+=d.css(e[0],"borderTopWidth",!0),n.left+=d.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-d.css(r,"marginTop",!0),left:t.left-n.left-d.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&!d.nodeName(e,"html")&&"static"===d.css(e,"position");)e=e.offsetParent;return e||Ie})}}),d.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);d.fn[e]=function(r){return Q(this,function(e,r,i){var o=sn(e);if(void 0===i)return o?t in o?o[t]:o.document.documentElement[r]:e[r];o?o.scrollTo(n?d(o).scrollLeft():i,n?i:d(o).scrollTop()):e[r]=i},e,r,arguments.length,null)}}),d.each(["top","left"],function(e,t){d.cssHooks[t]=Ue(f.pixelPosition,function(e,n){if(n)return n=ze(e,t),Be.test(n)?d(e).position()[t]+"px":n})}),d.each({Height:"height",Width:"width"},function(e,t){d.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){d.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(!0===r||!0===i?"margin":"border");return Q(this,function(t,n,r){var i;return d.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?d.css(t,n,a):d.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),d.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),d.fn.size=function(){return this.length},d.fn.andSelf=d.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return d});var un=e.jQuery,ln=e.$;return d.noConflict=function(t){return e.$===d&&(e.$=ln),t&&e.jQuery===d&&(e.jQuery=un),d},t||(e.jQuery=e.$=d),d}); // Snap this specific version of jQuery into H5P. jQuery.noConflict will // revert the globals to what they were before this file was loaded. From ad304fab5af7a83df94fda9c3ad4b6c71d00bf13 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 17 Sep 2020 09:56:38 +0200 Subject: [PATCH 087/182] MOBILE-3530 course: Fix error when removing course files with site plugins --- src/core/course/providers/module-prefetch-delegate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 462f7ce83..53a236b09 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -730,10 +730,10 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { getModuleFiles(module: any, courseId: number): Promise { const handler = this.getPrefetchHandlerFor(module); - if (handler.getFiles) { + if (handler && handler.getFiles) { // The handler defines a function to get files, use it. return Promise.resolve(handler.getFiles(module, courseId)); - } else if (handler.loadContents) { + } else if (handler && handler.loadContents) { // The handler defines a function to load contents, use it before returning module contents. return handler.loadContents(module, courseId).then(() => { return module.contents; From 793e029cbd634e51a924253a1b3001090f4f7e87 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 17 Sep 2020 11:56:00 +0200 Subject: [PATCH 088/182] MOBILE-3523 login: Move login checks and modals to providers --- src/core/login/pages/site/site.ts | 2 +- src/core/login/providers/helper.ts | 45 ++------------------- src/providers/sites.ts | 9 +++++ src/providers/utils/dom.ts | 63 ++++++++++++++++++++++++------ 4 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index 68b1c6142..18248c934 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -335,7 +335,7 @@ export class CoreLoginSitePage { * @return Promise resolved after logging in. */ protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise { - return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => { + return this.sitesProvider.checkApplication(response).then(() => { this.domUtils.triggerFormSubmittedEvent(this.formElement, true); diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 9e5ea3630..7d51cb745 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { Location } from '@angular/common'; -import { AlertController, NavController, NavOptions } from 'ionic-angular'; +import { NavController, NavOptions } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreApp, CoreStoreConfig } from '@providers/app'; import { CoreConfigProvider } from '@providers/config'; @@ -101,7 +101,6 @@ export class CoreLoginHelperProvider { private initDelegate: CoreInitDelegate, private sitePluginsProvider: CoreSitePluginsProvider, private location: Location, - private alertCtrl: AlertController, private courseProvider: CoreCourseProvider ) { this.logger = logger.getInstance('CoreLoginHelper'); @@ -1235,7 +1234,7 @@ export class CoreLoginHelperProvider { protected showWorkplaceNoticeModal(message: string): void { const link = CoreApp.instance.getAppStoreUrl({android: 'com.moodle.workplace', ios: 'id1470929705' }); - this.showDownloadAppNoticeModal(message, link); + this.domUtils.showDownloadAppNoticeModal(message, link); } /** @@ -1251,45 +1250,7 @@ export class CoreLoginHelperProvider { const link = CoreApp.instance.getAppStoreUrl(storesConfig); - this.showDownloadAppNoticeModal(message, link); - } - - /** - * Show a modal warning the user that he should use a different app. - * - * @param message The warning message. - * @param link Link to the app to download if any. - */ - protected showDownloadAppNoticeModal(message: string, link?: string): void { - const buttons: any[] = [ - { - text: this.translate.instant('core.ok'), - role: 'cancel' - } - ]; - - if (link) { - buttons.push({ - text: this.translate.instant('core.download'), - handler: (): void => { - this.utils.openInBrowser(link); - } - }); - } - - const alert = this.alertCtrl.create({ - message: message, - buttons: buttons - }); - - alert.present().then(() => { - const isDevice = CoreApp.instance.isAndroid() || CoreApp.instance.isIOS(); - if (!isDevice) { - // Treat all anchors so they don't override the app. - const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); - this.domUtils.treatAnchors(alertMessageEl); - } - }); + this.domUtils.showDownloadAppNoticeModal(message, link); } /** diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 8609022d6..aed300a25 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -1023,6 +1023,15 @@ export class CoreSitesProvider { return this.appDB.insertRecord(CoreSitesProvider.SITES_TABLE, entry); } + /** + * Check the app for a site and show a download dialogs if necessary. + * + * @param response Data obtained during site check. + */ + async checkApplication(response: CoreSiteCheckResponse): Promise { + await this.checkRequiredMinimumVersion(response.config); + } + /** * Check the required minimum version of the app for a site and shows a download dialog. * diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 5be93dddb..9297320de 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -1402,6 +1402,42 @@ export class CoreDomUtilsProvider { return loader; } + /** + * Show a modal warning the user that he should use a different app. + * + * @param message The warning message. + * @param link Link to the app to download if any. + */ + showDownloadAppNoticeModal(message: string, link?: string): void { + const buttons: any[] = [{ + text: this.translate.instant('core.ok'), + role: 'cancel' + }]; + + if (link) { + buttons.push({ + text: this.translate.instant('core.download'), + handler: (): void => { + this.openInBrowser(link); + } + }); + } + + const alert = this.alertCtrl.create({ + message: message, + buttons: buttons + }); + + alert.present().then(() => { + const isDevice = CoreApp.instance.isAndroid() || CoreApp.instance.isIOS(); + if (!isDevice) { + // Treat all anchors so they don't override the app. + const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); + this.treatAnchors(alertMessageEl); + } + }); + } + /** * Show a prompt modal to input some data. * @@ -1559,17 +1595,7 @@ export class CoreDomUtilsProvider { event.preventDefault(); event.stopPropagation(); - // We cannot use CoreDomUtilsProvider.openInBrowser due to circular dependencies. - if (CoreApp.instance.isDesktop()) { - // It's a desktop app, use Electron shell library to open the browser. - const shell = require('electron').shell; - if (!shell.openExternal(href)) { - // Open browser failed, open a new window in the app. - window.open(href, '_system'); - } - } else { - window.open(href, '_system'); - } + this.openInBrowser(href); } }); }); @@ -1680,6 +1706,21 @@ export class CoreDomUtilsProvider { online: !!online, }, siteId); } + + // We cannot use CoreUtilsProvider.openInBrowser due to circular dependencies. + protected openInBrowser(url: string): void { + if (CoreApp.instance.isDesktop()) { + // It's a desktop app, use Electron shell library to open the browser. + const shell = require('electron').shell; + if (!shell.openExternal(url)) { + // Open browser failed, open a new window in the app. + window.open(url, '_system'); + } + } else { + window.open(url, '_system'); + } + } + } export class CoreDomUtils extends makeSingleton(CoreDomUtilsProvider) {} From 3c3aa464c71fdbf1cc8b862ef2d198191f2f7d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 17 Sep 2020 14:25:56 +0200 Subject: [PATCH 089/182] MOBILE-3464 tabs: Show tabs again if height changed --- src/components/tabs/tabs.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index a85d4de24..69a046842 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -52,6 +52,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe // Minimum tab's width to display fully the word "Competencies" which is the longest tab in the app. static MIN_TAB_WIDTH = 107; + // Max height that allows tab hiding. + static MAX_HEIGHT_TO_HIDE_TABS = 768; @Input() selectedIndex = 0; // Index of the tab to select. @Input() hideUntil = true; // Determine when should the contents be shown. @@ -229,11 +231,24 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe * Calculate slides. */ calculateSlides(): void { - if (!this.isCurrentView || !this.tabsShown || !this.initialized) { + if (!this.isCurrentView || !this.initialized) { // Don't calculate if component isn't in current view, the calculations are wrong. return; } + if (!this.tabsShown) { + if (window.innerHeight >= CoreTabsComponent.MAX_HEIGHT_TO_HIDE_TABS) { + // Ensure tabbar is shown. + this.tabsShown = true; + this.tabBarElement.classList.remove('tabs-hidden'); + this.lastScroll = 0; + this.calculateTabBarHeight(); + } else { + // Don't recalculate. + return; + } + } + this.calculateMaxSlides().then(() => { this.updateSlides(); }); @@ -477,8 +492,8 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe * @param scrollElement Scroll element to check scroll position. */ showHideTabs(scrollElement: any): void { - // Do not scroll on very tall screens. - if (window.innerHeight >= 1024) { + // Always show on very tall screens. + if (window.innerHeight >= CoreTabsComponent.MAX_HEIGHT_TO_HIDE_TABS) { return; } From f4af4de3ca031bff222d77c5b4af00a1c654081b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 18 Sep 2020 09:29:30 +0200 Subject: [PATCH 090/182] MOBILE-3464 tabs: Add a setTimeout to get the right height in iOS --- src/components/tabs/tabs.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index 69a046842..3afb3e95b 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -132,7 +132,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe this.initializeTabs(); } - this.resizeFunction = this.calculateSlides.bind(this); + this.resizeFunction = this.windowResized.bind(this); window.addEventListener('resize', this.resizeFunction); } @@ -632,6 +632,15 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe }); } + /** + * Adapt tabs to a window resize. + */ + protected windowResized(): void { + setTimeout(() => { + this.calculateSlides(); + }); + } + /** * Component destroyed. */ From 90dc4572d921efb5d69f3d9f81f3b4e61a72a14c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 18 Sep 2020 11:10:17 +0200 Subject: [PATCH 091/182] MOBILE-3449 assign: Always allow submit grade if has offline data --- .../components/submission/submission.ts | 28 +++++++++++++------ src/addon/mod/assign/providers/assign-sync.ts | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index 42ea027ba..e3fc1cb28 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -110,6 +110,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { protected originalGrades: any = {}; // Object with the original grade data, to check for changes. protected isDestroyed: boolean; // Whether the component has been destroyed. protected syncObserver: CoreEventObserver; + protected hasOfflineGrade = false; constructor(protected navCtrl: NavController, protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected timeUtils: CoreTimeUtilsProvider, @@ -306,17 +307,23 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { /** * Check if there's data to save (grade). * + * @param isSubmit Whether the user is about to submit the grade. * @return Promise resolved with boolean: whether there's data to save. */ - protected hasDataToSave(): Promise { + protected async hasDataToSave(isSubmit?: boolean): Promise { if (!this.canSaveGrades || !this.loaded) { - return Promise.resolve(false); + return false; + } + + if (isSubmit && this.hasOfflineGrade) { + // Always allow sending if the grade is saved in offline. + return true; } // Check if numeric grade and toggles changed. if (this.originalGrades.grade != this.grade.grade || this.originalGrades.addAttempt != this.grade.addAttempt || this.originalGrades.applyToAll != this.grade.applyToAll) { - return Promise.resolve(true); + return true; } // Check if outcomes changed. @@ -326,20 +333,21 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { if (this.originalGrades.outcomes[outcome.id] == 'undefined' || this.originalGrades.outcomes[outcome.id] != outcome.selectedId) { - return Promise.resolve(true); + return true; } } } if (this.feedback && this.feedback.plugins) { - return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId) - .catch(() => { + try { + return this.assignHelper.hasFeedbackDataChanged(this.assign, this.userSubmission, this.feedback, this.submitId); + } catch (error) { // Error ocurred, consider there are no changes. return false; - }); + } } - return Promise.resolve(false); + return false; } /** @@ -645,11 +653,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { return this.assignOfflineProvider.getSubmissionGrade(this.assign.id, this.submitId).catch(() => { // Grade not found. }).then((data) => { + this.hasOfflineGrade = false; // Load offline grades. if (data && (!feedback || !feedback.gradeddate || feedback.gradeddate < data.timemodified)) { // If grade has been modified from gradebook, do not use offline. if (this.grade.modified < data.timemodified) { + this.hasOfflineGrade = true; this.grade.grade = !this.grade.scale ? this.utils.formatFloat(data.grade) : data.grade; this.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced'; this.gradingColor = ''; @@ -790,7 +800,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { */ submitGrade(): Promise { // Check if there's something to be saved. - return this.hasDataToSave().then((modified) => { + return this.hasDataToSave(true).then((modified) => { if (!modified) { return; } diff --git a/src/addon/mod/assign/providers/assign-sync.ts b/src/addon/mod/assign/providers/assign-sync.ts index f0e14fe95..de6ee611c 100644 --- a/src/addon/mod/assign/providers/assign-sync.ts +++ b/src/addon/mod/assign/providers/assign-sync.ts @@ -221,7 +221,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { */ protected async performSyncAssign(assignId: number, siteId?: string): Promise { - this.logger.error('Try to sync assign ' + assignId + ' in site ' + siteId); + this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId); const result: AddonModAssignSyncResult = { warnings: [], From 25b1b686558f5c2ae30459edccf1d5657460a727 Mon Sep 17 00:00:00 2001 From: sam marshall Date: Fri, 18 Sep 2020 12:10:38 +0100 Subject: [PATCH 092/182] MOBILE-3556 Login: Send extra parameter on token.php check The extra parameter can be used by Moodle to avoid throwing an error in server logs because other parameters e.g. username are not supplied. --- src/providers/sites.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/providers/sites.ts b/src/providers/sites.ts index aed300a25..c11732038 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -641,7 +641,8 @@ export class CoreSitesProvider { * @return A promise to be resolved if the site exists. */ siteExists(siteUrl: string): Promise { - return this.http.post(siteUrl + '/login/token.php', {}).timeout(this.wsProvider.getRequestTimeout()).toPromise() + return this.http.post(siteUrl + '/login/token.php', { appsitecheck: 1 }). + timeout(this.wsProvider.getRequestTimeout()).toPromise() .catch(() => { // Default error messages are kinda bad, return our own message. return Promise.reject({error: this.translate.instant('core.cannotconnecttrouble')}); From b8daa1dc1be3b8b5eb0f4e2c4a0fb4097c3ee0a1 Mon Sep 17 00:00:00 2001 From: Tien Nguyen Phuc Date: Thu, 13 Aug 2020 17:50:27 +0700 Subject: [PATCH 093/182] MOBILE-3432 files: File type exclusion list for mobile --- scripts/langindex.json | 2 + src/addon/mod/resource/providers/helper.ts | 14 +++++- src/assets/lang/en.json | 2 + src/components/file/file.ts | 9 +++- src/lang/en.json | 2 + src/providers/file-helper.ts | 55 +++++++++++++++++++++- 6 files changed, 81 insertions(+), 3 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index df9739345..a3738eb16 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1373,6 +1373,7 @@ "core.cannotconnecttrouble": "local_moodlemobileapp", "core.cannotconnectverify": "local_moodlemobileapp", "core.cannotdownloadfiles": "local_moodlemobileapp", + "core.cannotopeninapp": "moodle", "core.captureaudio": "local_moodlemobileapp", "core.capturedimage": "local_moodlemobileapp", "core.captureimage": "local_moodlemobileapp", @@ -1916,6 +1917,7 @@ "core.offline": "message", "core.ok": "moodle", "core.online": "message", + "core.openfile": "moodle", "core.openfullimage": "local_moodlemobileapp", "core.openinbrowser": "local_moodlemobileapp", "core.openmodinbrowser": "local_moodlemobileapp", diff --git a/src/addon/mod/resource/providers/helper.ts b/src/addon/mod/resource/providers/helper.ts index 62a00e511..8f43d77ee 100644 --- a/src/addon/mod/resource/providers/helper.ts +++ b/src/addon/mod/resource/providers/helper.ts @@ -183,7 +183,19 @@ export class AddonModResourceHelperProvider { * @param courseId Course Id, used for completion purposes. * @return Resolved when done. */ - openModuleFile(module: any, courseId: number): Promise { + async openModuleFile(module: any, courseId: number): Promise { + // Check whether the file type excluded to open in app. + if (!module.contents.length) { + await this.courseProvider.loadModuleContents(module, courseId); + } + + if (!this.fileHelper.isOpenableInApp(module.contents[0])) { + const confirmed = await this.fileHelper.showConfirmOpenUnsupportedFile(); + if (!confirmed) { + return; + } + } + const modal = this.domUtils.showModalLoading(); // Download and open the file from the resource contents. diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 6d3fd2ffe..699eea430 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1373,6 +1373,7 @@ "core.cannotconnecttrouble": "We're having trouble connecting to your site.", "core.cannotconnectverify": "Please check the address is correct.", "core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", + "core.cannotopeninapp": "This file may not work as expected on this device. Would you like to open it anyway?", "core.captureaudio": "Record audio", "core.capturedimage": "Taken picture.", "core.captureimage": "Take picture", @@ -1916,6 +1917,7 @@ "core.offline": "Offline", "core.ok": "OK", "core.online": "Online", + "core.openfile": "Open file", "core.openfullimage": "Click here to display the full size image", "core.openinbrowser": "Open in browser", "core.openmodinbrowser": "Open {{$a}} in browser", diff --git a/src/components/file/file.ts b/src/components/file/file.ts index 5df003e25..d920b54f9 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -148,7 +148,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { * @param e Click event. * @param openAfterDownload Whether the file should be opened after download. */ - download(e?: Event, openAfterDownload: boolean = false): void { + async download(e?: Event, openAfterDownload: boolean = false): Promise { e && e.preventDefault(); e && e.stopPropagation(); @@ -156,6 +156,13 @@ export class CoreFileComponent implements OnInit, OnDestroy { return; } + if (!this.fileHelper.isOpenableInApp(this.file)) { + const confirmed = await this.fileHelper.showConfirmOpenUnsupportedFile(); + if (!confirmed) { + return; + } + } + if (!this.canDownload || !this.state || this.state == CoreConstants.NOT_DOWNLOADABLE) { // File cannot be downloaded, just open it. if (this.file.toURL) { diff --git a/src/lang/en.json b/src/lang/en.json index 40da0174b..97fcaba84 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -16,6 +16,7 @@ "cannotconnecttrouble": "We're having trouble connecting to your site.", "cannotconnectverify": "Please check the address is correct.", "cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", + "cannotopeninapp": "This file may not work as expected on this device. Would you like to open it anyway?", "captureaudio": "Record audio", "capturedimage": "Taken picture.", "captureimage": "Take picture", @@ -200,6 +201,7 @@ "offline": "Offline", "ok": "OK", "online": "Online", + "openfile": "Open file", "openfullimage": "Click here to display the full size image", "openinbrowser": "Open in browser", "openmodinbrowser": "Open {{$a}} in browser", diff --git a/src/providers/file-helper.ts b/src/providers/file-helper.ts index 9a9ab8eaf..11e0e6345 100644 --- a/src/providers/file-helper.ts +++ b/src/providers/file-helper.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from './app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreFileProvider } from './file'; import { CoreFilepoolProvider } from './filepool'; import { CoreSitesProvider } from './sites'; @@ -30,7 +31,8 @@ import { makeSingleton } from '@singletons/core.singletons'; @Injectable() export class CoreFileHelperProvider { - constructor(protected fileProvider: CoreFileProvider, + constructor(protected domUtils: CoreDomUtilsProvider, + protected fileProvider: CoreFileProvider, protected filepoolProvider: CoreFilepoolProvider, protected sitesProvider: CoreSitesProvider, protected appProvider: CoreAppProvider, @@ -339,6 +341,57 @@ export class CoreFileHelperProvider { throw new Error('Couldn\'t determine file size: ' + file.fileurl); } + + /** + * Is the file openable in app. + * + * @param file The file to check. + * @return bool. + */ + isOpenableInApp(file: any): boolean { + const re = /(?:\.([^.]+))?$/; + + const ext = re.exec(file.filename)[1]; + + return !this.isFileTypeExcludedInApp(ext); + } + + /** + * Is the file openable in app. + * + * @param file The file to check. + * @return bool. + */ + async showConfirmOpenUnsupportedFile(): Promise { + try { + await this.domUtils.showConfirm(this.translate.instant('core.cannotopeninapp'), undefined, + this.translate.instant('core.openfile')); + + return true; + } + catch (e) { + return false; + } + } + + /** + * Is the file type excluded to open in app. + * + * @param file The file to check. + * @return bool. + */ + isFileTypeExcludedInApp(fileType: string): boolean { + const currentSite = this.sitesProvider.getCurrentSite(); + const fileTypeExcludeList = currentSite && currentSite.getStoredConfig('tool_mobile_filetypeexclusionlist'); + + if (!fileTypeExcludeList) { + return false; + } + + const regEx = new RegExp('(,|^)' + fileType + '(,|$)', 'g'); + + return !!fileTypeExcludeList.match(regEx); + } } export class CoreFileHelper extends makeSingleton(CoreFileHelperProvider) {} From 2c477ac93afb172296dbb2a34f11e837901ca699 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 8 Sep 2020 11:51:37 +0200 Subject: [PATCH 094/182] MOBILE-3489 location: Use diagnostic plugin to enable location --- package-lock.json | 279 ++++++++++-------- package.json | 5 +- .../data/fields/latlong/component/latlong.ts | 129 ++++++-- src/addon/mod/data/lang/en.json | 1 + src/app/app.module.ts | 2 + src/assets/lang/en.json | 1 + src/classes/error.ts | 33 +++ src/providers/app.ts | 29 ++ src/singletons/core.singletons.ts | 3 + 9 files changed, 339 insertions(+), 143 deletions(-) create mode 100644 src/classes/error.ts diff --git a/package-lock.json b/package-lock.json index 402606118..4379c23e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -127,6 +127,11 @@ "resolved": "https://registry.npmjs.org/@ionic-native/device/-/device-4.20.0.tgz", "integrity": "sha512-ogHZwlC1GLbj2sL/eRp+RDs7bWc1AuwKNhgtDLE3yjXey09I5ErkADLydugMTEYoU/Wja9+YjXdZGymuaHwgNg==" }, + "@ionic-native/diagnostic": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ionic-native/diagnostic/-/diagnostic-4.2.0.tgz", + "integrity": "sha512-hCRYVseQrsbuA4EKgvmwJB/nweHcrBK+avE2+GyYcUFoNNhM5yz9i7mlw4J0Vw5mr2udAVhPdLyvJV5p1AkU4g==" + }, "@ionic-native/file": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@ionic-native/file/-/file-4.20.0.tgz", @@ -2536,7 +2541,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", "dev": true } } @@ -3451,6 +3457,12 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, "com-darryncampbell-cordova-plugin-intent": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/com-darryncampbell-cordova-plugin-intent/-/com-darryncampbell-cordova-plugin-intent-1.3.0.tgz", @@ -5103,6 +5115,25 @@ "resolved": "https://registry.npmjs.org/cordova-support-google-services/-/cordova-support-google-services-1.3.2.tgz", "integrity": "sha512-RtEWzULreUX662MFWopGhFispLiHX7gUf2GijPOC2mY2oCNuUobj2mO4tl5q7PYbOreSxq+PrSekhmS6TAAWdw==" }, + "cordova.plugins.diagnostic": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/cordova.plugins.diagnostic/-/cordova.plugins.diagnostic-5.0.2.tgz", + "integrity": "sha512-H59o7YxJ2/COzvg+jyTpUqX8QoDcvti9dluJ9a+pHumE8lf3meWemwCl0QFa9GH+xgVd6X1Ikj/6P3+DKWd9eg==", + "dev": true, + "requires": { + "colors": "^1.1.2", + "elementtree": "^0.1.6", + "minimist": "1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "core-js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", @@ -5708,7 +5739,7 @@ }, "electron-osx-sign": { "version": "0.4.10", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz", + "resolved": "http://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz", "integrity": "sha1-vk87ibKnWh3F8eckkIGrKSnKOiY=", "dev": true, "requires": { @@ -5739,7 +5770,7 @@ }, "xmlbuilder": { "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", "dev": true } @@ -7359,25 +7390,25 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "optional": true }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "optional": true, "requires": { @@ -7387,13 +7418,13 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "optional": true, "requires": { @@ -7403,25 +7434,25 @@ }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, @@ -7432,31 +7463,31 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { @@ -7472,7 +7503,7 @@ }, "glob": { "version": "7.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "optional": true, "requires": { @@ -7486,13 +7517,13 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": false, + "resolved": "", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "optional": true, "requires": { @@ -7501,7 +7532,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "optional": true, "requires": { @@ -7510,7 +7541,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { @@ -7520,19 +7551,19 @@ }, "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "optional": true }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "optional": true, "requires": { @@ -7541,13 +7572,13 @@ }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "optional": true, "requires": { @@ -7593,7 +7624,7 @@ }, "needle": { "version": "2.3.0", - "resolved": false, + "resolved": "", "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "optional": true, "requires": { @@ -7623,7 +7654,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": false, + "resolved": "", "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "optional": true, "requires": { @@ -7641,7 +7672,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { @@ -7651,13 +7682,13 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": false, + "resolved": "", "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "optional": true, "requires": { @@ -7667,7 +7698,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { @@ -7679,19 +7710,19 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "optional": true, "requires": { @@ -7700,19 +7731,19 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { @@ -7722,19 +7753,19 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "optional": true }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "optional": true, "requires": { @@ -7754,7 +7785,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "optional": true, "requires": { @@ -7769,7 +7800,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": false, + "resolved": "", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "optional": true, "requires": { @@ -7778,42 +7809,42 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.7.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "optional": true, "requires": { @@ -7824,7 +7855,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { @@ -7833,7 +7864,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "optional": true, "requires": { @@ -7842,7 +7873,7 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, @@ -7875,13 +7906,13 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "optional": true, "requires": { @@ -7890,7 +7921,7 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "optional": true }, @@ -8369,28 +8400,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, @@ -8401,14 +8432,14 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "optional": true, @@ -8419,42 +8450,42 @@ }, "chownr": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, @@ -8464,28 +8495,28 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "resolved": false, + "resolved": "", "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, @@ -8495,14 +8526,14 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, @@ -8519,7 +8550,7 @@ }, "glob": { "version": "7.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, @@ -8534,14 +8565,14 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": false, + "resolved": "", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, @@ -8551,7 +8582,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, @@ -8561,7 +8592,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, @@ -8572,21 +8603,21 @@ }, "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, @@ -8596,14 +8627,14 @@ }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "optional": true, @@ -8613,14 +8644,14 @@ }, "minimist": { "version": "0.0.8", - "resolved": false, + "resolved": "", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true }, "minipass": { "version": "2.3.5", - "resolved": false, + "resolved": "", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, "optional": true, @@ -8631,7 +8662,7 @@ }, "minizlib": { "version": "1.2.1", - "resolved": false, + "resolved": "", "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, @@ -8642,6 +8673,7 @@ "mkdirp": { "version": "0.5.1", "resolved": "", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, "requires": { @@ -8650,14 +8682,14 @@ }, "ms": { "version": "2.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true }, "needle": { "version": "2.3.0", - "resolved": false, + "resolved": "", "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, @@ -8669,7 +8701,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": false, + "resolved": "", "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, @@ -8688,7 +8720,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, @@ -8699,14 +8731,14 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": false, + "resolved": "", "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, @@ -8717,7 +8749,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, @@ -8730,21 +8762,21 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "optional": true, @@ -8754,21 +8786,21 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, @@ -8779,21 +8811,21 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, @@ -8807,6 +8839,7 @@ "minimist": { "version": "1.2.0", "resolved": "", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -8814,7 +8847,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, @@ -8830,7 +8863,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": false, + "resolved": "", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, @@ -8840,49 +8873,49 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.7.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, @@ -8894,7 +8927,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, @@ -8904,7 +8937,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "optional": true, @@ -8914,7 +8947,7 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true @@ -8922,6 +8955,7 @@ "tar": { "version": "4.4.8", "resolved": "", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, "requires": { @@ -8936,14 +8970,14 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, @@ -8953,14 +8987,14 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "optional": true }, "yallist": { "version": "3.0.3", - "resolved": false, + "resolved": "", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true, "optional": true @@ -14615,7 +14649,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", "dev": true } } diff --git a/package.json b/package.json index 1e2674500..43ab97950 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@ionic-native/clipboard": "^4.20.0", "@ionic-native/core": "^4.20.0", "@ionic-native/device": "^4.20.0", + "@ionic-native/diagnostic": "^4.2.0", "@ionic-native/file": "^4.20.0", "@ionic-native/file-opener": "^4.20.0", "@ionic-native/file-transfer": "^4.20.0", @@ -147,6 +148,7 @@ "@types/node": "^8.10.59", "@types/promise.prototype.finally": "^2.0.4", "acorn": "^5.7.4", + "cordova.plugins.diagnostic": "^5.0.2", "electron-builder-lib": "^20.23.1", "electron-rebuild": "^1.10.0", "gulp": "4.0.2", @@ -223,7 +225,8 @@ "cordova-plugin-wkuserscript": {}, "cordova-plugin-media": { "KEEP_AVAUDIOSESSION_ALWAYS_ACTIVE": "NO" - } + }, + "cordova.plugins.diagnostic": {} } }, "main": "desktop/electron.js", diff --git a/src/addon/mod/data/fields/latlong/component/latlong.ts b/src/addon/mod/data/fields/latlong/component/latlong.ts index fb3d0b27e..6beb14285 100644 --- a/src/addon/mod/data/fields/latlong/component/latlong.ts +++ b/src/addon/mod/data/fields/latlong/component/latlong.ts @@ -15,10 +15,14 @@ import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { Platform } from 'ionic-angular'; -import { Geolocation, GeolocationOptions } from '@ionic-native/geolocation'; +import { Geolocation } from '@ionic-native/geolocation'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; import { CoreApp, CoreAppProvider } from '@providers/app'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { Diagnostic } from '@singletons/core.singletons'; +import { CoreError } from '@classes/error'; + +class AccessLocationError extends CoreError {} /** * Component to render data latlong field. @@ -116,33 +120,118 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo * * @param $event The event. */ - getLocation(event: Event): void { + async getLocation(event: Event): Promise { event.preventDefault(); const modal = this.domUtils.showModalLoading('addon.mod_data.gettinglocation', true); - const options: GeolocationOptions = { - enableHighAccuracy: true, - timeout: 30000 - }; + try { + await this.updateLocation(); + } catch (error) { + this.showErrorModal(error); + } - this.geolocation.getCurrentPosition(options).then((result) => { - this.form.controls['f_' + this.field.id + '_0'].setValue(result.coords.latitude); - this.form.controls['f_' + this.field.id + '_1'].setValue(result.coords.longitude); - }).catch((error) => { - if (this.isPermissionDeniedError(error)) { - this.domUtils.showErrorModal('addon.mod_data.locationpermissiondenied', true); - - return; - } - - this.domUtils.showErrorModalDefault(error, 'Error getting location'); - }).finally(() => { - modal.dismiss(); - }); + modal.dismiss(); } + /** + * Update component location. + */ + protected async updateLocation(): Promise { + await this.authorizeLocation(); + await this.enableLocation(); + + const result = await this.geolocation.getCurrentPosition({ + enableHighAccuracy: true, + timeout: 30000, + }); + + this.form.controls['f_' + this.field.id + '_0'].setValue(result.coords.latitude); + this.form.controls['f_' + this.field.id + '_1'].setValue(result.coords.longitude); + } + + /** + * Make sure that using device location has been authorize and ask for permission if it hasn't. + * + * @param failOnDeniedOnce Throw an exception if the permission has been denied once. + */ + protected async authorizeLocation(failOnDeniedOnce: boolean = false): Promise { + const authorizationStatus = await Diagnostic.instance.getLocationAuthorizationStatus(); + + switch (authorizationStatus) { + // This constant is hard-coded because it is not declared in @ionic-native/diagnostic v4. + case 'DENIED_ONCE': + if (failOnDeniedOnce) { + throw new AccessLocationError('addon.mod_data.locationpermissiondenied'); + } + // Fall through. + case Diagnostic.instance.permissionStatus.NOT_REQUESTED: + await Diagnostic.instance.requestLocationAuthorization(); + await CoreApp.instance.waitForResume(500); + await this.authorizeLocation(true); + + return; + case Diagnostic.instance.permissionStatus.GRANTED: + case Diagnostic.instance.permissionStatus.GRANTED_WHEN_IN_USE: + // Location is authorized. + return; + case Diagnostic.instance.permissionStatus.DENIED: + default: + throw new AccessLocationError('addon.mod_data.locationpermissiondenied'); + } + } + + /** + * Make sure that location is enabled and switch to settings if it hasn't. + */ + protected async enableLocation(): Promise { + let locationEnabled = await Diagnostic.instance.isLocationEnabled(); + + if (locationEnabled) { + // Location is enabled. + return; + } + + if (!CoreApp.instance.isIOS()) { + await Diagnostic.instance.switchToLocationSettings(); + await CoreApp.instance.waitForResume(30000); + + locationEnabled = await Diagnostic.instance.isLocationEnabled(); + } + + if (!locationEnabled) { + throw new AccessLocationError('addon.mod_data.locationnotenabled'); + } + } + + /** + * Check whether an error was caused by a PERMISSION_DENIED. + * + * @param error Error. + */ protected isPermissionDeniedError(error?: any): boolean { return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; } + + /** + * Show the appropriate error modal for the given error. + * + * @param error Error. + */ + protected showErrorModal(error: any): void { + if (error instanceof AccessLocationError) { + this.domUtils.showErrorModal(error.message, true); + + return; + } + + if (this.isPermissionDeniedError(error)) { + this.domUtils.showErrorModal('addon.mod_data.locationpermissiondenied', true); + + return; + } + + this.domUtils.showErrorModalDefault(error, 'Error getting location'); + } + } diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json index de89c6bca..106896f4f 100644 --- a/src/addon/mod/data/lang/en.json +++ b/src/addon/mod/data/lang/en.json @@ -23,6 +23,7 @@ "gettinglocation": "Getting location", "latlongboth": "Both latitude and longitude are required.", "locationpermissiondenied": "Permission to access your location has been denied.", + "locationnotenabled": "Location is not enabled", "menuchoose": "Choose...", "modulenameplural": "Databases", "more": "More", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d5080dccb..911f2d755 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -25,6 +25,7 @@ import { MockLocationStrategy } from '@angular/common/testing'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { Diagnostic } from '@ionic-native/diagnostic'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { MoodleMobileApp } from './app.component'; @@ -348,6 +349,7 @@ export const WP_PROVIDER: any = null; useClass: CoreInterceptor, multi: true, }, + Diagnostic, ScreenOrientation, {provide: COMPILER_OPTIONS, useValue: {}, multi: true}, {provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS]}, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 6d3fd2ffe..fafd7b6f2 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -508,6 +508,7 @@ "addon.mod_data.foundrecords": "Found records: {{$a.num}}/{{$a.max}} (Reset filters)", "addon.mod_data.gettinglocation": "Getting location", "addon.mod_data.latlongboth": "Both latitude and longitude are required.", + "addon.mod_data.locationnotenabled": "Location is not enabled", "addon.mod_data.locationpermissiondenied": "Permission to access your location has been denied.", "addon.mod_data.menuchoose": "Choose...", "addon.mod_data.modulenameplural": "Databases", diff --git a/src/classes/error.ts b/src/classes/error.ts new file mode 100644 index 000000000..2bbb0b737 --- /dev/null +++ b/src/classes/error.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Base Error class. + * + * The native Error class cannot be extended in Typescript without restoring the prototype chain, extend this + * class instead. + * + * @see https://stackoverflow.com/questions/41102060/typescript-extending-error-class + */ +export class CoreError extends Error { + + constructor(message?: string) { + super(message); + + // Fix prototype chain: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + this.name = new.target.name; + Object.setPrototypeOf(this, new.target.prototype); + } + +} diff --git a/src/providers/app.ts b/src/providers/app.ts index 206528dc7..70b07d37d 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -654,6 +654,35 @@ export class CoreAppProvider { return this.ssoAuthenticationPromise || Promise.resolve(); } + /** + * Wait until the application is resumed. + * + * @param timeout Maximum time to wait, use null to wait forever. + */ + async waitForResume(timeout: number | null = null): Promise { + let resolve: Function; + let resumeSubscription: any; + let timeoutId: NodeJS.Timer | false; + + const promise = new Promise((r): any => resolve = r); + const stopWaiting = (): any => { + if (!resolve) { + return; + } + + resolve(); + resumeSubscription.unsubscribe(); + timeoutId && clearTimeout(timeoutId); + + resolve = null; + }; + + resumeSubscription = this.platform.resume.subscribe(stopWaiting); + timeoutId = timeout ? setTimeout(stopWaiting, timeout) : false; + + await promise; + } + /** * Retrieve redirect data. * diff --git a/src/singletons/core.singletons.ts b/src/singletons/core.singletons.ts index 7eacfe6bc..ad230e5ca 100644 --- a/src/singletons/core.singletons.ts +++ b/src/singletons/core.singletons.ts @@ -16,6 +16,7 @@ import { AlertController, App } from 'ionic-angular'; import { Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { HttpClient } from '@angular/common/http'; +import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic'; import { CoreSingletonsFactory, CoreInjectionToken, CoreSingletonClass } from '@classes/singletons-factory'; @@ -46,4 +47,6 @@ export class Alerts extends makeSingleton(AlertController) {} export class Ionic extends makeSingleton(App) {} +export class Diagnostic extends makeSingleton(DiagnosticService) {} + export class Http extends makeSingleton(HttpClient) {} From af5f0102390d13c1160b7f32ac60d2318608ea61 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 14 Sep 2020 10:52:49 +0200 Subject: [PATCH 095/182] MOBILE-3489 location: Expose geolocation service to plugins --- .../data/fields/latlong/component/latlong.ts | 132 ++++------------ src/app/app.module.ts | 7 +- src/core/compile/providers/compile.ts | 3 + src/providers/geolocation.ts | 143 ++++++++++++++++++ src/singletons/core.singletons.ts | 3 + 5 files changed, 184 insertions(+), 104 deletions(-) create mode 100644 src/providers/geolocation.ts diff --git a/src/addon/mod/data/fields/latlong/component/latlong.ts b/src/addon/mod/data/fields/latlong/component/latlong.ts index 6beb14285..f34e13102 100644 --- a/src/addon/mod/data/fields/latlong/component/latlong.ts +++ b/src/addon/mod/data/fields/latlong/component/latlong.ts @@ -14,15 +14,10 @@ import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { Platform } from 'ionic-angular'; -import { Geolocation } from '@ionic-native/geolocation'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; import { CoreApp, CoreAppProvider } from '@providers/app'; +import { CoreGeolocation, CoreGeolocationError, CoreGeolocationErrorReason } from '@providers/geolocation'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { Diagnostic } from '@singletons/core.singletons'; -import { CoreError } from '@classes/error'; - -class AccessLocationError extends CoreError {} /** * Component to render data latlong field. @@ -37,13 +32,11 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo east: number; showGeolocation: boolean; - constructor(protected fb: FormBuilder, - protected platform: Platform, - protected geolocation: Geolocation, + constructor( + protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected sanitizer: DomSanitizer, - appProvider: CoreAppProvider - ) { + appProvider: CoreAppProvider) { super(fb); this.showGeolocation = !appProvider.isDesktop(); @@ -126,107 +119,25 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo const modal = this.domUtils.showModalLoading('addon.mod_data.gettinglocation', true); try { - await this.updateLocation(); + const coordinates = await CoreGeolocation.instance.getCoordinates(); + + this.form.controls['f_' + this.field.id + '_0'].setValue(coordinates.latitude); + this.form.controls['f_' + this.field.id + '_1'].setValue(coordinates.longitude); } catch (error) { - this.showErrorModal(error); + this.showLocationErrorModal(error); } modal.dismiss(); } /** - * Update component location. - */ - protected async updateLocation(): Promise { - await this.authorizeLocation(); - await this.enableLocation(); - - const result = await this.geolocation.getCurrentPosition({ - enableHighAccuracy: true, - timeout: 30000, - }); - - this.form.controls['f_' + this.field.id + '_0'].setValue(result.coords.latitude); - this.form.controls['f_' + this.field.id + '_1'].setValue(result.coords.longitude); - } - - /** - * Make sure that using device location has been authorize and ask for permission if it hasn't. + * Show the appropriate error modal for the given error getting the location. * - * @param failOnDeniedOnce Throw an exception if the permission has been denied once. + * @param error Location error. */ - protected async authorizeLocation(failOnDeniedOnce: boolean = false): Promise { - const authorizationStatus = await Diagnostic.instance.getLocationAuthorizationStatus(); - - switch (authorizationStatus) { - // This constant is hard-coded because it is not declared in @ionic-native/diagnostic v4. - case 'DENIED_ONCE': - if (failOnDeniedOnce) { - throw new AccessLocationError('addon.mod_data.locationpermissiondenied'); - } - // Fall through. - case Diagnostic.instance.permissionStatus.NOT_REQUESTED: - await Diagnostic.instance.requestLocationAuthorization(); - await CoreApp.instance.waitForResume(500); - await this.authorizeLocation(true); - - return; - case Diagnostic.instance.permissionStatus.GRANTED: - case Diagnostic.instance.permissionStatus.GRANTED_WHEN_IN_USE: - // Location is authorized. - return; - case Diagnostic.instance.permissionStatus.DENIED: - default: - throw new AccessLocationError('addon.mod_data.locationpermissiondenied'); - } - } - - /** - * Make sure that location is enabled and switch to settings if it hasn't. - */ - protected async enableLocation(): Promise { - let locationEnabled = await Diagnostic.instance.isLocationEnabled(); - - if (locationEnabled) { - // Location is enabled. - return; - } - - if (!CoreApp.instance.isIOS()) { - await Diagnostic.instance.switchToLocationSettings(); - await CoreApp.instance.waitForResume(30000); - - locationEnabled = await Diagnostic.instance.isLocationEnabled(); - } - - if (!locationEnabled) { - throw new AccessLocationError('addon.mod_data.locationnotenabled'); - } - } - - /** - * Check whether an error was caused by a PERMISSION_DENIED. - * - * @param error Error. - */ - protected isPermissionDeniedError(error?: any): boolean { - return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; - } - - /** - * Show the appropriate error modal for the given error. - * - * @param error Error. - */ - protected showErrorModal(error: any): void { - if (error instanceof AccessLocationError) { - this.domUtils.showErrorModal(error.message, true); - - return; - } - - if (this.isPermissionDeniedError(error)) { - this.domUtils.showErrorModal('addon.mod_data.locationpermissiondenied', true); + protected showLocationErrorModal(error: any): void { + if (error instanceof CoreGeolocationError) { + this.domUtils.showErrorModal(this.getGeolocationErrorMessage(error), true); return; } @@ -234,4 +145,19 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo this.domUtils.showErrorModalDefault(error, 'Error getting location'); } + /** + * Get error message from a geolocation error. + * + * @param error Geolocation error. + */ + protected getGeolocationErrorMessage(error: CoreGeolocationError): string { + // tslint:disable-next-line: switch-default + switch (error.reason) { + case CoreGeolocationErrorReason.PermissionDenied: + return 'addon.mod_data.locationpermissiondenied'; + case CoreGeolocationErrorReason.LocationNotEnabled: + return 'addon.mod_data.locationnotenabled'; + } + } + } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 911f2d755..cb49b0c71 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -26,6 +26,7 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { Diagnostic } from '@ionic-native/diagnostic'; +import { Geolocation } from '@ionic-native/geolocation'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { MoodleMobileApp } from './app.component'; @@ -60,6 +61,7 @@ import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; import { CoreSyncProvider } from '@providers/sync'; import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; +import { CoreGeolocationProvider } from '@providers/geolocation'; // Handlers. import { CoreSiteInfoCronHandler } from '@providers/handlers/site-info-cron-handler'; @@ -195,7 +197,8 @@ export const CORE_PROVIDERS: any[] = [ CorePluginFileDelegate, CoreSyncProvider, CoreFileHelperProvider, - CoreCustomURLSchemesProvider + CoreCustomURLSchemesProvider, + CoreGeolocationProvider, ]; export const WP_PROVIDER: any = null; @@ -343,6 +346,7 @@ export const WP_PROVIDER: any = null; CoreSyncProvider, CoreFileHelperProvider, CoreCustomURLSchemesProvider, + CoreGeolocationProvider, CoreSiteInfoCronHandler, { provide: HTTP_INTERCEPTORS, @@ -350,6 +354,7 @@ export const WP_PROVIDER: any = null; multi: true, }, Diagnostic, + Geolocation, ScreenOrientation, {provide: COMPILER_OPTIONS, useValue: {}, multi: true}, {provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS]}, diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index e62e8ba70..5a34199b7 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -67,6 +67,7 @@ import { CoreContentLinksModuleGradeHandler } from '@core/contentlinks/classes/m import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler'; +import { CoreGeolocationError, CoreGeolocationErrorReason } from '@providers/geolocation'; // Import all core modules that define components, directives and pipes. import { CoreComponentsModule } from '@components/components.module'; @@ -294,6 +295,8 @@ export class CoreCompileProvider { instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; instance['CoreSitePluginsAssignFeedbackComponent'] = CoreSitePluginsAssignFeedbackComponent; instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent; + instance['CoreGeolocationError'] = CoreGeolocationError; + instance['CoreGeolocationErrorReason'] = CoreGeolocationErrorReason; } /** diff --git a/src/providers/geolocation.ts b/src/providers/geolocation.ts new file mode 100644 index 000000000..138664645 --- /dev/null +++ b/src/providers/geolocation.ts @@ -0,0 +1,143 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { Coordinates } from '@ionic-native/geolocation'; +import { CoreApp } from '@providers/app'; +import { Geolocation, Diagnostic, makeSingleton } from '@singletons/core.singletons'; +import { CoreError } from '@classes/error'; + +export enum CoreGeolocationErrorReason { + PermissionDenied = 'permission-denied', + LocationNotEnabled = 'location-not-enabled', +} + +export class CoreGeolocationError extends CoreError { + + readonly reason: CoreGeolocationErrorReason; + + constructor(reason: CoreGeolocationErrorReason) { + super(`GeolocationError: ${reason}`); + + this.reason = reason; + } + +} + +@Injectable() +export class CoreGeolocationProvider { + + /** + * Get current user coordinates. + * + * @throws {CoreGeolocationError} + */ + async getCoordinates(): Promise { + try { + await this.authorizeLocation(); + await this.enableLocation(); + + const result = await Geolocation.instance.getCurrentPosition({ + enableHighAccuracy: true, + timeout: 30000, + }); + + return result.coords; + } catch (error) { + if (this.isCordovaPermissionDeniedError(error)) { + throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied); + } + + throw error; + } + } + + /** + * Make sure that using device location has been authorized and ask for permission if it hasn't. + * + * @throws {CoreGeolocationError} + */ + async authorizeLocation(): Promise { + await this.doAuthorizeLocation(); + } + + /** + * Make sure that location is enabled and open settings to enable it if necessary. + * + * @throws {CoreGeolocationError} + */ + async enableLocation(): Promise { + let locationEnabled = await Diagnostic.instance.isLocationEnabled(); + + if (locationEnabled) { + // Location is enabled. + return; + } + + if (!CoreApp.instance.isIOS()) { + await Diagnostic.instance.switchToLocationSettings(); + await CoreApp.instance.waitForResume(30000); + + locationEnabled = await Diagnostic.instance.isLocationEnabled(); + } + + if (!locationEnabled) { + throw new CoreGeolocationError(CoreGeolocationErrorReason.LocationNotEnabled); + } + } + + /** + * Recursive implementation of authorizeLocation method, protected to avoid exposing the failOnDeniedOnce parameter. + * + * @param failOnDeniedOnce Throw an exception if the permission has been denied once. + * @throws {CoreGeolocationError} + */ + protected async doAuthorizeLocation(failOnDeniedOnce: boolean = false): Promise { + const authorizationStatus = await Diagnostic.instance.getLocationAuthorizationStatus(); + + switch (authorizationStatus) { + // This constant is hard-coded because it is not declared in @ionic-native/diagnostic v4. + case 'DENIED_ONCE': + if (failOnDeniedOnce) { + throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied); + } + // Fall through. + case Diagnostic.instance.permissionStatus.NOT_REQUESTED: + await Diagnostic.instance.requestLocationAuthorization(); + await CoreApp.instance.waitForResume(500); + await this.doAuthorizeLocation(true); + + return; + case Diagnostic.instance.permissionStatus.GRANTED: + case Diagnostic.instance.permissionStatus.GRANTED_WHEN_IN_USE: + // Location is authorized. + return; + case Diagnostic.instance.permissionStatus.DENIED: + default: + throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied); + } + } + + /** + * Check whether an error was caused by a PERMISSION_DENIED from the cordova plugin. + * + * @param error Error. + */ + protected isCordovaPermissionDeniedError(error?: any): boolean { + return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; + } + +} + +export class CoreGeolocation extends makeSingleton(CoreGeolocationProvider) {} diff --git a/src/singletons/core.singletons.ts b/src/singletons/core.singletons.ts index ad230e5ca..349afea24 100644 --- a/src/singletons/core.singletons.ts +++ b/src/singletons/core.singletons.ts @@ -16,6 +16,7 @@ import { AlertController, App } from 'ionic-angular'; import { Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { HttpClient } from '@angular/common/http'; +import { Geolocation as GeolocationService } from '@ionic-native/geolocation'; import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic'; import { CoreSingletonsFactory, CoreInjectionToken, CoreSingletonClass } from '@classes/singletons-factory'; @@ -49,4 +50,6 @@ export class Ionic extends makeSingleton(App) {} export class Diagnostic extends makeSingleton(DiagnosticService) {} +export class Geolocation extends makeSingleton(GeolocationService) {} + export class Http extends makeSingleton(HttpClient) {} From 9dce6d408b9168092350f91760bb7e003fc52efc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 21 Sep 2020 10:39:23 +0200 Subject: [PATCH 096/182] MOBILE-3432 core: Make file type exclusion modal always on top --- src/app/app.scss | 4 ++++ src/assets/lang/en.json | 2 ++ src/lang/en.json | 2 ++ src/providers/utils/dom.ts | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/app.scss b/src/app/app.scss index 05557bc56..186461c52 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -873,6 +873,10 @@ ion-app.app-root { height: 100% !important; } + .core-modal-force-on-top { + z-index: 100000 !important; + } + @media only screen and (min-height: 400px) and (min-width: 300px) { .core-modal-lateral { @include core-split-area-end(); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 699eea430..71a709151 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1374,6 +1374,7 @@ "core.cannotconnectverify": "Please check the address is correct.", "core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", "core.cannotopeninapp": "This file may not work as expected on this device. Would you like to open it anyway?", + "core.cannotopeninappdownload": "This file may not work as expected on this device. Would you like to download it anyway?", "core.captureaudio": "Record audio", "core.capturedimage": "Taken picture.", "core.captureimage": "Take picture", @@ -1526,6 +1527,7 @@ "core.done": "Done", "core.download": "Download", "core.downloaded": "Downloaded", + "core.downloadfile": "Download file", "core.downloading": "Downloading", "core.edit": "Edit", "core.editor.autosavesucceeded": "Draft saved.", diff --git a/src/lang/en.json b/src/lang/en.json index 97fcaba84..f00f084d3 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -17,6 +17,7 @@ "cannotconnectverify": "Please check the address is correct.", "cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", "cannotopeninapp": "This file may not work as expected on this device. Would you like to open it anyway?", + "cannotopeninappdownload": "This file may not work as expected on this device. Would you like to download it anyway?", "captureaudio": "Record audio", "capturedimage": "Taken picture.", "captureimage": "Take picture", @@ -83,6 +84,7 @@ "done": "Done", "download": "Download", "downloaded": "Downloaded", + "downloadfile": "Download file", "downloading": "Downloading", "edit": "Edit", "emptysplit": "This page will appear blank if the left panel is empty or is loading.", diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 9297320de..0a0d7a016 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -1287,7 +1287,7 @@ export class CoreDomUtilsProvider { ]; if (!title) { - options.cssClass = 'core-nohead'; + options.cssClass = (options.cssClass || '') + ' core-nohead'; } this.showAlertWithOptions(options, 0); From b046d57f2b530c6d0d6caba7e58fc82759821b8d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 21 Sep 2020 10:41:43 +0200 Subject: [PATCH 097/182] MOBILE-3432 core: Move isOpenableInApp check to more reusable functions --- src/addon/mod/resource/providers/helper.ts | 14 +-- src/components/file/file.ts | 60 ++++++----- src/core/course/providers/helper.ts | 4 + src/providers/file-helper.ts | 116 ++++++++++----------- 4 files changed, 91 insertions(+), 103 deletions(-) diff --git a/src/addon/mod/resource/providers/helper.ts b/src/addon/mod/resource/providers/helper.ts index 8f43d77ee..62a00e511 100644 --- a/src/addon/mod/resource/providers/helper.ts +++ b/src/addon/mod/resource/providers/helper.ts @@ -183,19 +183,7 @@ export class AddonModResourceHelperProvider { * @param courseId Course Id, used for completion purposes. * @return Resolved when done. */ - async openModuleFile(module: any, courseId: number): Promise { - // Check whether the file type excluded to open in app. - if (!module.contents.length) { - await this.courseProvider.loadModuleContents(module, courseId); - } - - if (!this.fileHelper.isOpenableInApp(module.contents[0])) { - const confirmed = await this.fileHelper.showConfirmOpenUnsupportedFile(); - if (!confirmed) { - return; - } - } - + openModuleFile(module: any, courseId: number): Promise { const modal = this.domUtils.showModalLoading(); // Download and open the file from the resource contents. diff --git a/src/components/file/file.ts b/src/components/file/file.ts index d920b54f9..34c89be6a 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -156,13 +156,6 @@ export class CoreFileComponent implements OnInit, OnDestroy { return; } - if (!this.fileHelper.isOpenableInApp(this.file)) { - const confirmed = await this.fileHelper.showConfirmOpenUnsupportedFile(); - if (!confirmed) { - return; - } - } - if (!this.canDownload || !this.state || this.state == CoreConstants.NOT_DOWNLOADABLE) { // File cannot be downloaded, just open it. if (this.file.toURL) { @@ -188,32 +181,45 @@ export class CoreFileComponent implements OnInit, OnDestroy { if (openAfterDownload) { // File needs to be opened now. - this.openFile().catch((error) => { + try { + await this.openFile(); + } catch (error) { this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); - }); + } } else { - // File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big. - this.pluginFileDelegate.getFileSize({fileurl: this.fileUrl, filesize: this.fileSize}, this.siteId).then((size) => { + // File doesn't need to be opened (it's a prefetch). + if (!this.fileHelper.isOpenableInApp(this.file)) { + try { + await this.fileHelper.showConfirmOpenUnsupportedFile(true); + } catch (error) { + return; // Cancelled, stop. + } + } - const promise = size ? this.domUtils.confirmDownloadSize({ size: size, total: true }) : Promise.resolve(); + try { + // Show confirm modal if file size is defined and it's big. + const size = await this.pluginFileDelegate.getFileSize({fileurl: this.fileUrl, filesize: this.fileSize}, + this.siteId); - return promise.then(() => { - // User confirmed, add the file to queue. - return this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl).finally(() => { - this.isDownloading = true; + if (size) { + await this.domUtils.confirmDownloadSize({ size: size, total: true }); + } - this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component, - this.componentId, this.timemodified, undefined, undefined, 0, this.file).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); - this.calculateState(); - }); - }); - }).catch(() => { - // User cancelled. - }); - }).catch((error) => { + // User confirmed, add the file to queue. + await this.utils.ignoreErrors(this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl)); + + this.isDownloading = true; + + try { + await this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component, + this.componentId, this.timemodified, undefined, undefined, 0, this.file); + } catch (error) { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + this.calculateState(); + } + } catch (error) { this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); - }); + } } } diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index ec7939c3d..99e181b96 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -585,6 +585,10 @@ export class CoreCourseHelperProvider { return Promise.reject(this.utils.createFakeWSError('core.filenotfound', true)); } + if (!this.fileHelper.isOpenableInApp(module.contents[0])) { + return this.fileHelper.showConfirmOpenUnsupportedFile(); + } + }).then(() => { return this.sitesProvider.getSite(siteId); }).then((site) => { const mainFile = files[0], diff --git a/src/providers/file-helper.ts b/src/providers/file-helper.ts index 11e0e6345..c2c082038 100644 --- a/src/providers/file-helper.ts +++ b/src/providers/file-helper.ts @@ -51,63 +51,58 @@ export class CoreFileHelperProvider { * @param siteId The site ID. If not defined, current site. * @return Resolved on success. */ - downloadAndOpenFile(file: any, component: string, componentId: string | number, state?: string, - onProgress?: (event: any) => any, siteId?: string): Promise { + async downloadAndOpenFile(file: any, component: string, componentId: string | number, state?: string, + onProgress?: (event: any) => any, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - const fileUrl = this.getFileUrl(file), - timemodified = this.getFileTimemodified(file); + const fileUrl = this.getFileUrl(file); + const timemodified = this.getFileTimemodified(file); + + if (!this.isOpenableInApp(file)) { + await this.showConfirmOpenUnsupportedFile(); + } + + let url = await this.downloadFileIfNeeded(file, fileUrl, component, componentId, timemodified, state, onProgress, siteId); + + if (!url) { + return; + } + + if (!CoreUrlUtils.instance.isLocalFileUrl(url)) { + /* In iOS, if we use the same URL in embedded browser and background download then the download only + downloads a few bytes (cached ones). Add a hash to the URL so both URLs are different. */ + url = url + '#moodlemobile-embedded'; + + try { + await this.utils.openOnlineFile(url); - return this.downloadFileIfNeeded(file, fileUrl, component, componentId, timemodified, state, onProgress, siteId) - .then((url) => { - if (!url) { return; + } catch (error) { + // Error opening the file, some apps don't allow opening online files. + if (!this.fileProvider.isAvailable()) { + throw error; + } + + // Get the state. + if (!state) { + state = await this.filepoolProvider.getFileStateByUrl(siteId, fileUrl, timemodified); + } + + if (state == CoreConstants.DOWNLOADING) { + throw new Error(this.translate.instant('core.erroropenfiledownloading')); + } + + if (state === CoreConstants.NOT_DOWNLOADED) { + // File is not downloaded, download and then return the local URL. + url = await this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); + } else { + // File is outdated and can't be opened in online, return the local URL. + url = await this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); + } } + } - if (!CoreUrlUtils.instance.isLocalFileUrl(url)) { - /* In iOS, if we use the same URL in embedded browser and background download then the download only - downloads a few bytes (cached ones). Add a hash to the URL so both URLs are different. */ - url = url + '#moodlemobile-embedded'; - - return this.utils.openOnlineFile(url).catch((error) => { - // Error opening the file, some apps don't allow opening online files. - if (!this.fileProvider.isAvailable()) { - return Promise.reject(error); - } - - let promise; - - // Get the state. - if (state) { - promise = Promise.resolve(state); - } else { - promise = this.filepoolProvider.getFileStateByUrl(siteId, fileUrl, timemodified); - } - - return promise.then((state) => { - if (state == CoreConstants.DOWNLOADING) { - return Promise.reject(this.translate.instant('core.erroropenfiledownloading')); - } - - let promise; - - if (state === CoreConstants.NOT_DOWNLOADED) { - // File is not downloaded, download and then return the local URL. - promise = this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); - } else { - // File is outdated and can't be opened in online, return the local URL. - promise = this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl); - } - - return promise.then((url) => { - return this.utils.openFile(url); - }); - }); - }); - } else { - return this.utils.openFile(url); - } - }); + return this.utils.openFile(url); } /** @@ -357,21 +352,16 @@ export class CoreFileHelperProvider { } /** - * Is the file openable in app. + * Show a confirm asking the user if we wants to open the file. * - * @param file The file to check. - * @return bool. + * @param onlyDownload Whether the user is only downloading the file, not opening it. + * @return Promise resolved if confirmed, rejected otherwise. */ - async showConfirmOpenUnsupportedFile(): Promise { - try { - await this.domUtils.showConfirm(this.translate.instant('core.cannotopeninapp'), undefined, - this.translate.instant('core.openfile')); + showConfirmOpenUnsupportedFile(onlyDownload?: boolean): Promise { + const message = this.translate.instant('core.cannotopeninapp' + (onlyDownload ? 'download' : '')); + const okButton = this.translate.instant(onlyDownload ? 'core.downloadfile' : 'core.openfile'); - return true; - } - catch (e) { - return false; - } + return this.domUtils.showConfirm(message, undefined, okButton, undefined, { cssClass: 'core-modal-force-on-top' }); } /** From 9a3332f01df3f9ec7dbe690710b6541b13199142 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 21 Sep 2020 11:31:07 +0200 Subject: [PATCH 098/182] MOBILE-3432 core: Add isOpenableInApp to more places --- scripts/langindex.json | 6 +++-- src/components/local-file/local-file.ts | 11 +++++++- src/directives/link.ts | 36 ++++++++++++++++++------- src/providers/file-helper.ts | 4 +-- src/providers/utils/iframe.ts | 35 ++++++++++++++++++++---- src/singletons/window.ts | 11 ++++++++ 6 files changed, 83 insertions(+), 20 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index a3738eb16..db78fc2ed 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1373,7 +1373,8 @@ "core.cannotconnecttrouble": "local_moodlemobileapp", "core.cannotconnectverify": "local_moodlemobileapp", "core.cannotdownloadfiles": "local_moodlemobileapp", - "core.cannotopeninapp": "moodle", + "core.cannotopeninapp": "local_moodlemobileapp", + "core.cannotopeninappdownload": "local_moodlemobileapp", "core.captureaudio": "local_moodlemobileapp", "core.capturedimage": "local_moodlemobileapp", "core.captureimage": "local_moodlemobileapp", @@ -1526,6 +1527,7 @@ "core.done": "survey", "core.download": "moodle", "core.downloaded": "local_moodlemobileapp", + "core.downloadfile": "moodle", "core.downloading": "local_moodlemobileapp", "core.edit": "moodle", "core.editor.autosavesucceeded": "editor_atto", @@ -1917,7 +1919,7 @@ "core.offline": "message", "core.ok": "moodle", "core.online": "message", - "core.openfile": "moodle", + "core.openfile": "local_moodlemobileapp", "core.openfullimage": "local_moodlemobileapp", "core.openinbrowser": "local_moodlemobileapp", "core.openmodinbrowser": "local_moodlemobileapp", diff --git a/src/components/local-file/local-file.ts b/src/components/local-file/local-file.ts index cd0c573a3..eba54e903 100644 --- a/src/components/local-file/local-file.ts +++ b/src/components/local-file/local-file.ts @@ -15,6 +15,7 @@ import { Component, Input, Output, OnInit, EventEmitter, ViewChild, ElementRef } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; +import { CoreFileHelper } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; @@ -103,7 +104,7 @@ export class CoreLocalFileComponent implements OnInit { * * @param e Click event. */ - fileClicked(e: Event): void { + async fileClicked(e: Event): Promise { if (this.editMode) { return; } @@ -114,6 +115,14 @@ export class CoreLocalFileComponent implements OnInit { if (this.utils.isTrueOrOne(this.overrideClick) && this.onClick.observers.length) { this.onClick.emit(); } else { + if (!CoreFileHelper.instance.isOpenableInApp(this.file)) { + try { + await CoreFileHelper.instance.showConfirmOpenUnsupportedFile(); + } catch (error) { + return; // Cancelled, stop. + } + } + this.utils.openFile(this.file.toURL()); } } diff --git a/src/directives/link.ts b/src/directives/link.ts index 2384bbefa..9b8e96fe9 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -14,6 +14,7 @@ import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; import { NavController, Content } from 'ionic-angular'; +import { CoreFileHelper } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; @@ -21,7 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes'; +import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; /** * Directive to open a link in external browser. @@ -94,14 +95,27 @@ export class CoreLinkDirective implements OnInit { * Convenience function to correctly navigate, open file or url in the browser. * * @param href HREF to be opened. + * @return Promise resolved when done. */ - protected navigate(href: string): void { + protected async navigate(href: string): Promise { if (this.urlUtils.isLocalFileUrl(href)) { // We have a local file. - this.utils.openFile(href).catch((error) => { + const filename = href.substr(href.lastIndexOf('/') + 1); + + if (!CoreFileHelper.instance.isOpenableInApp({ filename })) { + try { + await CoreFileHelper.instance.showConfirmOpenUnsupportedFile(); + } catch (error) { + return; // Cancelled, stop. + } + } + + try { + await this.utils.openFile(href); + } catch (error) { this.domUtils.showErrorModal(error); - }); + } } else if (href.charAt(0) == '#') { href = href.substr(1); // In site links @@ -113,9 +127,11 @@ export class CoreLinkDirective implements OnInit { this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); } } else if (this.urlSchemesProvider.isCustomURL(href)) { - this.urlSchemesProvider.handleCustomURL(href).catch((error: CoreCustomURLSchemesHandleError) => { + try { + await this.urlSchemesProvider.handleCustomURL(href); + } catch (error) { this.urlSchemesProvider.treatHandleCustomURLError(error); - }); + } } else { // It's an external link, we will open with browser. Check if we need to auto-login. @@ -139,9 +155,9 @@ export class CoreLinkDirective implements OnInit { if (this.autoLogin == 'yes') { if (this.inApp) { - this.sitesProvider.getCurrentSite().openInAppWithAutoLogin(href); + await this.sitesProvider.getCurrentSite().openInAppWithAutoLogin(href); } else { - this.sitesProvider.getCurrentSite().openInBrowserWithAutoLogin(href); + await this.sitesProvider.getCurrentSite().openInBrowserWithAutoLogin(href); } } else if (this.autoLogin == 'no') { if (this.inApp) { @@ -151,9 +167,9 @@ export class CoreLinkDirective implements OnInit { } } else { if (this.inApp) { - this.sitesProvider.getCurrentSite().openInAppWithAutoLoginIfSameSite(href); + await this.sitesProvider.getCurrentSite().openInAppWithAutoLoginIfSameSite(href); } else { - this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(href); + await this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(href); } } } diff --git a/src/providers/file-helper.ts b/src/providers/file-helper.ts index c2c082038..9ec8d77a9 100644 --- a/src/providers/file-helper.ts +++ b/src/providers/file-helper.ts @@ -343,10 +343,10 @@ export class CoreFileHelperProvider { * @param file The file to check. * @return bool. */ - isOpenableInApp(file: any): boolean { + isOpenableInApp(file: {filename?: string, name?: string}): boolean { const re = /(?:\.([^.]+))?$/; - const ext = re.exec(file.filename)[1]; + const ext = re.exec(file.filename || file.name)[1]; return !this.isFileTypeExcludedInApp(ext); } diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index fc9d3afb9..0fdc5af74 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -18,6 +18,7 @@ import { TranslateService } from '@ngx-translate/core'; import { Network } from '@ionic-native/network'; import { CoreApp, CoreAppProvider } from '../app'; import { CoreFileProvider } from '../file'; +import { CoreFileHelper } from '../file-helper'; import { CoreLoggerProvider } from '../logger'; import { CoreSitesProvider } from '../sites'; import { CoreDomUtilsProvider } from './dom'; @@ -390,6 +391,16 @@ export class CoreIframeUtilsProvider { } } else if (this.urlUtils.isLocalFileUrl(url)) { // It's a local file. + const filename = url.substr(url.lastIndexOf('/') + 1); + + if (!CoreFileHelper.instance.isOpenableInApp({ filename })) { + try { + await CoreFileHelper.instance.showConfirmOpenUnsupportedFile(); + } catch (error) { + return; // Cancelled, stop. + } + } + try { await this.utils.openFile(url); } catch (error) { @@ -409,9 +420,10 @@ export class CoreIframeUtilsProvider { * @param link Data of the link clicked. * @param element Frame element. * @param event Click event. + * @return Promise resolved when done. */ - protected linkClicked(link: {href: string, target?: string}, element?: HTMLFrameElement | HTMLObjectElement, event?: Event) - : void { + protected async linkClicked(link: {href: string, target?: string}, element?: HTMLFrameElement | HTMLObjectElement, + event?: Event): Promise { if (event && event.defaultPrevented) { // Event already prevented by some other code. return; @@ -445,14 +457,27 @@ export class CoreIframeUtilsProvider { if (!this.sitesProvider.isLoggedIn()) { this.utils.openInBrowser(link.href); } else { - this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(link.href); + await this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(link.href); } } else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') { // Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser. event && event.preventDefault(); - this.utils.openFile(link.href).catch((error) => { + + const filename = link.href.substr(link.href.lastIndexOf('/') + 1); + + if (!CoreFileHelper.instance.isOpenableInApp({ filename })) { + try { + await CoreFileHelper.instance.showConfirmOpenUnsupportedFile(); + } catch (error) { + return; // Cancelled, stop. + } + } + + try { + await this.utils.openFile(link.href); + } catch (error) { this.domUtils.showErrorModal(error); - }); + } } else if (CoreApp.instance.isIOS() && (!link.target || link.target == '_self') && element) { // In cordova ios 4.1.0 links inside iframes stopped working. We'll manually treat them. event && event.preventDefault(); diff --git a/src/singletons/window.ts b/src/singletons/window.ts index e95e6dc6e..c17855ab1 100644 --- a/src/singletons/window.ts +++ b/src/singletons/window.ts @@ -13,6 +13,7 @@ // limitations under the License. import { NavController } from 'ionic-angular'; +import { CoreFileHelper } from '@providers/file-helper'; import { CoreSites } from '@providers/sites'; import { CoreUrlUtils } from '@providers/utils/url'; import { CoreUtils } from '@providers/utils/utils'; @@ -43,6 +44,16 @@ export class CoreWindow { */ static async open(url: string, name?: string, options?: CoreWindowOpenOptions): Promise { if (CoreUrlUtils.instance.isLocalFileUrl(url)) { + const filename = url.substr(url.lastIndexOf('/') + 1); + + if (!CoreFileHelper.instance.isOpenableInApp({ filename })) { + try { + await CoreFileHelper.instance.showConfirmOpenUnsupportedFile(); + } catch (error) { + return; // Cancelled, stop. + } + } + await CoreUtils.instance.openFile(url); } else { let treated: boolean; From d332c7e1d4b8f05d325d204bf231b907b966adb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 22 Sep 2020 09:38:16 +0200 Subject: [PATCH 099/182] MOBILE-3523 emulator: Change some wrong comments --- src/core/emulator/providers/local-notifications.ts | 2 +- src/core/emulator/providers/network.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index affff2e19..3809cf63d 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -24,7 +24,7 @@ import * as moment from 'moment'; import { Subject, Observable } from 'rxjs'; /** - * Emulates the Cordova Globalization plugin in desktop apps and in browser. + * Emulates the Local Notifications plugin in desktop apps and in browser. */ @Injectable() export class LocalNotificationsMock extends LocalNotifications { diff --git a/src/core/emulator/providers/network.ts b/src/core/emulator/providers/network.ts index cc036247e..bb90a2c6c 100644 --- a/src/core/emulator/providers/network.ts +++ b/src/core/emulator/providers/network.ts @@ -17,7 +17,7 @@ import { Network } from '@ionic-native/network'; import { Observable, Subject } from 'rxjs'; /** - * Emulates the Cordova Globalization plugin in desktop apps and in browser. + * Emulates the Ionic Network plugin in desktop apps and in browser. */ @Injectable() export class NetworkMock extends Network { From 06c63eb77e114c25b66e5dbc87ac0da6c60b8648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 22 Sep 2020 11:26:18 +0200 Subject: [PATCH 100/182] MOBILE-3557 course: Show Not available message on section title visible --- scripts/langindex.json | 1 + src/assets/lang/en.json | 1 + src/core/course/components/format/core-course-format.html | 5 +++-- src/core/course/pages/section-selector/section-selector.html | 1 + src/lang/en.json | 1 + 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index df9739345..51096032f 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1906,6 +1906,7 @@ "core.noresults": "moodle", "core.noselection": "form", "core.notapplicable": "local_moodlemobileapp", + "core.notavailable": "moodle", "core.notenrolledprofile": "moodle", "core.notice": "moodle", "core.notingroup": "moodle", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 6d3fd2ffe..c21658b17 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1906,6 +1906,7 @@ "core.noresults": "No results", "core.noselection": "No selection", "core.notapplicable": "n/a", + "core.notavailable": "Not available", "core.notenrolledprofile": "This profile is not available because this user is not enrolled in this course.", "core.notice": "Notice", "core.notingroup": "Sorry, but you need to be part of a group to see this page.", diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index 8ce6d7b62..fe87a85a1 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -73,12 +73,13 @@
- + -

+

{{ 'core.course.hiddenfromstudents' | translate }} + {{ 'core.notavailable' | translate }}

diff --git a/src/core/course/pages/section-selector/section-selector.html b/src/core/course/pages/section-selector/section-selector.html index e8af2bc87..9fdbf9eb8 100644 --- a/src/core/course/pages/section-selector/section-selector.html +++ b/src/core/course/pages/section-selector/section-selector.html @@ -16,6 +16,7 @@

{{ 'core.course.hiddenfromstudents' | translate }} + {{ 'core.notavailable' | translate }} diff --git a/src/lang/en.json b/src/lang/en.json index 40da0174b..b5f8447aa 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -189,6 +189,7 @@ "noresults": "No results", "noselection": "No selection", "notapplicable": "n/a", + "notavailable": "Not available", "notenrolledprofile": "This profile is not available because this user is not enrolled in this course.", "notice": "Notice", "nooptionavailable": "No option available", From 737e7f51e95ef74eda57eb1b0f39c94e04be2105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 22 Sep 2020 11:26:44 +0200 Subject: [PATCH 101/182] MOBILE-3557 course: Add section restriction info when selected section --- .../components/format/core-course-format.html | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index fe87a85a1..85b927443 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -24,13 +24,20 @@ - +
- - - + + + + + + {{ 'core.course.hiddenfromstudents' | translate }} + {{ 'core.notavailable' | translate }} + + +
From e52bf6f650c598cbe95927c3c54368436a42cbb4 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 18 Sep 2020 11:49:21 +0200 Subject: [PATCH 102/182] MOBILE-3516 core: Use fork of inappbrowser plugin --- package-lock.json | 251 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 126 insertions(+), 127 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4379c23e0..c44b655e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4718,9 +4718,8 @@ "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4=" }, "cordova-plugin-inappbrowser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-4.0.0.tgz", - "integrity": "sha512-w2LZzdF3R4G/EqVZ9aWch9Pksk76uw6/S5wFP1sgn7zjsSDpJBb/JhazLnioN1NZmZiCUBbROv1S4+9JCkeCgA==" + "version": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#715c858975cc1cb5d140afaa7973938511d38509", + "from": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle" }, "cordova-plugin-ionic-keyboard": { "version": "2.1.3", @@ -5739,7 +5738,7 @@ }, "electron-osx-sign": { "version": "0.4.10", - "resolved": "http://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz", "integrity": "sha1-vk87ibKnWh3F8eckkIGrKSnKOiY=", "dev": true, "requires": { @@ -5770,7 +5769,7 @@ }, "xmlbuilder": { "version": "8.2.2", - "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", "dev": true } @@ -7390,25 +7389,25 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": "", + "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "optional": true }, "aproba": { "version": "1.2.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": "", + "resolved": false, "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "optional": true, "requires": { @@ -7418,13 +7417,13 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": "", + "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "optional": true, "requires": { @@ -7434,25 +7433,25 @@ }, "code-point-at": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, @@ -7463,31 +7462,31 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": "", + "resolved": false, "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "optional": true }, "delegates": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs.realpath": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "resolved": "", + "resolved": false, "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { @@ -7503,7 +7502,7 @@ }, "glob": { "version": "7.1.3", - "resolved": "", + "resolved": false, "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "optional": true, "requires": { @@ -7517,13 +7516,13 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": "", + "resolved": false, "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "optional": true, "requires": { @@ -7532,7 +7531,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "optional": true, "requires": { @@ -7541,7 +7540,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": "", + "resolved": false, "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { @@ -7551,19 +7550,19 @@ }, "inherits": { "version": "2.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "optional": true }, "ini": { "version": "1.3.5", - "resolved": "", + "resolved": false, "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "optional": true, "requires": { @@ -7572,13 +7571,13 @@ }, "isarray": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": "", + "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "optional": true, "requires": { @@ -7624,7 +7623,7 @@ }, "needle": { "version": "2.3.0", - "resolved": "", + "resolved": false, "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "optional": true, "requires": { @@ -7654,7 +7653,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": "", + "resolved": false, "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "optional": true, "requires": { @@ -7672,7 +7671,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { @@ -7682,13 +7681,13 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": "", + "resolved": false, "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": "", + "resolved": false, "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "optional": true, "requires": { @@ -7698,7 +7697,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { @@ -7710,19 +7709,19 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": "", + "resolved": false, "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "resolved": "", + "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "optional": true, "requires": { @@ -7731,19 +7730,19 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "resolved": "", + "resolved": false, "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { @@ -7753,19 +7752,19 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "optional": true }, "rc": { "version": "1.2.8", - "resolved": "", + "resolved": false, "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "optional": true, "requires": { @@ -7785,7 +7784,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "", + "resolved": false, "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "optional": true, "requires": { @@ -7800,7 +7799,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": "", + "resolved": false, "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "optional": true, "requires": { @@ -7809,42 +7808,42 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safer-buffer": { "version": "2.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "resolved": "", + "resolved": false, "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.7.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "optional": true, "requires": { @@ -7855,7 +7854,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { @@ -7864,7 +7863,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "optional": true, "requires": { @@ -7873,7 +7872,7 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, @@ -7906,13 +7905,13 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": "", + "resolved": false, "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "optional": true, "requires": { @@ -7921,7 +7920,7 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "optional": true }, @@ -8400,28 +8399,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": "", + "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": "", + "resolved": false, "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, @@ -8432,14 +8431,14 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": "", + "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "optional": true, @@ -8450,42 +8449,42 @@ }, "chownr": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "4.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, @@ -8495,28 +8494,28 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": "", + "resolved": false, "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "resolved": "", + "resolved": false, "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, @@ -8526,14 +8525,14 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": "", + "resolved": false, "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, @@ -8550,7 +8549,7 @@ }, "glob": { "version": "7.1.3", - "resolved": "", + "resolved": false, "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, @@ -8565,14 +8564,14 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": "", + "resolved": false, "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, @@ -8582,7 +8581,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, @@ -8592,7 +8591,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": "", + "resolved": false, "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, @@ -8603,21 +8602,21 @@ }, "inherits": { "version": "2.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true }, "ini": { "version": "1.3.5", - "resolved": "", + "resolved": false, "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, @@ -8627,14 +8626,14 @@ }, "isarray": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": "", + "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "optional": true, @@ -8644,14 +8643,14 @@ }, "minimist": { "version": "0.0.8", - "resolved": "", + "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true }, "minipass": { "version": "2.3.5", - "resolved": "", + "resolved": false, "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, "optional": true, @@ -8662,7 +8661,7 @@ }, "minizlib": { "version": "1.2.1", - "resolved": "", + "resolved": false, "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, @@ -8672,7 +8671,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -8682,14 +8681,14 @@ }, "ms": { "version": "2.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true }, "needle": { "version": "2.3.0", - "resolved": "", + "resolved": false, "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, @@ -8701,7 +8700,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": "", + "resolved": false, "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, @@ -8720,7 +8719,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, @@ -8731,14 +8730,14 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": "", + "resolved": false, "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": "", + "resolved": false, "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, @@ -8749,7 +8748,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, @@ -8762,21 +8761,21 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": "", + "resolved": false, "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": "", + "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "optional": true, @@ -8786,21 +8785,21 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": "", + "resolved": false, "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, @@ -8811,21 +8810,21 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.8", - "resolved": "", + "resolved": false, "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, @@ -8838,7 +8837,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true @@ -8847,7 +8846,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "", + "resolved": false, "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, @@ -8863,7 +8862,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": "", + "resolved": false, "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, @@ -8873,49 +8872,49 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": "", + "resolved": false, "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.7.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, @@ -8927,7 +8926,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, @@ -8937,7 +8936,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "optional": true, @@ -8947,14 +8946,14 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "tar": { "version": "4.4.8", - "resolved": "", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, @@ -8970,14 +8969,14 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": "", + "resolved": false, "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, @@ -8987,14 +8986,14 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "optional": true }, "yallist": { "version": "3.0.3", - "resolved": "", + "resolved": false, "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true, "optional": true diff --git a/package.json b/package.json index 43ab97950..75c1227a1 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "cordova-plugin-file-transfer": "^1.7.1", "cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", "cordova-plugin-globalization": "^1.11.0", - "cordova-plugin-inappbrowser": "^4.0.0", + "cordova-plugin-inappbrowser": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle", "cordova-plugin-ionic-keyboard": "2.1.3", "cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle", "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", From 73fadf86e94225db21634573f77ab59d14a793f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 14 Sep 2020 15:50:11 +0200 Subject: [PATCH 103/182] MOBILE-3540 links: Force open in app or browser links --- src/directives/link.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/directives/link.ts b/src/directives/link.ts index 9b8e96fe9..f8894c7fc 100644 --- a/src/directives/link.ts +++ b/src/directives/link.ts @@ -22,6 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreConfigConstants } from '../configconstants'; import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; /** @@ -59,7 +60,7 @@ export class CoreLinkDirective implements OnInit { * Function executed when the component is initialized. */ ngOnInit(): void { - this.inApp = this.utils.isTrueOrOne(this.inApp); + this.inApp = typeof this.inApp == 'undefined' ? this.inApp : this.utils.isTrueOrOne(this.inApp); let navCtrl = this.navCtrl; @@ -76,15 +77,17 @@ export class CoreLinkDirective implements OnInit { event.preventDefault(); event.stopPropagation(); + const openIn = this.element.getAttribute('data-open-in'); + if (this.utils.isTrueOrOne(this.capture)) { href = this.textUtils.decodeURI(href); this.contentLinksHelper.handleLink(href, undefined, navCtrl, true, true).then((treated) => { if (!treated) { - this.navigate(href); + this.navigate(href, openIn); } }); } else { - this.navigate(href); + this.navigate(href, openIn); } } } @@ -95,9 +98,10 @@ export class CoreLinkDirective implements OnInit { * Convenience function to correctly navigate, open file or url in the browser. * * @param href HREF to be opened. + * @param openIn Open In App value coming from data-open-in attribute. * @return Promise resolved when done. */ - protected async navigate(href: string): Promise { + protected async navigate(href: string, openIn: string): Promise { if (this.urlUtils.isLocalFileUrl(href)) { // We have a local file. @@ -166,7 +170,19 @@ export class CoreLinkDirective implements OnInit { this.utils.openInBrowser(href); } } else { - if (this.inApp) { + // Priority order is: core-link inApp attribute > forceOpenLinksIn setting > data-open-in HTML attribute. + let openInApp; + if (typeof this.inApp == 'undefined') { + if (CoreConfigConstants['forceOpenLinksIn'] == 'browser') { + openInApp = false; + } else if (CoreConfigConstants['forceOpenLinksIn'] == 'app' || openIn == 'app') { + openInApp = true; + } + } else { + openInApp = this.inApp; + } + + if (openInApp) { await this.sitesProvider.getCurrentSite().openInAppWithAutoLoginIfSameSite(href); } else { await this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(href); From 22d4ba91767ed50b7015a003b7a662ac76b92405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 22 Sep 2020 14:40:39 +0200 Subject: [PATCH 104/182] MOBILE-3523 user-avatar: Fix unclosed parenthesis --- src/components/user-avatar/user-avatar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/user-avatar/user-avatar.ts b/src/components/user-avatar/user-avatar.ts index dc25a4bd3..56f41e5ea 100644 --- a/src/components/user-avatar/user-avatar.ts +++ b/src/components/user-avatar/user-avatar.ts @@ -87,7 +87,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { */ protected setFields(): void { const profileUrl = this.profileUrl || (this.user && (this.user.profileimageurl || this.user.userprofileimageurl || - this.user.userpictureurl || this.user.profileimageurlsmall || (this.user.urls && this.user.urls.profileimage)); + this.user.userpictureurl || this.user.profileimageurlsmall || (this.user.urls && this.user.urls.profileimage))); if (typeof profileUrl == 'string') { this.avatarUrl = profileUrl; From 421634dff7594a79a0f65e2b03739236e7e00d1e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 2 Sep 2020 16:00:18 +0200 Subject: [PATCH 105/182] MOBILE-3338 mod: Apply component + id to mod WS calls --- .../mod/assign/components/index/index.ts | 9 +- .../components/submission/submission.ts | 6 +- src/addon/mod/assign/pages/edit/edit.ts | 19 +- .../pages/submission-list/submission-list.ts | 5 +- src/addon/mod/assign/providers/assign-sync.ts | 24 +- src/addon/mod/assign/providers/assign.ts | 251 +++-- src/addon/mod/assign/providers/helper.ts | 31 +- .../mod/assign/providers/prefetch-handler.ts | 79 +- src/addon/mod/book/providers/book.ts | 28 +- src/addon/mod/chat/pages/chat/chat.ts | 5 +- .../session-messages/session-messages.ts | 4 +- src/addon/mod/chat/pages/sessions/sessions.ts | 2 +- src/addon/mod/chat/pages/users/users.ts | 3 +- src/addon/mod/chat/providers/chat.ts | 83 +- .../mod/chat/providers/prefetch-handler.ts | 26 +- .../mod/choice/components/index/index.ts | 4 +- src/addon/mod/choice/providers/choice.ts | 85 +- .../mod/choice/providers/prefetch-handler.ts | 17 +- src/addon/mod/data/components/index/index.ts | 19 +- src/addon/mod/data/pages/edit/edit.ts | 4 +- src/addon/mod/data/pages/entry/entry.ts | 22 +- src/addon/mod/data/providers/data.ts | 327 ++++--- src/addon/mod/data/providers/helper.ts | 44 +- .../mod/data/providers/prefetch-handler.ts | 55 +- src/addon/mod/data/providers/sync.ts | 23 +- .../mod/feedback/components/index/index.ts | 6 +- .../mod/feedback/pages/attempt/attempt.ts | 2 +- src/addon/mod/feedback/pages/form/form.ts | 41 +- .../pages/nonrespondents/nonrespondents.ts | 6 +- .../feedback/pages/respondents/respondents.ts | 6 +- src/addon/mod/feedback/providers/feedback.ts | 495 +++++----- src/addon/mod/feedback/providers/helper.ts | 24 +- .../feedback/providers/prefetch-handler.ts | 40 +- src/addon/mod/feedback/providers/sync.ts | 11 +- src/addon/mod/folder/providers/folder.ts | 29 +- .../discussion-options-menu.ts | 2 +- src/addon/mod/forum/components/index/index.ts | 11 +- .../post-options-menu/post-options-menu.ts | 10 +- src/addon/mod/forum/components/post/post.ts | 3 +- .../mod/forum/pages/discussion/discussion.ts | 6 +- .../pages/new-discussion/new-discussion.ts | 10 +- src/addon/mod/forum/providers/forum.ts | 180 ++-- src/addon/mod/forum/providers/helper.ts | 6 +- .../mod/forum/providers/module-handler.ts | 2 +- .../mod/forum/providers/prefetch-handler.ts | 49 +- src/addon/mod/forum/providers/sync.ts | 4 +- .../mod/glossary/components/index/index.ts | 8 +- src/addon/mod/glossary/pages/edit/edit.ts | 17 +- .../glossary/providers/entry-link-handler.ts | 2 +- src/addon/mod/glossary/providers/glossary.ts | 333 ++++--- .../glossary/providers/prefetch-handler.ts | 29 +- src/addon/mod/glossary/providers/sync.ts | 2 +- .../mod/h5pactivity/components/index/index.ts | 9 +- .../pages/attempt-results/attempt-results.ts | 27 +- .../pages/user-attempts/user-attempts.ts | 17 +- .../mod/h5pactivity/providers/h5pactivity.ts | 107 +-- .../h5pactivity/providers/prefetch-handler.ts | 16 +- src/addon/mod/h5pactivity/providers/sync.ts | 2 +- src/addon/mod/imscp/providers/imscp.ts | 22 +- .../mod/imscp/providers/prefetch-handler.ts | 7 +- src/addon/mod/label/providers/label.ts | 53 +- .../mod/label/providers/prefetch-handler.ts | 6 +- .../mod/lesson/components/index/index.ts | 18 +- src/addon/mod/lesson/pages/player/player.ts | 102 ++- .../lesson/pages/user-retake/user-retake.ts | 9 +- .../lesson/providers/grade-link-handler.ts | 2 +- src/addon/mod/lesson/providers/lesson-sync.ts | 25 +- src/addon/mod/lesson/providers/lesson.ts | 867 ++++++++++-------- .../mod/lesson/providers/prefetch-handler.ts | 130 ++- src/addon/mod/lti/providers/lti.ts | 29 +- src/addon/mod/page/providers/page.ts | 29 +- src/addon/mod/quiz/components/index/index.ts | 18 +- src/addon/mod/quiz/pages/attempt/attempt.ts | 13 +- src/addon/mod/quiz/pages/player/player.ts | 41 +- src/addon/mod/quiz/pages/review/review.ts | 6 +- src/addon/mod/quiz/providers/helper.ts | 19 +- .../mod/quiz/providers/prefetch-handler.ts | 139 ++- src/addon/mod/quiz/providers/quiz-sync.ts | 36 +- src/addon/mod/quiz/providers/quiz.ts | 444 +++++---- src/addon/mod/resource/providers/resource.ts | 30 +- src/addon/mod/scorm/components/index/index.ts | 20 +- src/addon/mod/scorm/pages/player/player.ts | 53 +- src/addon/mod/scorm/providers/helper.ts | 48 +- .../mod/scorm/providers/prefetch-handler.ts | 35 +- src/addon/mod/scorm/providers/scorm-sync.ts | 84 +- src/addon/mod/scorm/providers/scorm.ts | 302 +++--- .../mod/survey/components/index/index.ts | 2 +- .../mod/survey/providers/prefetch-handler.ts | 13 +- src/addon/mod/survey/providers/survey.ts | 72 +- src/addon/mod/url/providers/url.ts | 30 +- src/addon/mod/wiki/components/index/index.ts | 10 +- src/addon/mod/wiki/pages/edit/edit.ts | 18 +- .../mod/wiki/providers/create-link-handler.ts | 11 +- .../providers/page-or-map-link-handler.ts | 2 +- .../mod/wiki/providers/prefetch-handler.ts | 39 +- src/addon/mod/wiki/providers/wiki-sync.ts | 11 +- src/addon/mod/wiki/providers/wiki.ts | 360 ++++---- .../assessment-strategy.ts | 6 +- .../components/assessment/assessment.ts | 4 +- .../mod/workshop/components/index/index.ts | 20 +- .../workshop/pages/assessment/assessment.ts | 8 +- .../pages/edit-submission/edit-submission.ts | 3 +- .../workshop/pages/submission/submission.ts | 17 +- src/addon/mod/workshop/providers/helper.ts | 48 +- .../workshop/providers/prefetch-handler.ts | 109 ++- src/addon/mod/workshop/providers/sync.ts | 22 +- src/addon/mod/workshop/providers/workshop.ts | 356 ++++--- src/core/course/providers/course.ts | 9 +- src/providers/sites.ts | 20 + 109 files changed, 3535 insertions(+), 2929 deletions(-) diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts index d7370fea3..6f872d3c6 100644 --- a/src/addon/mod/assign/components/index/index.ts +++ b/src/addon/mod/assign/components/index/index.ts @@ -175,7 +175,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo this.hasOffline = hasOffline; // Get assignment submissions. - return this.assignProvider.getSubmissions(this.assign.id).then((data) => { + return this.assignProvider.getSubmissions(this.assign.id, {cmId: this.module.id}).then((data) => { const time = this.timeUtils.timestamp(); this.canViewAllSubmissions = data.canviewsubmissions; @@ -217,7 +217,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo } // Check if the user can view their own submission. - return this.assignProvider.getSubmissionStatus(this.assign.id).then(() => { + return this.assignProvider.getSubmissionStatus(this.assign.id, {cmId: this.module.id}).then(() => { this.canViewOwnSubmission = true; }).catch((error) => { this.canViewOwnSubmission = false; @@ -241,7 +241,10 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo setGroup(groupId: number): Promise { this.group = groupId; - return this.assignProvider.getSubmissionStatus(this.assign.id, undefined, this.group).then((response) => { + return this.assignProvider.getSubmissionStatus(this.assign.id, { + groupId: this.group, + cmId: this.module.id, + }).then((response) => { this.summary = response.gradingsummary; if (typeof this.summary.warnofungroupedusers == 'boolean' && this.summary.warnofungroupedusers) { this.summary.warnofungroupedusers = 'ungroupedusers'; diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index e3fc1cb28..92cf4167b 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -460,7 +460,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { await Promise.all(promises); // Get submission status. - const response = await this.assignProvider.getSubmissionStatusWithRetry(this.assign, this.submitId, undefined, isBlind); + const response = await this.assignProvider.getSubmissionStatusWithRetry(this.assign, {userId: this.submitId, isBlind}); promises = []; @@ -996,7 +996,9 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { response.lastattempt.submissiongroupmemberswhoneedtosubmit.forEach((member) => { if (this.blindMarking) { // Users not blinded! (Moodle < 3.1.1, 3.2). - promises.push(this.assignProvider.getAssignmentUserMappings(this.assign.id, member).then((blindId) => { + promises.push(this.assignProvider.getAssignmentUserMappings(this.assign.id, member, { + cmId: this.moduleId, + }).then((blindId) => { this.membersToSubmit.push(blindId); })); } else { diff --git a/src/addon/mod/assign/pages/edit/edit.ts b/src/addon/mod/assign/pages/edit/edit.ts index 33fcf85b8..d080ffd23 100644 --- a/src/addon/mod/assign/pages/edit/edit.ts +++ b/src/addon/mod/assign/pages/edit/edit.ts @@ -16,7 +16,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper'; @@ -125,11 +125,20 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { }).then(() => { // Get submission status. Ignore cache to get the latest data. - return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind, false, true) - .catch((err) => { + const options = { + userId: this.userId, + isBlind: this.isBlind, + cmId: this.assign.cmid, + filter: false, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }; + + return this.assignProvider.getSubmissionStatus(this.assign.id, options).catch((err) => { // Cannot connect. Get cached data. - return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, undefined, this.isBlind) - .then((response) => { + options.filter = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + return this.assignProvider.getSubmissionStatus(this.assign.id, options).then((response) => { const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt); // Check if the user can edit it in offline. diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.ts b/src/addon/mod/assign/pages/submission-list/submission-list.ts index 3ca5c7674..b0d6d2eca 100644 --- a/src/addon/mod/assign/pages/submission-list/submission-list.ts +++ b/src/addon/mod/assign/pages/submission-list/submission-list.ts @@ -153,7 +153,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { } // Get assignment submissions. - this.submissionsData = await this.assignProvider.getSubmissions(this.assign.id); + this.submissionsData = await this.assignProvider.getSubmissions(this.assign.id, {cmId: this.assign.cmid}); if (!this.submissionsData.canviewsubmissions) { // User shouldn't be able to reach here. @@ -192,7 +192,8 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { const promises = [ this.assignHelper.getSubmissionsUserData(this.assign, this.submissionsData.submissions, this.groupId), // Get assignment grades only if workflow is not enabled to check grading date. - !this.assign.markingworkflow ? this.assignProvider.getAssignmentGrades(this.assign.id) : Promise.resolve(null), + !this.assign.markingworkflow ? this.assignProvider.getAssignmentGrades(this.assign.id, {cmId: this.assign.cmid}) : + Promise.resolve(null), ]; return Promise.all(promises).then(([submissions, grades]: [AddonModAssignSubmissionFormatted[], AddonModAssignGrade[]]) => { diff --git a/src/addon/mod/assign/providers/assign-sync.ts b/src/addon/mod/assign/providers/assign-sync.ts index de6ee611c..3cc10f7f6 100644 --- a/src/addon/mod/assign/providers/assign-sync.ts +++ b/src/addon/mod/assign/providers/assign-sync.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -251,7 +251,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { const courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid; - const assign = await this.assignProvider.getAssignmentById(courseId, assignId, false, siteId); + const assign = await this.assignProvider.getAssignmentById(courseId, assignId, {siteId}); let promises = []; @@ -340,8 +340,14 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { const userId = offlineData.userid; const pluginData = {}; + const options = { + userId, + cmId: assign.cmid, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; - const status = await this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); + const status = await this.assignProvider.getSubmissionStatus(assign.id, options); const submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt); @@ -370,7 +376,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { } // Submission data sent, update cached data. No need to block the user for this. - this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); + this.assignProvider.getSubmissionStatus(assign.id, options); } catch (error) { if (!error || !this.utils.isWebServiceError(error)) { // Local error, reject. @@ -422,6 +428,12 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { const userId = offlineData.userid; const syncId = this.getGradeSyncId(assign.id, userId); + const options = { + userId, + cmId: assign.cmid, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; // Check if this grade sync is blocked. if (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, syncId, siteId)) { @@ -431,7 +443,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { {$a: this.translate.instant('addon.mod_assign.syncblockedusercomponent')})); } - const status = await this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId); + const status = await this.assignProvider.getSubmissionStatus(assign.id, options); const timemodified = status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified); @@ -483,7 +495,7 @@ export class AddonModAssignSyncProvider extends CoreSyncBaseProvider { } // Update cached data. - promises.push(this.assignProvider.getSubmissionStatus(assign.id, userId, undefined, false, true, true, siteId)); + promises.push(this.assignProvider.getSubmissionStatus(assign.id, options)); await Promise.all(promises); } catch (error) { diff --git a/src/addon/mod/assign/providers/assign.ts b/src/addon/mod/assign/providers/assign.ts index 2ab77ae3a..cf570beb7 100644 --- a/src/addon/mod/assign/providers/assign.ts +++ b/src/addon/mod/assign/providers/assign.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -25,9 +25,10 @@ import { CoreGradesProvider } from '@core/grades/providers/grades'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignOfflineProvider } from './assign-offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreInterceptor } from '@classes/interceptor'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some functions for assign. @@ -143,12 +144,11 @@ export class AddonModAssignProvider { * * @param courseId Course ID the assignment belongs to. * @param cmId Assignment module ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the assignment. */ - getAssignment(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.getAssignmentByField(courseId, 'cmid', cmId, ignoreCache, siteId); + getAssignment(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getAssignmentByField(courseId, 'cmid', cmId, options); } /** @@ -157,27 +157,23 @@ export class AddonModAssignProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the assignment is retrieved. */ - protected getAssignmentByField(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) + protected getAssignmentByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId], - includenotenrolledcourses: 1 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAssignmentCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + includenotenrolledcourses: 1, + }; + const preSets = { + cacheKey: this.getAssignmentCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModAssignProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_assignments', params, preSets).catch(() => { // In 3.6 we added a new parameter includenotenrolledcourses that could cause offline data not to be found. @@ -206,13 +202,12 @@ export class AddonModAssignProvider { * Get an assignment by instance ID. * * @param courseId Course ID the assignment belongs to. - * @param cmId Assignment instance ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param id Assignment instance ID. + * @param options Other options. * @return Promise resolved with the assignment. */ - getAssignmentById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.getAssignmentByField(courseId, 'id', id, ignoreCache, siteId); + getAssignmentById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getAssignmentByField(courseId, 'id', id, options); } /** @@ -230,24 +225,22 @@ export class AddonModAssignProvider { * * @param assignId Assignment Id. * @param userId User Id to be blinded. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the user blind id. */ - getAssignmentUserMappings(assignId: number, userId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - assignmentids: [assignId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAssignmentUserMappingsCacheKey(assignId), - updateFrequency: CoreSite.FREQUENCY_OFTEN - }; + getAssignmentUserMappings(assignId: number, userId: number, options: CoreCourseCommonModWSOptions = {}): Promise { - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + assignmentids: [assignId], + }; + const preSets = { + cacheKey: this.getAssignmentUserMappingsCacheKey(assignId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_user_mappings', params, preSets) .then((response: AddonModAssignGetUserMappingsResult): any => { @@ -293,23 +286,21 @@ export class AddonModAssignProvider { * Returns grade information from assign_grades for the requested assignment id * * @param assignId Assignment Id. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with requested info when done. */ - getAssignmentGrades(assignId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - assignmentids: [assignId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAssignmentGradesCacheKey(assignId) - }; + getAssignmentGrades(assignId: number, options: CoreCourseCommonModWSOptions = {}): Promise { - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + assignmentids: [assignId], + }; + const preSets = { + cacheKey: this.getAssignmentGradesCacheKey(assignId), + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_grades', params, preSets).then((response: AddonModAssignGetGradesResult): any => { // Search the assignment. @@ -455,26 +446,23 @@ export class AddonModAssignProvider { * Get an assignment submissions. * * @param assignId Assignment id. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - getSubmissions(assignId: number, ignoreCache?: boolean, siteId?: string) + getSubmissions(assignId: number, options: CoreCourseCommonModWSOptions = {}) : Promise<{canviewsubmissions: boolean, submissions?: AddonModAssignSubmission[]}> { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - assignmentids: [assignId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubmissionsCacheKey(assignId), - updateFrequency: CoreSite.FREQUENCY_OFTEN - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + assignmentids: [assignId], + }; + const preSets = { + cacheKey: this.getSubmissionsCacheKey(assignId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_submissions', params, preSets) .then((response: AddonModAssignGetSubmissionsResult): any => { @@ -510,46 +498,40 @@ export class AddonModAssignProvider { * Get information about an assignment submission status for a given user. * * @param assignId Assignment instance id. - * @param userId User Id (empty for current user). - * @param groupId Group Id (empty for all participants). - * @param isBlind If blind marking is enabled or not. - * @param filter True to filter WS response and rewrite URLs, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site id (empty for current site). + * @param options Other options. * @return Promise always resolved with the user submission status. */ - getSubmissionStatus(assignId: number, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, - ignoreCache?: boolean, siteId?: string): Promise { + getSubmissionStatus(assignId: number, options: AddonModAssignSubmissionStatusOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const fixedParams = this.fixSubmissionStatusParams(site, userId, groupId, isBlind); + if (options.filter === undefined || options.filter === null) { + options.filter = true; + } + + return this.sitesProvider.getSite(options.siteId).then((site) => { + const fixedParams = this.fixSubmissionStatusParams(site, options.userId, options.groupId, options.isBlind); const params = { - assignid: assignId, - userid: fixedParams.userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId, - fixedParams.isBlind), - getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account. - filter: filter, - rewriteurls: filter - }; - + assignid: assignId, + userid: fixedParams.userId, + }; if (fixedParams.groupId) { params['groupid'] = fixedParams.groupId; } - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - - if (!filter) { + const preSets = { + cacheKey: this.getSubmissionStatusCacheKey(assignId, fixedParams.userId, fixedParams.groupId, + fixedParams.isBlind), + getCacheUsingCacheKey: true, // We use the cache key to take isBlind into account. + filter: options.filter, + rewriteurls: options.filter, + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, // Don't cache when getting text without filters. // @todo Change this to support offline editing. - preSets.saveToCache = false; - } + saveToCache: options.filter, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_get_submission_status', params, preSets); }); @@ -560,23 +542,24 @@ export class AddonModAssignProvider { * If the data doesn't include the user submission, retry ignoring cache. * * @param assign Assignment. - * @param userId User id (empty for current user). - * @param groupId Group Id (empty for all participants). - * @param isBlind If blind marking is enabled or not. - * @param filter True to filter WS response and rewrite URLs, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site id (empty for current site). + * @param options Other options. * @return Promise always resolved with the user submission status. */ - getSubmissionStatusWithRetry(assign: any, userId?: number, groupId?: number, isBlind?: boolean, filter: boolean = true, - ignoreCache?: boolean, siteId?: string): Promise { + getSubmissionStatusWithRetry(assign: any, options: AddonModAssignSubmissionStatusOptions = {}) + : Promise { + options.cmId = options.cmId || assign.cmid; - return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, ignoreCache, siteId).then((response) => { + return this.getSubmissionStatus(assign.id, options).then((response) => { const userSubmission = this.getSubmissionObjectFromAttempt(assign, response.lastattempt); if (!userSubmission) { // Try again, ignoring cache. - return this.getSubmissionStatus(assign.id, userId, groupId, isBlind, filter, true, siteId).catch(() => { + const newOptions = { + ...options, // Include all the original options. + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }; + + return this.getSubmissionStatus(assign.id, newOptions).catch(() => { // Error, return the first result even if it doesn't have the user submission. return response; }); @@ -650,35 +633,32 @@ export class AddonModAssignProvider { * * @param assignId Assignment id. * @param groupId Group id. If not defined, 0. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of participants and summary of submissions. */ - listParticipants(assignId: number, groupId?: number, ignoreCache?: boolean, siteId?: string) + listParticipants(assignId: number, groupId?: number, options: CoreCourseCommonModWSOptions = {}) : Promise { groupId = groupId || 0; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { if (!site.wsAvailable('mod_assign_list_participants')) { // Silently fail if is not available. (needs Moodle version >= 3.2) return Promise.reject(null); } const params = { - assignid: assignId, - groupid: groupId, - filter: '' - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.listParticipantsCacheKey(assignId, groupId), - updateFrequency: CoreSite.FREQUENCY_OFTEN - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + assignid: assignId, + groupid: groupId, + filter: '', + }; + const preSets = { + cacheKey: this.listParticipantsCacheKey(assignId, groupId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModAssignProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_assign_list_participants', params, preSets); }); @@ -769,7 +749,7 @@ export class AddonModAssignProvider { invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.getAssignment(courseId, moduleId, false, siteId).then((assign) => { + return this.getAssignment(courseId, moduleId, {siteId}).then((assign) => { const promises = []; // Do not invalidate assignment data before getting assignment info, we need it! @@ -1014,7 +994,10 @@ export class AddonModAssignProvider { } // We need more data to decide that. - return this.getSubmissionStatus(assignId, submission.submitid, undefined, submission.blindid).then((response) => { + return this.getSubmissionStatus(assignId, { + userId: submission.submitid, + isBlind: !!submission.blindid, + }).then((response) => { if (!response.feedback || !response.feedback.gradeddate) { // Not graded. return true; @@ -1304,6 +1287,16 @@ export class AddonModAssignProvider { } } +/** + * Options to pass to get submission status. + */ +export type AddonModAssignSubmissionStatusOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User Id (empty for current user). + groupId?: number; // Group Id (empty for all participants). + isBlind?: boolean; // If blind marking is enabled or not. + filter?: boolean; // True to filter WS response and rewrite URLs, false otherwise. Defaults to true. +}; + /** * Assign data returned by mod_assign_get_assignments. */ diff --git a/src/addon/mod/assign/providers/helper.ts b/src/addon/mod/assign/providers/helper.ts index 7cea16eef..4e7cc288e 100644 --- a/src/addon/mod/assign/providers/helper.ts +++ b/src/addon/mod/assign/providers/helper.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileProvider } from '@providers/file'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; @@ -209,29 +209,29 @@ export class AddonModAssignHelperProvider { * * @param assign Assignment object. * @param groupId Group Id. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of participants and summary of submissions. */ - getParticipants(assign: AddonModAssignAssign, groupId?: number, ignoreCache?: boolean, siteId?: string) + getParticipants(assign: AddonModAssignAssign, groupId?: number, options: CoreSitesCommonWSOptions = {}) : Promise { groupId = groupId || 0; - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.assignProvider.listParticipants(assign.id, groupId, ignoreCache, siteId).then((participants) => { + const modOptions = {cmId: assign.cmid, ...options}; // Create new options including all existing ones. + + return this.assignProvider.listParticipants(assign.id, groupId, modOptions).then((participants) => { if (groupId || participants && participants.length > 0) { return participants; } // If no participants returned and all groups specified, get participants by groups. - return this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((info) => { + return this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, modOptions.siteId).then((info) => { const promises = [], participants: {[id: number]: AddonModAssignParticipant} = {}; info.groups.forEach((userGroup) => { - promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, ignoreCache, siteId) - .then((parts) => { + promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, modOptions).then((parts) => { // Do not get repeated users. parts.forEach((participant) => { participants[participant.id] = participant; @@ -355,14 +355,15 @@ export class AddonModAssignHelperProvider { * @param assign Assignment object. * @param submissions Submissions to get the data for. * @param groupId Group Id. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site id (empty for current site). + * @param options Other options. * @return Promise always resolved. Resolve param is the formatted submissions. */ getSubmissionsUserData(assign: AddonModAssignAssign, submissions: AddonModAssignSubmissionFormatted[], groupId?: number, - ignoreCache?: boolean, siteId?: string): Promise { + options: CoreSitesCommonWSOptions = {}): Promise { - return this.getParticipants(assign, groupId).then((parts) => { + const modOptions = {cmId: assign.cmid, ...options}; // Create new options including all existing ones. + + return this.getParticipants(assign, groupId, modOptions).then((parts) => { const blind = assign.blindmarking && !assign.revealidentities; const promises = []; const result: AddonModAssignSubmissionFormatted[] = []; @@ -399,8 +400,8 @@ export class AddonModAssignHelperProvider { // Blind but not blinded! (Moodle < 3.1.1, 3.2). delete submission.userid; - promise = this.assignProvider.getAssignmentUserMappings(assign.id, submission.submitid, ignoreCache, siteId). - then((blindId) => { + promise = this.assignProvider.getAssignmentUserMappings(assign.id, submission.submitid, modOptions) + .then((blindId) => { submission.blindid = blindId; }); } diff --git a/src/addon/mod/assign/providers/prefetch-handler.ts b/src/addon/mod/assign/providers/prefetch-handler.ts index e67846720..26e2a11c4 100644 --- a/src/addon/mod/assign/providers/prefetch-handler.ts +++ b/src/addon/mod/assign/providers/prefetch-handler.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreGroupsProvider } from '@providers/groups'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -80,13 +80,13 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan canUseCheckUpdates(module: any, courseId: number): boolean | Promise { // Teachers cannot use the WS because it doesn't check student submissions. return this.assignProvider.getAssignment(courseId, module.id).then((assign) => { - return this.assignProvider.getSubmissions(assign.id).then((data) => { + return this.assignProvider.getSubmissions(assign.id, {cmId: module.id}).then((data) => { if (data.canviewsubmissions) { return false; } // Check if the user can view their own submission. - return this.assignProvider.getSubmissionStatus(assign.id).then(() => { + return this.assignProvider.getSubmissionStatus(assign.id, {cmId: module.id}).then(() => { return true; }); }); @@ -108,18 +108,18 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.assignProvider.getAssignment(courseId, module.id, false, siteId).then((assign) => { + return this.assignProvider.getAssignment(courseId, module.id, {siteId}).then((assign) => { // Get intro files and attachments. let files = assign.introattachments || []; files = files.concat(this.getIntroFilesFromInstance(module, assign)); // Now get the files in the submissions. - return this.assignProvider.getSubmissions(assign.id, false, siteId).then((data) => { + return this.assignProvider.getSubmissions(assign.id, {cmId: module.id, siteId}).then((data) => { const blindMarking = assign.blindmarking && !assign.revealidentities; if (data.canviewsubmissions) { // Teacher, get all submissions. - return this.assignHelper.getSubmissionsUserData(assign, data.submissions, 0, false, siteId) + return this.assignHelper.getSubmissionsUserData(assign, data.submissions, 0, {siteId}) .then((submissions: AddonModAssignSubmissionFormatted[]) => { const promises = []; @@ -172,8 +172,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string) : Promise { - return this.assignProvider.getSubmissionStatusWithRetry(assign, submitId, undefined, blindMarking, true, false, siteId) - .then((response) => { + return this.assignProvider.getSubmissionStatusWithRetry(assign, { + userId: submitId, + isBlind: blindMarking, + siteId, + }).then((response) => { const promises = []; let userSubmission: AddonModAssignSubmission; @@ -261,20 +264,24 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when done. */ protected prefetchAssign(module: any, courseId: number, single: boolean, siteId: string): Promise { - const userId = this.sitesProvider.getCurrentSiteUserId(), - promises = []; + const userId = this.sitesProvider.getCurrentSiteUserId(); + const promises = []; siteId = siteId || this.sitesProvider.getCurrentSiteId(); + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + // Get assignment to retrieve all its submissions. - promises.push(this.assignProvider.getAssignment(courseId, module.id, true, siteId).then((assign) => { + promises.push(this.assignProvider.getAssignment(courseId, module.id, options).then((assign) => { const subPromises = [], blindMarking = assign.blindmarking && !assign.revealidentities; if (blindMarking) { - subPromises.push(this.assignProvider.getAssignmentUserMappings(assign.id, undefined, true, siteId).catch(() => { - // Ignore errors. - })); + subPromises.push(this.utils.ignoreErrors(this.assignProvider.getAssignmentUserMappings(assign.id, -1, options))); } subPromises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId)); @@ -304,8 +311,14 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when prefetched, rejected otherwise. */ protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId: number, siteId: string): Promise { + const options = { + cmId: moduleId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + // Get submissions. - return this.assignProvider.getSubmissions(assign.id, true, siteId).then((data) => { + return this.assignProvider.getSubmissions(assign.id, options).then((data) => { const promises = []; promises.push(this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((groupInfo) => { @@ -317,14 +330,22 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan } groupInfo.groups.forEach((group) => { - groupProms.push(this.assignHelper.getSubmissionsUserData(assign, data.submissions, group.id, true, siteId) + groupProms.push(this.assignHelper.getSubmissionsUserData(assign, data.submissions, group.id, options) .then((submissions: AddonModAssignSubmissionFormatted[]) => { const subPromises = []; submissions.forEach((submission) => { - subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submission.submitid, - group.id, !!submission.blindid, true, true, siteId).then((subm) => { + const submissionOptions = { + userId: submission.submitid, + groupId: group.id, + isBlind: !!submission.blindid, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + + subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submissionOptions) + .then((subm) => { return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId); }).catch((error) => { if (error && error.errorcode == 'nopermission') { @@ -338,14 +359,21 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan if (!assign.markingworkflow) { // Get assignment grades only if workflow is not enabled to check grading date. - subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, true, siteId)); + subPromises.push(this.assignProvider.getAssignmentGrades(assign.id, options)); } // Prefetch the submission of the current user even if it does not exist, this will be create it. if (!data.submissions || !data.submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) { - subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, userId, group.id, - false, true, true, siteId).then((subm) => { + const submissionOptions = { + userId, + groupId: group.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + + subPromises.push(this.assignProvider.getSubmissionStatusWithRetry(assign, submissionOptions) + .then((subm) => { return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); })); } @@ -353,7 +381,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan return Promise.all(subPromises); }).then(() => { // Participiants already fetched, we don't need to ignore cache now. - return this.assignHelper.getParticipants(assign, group.id, false, siteId).then((participants) => { + return this.assignHelper.getParticipants(assign, group.id, {siteId}).then((participants) => { return this.userProvider.prefetchUserAvatars(participants, 'profileimageurl', siteId); }).catch(() => { // Fail silently (Moodle < 3.2). @@ -367,8 +395,11 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan // Prefetch own submission, we need to do this for teachers too so the response with error is cached. promises.push( - this.assignProvider.getSubmissionStatusWithRetry(assign, userId, undefined, false, true, true, siteId) - .then((subm) => { + this.assignProvider.getSubmissionStatusWithRetry(assign, { + userId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((subm) => { return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId); }).catch((error) => { // Ignore if the user can't view their own submission. diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts index 95d3264fd..a9e4f143b 100644 --- a/src/addon/mod/book/providers/book.ts +++ b/src/addon/mod/book/providers/book.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileProvider } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -73,11 +73,11 @@ export class AddonModBookProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the book is retrieved. */ - getBook(courseId: number, cmId: number, siteId?: string): Promise { - return this.getBookByField(courseId, 'coursemodule', cmId, siteId); + getBook(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getBookByField(courseId, 'coursemodule', cmId, options); } /** @@ -89,15 +89,19 @@ export class AddonModBookProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the book is retrieved. */ - protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getBookByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getBookDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId] + }; + const preSets = { + cacheKey: this.getBookDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModBookProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_book_get_books_by_courses', params, preSets) .then((response: AddonModBookGetBooksByCoursesResult): any => { diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts index 3130f397c..f5f50d7ba 100644 --- a/src/addon/mod/chat/pages/chat/chat.ts +++ b/src/addon/mod/chat/pages/chat/chat.ts @@ -127,7 +127,8 @@ export class AddonModChatChatPage { showChatUsers(): void { // Create the toc modal. const modal = this.modalCtrl.create('AddonModChatUsersPage', { - sessionId: this.sessionId + sessionId: this.sessionId, + cmId: this.cmId, }, { cssClass: 'core-modal-lateral', showBackdrop: true, enableBackdropDismiss: true, @@ -168,7 +169,7 @@ export class AddonModChatChatPage { return Promise.resolve(user.fullname); } - return this.chatProvider.getChatUsers(this.sessionId).then((data) => { + return this.chatProvider.getChatUsers(this.sessionId, {cmId: this.cmId}).then((data) => { this.users = data.users; const user = this.users.find((user) => user.id == id); diff --git a/src/addon/mod/chat/pages/session-messages/session-messages.ts b/src/addon/mod/chat/pages/session-messages/session-messages.ts index 39e15433e..aa894c218 100644 --- a/src/addon/mod/chat/pages/session-messages/session-messages.ts +++ b/src/addon/mod/chat/pages/session-messages/session-messages.ts @@ -60,8 +60,8 @@ export class AddonModChatSessionMessagesPage { * @return Promise resolved when done. */ protected fetchMessages(): Promise { - return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId) - .then((messages) => { + return this.chatProvider.getSessionMessages(this.chatId, this.sessionStart, this.sessionEnd, this.groupId, + {cmId: this.cmId}).then((messages) => { return this.chatProvider.getMessagesUserData(messages, this.courseId).then((messages) => { this.messages = messages; diff --git a/src/addon/mod/chat/pages/sessions/sessions.ts b/src/addon/mod/chat/pages/sessions/sessions.ts index 997341b4c..2b62d4ad9 100644 --- a/src/addon/mod/chat/pages/sessions/sessions.ts +++ b/src/addon/mod/chat/pages/sessions/sessions.ts @@ -72,7 +72,7 @@ export class AddonModChatSessionsPage { this.groupInfo = groupInfo; this.groupId = this.groupsProvider.validateGroupId(this.groupId, groupInfo); - return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll); + return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll, {cmId: this.cmId}); }).then((sessions: AddonModChatSessionFormatted[]) => { // Fetch user profiles. const promises = []; diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts index e3e3c4028..6879416e9 100644 --- a/src/addon/mod/chat/pages/users/users.ts +++ b/src/addon/mod/chat/pages/users/users.ts @@ -36,6 +36,7 @@ export class AddonModChatUsersPage { isOnline: boolean; protected sessionId: string; + protected cmId: number; protected onlineObserver: any; constructor(navParams: NavParams, network: Network, zone: NgZone, private appProvider: CoreAppProvider, @@ -56,7 +57,7 @@ export class AddonModChatUsersPage { * View loaded. */ ionViewDidLoad(): void { - this.chatProvider.getChatUsers(this.sessionId).then((data) => { + this.chatProvider.getChatUsers(this.sessionId, {cmId: this.cmId}).then((data) => { this.users = data.users; }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhilegettingchatusers', true); diff --git a/src/addon/mod/chat/providers/chat.ts b/src/addon/mod/chat/providers/chat.ts index 8fa311954..688c051fe 100644 --- a/src/addon/mod/chat/providers/chat.ts +++ b/src/addon/mod/chat/providers/chat.ts @@ -14,13 +14,14 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; import { AddonModChatMessageForView, AddonModChatSessionMessageForView } from './helper'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for chats. @@ -40,17 +41,19 @@ export class AddonModChatProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the chat is retrieved. */ - getChat(courseId: number, cmId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getChat(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { courseids: [courseId] }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getChatsCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChatProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_chat_get_chats_by_courses', params, preSets) @@ -179,17 +182,25 @@ export class AddonModChatProvider { * Get the actives users of a current chat. * * @param sessionId Chat sessiond ID. + * @param options Other options. * @return Promise resolved when the WS is executed. */ - getChatUsers(sessionId: string): Promise { - const params = { - chatsid: sessionId - }; - const preSets = { - getFromCache: false - }; + getChatUsers(sessionId: string, options: CoreCourseCommonModWSOptions = {}): Promise { + // By default, always try to get the latest data. + options.readingStrategy = options.readingStrategy || CoreSitesReadingStrategy.PreferNetwork; - return this.sitesProvider.getCurrentSite().read('mod_chat_get_chat_users', params, preSets); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + chatsid: sessionId, + }; + const preSets = { + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_chat_get_chat_users', params, preSets); + }); } /** @@ -210,28 +221,26 @@ export class AddonModChatProvider { * @param chatId Chat ID. * @param groupId Group ID, 0 means that the function will determine the user group. * @param showAll Whether to include incomplete sessions or not. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of sessions. * @since 3.5 */ - getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, ignoreCache: boolean = false, siteId?: string): + getSessions(chatId: number, groupId: number = 0, showAll: boolean = false, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { chatid: chatId, groupid: groupId, - showall: showAll ? 1 : 0 + showall: showAll ? 1 : 0, }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getSessionsCacheKey(chatId, groupId, showAll), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } return site.read('mod_chat_get_sessions', params, preSets).then((response: AddonModChatGetSessionsResult): any => { if (!response || !response.sessions) { @@ -250,29 +259,27 @@ export class AddonModChatProvider { * @param sessionStart Session start time. * @param sessionEnd Session end time. * @param groupId Group ID, 0 means that the function will determine the user group. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of messages. * @since 3.5 */ - getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, ignoreCache: boolean = false, - siteId?: string): Promise { + getSessionMessages(chatId: number, sessionStart: number, sessionEnd: number, groupId: number = 0, + options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { chatid: chatId, sessionstart: sessionStart, sessionend: sessionEnd, - groupid: groupId + groupid: groupId, }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getSessionMessagesCacheKey(chatId, sessionStart, groupId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChatProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } return site.read('mod_chat_get_session_messages', params, preSets) .then((response: AddonModChatGetSessionMessagesResult): any => { diff --git a/src/addon/mod/chat/providers/prefetch-handler.ts b/src/addon/mod/chat/providers/prefetch-handler.ts index f5c5900b1..62821a577 100644 --- a/src/addon/mod/chat/providers/prefetch-handler.ts +++ b/src/addon/mod/chat/providers/prefetch-handler.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -122,9 +122,14 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl protected prefetchChat(module: any, courseId: number, single: boolean, siteId: string): Promise { // Prefetch chat and group info. const promises: Promise[] = [ - this.chatProvider.getChat(courseId, module.id, siteId), + this.chatProvider.getChat(courseId, module.id, {readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, siteId}), this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId) ]; + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; return Promise.all(promises).then(([chat, groupInfo]: [AddonModChatChat, CoreGroupInfo]) => { const promises = []; @@ -136,7 +141,7 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl groupIds.forEach((groupId) => { // Prefetch complete sessions. - promises.push(this.chatProvider.getSessions(chat.id, groupId, false, true, siteId).catch((error) => { + promises.push(this.chatProvider.getSessions(chat.id, groupId, false, options).catch((error) => { // Ignore group error. if (error.errorcode != 'notingroup') { return Promise.reject(error); @@ -144,8 +149,9 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl })); // Prefetch all sessions. - promises.push(this.chatProvider.getSessions(chat.id, groupId, true, true, siteId).then((sessions) => { - const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, siteId)); + promises.push(this.chatProvider.getSessions(chat.id, groupId, true, options).then((sessions) => { + const promises = sessions.map((session) => this.prefetchSession(chat.id, session, 0, courseId, module.id, + siteId)); return Promise.all(promises); }).catch((error) => { @@ -170,9 +176,13 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param siteId Site ID. * @return Promise resolved when done. */ - protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, siteId: string): Promise { - return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, true, siteId) - .then((messages) => { + protected prefetchSession(chatId: number, session: any, groupId: number, courseId: number, cmId: number, siteId: string) + : Promise { + return this.chatProvider.getSessionMessages(chatId, session.sessionstart, session.sessionend, groupId, { + cmId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((messages) => { const users = {}; session.sessionusers.forEach((user) => { users[user.userid] = true; diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index 2ce8f309b..89dbcc0dc 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -174,7 +174,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo * @return Promise resolved when done. */ protected fetchOptions(hasOffline: boolean): Promise { - return this.choiceProvider.getOptions(this.choice.id).then((options) => { + return this.choiceProvider.getOptions(this.choice.id, {cmId: this.module.id}).then((options) => { let promise; // Check if the user has answered (synced) to allow show results. @@ -294,7 +294,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo return Promise.resolve(); } - return this.choiceProvider.getResults(this.choice.id).then((results) => { + return this.choiceProvider.getResults(this.choice.id, {cmId: this.module.id}).then((results) => { let hasVotes = false; this.data = []; this.labels = []; diff --git a/src/addon/mod/choice/providers/choice.ts b/src/addon/mod/choice/providers/choice.ts index 88808a6ba..3e74cfb66 100644 --- a/src/addon/mod/choice/providers/choice.ts +++ b/src/addon/mod/choice/providers/choice.ts @@ -13,14 +13,15 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModChoiceOfflineProvider } from './offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for choices. @@ -173,34 +174,26 @@ export class AddonModChoiceProvider { /** * Get a choice with key=value. If more than one is found, only the first will be returned. * - * @param siteId Site ID. * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the choice is retrieved. */ - protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache?: boolean, - ignoreCache?: boolean): Promise { + protected getChoiceByDataKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { courseids: [courseId] }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getChoiceDataCacheKey(courseId), - omitExpires: forceCache, - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChoiceProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - 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: AddonModChoiceGetChoicesByCoursesResult): any => { @@ -221,14 +214,11 @@ export class AddonModChoiceProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the choice is retrieved. */ - getChoice(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean) - : Promise { - return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache, ignoreCache); + getChoice(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getChoiceByDataKey(courseId, 'coursemodule', cmId, options); } /** @@ -236,39 +226,33 @@ export class AddonModChoiceProvider { * * @param courseId Course ID. * @param choiceId Choice ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the choice is retrieved. */ - getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean) - : Promise { - return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache, ignoreCache); + getChoiceById(courseId: number, choiceId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getChoiceByDataKey(courseId, 'id', choiceId, options); } /** * Get choice options. * * @param choiceId Choice ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with choice options. */ - getOptions(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getOptions(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getChoiceOptionsCacheKey(choiceId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModChoiceProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - return site.read('mod_choice_get_choice_options', params, preSets) .then((response: AddonModChoiceGetChoiceOptionsResult): any => { @@ -285,24 +269,21 @@ export class AddonModChoiceProvider { * Get choice results. * * @param choiceId Choice ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with choice results. */ - getResults(choiceId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getResults(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { choiceid: choiceId }; - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getChoiceResultsCacheKey(choiceId) + const preSets = { + cacheKey: this.getChoiceOptionsCacheKey(choiceId), + component: AddonModChoiceProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - return site.read('mod_choice_get_choice_results', params, preSets) .then((response: AddonModChoiceGetChoiceResults): any => { diff --git a/src/addon/mod/choice/providers/prefetch-handler.ts b/src/addon/mod/choice/providers/prefetch-handler.ts index 656ab5efc..f319c6044 100644 --- a/src/addon/mod/choice/providers/prefetch-handler.ts +++ b/src/addon/mod/choice/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -79,12 +79,21 @@ export class AddonModChoicePrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when done. */ protected prefetchChoice(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.choiceProvider.getChoice(courseId, module.id, siteId, false, true).then((choice) => { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + + return this.choiceProvider.getChoice(courseId, module.id, commonOptions).then((choice) => { const promises = []; // Get the options and results. - promises.push(this.choiceProvider.getOptions(choice.id, true, siteId)); - promises.push(this.choiceProvider.getResults(choice.id, true, siteId).then((options) => { + promises.push(this.choiceProvider.getOptions(choice.id, modOptions)); + promises.push(this.choiceProvider.getResults(choice.id, modOptions).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/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index b63f00ce0..34feeef8f 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -198,7 +198,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp }); } }).then(() => { - return this.dataProvider.getDatabaseAccessInformation(this.data.id); + return this.dataProvider.getDatabaseAccessInformation(this.data.id, {cmId: this.module.id}); }).then((accessData) => { this.access = accessData; @@ -226,7 +226,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo); }); }).then(() => { - return this.dataProvider.getFields(this.data.id).then((fields) => { + return this.dataProvider.getFields(this.data.id, {cmId: this.module.id}).then((fields) => { if (fields.length == 0) { canSearch = false; canAdd = false; @@ -252,15 +252,24 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp */ protected fetchEntriesData(): Promise { - return this.dataProvider.getDatabaseAccessInformation(this.data.id, this.selectedGroup).then((accessData) => { + return this.dataProvider.getDatabaseAccessInformation(this.data.id, { + groupId: this.selectedGroup, + cmId: this.module.id, + }).then((accessData) => { // Update values for current group. this.access.canaddentry = accessData.canaddentry; const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined; const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined; - return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, search, advSearch, - this.search.sortBy, this.search.sortDirection, this.search.page); + return this.dataHelper.fetchEntries(this.data, this.fieldsArray, { + groupId: this.selectedGroup, + search, + advSearch, + sort: Number(this.search.sortBy), + order: this.search.sortDirection, + page: this.search.page, + }); }).then((entries) => { const numEntries = entries.entries.length; const numOfflineEntries = entries.offlineEntries.length; diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index 877785bbd..b3542eebf 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -128,7 +128,7 @@ export class AddonModDataEditPage { this.data = data; this.cssClass = 'addon-data-entries-' + data.id; - return this.dataProvider.getDatabaseAccessInformation(data.id); + return this.dataProvider.getDatabaseAccessInformation(data.id, {cmId: this.module.id}); }).then((accessData) => { if (this.entryId) { return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => { @@ -137,7 +137,7 @@ export class AddonModDataEditPage { }); } }).then(() => { - return this.dataProvider.getFields(this.data.id); + return this.dataProvider.getFields(this.data.id, {cmId: this.module.id}); }).then((fieldsData) => { this.fieldsArray = fieldsData; this.fields = this.utils.arrayToObject(fieldsData, 'id'); diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index ed8e2a84e..7a10b347a 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -142,13 +142,13 @@ export class AddonModDataEntryPage implements OnDestroy { this.title = data.name || this.title; this.data = data; - return this.dataProvider.getFields(this.data.id).then((fieldsData) => { + return this.dataProvider.getFields(this.data.id, {cmId: this.module.id}).then((fieldsData) => { this.fields = this.utils.arrayToObject(fieldsData, 'id'); this.fieldsArray = fieldsData; }); }).then(() => { return this.setEntryFromOffset().then(() => { - return this.dataProvider.getDatabaseAccessInformation(this.data.id); + return this.dataProvider.getDatabaseAccessInformation(this.data.id, {cmId: this.module.id}); }); }).then((accessData) => { this.access = accessData; @@ -290,8 +290,13 @@ export class AddonModDataEntryPage implements OnDestroy { const perPage = AddonModDataProvider.PER_PAGE; const page = !emptyOffset && this.offset >= 0 ? Math.floor(this.offset / perPage) : 0; - return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, undefined, undefined, '0', 'DESC', - page, perPage).then((entries) => { + return this.dataHelper.fetchEntries(this.data, this.fieldsArray, { + groupId: this.selectedGroup, + sort: 0, + order: 'DESC', + page, + perPage, + }).then((entries) => { const pageEntries = entries.offlineEntries.concat(entries.entries); let pageIndex; // Index of the entry when concatenating offline and online page entries. @@ -321,8 +326,11 @@ export class AddonModDataEntryPage implements OnDestroy { this.nextOffset = null; } else { // Last entry of the page, check if there are more pages. - promise = this.dataProvider.getEntries(this.data.id, this.selectedGroup, '0', 'DESC', page + 1, perPage) - .then((entries) => { + promise = this.dataProvider.getEntries(this.data.id, { + groupId: this.selectedGroup, + page: page + 1, + perPage: perPage, + }).then((entries) => { this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? this.offset + 1 : null; }); } @@ -330,7 +338,7 @@ export class AddonModDataEntryPage implements OnDestroy { return Promise.resolve(promise).then(() => { if (this.entryId > 0) { // Online entry, we need to fetch the the rating info. - return this.dataProvider.getEntry(this.data.id, this.entryId).then((entry) => { + return this.dataProvider.getEntry(this.data.id, this.entryId, {cmId: this.module.id}).then((entry) => { this.ratingInfo = entry.ratinginfo; }); } diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index 481896b28..cde86d294 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -23,6 +23,7 @@ import { AddonModDataOfflineProvider } from './offline'; import { AddonModDataFieldsDelegate } from './fields-delegate'; import { CoreRatingInfo } from '@core/rating/providers/rating'; import { CoreSite } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Database entry (online or offline). @@ -482,49 +483,34 @@ export class AddonModDataProvider { * Performs the whole fetch of the entries in the database. * * @param dataId Data ID. - * @param groupId Group ID. - * @param sort Sort the records by this field id. See AddonModDataProvider#getEntries for more info. - * @param order The direction of the sorting. See AddonModDataProvider#getEntries for more info. - * @param perPage Records per page to fetch. It has to match with the prefetch. - * Default on AddonModDataProvider.PER_PAGE. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - fetchAllEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', - perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + fetchAllEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); + options.page = 0; - return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId); + return this.fetchEntriesRecursive(dataId, [], options); } /** * Recursive call on fetch all entries. * * @param dataId Data ID. - * @param groupId Group ID. - * @param sort Sort the records by this field id. See AddonModDataProvider#getEntries for more info. - * @param order The direction of the sorting. See AddonModDataProvider#getEntries for more info. - * @param perPage Records per page to fetch. It has to match with the prefetch. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param entries Entries already fetch (just to concatenate them). - * @param page Page of records to return. - * @param siteId Site ID. + * @param options Other options. * @return Promise resolved when done. */ - protected fetchEntriesRecursive(dataId: number, groupId: number, sort: string, order: string, perPage: number, - forceCache: boolean, ignoreCache: boolean, entries: any, page: number, siteId: string): Promise { - return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId) - .then((result) => { + protected fetchEntriesRecursive(dataId: number, entries: any, options: AddonModDataGetEntriesOptions = {}) + : Promise { + return this.getEntries(dataId, options).then((result) => { entries = entries.concat(result.entries); - const canLoadMore = perPage > 0 && ((page + 1) * perPage) < result.totalcount; + const canLoadMore = options.perPage > 0 && ((options.page + 1) * options.perPage) < result.totalcount; if (canLoadMore) { - return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, entries, page + 1, - siteId); + options.page++; + + return this.fetchEntriesRecursive(dataId, entries, options); } return entries; @@ -557,23 +543,21 @@ export class AddonModDataProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the data is retrieved. */ - protected getDatabaseByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false): + protected getDatabaseByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getDatabaseDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - if (forceCache) { - preSets['omitExpires'] = true; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getDatabaseDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModDataProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_databases_by_courses', params, preSets).then((response) => { if (response && response.databases) { @@ -593,12 +577,11 @@ export class AddonModDataProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the data is retrieved. */ - getDatabase(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getDatabaseByKey(courseId, 'coursemodule', cmId, siteId, forceCache); + getDatabase(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getDatabaseByKey(courseId, 'coursemodule', cmId, options); } /** @@ -606,12 +589,11 @@ export class AddonModDataProvider { * * @param courseId Course ID. * @param id Data ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the data is retrieved. */ - getDatabaseById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getDatabaseByKey(courseId, 'id', id, siteId, forceCache); + getDatabaseById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getDatabaseByKey(courseId, 'id', id, options); } /** @@ -639,31 +621,23 @@ export class AddonModDataProvider { * Get access information for a given database. * * @param dataId Data ID. - * @param groupId Group ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the database is retrieved. */ - getDatabaseAccessInformation(dataId: number, groupId?: number, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getDatabaseAccessInformation(dataId: number, options: AddonModDataAccessInfoOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - databaseid: dataId - }, - preSets = { - cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, groupId) - }; + databaseid: dataId, + }; + const preSets = { + cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, options.groupId), + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof groupId !== 'undefined') { - params['groupid'] = groupId; - } - - if (offline) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; + if (typeof options.groupId !== 'undefined') { + params['groupid'] = options.groupId; } return site.read('mod_data_get_data_access_information', params, preSets); @@ -674,48 +648,34 @@ export class AddonModDataProvider { * Get entries for a specific database and group. * * @param dataId Data ID. - * @param groupId Group ID. - * @param sort Sort the records by this field id, reserved ids are: - * 0: timeadded - * -1: firstname - * -2: lastname - * -3: approved - * -4: timemodified. - * Empty for using the default database setting. - * @param order The direction of the sorting: 'ASC' or 'DESC'. - * Empty for using the default database setting. - * @param page Page of records to return. - * @param perPage Records per page to return. Default on PER_PAGE. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the database is retrieved. */ - getEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', page: number = 0, - perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.sort = options.sort || 0; + options.order || options.order || 'DESC'; + options.page = options.page || 0; + options.perPage = options.perPage || AddonModDataProvider.PER_PAGE; + + return this.sitesProvider.getSite(options.siteId).then((site) => { // Always use sort and order params to improve cache usage (entries are identified by params). const params = { - databaseid: dataId, - returncontents: 1, - page: page, - perpage: perPage, - groupid: groupId, - sort: sort, - order: order - }, - preSets = { - cacheKey: this.getEntriesCacheKey(dataId, groupId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (forceCache) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + databaseid: dataId, + returncontents: 1, + page: options.page, + perpage: options.perPage, + groupid: options.groupId, + sort: options.sort, + order: options.order, + }; + const preSets = { + cacheKey: this.getEntriesCacheKey(dataId, options.groupId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_entries', params, preSets).then((response) => { response.entries.forEach((entry) => { @@ -753,26 +713,23 @@ export class AddonModDataProvider { * * @param dataId Data ID for caching purposes. * @param entryId Entry ID. - * @param ignoreCache True if it should ignore cached data (it'll always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the entry is retrieved. */ - getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string): + getEntry(dataId: number, entryId: number, options: CoreCourseCommonModWSOptions = {}): Promise<{entry: AddonModDataEntry, ratinginfo: CoreRatingInfo}> { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - entryid: entryId, - returncontents: 1 - }, - preSets = { - cacheKey: this.getEntryCacheKey(dataId, entryId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + entryid: entryId, + returncontents: 1, + }; + const preSets = { + cacheKey: this.getEntryCacheKey(dataId, entryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_entry', params, preSets).then((response) => { response.entry.contents = this.utils.arrayToObject(response.entry.contents, 'fieldid'); @@ -797,27 +754,21 @@ export class AddonModDataProvider { * Get the list of configured fields for the given database. * * @param dataId Data ID. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the fields are retrieved. */ - getFields(dataId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getFields(dataId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - databaseid: dataId - }, - preSets = { - cacheKey: this.getFieldsCacheKey(dataId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + databaseid: dataId, + }; + const preSets = { + cacheKey: this.getFieldsCacheKey(dataId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_data_get_fields', params, preSets).then((response) => { if (response && response.fields) { @@ -993,46 +944,45 @@ export class AddonModDataProvider { * Performs search over a database. * * @param dataId The data instance id. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param search Search text. It will be used if advSearch is not defined. - * @param advSearch Advanced search data. - * @param sort Sort by this field. - * @param order The direction of the sorting. - * @param page Page of records to return. - * @param perPage Records per page to return. Default on AddonModDataProvider.PER_PAGE. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the action is done. */ - searchEntries(dataId: number, groupId: number = 0, search?: string, advSearch?: any, sort?: string, order?: string, - page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + searchEntries(dataId: number, options?: AddonModDataSearchEntriesOptions): Promise { + options.groupId = options.groupId || 0; + options.sort = options.sort || 0; + options.order || options.order || 'DESC'; + options.page = options.page || 0; + options.perPage = options.perPage || AddonModDataProvider.PER_PAGE; + options.readingStrategy = options.readingStrategy || CoreSitesReadingStrategy.PreferNetwork; + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - databaseid: dataId, - groupid: groupId, - returncontents: 1, - page: page, - perpage: perPage - }, - preSets = { - getFromCache: false, - saveToCache: true, - emergencyCache: true - }; + databaseid: dataId, + groupid: options.groupId, + returncontents: 1, + page: options.page, + perpage: options.perPage, + }; + const preSets = { + component: AddonModDataProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof sort != 'undefined') { - params['sort'] = sort; + if (typeof options.sort != 'undefined') { + params['sort'] = options.sort; } - if (typeof order !== 'undefined') { - params['order'] = order; + if (typeof options.order !== 'undefined') { + params['order'] = options.order; } - if (typeof search !== 'undefined') { - params['search'] = search; + if (typeof options.search !== 'undefined') { + params['search'] = options.search; } - if (typeof advSearch !== 'undefined') { - params['advsearch'] = advSearch; + if (typeof options.advSearch !== 'undefined') { + params['advsearch'] = options.advSearch; } return site.read('mod_data_search_entries', params, preSets).then((response) => { @@ -1045,3 +995,34 @@ export class AddonModDataProvider { }); } } + +/** + * Options to pass to get access info. + */ +export type AddonModDataAccessInfoOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group Id. +}; + +/** + * Options to pass to get entries. + */ +export type AddonModDataGetEntriesOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group Id. + sort?: number; // Sort the records by this field id, defaults to 0. Reserved ids are: + // 0: timeadded + // -1: firstname + // -2: lastname + // -3: approved + // -4: timemodified + order?: string; // The direction of the sorting: 'ASC' or 'DESC'. Defaults to 'DESC'. + page?: number; // Page of records to return. Defaults to 0. + perPage?: number; // Records per page to return. Defaults to AddonModDataProvider.PER_PAGE. +}; + +/** + * Options to pass to search entries. + */ +export type AddonModDataSearchEntriesOptions = AddonModDataGetEntriesOptions & { + search?: string; // Search text. It will be used if advSearch is not defined. + advSearch?: any; // Advanced search data. +}; diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 2d48ef390..ea686e422 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -23,7 +23,9 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { AddonModDataFieldsDelegate } from './fields-delegate'; import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; -import { AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields, AddonModDataEntries } from './data'; +import { + AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields, AddonModDataEntries, AddonModDataSearchEntriesOptions +} from './data'; import { CoreRatingInfo } from '@core/rating/providers/rating'; import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; @@ -210,33 +212,21 @@ export class AddonModDataHelperProvider { * * @param data Database object. * @param fields The fields that define the contents. - * @param groupId Group ID. - * @param search Search text. It will be used if advSearch is not defined. - * @param advSearch Advanced search data. - * @param sort Sort the records by this field id, reserved ids are: - * 0: timeadded - * -1: firstname - * -2: lastname - * -3: approved - * -4: timemodified. - * Empty for using the default database setting. - * @param order The direction of the sorting: 'ASC' or 'DESC'. - * Empty for using the default database setting. - * @param page Page of records to return. - * @param perPage Records per page to return. Default on PER_PAGE. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the database is retrieved. */ - fetchEntries(data: any, fields: any[], groupId: number = 0, search?: string, advSearch?: any[], sort: string = '0', - order: string = 'DESC', page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): - Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + fetchEntries(data: any, fields: any[], options: AddonModDataSearchEntriesOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.page = options.page || 0; + + return this.sitesProvider.getSite(options.siteId).then((site) => { const offlineActions = {}; const result: AddonModDataEntries = { entries: [], totalcount: 0, offlineEntries: [] }; + options.siteId = site.id; const offlinePromise = this.dataOffline.getDatabaseEntries(data.id, site.id).then((actions) => { result.hasOfflineActions = !!actions.length; @@ -248,8 +238,8 @@ export class AddonModDataHelperProvider { offlineActions[action.entryid].push(action); // We only display new entries in the first page when not searching. - if (action.action == 'add' && page == 0 && !search && !advSearch && - (!action.groupid || !groupId || action.groupid == groupId)) { + if (action.action == 'add' && options.page == 0 && !options.search && !options.advSearch && + (!action.groupid || !options.groupId || action.groupid == options.groupId)) { result.offlineEntries.push({ id: action.entryid, canmanageentry: true, @@ -275,16 +265,14 @@ export class AddonModDataHelperProvider { }); let fetchPromise: Promise; - if (search || advSearch) { - fetchPromise = this.dataProvider.searchEntries(data.id, groupId, search, advSearch, sort, order, page, perPage, - site.id).then((fetchResult) => { + if (options.search || options.advSearch) { + fetchPromise = this.dataProvider.searchEntries(data.id, options).then((fetchResult) => { result.entries = fetchResult.entries; result.totalcount = fetchResult.totalcount; result.maxcount = fetchResult.maxcount; }); } else { - fetchPromise = this.dataProvider.getEntries(data.id, groupId, sort, order, page, perPage, false, false, site.id) - .then((fetchResult) => { + fetchPromise = this.dataProvider.getEntries(data.id, options).then((fetchResult) => { result.entries = fetchResult.entries; result.totalcount = fetchResult.totalcount; }); @@ -324,7 +312,7 @@ export class AddonModDataHelperProvider { if (entryId > 0) { // Online entry. - promise = this.dataProvider.getEntry(data.id, entryId, false, site.id); + promise = this.dataProvider.getEntry(data.id, entryId, {cmId: data.coursemodule, siteId: site.id}); } else { // Offline entry or new entry. promise = Promise.resolve({ diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts index ecdf8927a..4355a8fe6 100644 --- a/src/addon/mod/data/providers/prefetch-handler.ts +++ b/src/addon/mod/data/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; @@ -65,16 +65,17 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * * @param dataId Database Id. * @param groups Array of groups in the activity. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. + * @param options Other options. * @return All unique entries. */ - protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { + protected getAllUniqueEntries(dataId: number, groups: any[], options: CoreSitesCommonWSOptions = {}) + : Promise { + const promises = groups.map((group) => { - return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache, - siteId); + return this.dataProvider.fetchAllEntries(dataId, { + groupId: group.id, + ...options, // Include all options. + }); }); return Promise.all(promises).then((responses) => { @@ -96,31 +97,29 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param module Module to get the files. * @param courseId Course ID the module belongs to. * @param omitFail True to always return even if fails. Default false. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. + * @param options Other options. * @return Promise resolved with the info fetched. */ - protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { + protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean, options: CoreSitesCommonWSOptions = {}) + : Promise { let database, groups = [], entries = [], files = []; - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.dataProvider.getDatabase(courseId, module.id, siteId, forceCache).then((data) => { + return this.dataProvider.getDatabase(courseId, module.id, options).then((data) => { files = this.getIntroFilesFromInstance(module, data); database = data; - return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => { + return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, options.siteId).then((groupInfo) => { if (!groupInfo.groups || groupInfo.groups.length == 0) { groupInfo.groups = [{id: 0}]; } groups = groupInfo.groups; - return this.getAllUniqueEntries(database.id, groups, forceCache, ignoreCache, siteId); + return this.getAllUniqueEntries(database.id, groups, options); }); }).then((uniqueEntries) => { entries = uniqueEntries; @@ -229,8 +228,10 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * @return Promise resolved with true if downloadable, resolved with false otherwise. */ isDownloadable(module: any, courseId: number): boolean | Promise { - return this.dataProvider.getDatabase(courseId, module.id, undefined, true).then((database) => { - return this.dataProvider.getDatabaseAccessInformation(database.id).then((accessData) => { + return this.dataProvider.getDatabase(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((database) => { + return this.dataProvider.getDatabaseAccessInformation(database.id, {cmId: module.id}).then((accessData) => { // Check if database is restricted by time. if (!accessData.timeavailable) { const time = this.timeUtils.timestamp(); @@ -281,23 +282,31 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * @return Promise resolved when done. */ protected prefetchDatabase(module: any, courseId: number, single: boolean, siteId: string): Promise { + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; - return this.getDatabaseInfoHelper(module, courseId, false, false, true, siteId).then((info) => { + return this.getDatabaseInfoHelper(module, courseId, false, options).then((info) => { // Prefetch the database data. const database = info.database, commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(), promises = []; - promises.push(this.dataProvider.getFields(database.id, false, true, siteId)); + promises.push(this.dataProvider.getFields(database.id, options)); promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id)); info.groups.forEach((group) => { - promises.push(this.dataProvider.getDatabaseAccessInformation(database.id, group.id, false, true, siteId)); + promises.push(this.dataProvider.getDatabaseAccessInformation(database.id, { + groupId: group.id, + ...options, // Include all options. + })); }); info.entries.forEach((entry) => { - promises.push(this.dataProvider.getEntry(database.id, entry.id, true, siteId)); + promises.push(this.dataProvider.getEntry(database.id, entry.id, options)); if (commentsEnabled && database.comments) { promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id, diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts index 4cf76575d..dbc3792dd 100644 --- a/src/addon/mod/data/providers/sync.ts +++ b/src/addon/mod/data/providers/sync.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -188,7 +188,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { courseId = offlineActions[0].courseid; // Send the answers. - return this.dataProvider.getDatabaseById(courseId, dataId, siteId).then((database) => { + return this.dataProvider.getDatabaseById(courseId, dataId, {siteId}).then((database) => { data = database; const offlineEntries = {}; @@ -233,18 +233,23 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { * @return Promise resolved if success, rejected otherwise. */ protected syncEntry(data: any, entryActions: AddonModDataOfflineAction[], result: any, siteId?: string): Promise { - let discardError, - timePromise, - entryId = entryActions[0].entryid, - offlineId, - deleted = false; + let discardError; + let timePromise; + let entryId = entryActions[0].entryid; + let offlineId; + let deleted = false; const editAction = entryActions.find((action) => action.action == 'add' || action.action == 'edit'); const approveAction = entryActions.find((action) => action.action == 'approve' || action.action == 'disapprove'); const deleteAction = entryActions.find((action) => action.action == 'delete'); + const options = { + cmId: data.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; if (entryId > 0) { - timePromise = this.dataProvider.getEntry(data.id, entryId, true, siteId).then((entry) => { + timePromise = this.dataProvider.getEntry(data.id, entryId, options).then((entry) => { return entry.entry.timemodified; }).catch((error) => { if (error && this.utils.isWebServiceError(error)) { @@ -402,7 +407,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { const promises = []; results.forEach((result) => { - promises.push(this.dataProvider.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, siteId) + promises.push(this.dataProvider.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, {siteId}) .then((data) => { const promises = []; diff --git a/src/addon/mod/feedback/components/index/index.ts b/src/addon/mod/feedback/components/index/index.ts index daa067482..0f58c27d7 100644 --- a/src/addon/mod/feedback/components/index/index.ts +++ b/src/addon/mod/feedback/components/index/index.ts @@ -184,7 +184,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity } }).then(() => { // Check if there are answers stored in offline. - return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id); + return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, {cmId: this.module.id}); }).then((accessData) => { this.access = accessData; this.showTabs = (accessData.canviewreports || accessData.canviewanalysis) && !accessData.isempty; @@ -220,7 +220,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity const promises = []; if (accessData.cancomplete && accessData.cansubmit && accessData.isopen) { - promises.push(this.feedbackProvider.getResumePage(this.feedback.id).then((goPage) => { + promises.push(this.feedbackProvider.getResumePage(this.feedback.id, {cmId: this.module.id}).then((goPage) => { this.goPage = goPage > 0 ? goPage : false; })); } @@ -421,7 +421,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity setGroup(groupId: number): Promise { this.group = groupId; - return this.feedbackProvider.getAnalysis(this.feedback.id, groupId).then((analysis) => { + return this.feedbackProvider.getAnalysis(this.feedback.id, {groupId, cmId: this.module.id}).then((analysis) => { this.feedback.completedCount = analysis.completedcount; this.feedback.itemsCount = analysis.itemscount; diff --git a/src/addon/mod/feedback/pages/attempt/attempt.ts b/src/addon/mod/feedback/pages/attempt/attempt.ts index 4b17573e1..d6ba346e9 100644 --- a/src/addon/mod/feedback/pages/attempt/attempt.ts +++ b/src/addon/mod/feedback/pages/attempt/attempt.ts @@ -65,7 +65,7 @@ export class AddonModFeedbackAttemptPage { return this.feedbackProvider.getFeedbackById(this.courseId, this.feedbackId).then((feedback) => { this.feedback = feedback; - return this.feedbackProvider.getItems(this.feedbackId); + return this.feedbackProvider.getItems(this.feedbackId, {cmId: this.feedback.coursemodule}); }).then((items) => { // Add responses and format items. this.items = items.items.map((item) => { diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts index 8e136401a..601b1a416 100644 --- a/src/addon/mod/feedback/pages/form/form.ts +++ b/src/addon/mod/feedback/pages/form/form.ts @@ -27,7 +27,7 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; /** * Page that displays feedback form. @@ -141,6 +141,10 @@ export class AddonModFeedbackFormPage implements OnDestroy { */ protected fetchData(): Promise { this.offline = !this.appProvider.isOnline(); + const options = { + cmId: this.module.id, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; return this.feedbackProvider.getFeedback(this.courseId, this.module.id).then((feedbackData) => { this.feedback = feedbackData; @@ -151,8 +155,7 @@ export class AddonModFeedbackFormPage implements OnDestroy { }).then((accessData) => { if (!this.preview && accessData.cansubmit && !accessData.isempty) { return typeof this.currentPage == 'undefined' ? - this.feedbackProvider.getResumePage(this.feedback.id, this.offline, true) : - Promise.resolve(this.currentPage); + this.feedbackProvider.getResumePage(this.feedback.id, options) : Promise.resolve(this.currentPage); } else { this.preview = true; @@ -162,8 +165,9 @@ export class AddonModFeedbackFormPage implements OnDestroy { if (!this.offline && !this.utils.isWebServiceError(error)) { // If it fails, go offline. this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; - return this.feedbackProvider.getResumePage(this.feedback.id, true); + return this.feedbackProvider.getResumePage(this.feedback.id, options); } return Promise.reject(error); @@ -186,12 +190,18 @@ export class AddonModFeedbackFormPage implements OnDestroy { * @return Promise resolved when done. */ protected fetchAccessData(): Promise { - return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, this.offline, true).catch((error) => { + const options = { + cmId: this.module.id, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; + + return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, options).catch((error) => { if (!this.offline && !this.utils.isWebServiceError(error)) { // If it fails, go offline. this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; - return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, true); + return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, options); } return Promise.reject(error); @@ -203,20 +213,25 @@ export class AddonModFeedbackFormPage implements OnDestroy { } protected fetchFeedbackPageData(page: number = 0): Promise { + const options = { + cmId: this.module.id, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; let promise; this.items = []; if (this.preview) { - promise = this.feedbackProvider.getItems(this.feedback.id); + promise = this.feedbackProvider.getItems(this.feedback.id, {cmId: this.module.id}); } else { this.currentPage = page; - promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, this.offline, true).catch((error) => { + promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, options).catch((error) => { if (!this.offline && !this.utils.isWebServiceError(error)) { // If it fails, go offline. this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; - return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, true); + return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, options); } return Promise.reject(error); @@ -262,8 +277,12 @@ export class AddonModFeedbackFormPage implements OnDestroy { return this.feedbackSync.syncFeedback(this.feedback.id).catch(() => { // Ignore errors. }).then(() => { - return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, goPrevious, formHasErrors, - this.courseId).then((response) => { + return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, { + goPrevious, + formHasErrors, + courseId: this.courseId, + cmId: this.module.id, + }).then((response) => { const jumpTo = parseInt(response.jumpto, 10); if (response.completed) { diff --git a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts index aab3cdac3..35425ea36 100644 --- a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts +++ b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts @@ -111,7 +111,11 @@ export class AddonModFeedbackNonRespondentsPage { this.feedbackLoaded = false; } - return this.feedbackHelper.getNonRespondents(this.feedbackId, this.selectedGroup, this.page).then((response) => { + return this.feedbackHelper.getNonRespondents(this.feedbackId, { + groupId: this.selectedGroup, + page: this.page, + cmId: this.moduleId, + }).then((response) => { this.total = response.total; if (this.users.length < response.total) { diff --git a/src/addon/mod/feedback/pages/respondents/respondents.ts b/src/addon/mod/feedback/pages/respondents/respondents.ts index e05ce6c70..ca21c2325 100644 --- a/src/addon/mod/feedback/pages/respondents/respondents.ts +++ b/src/addon/mod/feedback/pages/respondents/respondents.ts @@ -134,7 +134,11 @@ export class AddonModFeedbackRespondentsPage { this.feedbackLoaded = false; } - return this.feedbackHelper.getResponsesAnalysis(this.feedbackId, this.selectedGroup, this.page).then((responses) => { + return this.feedbackHelper.getResponsesAnalysis(this.feedbackId, { + groupId: this.selectedGroup, + page: this.page, + cmId: this.moduleId, + }).then((responses) => { this.responses.total = responses.totalattempts; this.anonResponses.total = responses.totalanonattempts; diff --git a/src/addon/mod/feedback/providers/feedback.ts b/src/addon/mod/feedback/providers/feedback.ts index cdb16569e..19e39fab9 100644 --- a/src/addon/mod/feedback/providers/feedback.ts +++ b/src/addon/mod/feedback/providers/feedback.ts @@ -14,13 +14,14 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreAppProvider } from '@providers/app'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModFeedbackOfflineProvider } from './offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for feedbacks. @@ -35,7 +36,7 @@ export class AddonModFeedbackProvider { static MULTICHOICE_HIDENOSELECT = 'h'; static MULTICHOICERATED_VALUE_SEP = '####'; - protected ROOT_CACHE_KEY = this.ROOT_CACHE_KEY + ''; + protected ROOT_CACHE_KEY = ''; protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, @@ -130,13 +131,11 @@ export class AddonModFeedbackProvider { * * @param feedbackId Feedback ID. * @param items Item to fill the value. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. + * @param options Other options. * @return Resolved with values when done. */ - protected fillValues(feedbackId: number, items: any[], offline: boolean, ignoreCache: boolean, siteId: string): Promise { - return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId).then((valuesArray) => { + protected fillValues(feedbackId: number, items: any[], options: CoreCourseCommonModWSOptions = {}): Promise { + return this.getCurrentValues(feedbackId, options).then((valuesArray) => { const values = {}; valuesArray.forEach((value) => { @@ -152,7 +151,7 @@ export class AddonModFeedbackProvider { // Ignore errors. }).then(() => { // Merge with offline data. - return this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).then((offlineValuesArray) => { + return this.feedbackOffline.getFeedbackResponses(feedbackId, options.siteId).then((offlineValuesArray) => { const offlineValues = {}; // Merge all values into one array. @@ -203,24 +202,22 @@ export class AddonModFeedbackProvider { * Returns all the feedback non respondents users. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @param previous Only for recurrent use. Object with the previous fetched info. * @return Promise resolved when the info is retrieved. */ - getAllNonRespondents(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) - : Promise { + getAllNonRespondents(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - if (typeof previous == 'undefined') { - previous = { - page: 0, - users: [] - }; - } + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); + previous = previous || { + page: 0, + users: [] + }; - return this.getNonRespondents(feedbackId, groupId, previous.page, ignoreCache, siteId).then((response) => { + return this.getNonRespondents(feedbackId, { + page: previous.page, + ...options, // Include all options. + }).then((response) => { if (previous.users.length < response.total) { previous.users = previous.users.concat(response.users); } @@ -229,7 +226,7 @@ export class AddonModFeedbackProvider { // Can load more. previous.page++; - return this.getAllNonRespondents(feedbackId, groupId, ignoreCache, siteId, previous); + return this.getAllNonRespondents(feedbackId, options, previous); } previous.total = response.total; @@ -241,25 +238,23 @@ export class AddonModFeedbackProvider { * Returns all the feedback user responses. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @param previous Only for recurrent use. Object with the previous fetched info. * @return Promise resolved when the info is retrieved. */ - getAllResponsesAnalysis(feedbackId: number, groupId: number, ignoreCache?: boolean, siteId?: string, previous?: any) - : Promise { + getAllResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}, previous?: any): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - if (typeof previous == 'undefined') { - previous = { - page: 0, - attempts: [], - anonattempts: [] - }; - } + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); + previous = previous || { + page: 0, + attempts: [], + anonattempts: [] + }; - return this.getResponsesAnalysis(feedbackId, groupId, previous.page, ignoreCache, siteId).then((responses) => { + return this.getResponsesAnalysis(feedbackId, { + page: previous.page, + ...options, // Include all options. + }).then((responses) => { if (previous.anonattempts.length < responses.totalanonattempts) { previous.anonattempts = previous.anonattempts.concat(responses.anonattempts); } @@ -272,7 +267,7 @@ export class AddonModFeedbackProvider { // Can load more. previous.page++; - return this.getAllResponsesAnalysis(feedbackId, groupId, ignoreCache, siteId, previous); + return this.getAllResponsesAnalysis(feedbackId, options, previous); } previous.totalattempts = responses.totalattempts; @@ -286,27 +281,23 @@ export class AddonModFeedbackProvider { * Get analysis information for a given feedback. * * @param feedbackId Feedback ID. - * @param groupId Group ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - getAnalysis(feedbackId: number, groupId?: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAnalysis(feedbackId: number, options: AddonModFeedbackGroupOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAnalysisDataCacheKey(feedbackId, groupId) - }; + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getAnalysisDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (groupId) { - params['groupid'] = groupId; - } - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; + if (options.groupId) { + params['groupid'] = options.groupId; } return site.read('mod_feedback_get_analysis', params, preSets); @@ -339,22 +330,23 @@ export class AddonModFeedbackProvider { * * @param feedbackId Feedback ID. * @param attemptId Attempt id to find. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @param previous Only for recurrent use. Object with the previous fetched info. * @return Promise resolved when the info is retrieved. */ - getAttempt(feedbackId: number, attemptId: number, ignoreCache?: boolean, siteId?: string, previous?: any): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - if (typeof previous == 'undefined') { - previous = { - page: 0, - attemptsLoaded: 0, - anonAttemptsLoaded: 0 - }; - } + getAttempt(feedbackId: number, attemptId: number, options: CoreCourseCommonModWSOptions = {}, previous?: any): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); + previous = previous || { + page: 0, + attemptsLoaded: 0, + anonAttemptsLoaded: 0 + }; - return this.getResponsesAnalysis(feedbackId, 0, previous.page, ignoreCache, siteId).then((responses) => { + return this.getResponsesAnalysis(feedbackId, { + page: previous.page, + groupId: 0, + ...options, // Include all options. + }).then((responses) => { let attempt; attempt = responses.attempts.find((attempt) => { @@ -385,7 +377,7 @@ export class AddonModFeedbackProvider { // Can load more. Check there. previous.page++; - return this.getAttempt(feedbackId, attemptId, ignoreCache, siteId, previous); + return this.getAttempt(feedbackId, attemptId, options, previous); } // Not found and all loaded. Reject. @@ -407,23 +399,20 @@ export class AddonModFeedbackProvider { * Returns the temporary completion timemodified for the current user. * * @param feedbackId Feedback ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getCurrentCompletedTimeModified(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getCurrentCompletedTimeModified(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_current_completed_tmp', params, preSets).then((response) => { if (response && typeof response.feedback != 'undefined' && typeof response.feedback.timemodified != 'undefined') { @@ -452,26 +441,20 @@ export class AddonModFeedbackProvider { * Returns the temporary responses or responses of the last submission for the current user. * * @param feedbackId Feedback ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getCurrentValues(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getCurrentValues(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets = { - cacheKey: this.getCurrentValuesDataCacheKey(feedbackId) - }; - - if (offline) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getCurrentValuesDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_unfinished_responses', params, preSets).then((response) => { if (!response || typeof response.responses == 'undefined') { @@ -508,27 +491,20 @@ export class AddonModFeedbackProvider { * Get access information for a given feedback. * * @param feedbackId Feedback ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - getFeedbackAccessInformation(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): - Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getFeedbackAccessInformation(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets = { - cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId) - }; - - if (offline) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_feedback_access_information', params, preSets); }); @@ -570,29 +546,22 @@ export class AddonModFeedbackProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean, - ignoreCache?: boolean): Promise { + protected getFeedbackDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getFeedbackCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getFeedbackCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModFeedbackProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_feedbacks_by_courses', params, preSets).then((response) => { if (response && response.feedbacks) { @@ -614,13 +583,11 @@ export class AddonModFeedbackProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { - return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache, ignoreCache); + getFeedback(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, options); } /** @@ -628,37 +595,32 @@ export class AddonModFeedbackProvider { * * @param courseId Course ID. * @param id Feedback ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param options Other options. * @return Promise resolved when the feedback is retrieved. */ - getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean, ignoreCache?: boolean): Promise { - return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache, ignoreCache); + getFeedbackById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getFeedbackDataByKey(courseId, 'id', id, options); } /** * Returns the items (questions) in the given feedback. * * @param feedbackId Feedback ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getItems(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getItems(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getItemsDataCacheKey(feedbackId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getItemsDataCacheKey(feedbackId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_items', params, preSets); }); @@ -678,29 +640,25 @@ export class AddonModFeedbackProvider { * Retrieves a list of students who didn't submit the feedback. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page The page of records to return. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, ignoreCache?: boolean, siteId?: string) - : Promise { + getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.page = options.page || 0; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId, - groupid: groupId, - page: page - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, groupId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + feedbackid: feedbackId, + groupid: options.groupId, + page: options.page, + }; + const preSets = { + cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_non_respondents', params, preSets); }); @@ -751,25 +709,22 @@ export class AddonModFeedbackProvider { * * @param feedbackId Feedback ID. * @param page The page to get. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getPageItemsWithValues(feedbackId: number, page: number, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getPageItemsWithValues(feedbackId: number, page: number, options: CoreCourseCommonModWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.getPageItems(feedbackId, page, siteId).then((response) => { - return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { + return this.getPageItems(feedbackId, page, options.siteId).then((response) => { + return this.fillValues(feedbackId, response.items, options).then((items) => { response.items = items; return response; }); }).catch(() => { // If getPageItems fail we should calculate it using getItems. - return this.getItems(feedbackId, false, siteId).then((response) => { - return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => { + return this.getItems(feedbackId, options).then((response) => { + return this.fillValues(feedbackId, response.items, options).then((items) => { // Separate items by pages. let currentPage = 0; const previousPageItems = []; @@ -819,11 +774,17 @@ export class AddonModFeedbackProvider { * @param feedbackId Feedback ID. * @param page Page where we want to jump. * @param changePage If page change is forward (1) or backward (-1). - * @param siteId Site ID. + * @param options Other options. * @return Page number where to jump. Or false if completed or first page. */ - protected getPageJumpTo(feedbackId: number, page: number, changePage: number, siteId: string): Promise { - return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => { + protected getPageJumpTo(feedbackId: number, page: number, changePage: number, options: {cmId?: number, siteId?: string}) + : Promise { + + return this.getPageItemsWithValues(feedbackId, page, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((resp) => { // The page we are going has items. if (resp.items.length > 0) { return page; @@ -831,7 +792,7 @@ export class AddonModFeedbackProvider { // Check we can jump futher. if ((changePage == 1 && resp.hasnextpage) || (changePage == -1 && resp.hasprevpage)) { - return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId); + return this.getPageJumpTo(feedbackId, page + changePage, changePage, options); } // Completed or first page. @@ -843,27 +804,25 @@ export class AddonModFeedbackProvider { * Returns the feedback user responses. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page The page of records to return. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - 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: CoreSiteWSPreSets = { - cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, groupId) - }; + getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise { + options.groupId = options.groupId || 0; + options.page = options.page || 0; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + feedbackid: feedbackId, + groupid: options.groupId, + page: options.page, + }; + const preSets = { + cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_get_responses_analysis', params, preSets); }); @@ -894,26 +853,20 @@ export class AddonModFeedbackProvider { * Gets the resume page information. * * @param feedbackId Feedback ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getResumePage(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getResumePage(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets = { - cacheKey: this.getResumePageDataCacheKey(feedbackId) - }; - - if (offline) { - preSets['omitExpires'] = true; - } else if (ignoreCache) { - preSets['getFromCache'] = false; - preSets['emergencyCache'] = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getResumePageDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_feedback_launch_feedback', params, preSets).then((response) => { if (response && typeof response.gopage != 'undefined') { @@ -964,7 +917,7 @@ export class AddonModFeedbackProvider { /** * Invalidate the prefetched content. - * To invalidate files, use AddonFeedbackProvider#invalidateFiles. + * To invalidate files, use AddonModFeedbackProvider#invalidateFiles. * * @param moduleId The module ID. * @param courseId Course ID of the module. @@ -976,7 +929,7 @@ export class AddonModFeedbackProvider { const promises = []; - promises.push(this.getFeedback(courseId, moduleId, siteId).then((feedback) => { + promises.push(this.getFeedback(courseId, moduleId, {siteId}).then((feedback) => { const ps = []; // Do not invalidate module data before getting module info, we need it! @@ -1086,23 +1039,20 @@ export class AddonModFeedbackProvider { * Returns if feedback has been completed * * @param feedbackId Feedback ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - isCompleted(feedbackId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + isCompleted(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - feedbackid: feedbackId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getCompletedDataCacheKey(feedbackId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getCompletedDataCacheKey(feedbackId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModFeedbackProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return this.utils.promiseWorks(site.read('mod_feedback_get_last_completed', params, preSets)); }); @@ -1147,19 +1097,15 @@ export class AddonModFeedbackProvider { * @param feedbackId Feedback ID. * @param page The page being processed. * @param responses The data to be processed the key is the field name (usually type[index]_id). - * @param goPrevious Whether we want to jump to previous page. - * @param formHasErrors Whether the form we sent has required but empty fields (only used in offline). - * @param courseId Course ID the feedback belongs to. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - processPage(feedbackId: number, page: number, responses: any, goPrevious: boolean, formHasErrors: boolean, courseId: number, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + processPage(feedbackId: number, page: number, responses: any, options: AddonModFeedbackProcessPageOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a message to be synchronized later. const storeOffline = (): Promise => { - return this.feedbackOffline.saveResponses(feedbackId, page, responses, courseId, siteId).then(() => { + return this.feedbackOffline.saveResponses(feedbackId, page, responses, options.courseId, options.siteId).then(() => { // Simulate process_page response. const response = { jumpto: page, @@ -1168,11 +1114,11 @@ export class AddonModFeedbackProvider { }; let changePage = 0; - if (goPrevious) { + if (options.goPrevious) { if (page > 0) { changePage = -1; } - } else if (!formHasErrors) { + } else if (!options.formHasErrors) { // We can only go next if it has no errors. changePage = 1; } @@ -1181,7 +1127,11 @@ export class AddonModFeedbackProvider { return response; } - return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => { + return this.getPageItemsWithValues(feedbackId, page, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((resp) => { // Check completion. if (changePage == 1 && !resp.hasnextpage) { response.completed = true; @@ -1189,7 +1139,7 @@ export class AddonModFeedbackProvider { return response; } - return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId).then((loadPage) => { + return this.getPageJumpTo(feedbackId, page + changePage, changePage, options).then((loadPage) => { if (loadPage === false) { // Completed or first page. if (changePage == -1) { @@ -1215,8 +1165,8 @@ export class AddonModFeedbackProvider { } // If there's already a response to be sent to the server, discard it first. - return this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, siteId).then(() => { - return this.processPageOnline(feedbackId, page, responses, goPrevious, siteId).catch((error) => { + return this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, options.siteId).then(() => { + return this.processPageOnline(feedbackId, page, responses, options.goPrevious, options.siteId).catch((error) => { if (this.utils.isWebServiceError(error)) { // The WebService has thrown an error, this means that responses cannot be submitted. return Promise.reject(error); @@ -1252,7 +1202,7 @@ export class AddonModFeedbackProvider { }).then((response) => { // Invalidate and update current values because they will change. return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => { - return this.getCurrentValues(feedbackId, false, false, site.getId()); + return this.getCurrentValues(feedbackId, {siteId: site.getId()}); }).catch(() => { // Ignore errors. }).then(() => { @@ -1262,3 +1212,28 @@ export class AddonModFeedbackProvider { }); } } + +/** + * Common options with a group ID. + */ +export type AddonModFeedbackGroupOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group id, 0 means that the function will determine the user group. Defaults to 0. +}; + +/** + * Common options with a group ID and page. + */ +export type AddonModFeedbackGroupPaginatedOptions = AddonModFeedbackGroupOptions & { + page?: number; // The page of records to return. The page of records to return. +}; + +/** + * Common options with a group ID and page. + */ +export type AddonModFeedbackProcessPageOptions = { + goPrevious?: boolean; // Whether we want to jump to previous page. + formHasErrors?: boolean; // Whether the form we sent has required but empty fields (only used in offline). + cmId?: number; // Module ID. + courseId?: number; // Course ID the feedback belongs to. + siteId?: string; // Site ID. If not defined, current site.; +}; diff --git a/src/addon/mod/feedback/providers/helper.ts b/src/addon/mod/feedback/providers/helper.ts index 2639cb5c6..f136c2456 100644 --- a/src/addon/mod/feedback/providers/helper.ts +++ b/src/addon/mod/feedback/providers/helper.ts @@ -14,11 +14,11 @@ import { Injectable } from '@angular/core'; import { NavController, ViewController } from 'ionic-angular'; -import { AddonModFeedbackProvider } from './feedback'; +import { AddonModFeedbackProvider, AddonModFeedbackGroupPaginatedOptions } from './feedback'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -86,12 +86,11 @@ export class AddonModFeedbackHelperProvider { * Retrieves a list of students who didn't submit the feedback with extra info. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page The page of records to return. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getNonRespondents(feedbackId: number, groupId: number, page: number): Promise { - return this.feedbackProvider.getNonRespondents(feedbackId, groupId, page).then((responses) => { + getNonRespondents(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise { + return this.feedbackProvider.getNonRespondents(feedbackId, options).then((responses) => { return this.addImageProfileToAttempts(responses.users).then((users) => { responses.users = users; @@ -186,12 +185,11 @@ export class AddonModFeedbackHelperProvider { * Returns the feedback user responses with extra info. * * @param feedbackId Feedback ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page The page of records to return. + * @param options Other options. * @return Promise resolved when the info is retrieved. */ - getResponsesAnalysis(feedbackId: number, groupId: number, page: number): Promise { - return this.feedbackProvider.getResponsesAnalysis(feedbackId, groupId, page).then((responses) => { + getResponsesAnalysis(feedbackId: number, options: AddonModFeedbackGroupPaginatedOptions = {}): Promise { + return this.feedbackProvider.getResponsesAnalysis(feedbackId, options).then((responses) => { return this.addImageProfileToAttempts(responses.attempts).then((attempts) => { responses.attempts = attempts; @@ -227,7 +225,11 @@ export class AddonModFeedbackHelperProvider { return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackRespondentsPage', stateParams, siteId); } - return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, true, siteId).then((attempt) => { + return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, { + cmId: moduleId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((attempt) => { stateParams = { moduleId: module.id, attempt: attempt, diff --git a/src/addon/mod/feedback/providers/prefetch-handler.ts b/src/addon/mod/feedback/providers/prefetch-handler.ts index 0c0562638..375e6a049 100644 --- a/src/addon/mod/feedback/providers/prefetch-handler.ts +++ b/src/addon/mod/feedback/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -143,7 +143,9 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH * @return Promise resolved with true if downloadable, resolved with false otherwise. */ isDownloadable(module: any, courseId: number): boolean | Promise { - return this.feedbackProvider.getFeedback(courseId, module.id, undefined, true).then((feedback) => { + return this.feedbackProvider.getFeedback(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((feedback) => { const now = this.timeUtils.timestamp(); // Check time first if available. @@ -154,7 +156,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH return false; } - return this.feedbackProvider.getFeedbackAccessInformation(feedback.id).then((accessData) => { + return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, {cmId: module.id}).then((accessData) => { return accessData.isopen; }); }); @@ -192,15 +194,24 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH * @return Promise resolved when done. */ protected prefetchFeedback(module: any, courseId: number, single: boolean, siteId: string): Promise { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + // Prefetch the feedback data. - return this.feedbackProvider.getFeedback(courseId, module.id, siteId, false, true).then((feedback) => { + return this.feedbackProvider.getFeedback(courseId, module.id, commonOptions).then((feedback) => { let files = (feedback.pageaftersubmitfiles || []).concat(this.getIntroFilesFromInstance(module, feedback)); - return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, false, true, siteId).then((accessData) => { + return this.feedbackProvider.getFeedbackAccessInformation(feedback.id, modOptions).then((accessData) => { const p2 = []; if (accessData.canedititems || accessData.canviewreports) { // Get all groups analysis. - p2.push(this.feedbackProvider.getAnalysis(feedback.id, undefined, true, siteId)); + p2.push(this.feedbackProvider.getAnalysis(feedback.id, modOptions)); p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId, true) .then((groupInfo) => { const p3 = []; @@ -209,11 +220,16 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH groupInfo.groups = [{id: 0}]; } groupInfo.groups.forEach((group) => { - p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, true, siteId)); - p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, true, siteId)); + const groupOptions = { + groupId: group.id, + ...modOptions, // Include all mod options. + }; + + p3.push(this.feedbackProvider.getAnalysis(feedback.id, groupOptions)); + p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, groupOptions)); if (!accessData.isanonymous) { - p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, group.id, true, siteId)); + p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, groupOptions)); } }); @@ -221,7 +237,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH })); } - p2.push(this.feedbackProvider.getItems(feedback.id, true, siteId).then((response) => { + p2.push(this.feedbackProvider.getItems(feedback.id, commonOptions).then((response) => { response.items.forEach((item) => { files = files.concat(item.itemfiles); }); @@ -234,8 +250,8 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH p2.push(this.feedbackProvider.processPageOnline(feedback.id, 0, {}, undefined, siteId).finally(() => { const p4 = []; - p4.push(this.feedbackProvider.getCurrentValues(feedback.id, false, true, siteId)); - p4.push(this.feedbackProvider.getResumePage(feedback.id, false, true, siteId)); + p4.push(this.feedbackProvider.getCurrentValues(feedback.id, modOptions)); + p4.push(this.feedbackProvider.getResumePage(feedback.id, modOptions)); return Promise.all(p4); })); diff --git a/src/addon/mod/feedback/providers/sync.ts b/src/addon/mod/feedback/providers/sync.ts index f3643f219..f4716e7b5 100644 --- a/src/addon/mod/feedback/providers/sync.ts +++ b/src/addon/mod/feedback/providers/sync.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -192,12 +192,12 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv courseId = responses[0].courseid; - return this.feedbackProvider.getFeedbackById(courseId, feedbackId, siteId).then((feedbackData) => { + return this.feedbackProvider.getFeedbackById(courseId, feedbackId, {siteId}).then((feedbackData) => { feedback = feedbackData; if (!feedback.multiple_submit) { // If it does not admit multiple submits, check if it is completed to know if we can submit. - return this.feedbackProvider.isCompleted(feedbackId); + return this.feedbackProvider.isCompleted(feedbackId, {cmId: feedback.coursemodule, siteId}); } else { return false; } @@ -220,7 +220,10 @@ export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProv return Promise.all(promises); } - return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, true, siteId).then((timemodified) => { + return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((timemodified) => { // Sort by page. responses.sort((a, b) => { return a.page - b.page; diff --git a/src/addon/mod/folder/providers/folder.ts b/src/addon/mod/folder/providers/folder.ts index 1f1281e57..a75520c16 100644 --- a/src/addon/mod/folder/providers/folder.ts +++ b/src/addon/mod/folder/providers/folder.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -41,11 +41,11 @@ export class AddonModFolderProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the book is retrieved. */ - getFolder(courseId: number, cmId: number, siteId?: string): Promise { - return this.getFolderByKey(courseId, 'coursemodule', cmId, siteId); + getFolder(courseId: number, cmId: number, options?: CoreSitesCommonWSOptions): Promise { + return this.getFolderByKey(courseId, 'coursemodule', cmId, options); } /** @@ -54,18 +54,21 @@ export class AddonModFolderProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the book is retrieved. */ - protected getFolderByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getFolderByKey(courseId: number, key: string, value: any, options?: CoreSitesCommonWSOptions) + : Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getFolderCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getFolderCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModFolderProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_folder_get_folders_by_courses', params, preSets) .then((response: AddonModFolderGetFoldersByCoursesResult): any => { diff --git a/src/addon/mod/forum/components/discussion-options-menu/discussion-options-menu.ts b/src/addon/mod/forum/components/discussion-options-menu/discussion-options-menu.ts index 142c25706..754ecff79 100644 --- a/src/addon/mod/forum/components/discussion-options-menu/discussion-options-menu.ts +++ b/src/addon/mod/forum/components/discussion-options-menu/discussion-options-menu.ts @@ -49,7 +49,7 @@ export class AddonForumDiscussionOptionsMenuComponent implements OnInit { ngOnInit(): void { if (this.forumProvider.isSetPinStateAvailableForSite()) { // Use the canAddDiscussion WS to check if the user can pin discussions. - this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => { + this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => { this.canPin = !!response.canpindiscussions; }).catch(() => { this.canPin = false; diff --git a/src/addon/mod/forum/components/index/index.ts b/src/addon/mod/forum/components/index/index.ts index 5c3147867..9642aaddf 100644 --- a/src/addon/mod/forum/components/index/index.ts +++ b/src/addon/mod/forum/components/index/index.ts @@ -250,7 +250,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom promises.push(this.groupsProvider.getActivityGroupMode(this.forum.cmid).then((mode) => { this.usesGroups = (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS); })); - promises.push(this.forumProvider.getAccessInformation(this.forum.id).then((accessInfo) => { + promises.push(this.forumProvider.getAccessInformation(this.forum.id, {cmId: this.module.id}).then((accessInfo) => { // Disallow adding discussions if cut-off date is reached and the user has not the capability to override it. // Just in case the forum was fetched from WS when the cut-off date was not reached but it is now. const cutoffDateReached = this.forumHelper.isCutoffDateReached(this.forum) && !accessInfo.cancanoverridecutoff; @@ -259,7 +259,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (this.forumProvider.isSetPinStateAvailableForSite()) { // Use the canAddDiscussion WS to check if the user can pin discussions. - promises.push(this.forumProvider.canAddDiscussionToAll(this.forum.id).then((response) => { + promises.push(this.forumProvider.canAddDiscussionToAll(this.forum.id, {cmId: this.module.id}).then((response) => { this.canPin = !!response.canpindiscussions; }).catch(() => { this.canPin = false; @@ -354,8 +354,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.page = 0; } - return this.forumProvider.getDiscussions(this.forum.id, this.forum.cmid, - this.selectedSortOrder.value, this.page).then((response) => { + return this.forumProvider.getDiscussions(this.forum.id, { + cmId: this.forum.cmid, + sortOrder: this.selectedSortOrder.value, + page: this.page, + }).then((response) => { let promise; if (this.usesGroups) { promise = this.forumProvider.formatDiscussionsGroups(this.forum.cmid, response.discussions); diff --git a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts index 3479d7154..7ddaf862f 100644 --- a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts +++ b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts @@ -15,7 +15,7 @@ import { Component, OnInit } from '@angular/core'; import { NavParams, ViewController } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { AddonModForumProvider } from '../../providers/forum'; @@ -35,6 +35,8 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { loaded = false; url: string; + protected cmId: number; + constructor(navParams: NavParams, protected viewCtrl: ViewController, protected domUtils: CoreDomUtilsProvider, @@ -42,6 +44,7 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { protected sitesProvider: CoreSitesProvider) { this.post = navParams.get('post'); this.forumId = navParams.get('forumId'); + this.cmId = navParams.get('cmId'); } /** @@ -64,7 +67,10 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { if (this.forumId) { try { this.post = - await this.forumProvider.getDiscussionPost(this.forumId, this.post.discussionid, this.post.id, true); + await this.forumProvider.getDiscussionPost(this.forumId, this.post.discussionid, this.post.id, { + cmId: this.cmId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }); } catch (error) { this.domUtils.showErrorModalDefault(error, 'Error getting discussion post.'); } diff --git a/src/addon/mod/forum/components/post/post.ts b/src/addon/mod/forum/components/post/post.ts index b55449ef5..48d0fc8e4 100644 --- a/src/addon/mod/forum/components/post/post.ts +++ b/src/addon/mod/forum/components/post/post.ts @@ -193,7 +193,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges const popover = this.popoverCtrl.create(AddonForumPostOptionsMenuComponent, { post: this.post, - forumId: this.forum.id + forumId: this.forum.id, + cmId: this.forum.cmid, }); popover.onDidDismiss((data) => { if (data && data.action) { diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts index 65fac0147..cee6fa087 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.ts +++ b/src/addon/mod/forum/pages/discussion/discussion.ts @@ -329,7 +329,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { let ratingInfo; return syncPromise.then(() => { - return this.forumProvider.getDiscussionPosts(this.discussionId, this.cmId).then((response) => { + return this.forumProvider.getDiscussionPosts(this.discussionId, {cmId: this.cmId}).then((response) => { onlinePosts = response.posts; ratingInfo = response.ratinginfo; this.courseId = response.courseid || this.courseId; @@ -403,7 +403,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { const promises = []; - promises.push(this.forumProvider.getAccessInformation(this.forumId).then((accessInfo) => { + promises.push(this.forumProvider.getAccessInformation(this.forumId, {cmId: this.cmId}).then((accessInfo) => { this.accessInfo = accessInfo; // Disallow replying if cut-off date is reached and the user has not the capability to override it. @@ -448,7 +448,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { }).then(() => { if (this.forumProvider.isSetPinStateAvailableForSite()) { // Use the canAddDiscussion WS to check if the user can pin discussions. - return this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => { + return this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => { this.canPin = !!response.canpindiscussions; }).catch(() => { this.canPin = false; diff --git a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts index b4aea7323..bde109ba0 100644 --- a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts +++ b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts @@ -176,7 +176,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { this.newDiscussion.postToAllGroups = false; // Use the canAddDiscussion WS to check if the user can add attachments and pin discussions. - promises.push(this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => { + promises.push(this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => { this.canPin = !!response.canpindiscussions; this.canCreateAttachments = !!response.cancreateattachment; }).catch(() => { @@ -190,7 +190,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { })); // Get access information. - promises.push(this.forumProvider.getAccessInformation(this.forumId).then((accessInfo) => { + promises.push(this.forumProvider.getAccessInformation(this.forumId, {cmId: this.cmId}).then((accessInfo) => { this.accessInfo = accessInfo; })); @@ -265,7 +265,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { */ protected validateVisibleGroups(forumGroups: any[]): Promise { // We first check if the user can post to all the groups. - return this.forumProvider.canAddDiscussionToAll(this.forumId).catch(() => { + return this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).catch(() => { // The call failed, let's assume he can't. return { status: false, @@ -285,7 +285,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { const filtered = []; forumGroups.forEach((group) => { - promises.push(this.forumProvider.canAddDiscussion(this.forumId, group.id).catch(() => { + promises.push(this.forumProvider.canAddDiscussion(this.forumId, group.id, {cmId: this.cmId}).catch(() => { /* The call failed, let's return true so the group is shown. If the user can't post to it an error will be shown when he tries to add the discussion. */ return { @@ -342,7 +342,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { if (check) { // We need to check if the user can add a discussion to all participants. - promise = this.forumProvider.canAddDiscussionToAll(this.forumId).then((response) => { + promise = this.forumProvider.canAddDiscussionToAll(this.forumId, {cmId: this.cmId}).then((response) => { this.canPin = !!response.canpindiscussions; this.canCreateAttachments = !!response.cancreateattachment; diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index 1ac3fdaf7..cdb8b6b5b 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -14,16 +14,17 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreGroupsProvider } from '@providers/groups'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModForumOfflineProvider } from './offline'; import { CoreRatingInfo } from '@core/rating/providers/rating'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for forums. @@ -206,26 +207,29 @@ export class AddonModForumProvider { * * @param forumId Forum ID. * @param groupId Group ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object with the following properties: * - status (boolean) * - canpindiscussions (boolean) * - cancreateattachment (boolean) */ - canAddDiscussion(forumId: number, groupId: number, siteId?: string): Promise { + canAddDiscussion(forumId: number, groupId: number, options: CoreCourseCommonModWSOptions = {}): Promise { const params = { forumid: forumId, - groupid: groupId + groupid: groupId, }; const preSets = { - cacheKey: this.getCanAddDiscussionCacheKey(forumId, groupId) + cacheKey: this.getCanAddDiscussionCacheKey(forumId, groupId), + component: AddonModForumProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { return site.read('mod_forum_can_add_discussion', params, preSets).then((result) => { if (result) { if (typeof result.canpindiscussions == 'undefined') { - // WS doesn't support it yet, default it to false to prevent students from seing the option. + // WS doesn't support it yet, default it to false to prevent students from seeing the option. result.canpindiscussions = false; } if (typeof result.cancreateattachment == 'undefined') { @@ -245,14 +249,14 @@ export class AddonModForumProvider { * Check if a user can post to all groups. * * @param forumId Forum ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object with the following properties: * - status (boolean) * - canpindiscussions (boolean) * - cancreateattachment (boolean) */ - canAddDiscussionToAll(forumId: number, siteId?: string): Promise { - return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, siteId); + canAddDiscussionToAll(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, options); } /** @@ -382,17 +386,19 @@ export class AddonModForumProvider { * Get all course forums. * * @param courseId Course ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the forums are retrieved. */ - getCourseForums(courseId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] + courseids: [courseId], }; const preSets = { cacheKey: this.getForumDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModForumProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_forum_get_forums_by_courses', params, preSets); @@ -405,24 +411,23 @@ export class AddonModForumProvider { * @param forumId Forum ID. * @param discussionId Discussion ID. * @param postId Post ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the post is retrieved. */ - getDiscussionPost(forumId: number, discussionId: number, postId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const params = { - postid: postId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId), - updateFrequency: CoreSite.FREQUENCY_USUALLY - }; + getDiscussionPost(forumId: number, discussionId: number, postId: number, options: CoreCourseCommonModWSOptions = {}) + : Promise { - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + return this.sitesProvider.getSite(options.siteId).then((site) => { + const params = { + postid: postId, + }; + const preSets = { + cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId), + updateFrequency: CoreSite.FREQUENCY_USUALLY, + component: AddonModForumProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_forum_get_discussion_post', params, preSets).then((response) => { if (response.post) { @@ -439,11 +444,11 @@ export class AddonModForumProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the forum is retrieved. */ - getForum(courseId: number, cmId: number, siteId?: string): Promise { - return this.getCourseForums(courseId, siteId).then((forums) => { + getForum(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getCourseForums(courseId, options).then((forums) => { const forum = forums.find((forum) => forum.cmid == cmId); if (forum) { return forum; @@ -458,11 +463,11 @@ export class AddonModForumProvider { * * @param courseId Course ID. * @param forumId Forum ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the forum is retrieved. */ - getForumById(courseId: number, forumId: number, siteId?: string): Promise { - return this.getCourseForums(courseId, siteId).then((forums) => { + getForumById(courseId: number, forumId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getCourseForums(courseId, options).then((forums) => { const forum = forums.find((forum) => forum.id == forumId); if (forum) { return forum; @@ -476,24 +481,25 @@ export class AddonModForumProvider { * Get access information for a given forum. * * @param forumId Forum ID. - * @param forceCache True to always get the value from cache. false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Object with access information. * @since 3.7 */ - getAccessInformation(forumId: number, forceCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAccessInformation(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { if (!site.wsAvailable('mod_forum_get_forum_access_information')) { // Access information not available for 3.6 or older sites. return Promise.resolve({}); } const params = { - forumid: forumId + forumid: forumId, }; const preSets = { cacheKey: this.getAccessInformationCacheKey(forumId), - omitExpires: forceCache + component: AddonModForumProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_forum_get_forum_access_information', params, preSets); @@ -504,11 +510,10 @@ export class AddonModForumProvider { * Get forum discussion posts. * * @param discussionId Discussion ID. - * @param cmId Forum cmid. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with forum posts and rating info. */ - getDiscussionPosts(discussionId: number, cmId: number, siteId?: string): Promise<{posts: any[], courseid?: number, + getDiscussionPosts(discussionId: number, options: CoreCourseCommonModWSOptions = {}): Promise<{posts: any[], courseid?: number, forumid?: number, ratinginfo?: CoreRatingInfo}> { // Convenience function to translate legacy data to new format. @@ -546,15 +551,16 @@ export class AddonModForumProvider { }; const params = { - discussionid: discussionId + discussionid: discussionId, }; const preSets = { cacheKey: this.getDiscussionPostsCacheKey(discussionId), component: AddonModForumProvider.COMPONENT, - componentId: cmId + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const wsName = this.isGetDiscussionPostsAvailable(site) ? 'mod_forum_get_discussion_posts' : 'mod_forum_get_forum_discussion_posts'; @@ -650,34 +656,30 @@ export class AddonModForumProvider { * Get forum discussions. * * @param forumId Forum ID. - * @param cmId Forum cmid - * @param sortOrder Sort order. - * @param page Page. - * @param forceCache True to always get the value from cache. false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object with: * - discussions: List of discussions. Note that for every discussion in the list discussion.id is the main post ID but * discussion ID is discussion.discussion. * - canLoadMore: True if there may be more discussions to load. */ - getDiscussions(forumId: number, cmId: number, sortOrder?: number, page: number = 0, - forceCache?: boolean, siteId?: string): Promise { - sortOrder = sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC; + getDiscussions(forumId: number, options: AddonModForumGetDiscussionsOptions = {}): Promise { + options.sortOrder = options.sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC; + options.page = options.page || 0; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { let method = 'mod_forum_get_forum_discussions_paginated'; const params: any = { forumid: forumId, - page: page, - perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE + page: options.page, + perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, }; if (site.wsAvailable('mod_forum_get_forum_discussions')) { // Since Moodle 3.7. method = 'mod_forum_get_forum_discussions'; - params.sortorder = sortOrder; + params.sortorder = options.sortOrder; } else { - if (sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { + if (options.sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { params.sortby = 'timemodified'; params.sortdirection = 'DESC'; } else { @@ -685,31 +687,27 @@ export class AddonModForumProvider { return Promise.reject(null); } } - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getDiscussionsListCacheKey(forumId, sortOrder), + + const preSets = { + cacheKey: this.getDiscussionsListCacheKey(forumId, options.sortOrder), component: AddonModForumProvider.COMPONENT, - componentId: cmId + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (forceCache) { - preSets.omitExpires = true; - } return site.read(method, params, preSets).catch((error) => { // Try to get the data from cache stored with the old WS method. if (!this.appProvider.isOnline() && method == 'mod_forum_get_forum_discussion' && - sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { + options.sortOrder == AddonModForumProvider.SORTORDER_LASTPOST_DESC) { const params = { forumid: forumId, - page: page, + page: options.page, perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, sortby: 'timemodified', sortdirection: 'DESC' }; - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getDiscussionsListCacheKey(forumId, sortOrder), - omitExpires: true - }; + Object.assign(preSets, this.sitesProvider.getReadingStrategyPreSets(CoreSitesReadingStrategy.PreferCache)); return site.read('mod_forum_get_forum_discussions_paginated', params, preSets); } @@ -745,17 +743,14 @@ export class AddonModForumProvider { * - discussions: List of discussions. * - error: True if an error occurred, false otherwise. */ - getDiscussionsInPages(forumId: number, cmId: number, sortOrder?: number, forceCache?: boolean, - numPages?: number, startPage?: number, siteId?: string): Promise { - if (typeof numPages == 'undefined') { - numPages = -1; - } - startPage = startPage || 0; + getDiscussionsInPages(forumId: number, options: AddonModForumGetDiscussionsInPagesOptions = {}): Promise { + options.page = options.page || 0; const result = { discussions: [], error: false }; + let numPages = typeof options.numPages == 'undefined' ? -1 : options.numPages; if (!numPages) { return Promise.resolve(result); @@ -763,7 +758,7 @@ export class AddonModForumProvider { const getPage = (page: number): Promise => { // Get page discussions. - return this.getDiscussions(forumId, cmId, sortOrder, page, forceCache, siteId).then((response) => { + return this.getDiscussions(forumId, options).then((response) => { result.discussions = result.discussions.concat(response.discussions); numPages--; @@ -780,7 +775,7 @@ export class AddonModForumProvider { }); }; - return getPage(startPage); + return getPage(options.page); } /** @@ -816,7 +811,11 @@ export class AddonModForumProvider { this.getAvailableSortOrders().forEach((sortOrder) => { // We need to get the list of discussions to be able to invalidate their posts. - promises.push(this.getDiscussionsInPages(forum.id, forum.cmid, sortOrder.value, true).then((response) => { + promises.push(this.getDiscussionsInPages(forum.id, { + cmId: forum.cmid, + sortOrder: sortOrder.value, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((response) => { // Now invalidate the WS calls. const promises = []; @@ -1164,3 +1163,18 @@ export class AddonModForumProvider { }); } } + +/** + * Options to pass to get discussions. + */ +export type AddonModForumGetDiscussionsOptions = CoreCourseCommonModWSOptions & { + sortOrder?: number; // Sort order. + page?: number; // Page. Defaults to 0. +}; + +/** + * Options to pass to get discussions in pages. + */ +export type AddonModForumGetDiscussionsInPagesOptions = AddonModForumGetDiscussionsOptions & { + numPages?: number; // Number of pages to get. If not defined, all pages. +}; diff --git a/src/addon/mod/forum/providers/helper.ts b/src/addon/mod/forum/providers/helper.ts index 7b3db2efa..a3d72dccd 100644 --- a/src/addon/mod/forum/providers/helper.ts +++ b/src/addon/mod/forum/providers/helper.ts @@ -279,7 +279,11 @@ export class AddonModForumHelperProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); const findDiscussion = (page: number): Promise => { - return this.forumProvider.getDiscussions(forumId, cmId, undefined, page, false, siteId).then((response) => { + return this.forumProvider.getDiscussions(forumId, { + cmId, + page, + siteId, + }).then((response) => { if (response.discussions && response.discussions.length > 0) { // Note that discussion.id is the main post ID but discussion ID is discussion.discussion. const discussion = response.discussions.find((discussion) => discussion.discussion == discussionId); diff --git a/src/addon/mod/forum/providers/module-handler.ts b/src/addon/mod/forum/providers/module-handler.ts index 67839c7bc..f79aa5a75 100644 --- a/src/addon/mod/forum/providers/module-handler.ts +++ b/src/addon/mod/forum/providers/module-handler.ts @@ -143,7 +143,7 @@ export class AddonModForumModuleHandler implements CoreCourseModuleHandler { this.forumProvider.invalidateForumData(courseId).finally(() => { // Handle unread posts. - this.forumProvider.getForum(courseId, moduleId, siteId).then((forumData) => { + this.forumProvider.getForum(courseId, moduleId, {siteId}).then((forumData) => { data.extraBadgeColor = ''; data.extraBadge = forumData.unreadpostscount ? this.translate.instant('addon.mod_forum.unreadpostsnumber', {$a : forumData.unreadpostscount }) : ''; diff --git a/src/addon/mod/forum/providers/prefetch-handler.ts b/src/addon/mod/forum/providers/prefetch-handler.ts index 5a75af10a..fd09775d1 100644 --- a/src/addon/mod/forum/providers/prefetch-handler.ts +++ b/src/addon/mod/forum/providers/prefetch-handler.ts @@ -16,10 +16,10 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreGroupsProvider } from '@providers/groups'; @@ -69,7 +69,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand const files = this.getIntroFilesFromInstance(module, forum); // Get posts. - return this.getPostsForPrefetch(forum).then((posts) => { + return this.getPostsForPrefetch(forum, {cmId: module.id}).then((posts) => { // Add posts attachments and embedded files. return files.concat(this.getPostsFiles(posts)); }); @@ -108,14 +108,19 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand * Get the posts to be prefetched. * * @param forum Forum instance. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with array of posts. */ - protected getPostsForPrefetch(forum: any, siteId?: string): Promise { + protected getPostsForPrefetch(forum: any, options: CoreCourseCommonModWSOptions = {}): Promise { const promises = this.forumProvider.getAvailableSortOrders().map((sortOrder) => { // Get discussions in first 2 pages. - return this.forumProvider.getDiscussionsInPages(forum.id, forum.cmid, - sortOrder.value, false, 2, 0, siteId).then((response) => { + const discussionsOptions = { + sortOrder: sortOrder.value, + numPages: 2, + ...options, // Include all options. + }; + + return this.forumProvider.getDiscussionsInPages(forum.id, discussionsOptions).then((response) => { if (response.error) { return Promise.reject(null); } @@ -123,7 +128,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand const promises = []; response.discussions.forEach((discussion) => { - promises.push(this.forumProvider.getDiscussionPosts(discussion.discussion, forum.cmid, siteId)); + promises.push(this.forumProvider.getDiscussionPosts(discussion.discussion, options)); }); return Promise.all(promises); @@ -202,12 +207,21 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Promise resolved when done. */ protected prefetchForum(module: any, courseId: number, single: boolean, siteId: string): Promise { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + // Get the forum data. - return this.forumProvider.getForum(courseId, module.id, siteId).then((forum) => { + return this.forumProvider.getForum(courseId, module.id, commonOptions).then((forum) => { const promises = []; // Prefetch the posts. - promises.push(this.getPostsForPrefetch(forum, siteId).then((posts) => { + promises.push(this.getPostsForPrefetch(forum, modOptions).then((posts) => { const promises = []; const files = this.getIntroFilesFromInstance(module, forum).concat(this.getPostsFiles(posts)); @@ -223,7 +237,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand })); // Prefetch access information. - promises.push(this.forumProvider.getAccessInformation(forum.id, false, siteId)); + promises.push(this.forumProvider.getAccessInformation(forum.id, modOptions)); // Prefetch sort order preference. if (this.forumProvider.isDiscussionListSortingAvailable()) { @@ -244,11 +258,16 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Promise resolved when group data has been prefetched. */ protected prefetchGroupsInfo(forum: any, courseId: number, canCreateDiscussions: boolean, siteId?: string): any { + const options = { + cmId: forum.cmid, + siteId, + }; + // Check group mode. return this.groupsProvider.getActivityGroupMode(forum.cmid, siteId).then((mode) => { if (mode !== CoreGroupsProvider.SEPARATEGROUPS && mode !== CoreGroupsProvider.VISIBLEGROUPS) { // Activity doesn't use groups. Prefetch canAddDiscussionToAll to determine if user can pin/attach. - return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => { + return this.forumProvider.canAddDiscussionToAll(forum.id, options).catch(() => { // Ignore errors. }); } @@ -257,14 +276,14 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand return this.groupsProvider.getActivityAllowedGroups(forum.cmid, undefined, siteId).then((result) => { if (mode === CoreGroupsProvider.SEPARATEGROUPS) { // Groups are already filtered by WS. Prefetch canAddDiscussionToAll to determine if user can pin/attach. - return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => { + return this.forumProvider.canAddDiscussionToAll(forum.id, options).catch(() => { // Ignore errors. }); } if (canCreateDiscussions) { // Prefetch data to check the visible groups when creating discussions. - return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => { + return this.forumProvider.canAddDiscussionToAll(forum.id, options).catch(() => { // The call failed, let's assume he can't. return { status: false @@ -278,7 +297,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand // The user can't post to all groups, let's check which groups he can post to. const groupPromises = []; result.groups.forEach((group) => { - groupPromises.push(this.forumProvider.canAddDiscussion(forum.id, group.id, siteId).catch(() => { + groupPromises.push(this.forumProvider.canAddDiscussion(forum.id, group.id, options).catch(() => { // Ignore errors. })); }); diff --git a/src/addon/mod/forum/providers/sync.ts b/src/addon/mod/forum/providers/sync.ts index 2b204df72..9ca5c5a18 100644 --- a/src/addon/mod/forum/providers/sync.ts +++ b/src/addon/mod/forum/providers/sync.ts @@ -227,7 +227,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider { let groupsPromise; if (data.groupid == AddonModForumProvider.ALL_GROUPS) { // Fetch all group ids. - groupsPromise = this.forumProvider.getForumById(data.courseid, data.forumid, siteId).then((forum) => { + groupsPromise = this.forumProvider.getForumById(data.courseid, data.forumid, {siteId}).then((forum) => { return this.groupsProvider.getActivityAllowedGroups(forum.cmid).then((result) => { return result.groups.map((group) => group.id); }); @@ -330,7 +330,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider { } if (result.warnings.length) { // Fetch forum to construct the warning message. - promises.push(this.forumProvider.getForum(result.itemSet.courseId, result.itemSet.instanceId, siteId) + promises.push(this.forumProvider.getForum(result.itemSet.courseId, result.itemSet.instanceId, {siteId}) .then((forum) => { result.warnings.forEach((warning) => { warnings.push(this.translate.instant('core.warningofflinedatadeleted', { diff --git a/src/addon/mod/glossary/components/index/index.ts b/src/addon/mod/glossary/components/index/index.ts index 41bfbd760..3b759ea5e 100644 --- a/src/addon/mod/glossary/components/index/index.ts +++ b/src/addon/mod/glossary/components/index/index.ts @@ -181,10 +181,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity return Promise.resolve({entries: [], count: 0}); } - const limitFrom = append ? this.entries.length : 0; - const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES; - - return this.glossaryProvider.fetchEntries(this.fetchFunction, this.fetchArguments, limitFrom, limitNum).then((result) => { + return this.glossaryProvider.fetchEntries(this.fetchFunction, this.fetchArguments, { + from: append ? this.entries.length : 0, + cmId: this.module.id, + }).then((result) => { if (append) { Array.prototype.push.apply(this.entries, result.entries); } else { diff --git a/src/addon/mod/glossary/pages/edit/edit.ts b/src/addon/mod/glossary/pages/edit/edit.ts index 9bc2ee645..498568084 100644 --- a/src/addon/mod/glossary/pages/edit/edit.ts +++ b/src/addon/mod/glossary/pages/edit/edit.ts @@ -125,7 +125,9 @@ export class AddonModGlossaryEditPage implements OnInit { this.definitionControl.setValue(this.entry.definition); Promise.resolve(promise).then(() => { - this.glossaryProvider.getAllCategories(this.glossary.id).then((categories) => { + this.glossaryProvider.getAllCategories(this.glossary.id, { + cmId: this.module.id, + }).then((categories) => { this.categories = categories; }).finally(() => { this.loaded = true; @@ -215,8 +217,10 @@ export class AddonModGlossaryEditPage implements OnInit { let promise; if (this.entry && !this.glossary.allowduplicatedentries) { // Check if the entry is duplicated in online or offline mode. - promise = this.glossaryProvider.isConceptUsed(this.glossary.id, this.entry.concept, this.entry.timecreated) - .then((used) => { + promise = this.glossaryProvider.isConceptUsed(this.glossary.id, this.entry.concept, { + timeCreated: this.entry.timecreated, + cmId: this.module.id, + }).then((used) => { if (used) { // There's a entry with same name, reject with error message. return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists')); @@ -237,7 +241,12 @@ export class AddonModGlossaryEditPage implements OnInit { // Try to send it to server. // Don't allow offline if there are attachments since they were uploaded fine. return this.glossaryProvider.addEntry(this.glossary.id, this.entry.concept, definition, this.courseId, options, - attach, timecreated, undefined, this.entry, !this.attachments.length, !this.glossary.allowduplicatedentries); + attach, { + timeCreated: timecreated, + discardEntry: this.entry, + allowOffline: !this.attachments.length, + checkDuplicates: !this.glossary.allowduplicatedentries, + }); } }).then((entryId) => { // Delete the local files from the tmp folder. diff --git a/src/addon/mod/glossary/providers/entry-link-handler.ts b/src/addon/mod/glossary/providers/entry-link-handler.ts index 98ec8a6e3..4c7d5a6fd 100644 --- a/src/addon/mod/glossary/providers/entry-link-handler.ts +++ b/src/addon/mod/glossary/providers/entry-link-handler.ts @@ -63,7 +63,7 @@ export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBas if (courseId) { promise = Promise.resolve(courseId); } else { - promise = this.glossaryProvider.getEntry(entryId, siteId).catch((error) => { + promise = this.glossaryProvider.getEntry(entryId, {siteId}).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); return Promise.reject(null); diff --git a/src/addon/mod/glossary/providers/glossary.ts b/src/addon/mod/glossary/providers/glossary.ts index d80820756..79f9887ad 100644 --- a/src/addon/mod/glossary/providers/glossary.ts +++ b/src/addon/mod/glossary/providers/glossary.ts @@ -17,12 +17,13 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreSite } from '@classes/site'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreRatingInfo } from '@core/rating/providers/rating'; import { AddonModGlossaryOfflineProvider } from './offline'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for glossaries. @@ -92,17 +93,19 @@ export class AddonModGlossaryProvider { * Get all the glossaries in a course. * * @param courseId Course Id. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the glossaries. */ - getCourseGlossaries(courseId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getCourseGlossaries(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] + courseids: [courseId], }; const preSets = { cacheKey: this.getCourseGlossariesCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModGlossaryProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_glossaries_by_courses', params, preSets).then((result) => { @@ -146,29 +149,27 @@ export class AddonModGlossaryProvider { * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. * @param field Search and order using: FIRSTNAME or LASTNAME * @param sort The direction of the order: ASC or DESC - * @param from Start returning records from here. - * @param limit Number of records to return. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the entries. */ - getEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, from: number, limit: number, - omitExpires: boolean, forceOffline: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, + options: AddonModGlossaryGetEntriesOptions = {}): Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, letter: letter, field: field, sort: sort, - from: from, - limit: limit + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets = { cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_author', params, preSets); @@ -199,28 +200,24 @@ export class AddonModGlossaryProvider { * @param glossaryId Glossary Id. * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or * constant SHOW_NOT_CATEGORISED for uncategorised entries. - * @param from Start returning records from here. - * @param limit Number of records to return. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the entries. */ - getEntriesByCategory(glossaryId: number, categoryId: number, from: number, limit: number, omitExpires: boolean, - forceOffline: boolean, siteId?: string): Promise { + getEntriesByCategory(glossaryId: number, categoryId: number, options: AddonModGlossaryGetEntriesOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, categoryid: categoryId, - from: from, - limit: limit + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets = { cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_category', params, preSets); @@ -274,29 +271,26 @@ export class AddonModGlossaryProvider { * @param glossaryId Glossary Id. * @param order The way to order the records. * @param sort The direction of the order. - * @param from Start returning records from here. - * @param limit Number of records to return. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the entries. */ - getEntriesByDate(glossaryId: number, order: string, sort: string, from: number, limit: number, omitExpires: boolean, - forceOffline: boolean, siteId?: string): Promise { + getEntriesByDate(glossaryId: number, order: string, sort: string, options: AddonModGlossaryGetEntriesOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, order: order, sort: sort, - from: from, - limit: limit + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets = { cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_date', params, preSets); @@ -336,35 +330,33 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary Id. * @param letter A letter, or a special keyword. - * @param from Start returning records from here. - * @param limit Number of records to return. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the entries. */ - getEntriesByLetter(glossaryId: number, letter: string, from: number, limit: number, omitExpires: boolean, forceOffline: boolean, - siteId?: string): Promise { + getEntriesByLetter(glossaryId: number, letter: string, options: AddonModGlossaryGetEntriesOptions = {}): Promise { + options.from = options.from || 0; + options.limit = options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, letter: letter, - from: from, - limit: limit + from: options.from, + limit: options.limit, }; const preSets = { cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_letter', params, preSets).then((result) => { - if (limit == AddonModGlossaryProvider.LIMIT_ENTRIES) { + if (options.limit == AddonModGlossaryProvider.LIMIT_ENTRIES) { // Store entries in background, don't block the user for this. - this.storeEntries(glossaryId, result.entries, from, site.getId()).catch(() => { + this.storeEntries(glossaryId, result.entries, options.from, site.getId()).catch(() => { // Ignore errors. }); } @@ -420,23 +412,25 @@ export class AddonModGlossaryProvider { * @param siteId Site ID. If not defined, current site. * @return Resolved with the entries. */ - getEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, from: number, - limit: number, omitExpires: boolean, forceOffline: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, + options: AddonModGlossaryGetEntriesOptions = {}): Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { id: glossaryId, query: query, fullsearch: fullSearch, order: order, sort: sort, - from: from, - limit: limit + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets = { cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort), - omitExpires: omitExpires, - forceOffline: forceOffline, - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entries_by_search', params, preSets); @@ -477,12 +471,12 @@ export class AddonModGlossaryProvider { * Get all the categories related to the glossary. * * @param glossaryId Glossary Id. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the categories if supported or empty array if not. */ - getAllCategories(glossaryId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - return this.getCategories(glossaryId, 0, AddonModGlossaryProvider.LIMIT_CATEGORIES, [], site); + getAllCategories(glossaryId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + return this.getCategories(glossaryId, [], site, options); }); } @@ -490,30 +484,37 @@ export class AddonModGlossaryProvider { * Get the categories related to the glossary by sections. It's a recursive function see initial call values. * * @param glossaryId Glossary Id. - * @param from Number of categories already fetched, so fetch will be done from this number. Initial value 0. - * @param limit Number of categories to fetch. Initial value LIMIT_CATEGORIES. - * @param categories Already fetched categories where to append the fetch. Initial value []. + * @param categories Already fetched categories where to append the fetch. * @param site Site object. + * @param options Other options. * @return Promise resolved with the categories. */ - protected getCategories(glossaryId: number, from: number, limit: number, categories: any[], site: CoreSite): Promise { + protected getCategories(glossaryId: number, categories: any[], site: CoreSite, + options: AddonModGlossaryGetCategoriesOptions = {}): Promise { + + options.from = options.from || 0; + options.limit = options.limit || AddonModGlossaryProvider.LIMIT_CATEGORIES; + const params = { id: glossaryId, - from: from, - limit: limit + from: options.from, + limit: options.limit, }; const preSets = { cacheKey: this.getCategoriesCacheKey(glossaryId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_categories', params, preSets).then((response) => { categories = categories.concat(response.categories); - const canLoadMore = (from + limit) < response.count; + const canLoadMore = (options.from + options.limit) < response.count; if (canLoadMore) { - from += limit; + options.from += options.limit; - return this.getCategories(glossaryId, from, limit, categories, site); + return this.getCategories(glossaryId, categories, site, options); } return categories; @@ -547,17 +548,22 @@ export class AddonModGlossaryProvider { * Get one entry by ID. * * @param entryId Entry ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the entry. */ - getEntry(entryId: number, siteId?: string): Promise<{entry: any, ratinginfo: CoreRatingInfo, from?: number}> { - return this.sitesProvider.getSite(siteId).then((site) => { + getEntry(entryId: number, options: CoreCourseCommonModWSOptions = {}) + : Promise<{entry: any, ratinginfo: CoreRatingInfo, from?: number}> { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - id: entryId + id: entryId, }; const preSets = { cacheKey: this.getEntryCacheKey(entryId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_glossary_get_entry_by_id', params, preSets).then((response) => { @@ -572,8 +578,12 @@ export class AddonModGlossaryProvider { const searchEntry = (from: number, loadNext: boolean): Promise => { // Get the entries from this "page" and check if the entry we're looking for is in it. - return this.getEntriesByLetter(glossaryId, 'ALL', from, AddonModGlossaryProvider.LIMIT_ENTRIES, false, true, - siteId).then((result) => { + return this.getEntriesByLetter(glossaryId, 'ALL', { + from: from, + readingStrategy: CoreSitesReadingStrategy.OnlyCache, + cmId: options.cmId, + siteId: options.siteId, + }).then((result) => { for (let i = 0; i < result.entries.length; i++) { const entry = result.entries[i]; @@ -643,48 +653,34 @@ export class AddonModGlossaryProvider { * * @param fetchFunction Function to fetch. * @param fetchArguments Arguments to call the fetching. - * @param limitFrom Number of entries already fetched, so fetch will be done from this number. - * @param limitNum Number of records to return. Defaults to LIMIT_ENTRIES. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the response. */ - fetchEntries(fetchFunction: Function, fetchArguments: any[], limitFrom: number = 0, limitNum?: number, - omitExpires: boolean = false, forceOffline: boolean = false, siteId?: string): Promise { - limitNum = limitNum || AddonModGlossaryProvider.LIMIT_ENTRIES; - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - + fetchEntries(fetchFunction: Function, fetchArguments: any[], options: AddonModGlossaryGetEntriesOptions = {}): Promise { const args = fetchArguments.slice(); - args.push(limitFrom); - args.push(limitNum); - args.push(omitExpires); - args.push(forceOffline); - args.push(siteId); + args.push(options); return fetchFunction.apply(this, args); } /** - * Performs the whole fetch of the entries using the propper function and arguments. + * Performs the whole fetch of the entries using the proper function and arguments. * * @param fetchFunction Function to fetch. * @param fetchArguments Arguments to call the fetching. - * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. - * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with all entrries. */ - fetchAllEntries(fetchFunction: Function, fetchArguments: any[], omitExpires: boolean = false, forceOffline: boolean = false, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + fetchAllEntries(fetchFunction: Function, fetchArguments: any[], options: CoreCourseCommonModWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); const entries = []; - const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES; const fetchMoreEntries = (): Promise => { - return this.fetchEntries(fetchFunction, fetchArguments, entries.length, limitNum, omitExpires, forceOffline, siteId) - .then((result) => { + return this.fetchEntries(fetchFunction, fetchArguments, { + from: entries.length, + ...options, // Include all options. + }).then((result) => { Array.prototype.push.apply(entries, result.entries); return entries.length < result.count ? fetchMoreEntries() : entries; @@ -759,8 +755,11 @@ export class AddonModGlossaryProvider { const promises = []; if (!onlyEntriesList) { - promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true, false, siteId) - .then((entries) => { + promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], { + cmId: glossary.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((entries) => { return this.invalidateEntries(entries, siteId); })); } @@ -804,11 +803,11 @@ export class AddonModGlossaryProvider { * * @param courseId Course Id. * @param cmId Course Module Id. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the glossary. */ - getGlossary(courseId: number, cmId: number, siteId?: string): Promise { - return this.getCourseGlossaries(courseId, siteId).then((glossaries) => { + getGlossary(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getCourseGlossaries(courseId, options).then((glossaries) => { const glossary = glossaries.find((glossary) => glossary.coursemodule == cmId); if (glossary) { @@ -824,11 +823,11 @@ export class AddonModGlossaryProvider { * * @param courseId Course Id. * @param glossaryId Glossary Id. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the glossary. */ - getGlossaryById(courseId: number, glossaryId: number, siteId?: string): Promise { - return this.getCourseGlossaries(courseId, siteId).then((glossaries) => { + getGlossaryById(courseId: number, glossaryId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getCourseGlossaries(courseId, options).then((glossaries) => { const glossary = glossaries.find((glossary) => glossary.id == glossaryId); if (glossary) { @@ -846,28 +845,27 @@ export class AddonModGlossaryProvider { * @param concept Glossary entry concept. * @param definition Glossary entry concept definition. * @param courseId Course ID of the glossary. - * @param options Array of options for the entry. + * @param entryOptions Array of options for the entry. * @param attach Attachments ID if sending online, result of CoreFileUploaderProvider#storeFilesToUpload * otherwise. - * @param timeCreated The time the entry was created. If not defined, current time. - * @param siteId Site ID. If not defined, current site. - * @param discardEntry The entry provided will be discarded if found. - * @param allowOffline True if it can be stored in offline, false otherwise. - * @param checkDuplicates Check for duplicates before storing offline. Only used if allowOffline is true. + * @param otherOptions Other options. * @return Promise resolved with entry ID if entry was created in server, false if stored in device. */ - addEntry(glossaryId: number, concept: string, definition: string, courseId: number, options: any, attach: any, - timeCreated: number, siteId?: string, discardEntry?: any, allowOffline?: boolean, checkDuplicates?: boolean): - Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + addEntry(glossaryId: number, concept: string, definition: string, courseId: number, entryOptions: any, attach: any, + otherOptions: AddonModGlossaryAddEntryOptions = {}): Promise { + otherOptions.siteId = otherOptions.siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a new entry to be synchronized later. const storeOffline = (): Promise => { - const discardTime = discardEntry && discardEntry.timecreated; + const discardTime = otherOptions.discardEntry && otherOptions.discardEntry.timecreated; let duplicatesPromise; - if (checkDuplicates) { - duplicatesPromise = this.isConceptUsed(glossaryId, concept, discardTime, siteId); + if (otherOptions.checkDuplicates) { + duplicatesPromise = this.isConceptUsed(glossaryId, concept, { + cmId: otherOptions.cmId, + timeCreated: discardTime, + siteId: otherOptions.siteId, + }); } else { duplicatesPromise = Promise.resolve(false); } @@ -878,33 +876,34 @@ export class AddonModGlossaryProvider { return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists')); } - return this.glossaryOffline.addNewEntry(glossaryId, concept, definition, courseId, attach, options, timeCreated, - siteId, undefined, discardEntry).then(() => { + return this.glossaryOffline.addNewEntry(glossaryId, concept, definition, courseId, attach, entryOptions, + otherOptions.timeCreated, otherOptions.siteId, undefined, otherOptions.discardEntry).then(() => { return false; }); }); }; - if (!this.appProvider.isOnline() && allowOffline) { + if (!this.appProvider.isOnline() && otherOptions.allowOffline) { // App is offline, store the action. return storeOffline(); } // If we are editing an offline entry, discard previous first. let discardPromise; - if (discardEntry) { + if (otherOptions.discardEntry) { discardPromise = this.glossaryOffline.deleteNewEntry( - glossaryId, discardEntry.concept, discardEntry.timecreated, siteId); + glossaryId, otherOptions.discardEntry.concept, otherOptions.discardEntry.timecreated, otherOptions.siteId); } else { discardPromise = Promise.resolve(); } return discardPromise.then(() => { // Try to add it in online. - return this.addEntryOnline(glossaryId, concept, definition, options, attach, siteId).then((entryId) => { + return this.addEntryOnline(glossaryId, concept, definition, entryOptions, attach, otherOptions.siteId) + .then((entryId) => { return entryId; }).catch((error) => { - if (allowOffline && !this.utils.isWebServiceError(error)) { + if (otherOptions.allowOffline && !this.utils.isWebServiceError(error)) { // Couldn't connect to server, store in offline. return storeOffline(); } else { @@ -964,20 +963,23 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary ID. * @param concept Concept to check. - * @param timeCreated Timecreated to check that is not the timecreated we are editing. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with true if used, resolved with false if not used or error. */ - isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise { + isConceptUsed(glossaryId: number, concept: string, options: AddonModGlossaryIsConceptUsedOptions = {}): Promise { // Check offline first. - return this.glossaryOffline.isConceptUsed(glossaryId, concept, timeCreated, siteId).then((exists) => { + return this.glossaryOffline.isConceptUsed(glossaryId, concept, options.timeCreated, options.siteId).then((exists) => { if (exists) { return true; } // If we get here, there's no offline entry with this name, check online. // Get entries from the cache. - return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], true, false, siteId).then((entries) => { + return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((entries) => { // Check if there's any entry with the same concept. return entries.some((entry) => entry.concept == concept); }); @@ -1074,3 +1076,40 @@ export class AddonModGlossaryProvider { }); } } + +/** + * Options to pass to add entry. + */ +export type AddonModGlossaryAddEntryOptions = { + timeCreated?: number; // The time the entry was created. If not defined, current time. + discardEntry?: any; // The entry provided will be discarded if found. + allowOffline?: boolean; // True if it can be stored in offline, false otherwise. + checkDuplicates?: boolean; // Check for duplicates before storing offline. Only used if allowOffline is true. + cmId?: number; // Module ID. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to the different get entries functions. + */ +export type AddonModGlossaryGetEntriesOptions = CoreCourseCommonModWSOptions & { + from?: number; // Start returning records from here. Defaults to 0. + limit?: number; // Number of records to return. Defaults to AddonModGlossaryProvider.LIMIT_ENTRIES. +}; + +/** + * Options to pass to get categories. + */ +export type AddonModGlossaryGetCategoriesOptions = CoreCourseCommonModWSOptions & { + from?: number; // Start returning records from here. Defaults to 0. + limit?: number; // Number of records to return. Defaults to AddonModGlossaryProvider.LIMIT_CATEGORIES. +}; + +/** + * Options to pass to is concept used. + */ +export type AddonModGlossaryIsConceptUsedOptions = { + cmId?: number; // Module ID. + timeCreated?: number; // Timecreated to check that is not the timecreated we are editing. + siteId?: string; // Site ID. If not defined, current site. +}; diff --git a/src/addon/mod/glossary/providers/prefetch-handler.ts b/src/addon/mod/glossary/providers/prefetch-handler.ts index c202a88e6..cfb5c4785 100644 --- a/src/addon/mod/glossary/providers/prefetch-handler.ts +++ b/src/addon/mod/glossary/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -66,8 +66,9 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH */ getFiles(module: any, courseId: number, single?: boolean): Promise { return this.glossaryProvider.getGlossary(courseId, module.id).then((glossary) => { - return this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, [glossary.id, 'ALL']) - .then((entries) => { + return this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, [glossary.id, 'ALL'], { + cmId: module.id, + }).then((entries) => { return this.getFilesFromGlossaryAndEntries(module, glossary, entries); }); }).catch(() => { @@ -139,8 +140,14 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH protected prefetchGlossary(module: any, courseId: number, single: boolean, siteId: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + // Prefetch the glossary data. - return this.glossaryProvider.getGlossary(courseId, module.id, siteId).then((glossary) => { + return this.glossaryProvider.getGlossary(courseId, module.id, {siteId}).then((glossary) => { const promises = []; glossary.browsemodes.forEach((mode) => { @@ -149,25 +156,25 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH break; case 'cat': // Not implemented. promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByCategory, - [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES], false, false, siteId)); + [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES], options)); break; case 'date': promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate, - [glossary.id, 'CREATION', 'DESC'], false, false, siteId)); + [glossary.id, 'CREATION', 'DESC'], options)); promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate, - [glossary.id, 'UPDATE', 'DESC'], false, false, siteId)); + [glossary.id, 'UPDATE', 'DESC'], options)); break; case 'author': promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByAuthor, - [glossary.id, 'ALL', 'LASTNAME', 'ASC'], false, false, siteId)); + [glossary.id, 'ALL', 'LASTNAME', 'ASC'], options)); break; default: } }); // Fetch all entries to get information from. - promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, - [glossary.id, 'ALL'], false, false, siteId).then((entries) => { + promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, [glossary.id, 'ALL'], + options).then((entries) => { const promises = []; const commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(); @@ -190,7 +197,7 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH })); // Get all categories. - promises.push(this.glossaryProvider.getAllCategories(glossary.id, siteId)); + promises.push(this.glossaryProvider.getAllCategories(glossary.id, options)); // Prefetch data for link handlers. promises.push(this.courseProvider.getModuleBasicInfo(module.id, siteId)); diff --git a/src/addon/mod/glossary/providers/sync.ts b/src/addon/mod/glossary/providers/sync.ts index 5ad4034c5..87ea9ba5c 100644 --- a/src/addon/mod/glossary/providers/sync.ts +++ b/src/addon/mod/glossary/providers/sync.ts @@ -281,7 +281,7 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider { }); } if (result.warnings.length) { - promises.push(this.glossaryProvider.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, siteId) + promises.push(this.glossaryProvider.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, {siteId}) .then((glossary) => { result.warnings.forEach((warning) => { warnings.push(this.translate.instant('core.warningofflinedatadeleted', { diff --git a/src/addon/mod/h5pactivity/components/index/index.ts b/src/addon/mod/h5pactivity/components/index/index.ts index 2dc01a2d5..c0f1077e7 100644 --- a/src/addon/mod/h5pactivity/components/index/index.ts +++ b/src/addon/mod/h5pactivity/components/index/index.ts @@ -108,7 +108,9 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv */ protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { try { - this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, false, this.siteId); + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, { + siteId: this.siteId, + }); this.dataRetrieved.emit(this.h5pActivity); this.description = this.h5pActivity.intro; @@ -161,7 +163,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv * @return Promise resolved when done. */ protected async fetchAccessInfo(): Promise { - this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, false, this.siteId); + this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, { + cmId: this.module.id, + siteId: this.siteId, + }); } /** diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts index 96b99fc16..996ddb0cf 100644 --- a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts @@ -77,32 +77,15 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit { * @return Promise resolved when done. */ protected async fetchData(): Promise { - await Promise.all([ - this.fetchActivity(), - this.fetchAttempt(), - ]); + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); + + this.attempt = await AddonModH5PActivity.instance.getAttemptResults(this.h5pActivityId, this.attemptId, { + cmId: this.h5pActivity.coursemodule, + }); await this.fetchUserProfile(); } - /** - * Get activity data. - * - * @return Promise resolved when done. - */ - protected async fetchActivity(): Promise { - this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); - } - - /** - * Get attempts. - * - * @return Promise resolved when done. - */ - protected async fetchAttempt(): Promise { - this.attempt = await AddonModH5PActivity.instance.getAttemptResults(this.h5pActivityId, this.attemptId); - } - /** * Get user profile. * diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts index cc8499405..bffee1f21 100644 --- a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts @@ -79,29 +79,24 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit { * @return Promise resolved when done. */ protected async fetchData(): Promise { + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); + await Promise.all([ - this.fetchActivity(), this.fetchAttempts(), this.fetchUserProfile(), ]); } - /** - * Get activity data. - * - * @return Promise resolved when done. - */ - protected async fetchActivity(): Promise { - this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); - } - /** * Get attempts. * * @return Promise resolved when done. */ protected async fetchAttempts(): Promise { - this.attemptsData = await AddonModH5PActivity.instance.getUserAttempts(this.h5pActivityId, { userId: this.userId }); + this.attemptsData = await AddonModH5PActivity.instance.getUserAttempts(this.h5pActivityId, { + cmId: this.h5pActivity.coursemodule, + userId: this.userId, + }); } /** diff --git a/src/addon/mod/h5pactivity/providers/h5pactivity.ts b/src/addon/mod/h5pactivity/providers/h5pactivity.ts index 5e1fabc42..1cf09d1c8 100644 --- a/src/addon/mod/h5pactivity/providers/h5pactivity.ts +++ b/src/addon/mod/h5pactivity/providers/h5pactivity.ts @@ -14,14 +14,15 @@ import { Injectable } from '@angular/core'; -import { CoreSites } from '@providers/sites'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; import { CoreTimeUtils } from '@providers/utils/time'; import { CoreUtils } from '@providers/utils/utils'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreCourseLogHelper } from '@core/course/providers/log-helper'; import { CoreH5P } from '@core/h5p/providers/h5p'; import { CoreH5PDisplayOptions } from '@core/h5p/classes/core'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { makeSingleton, Translate } from '@singletons/core.singletons'; @@ -121,20 +122,22 @@ export class AddonModH5PActivityProvider { * Get access information for a given H5P activity. * * @param id H5P activity ID. - * @param forceCache True to always get the value from cache. false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the data. */ - async getAccessInformation(id: number, forceCache?: boolean, siteId?: string): Promise { + async getAccessInformation(id: number, options: CoreCourseCommonModWSOptions = {}): Promise { - const site = await CoreSites.instance.getSite(siteId); + const site = await CoreSites.instance.getSite(options.siteId); const params = { h5pactivityid: id, }; const preSets = { cacheKey: this.getAccessInformationCacheKey(id), - omitExpires: forceCache, + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_h5pactivity_get_h5pactivity_access_information', params, preSets); @@ -209,18 +212,14 @@ export class AddonModH5PActivityProvider { h5pactivityid: id, attemptids: [attemptId], }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (options.forceCache) { - preSets.omitExpires = true; - } else if (options.ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - try { const response: AddonModH5PActivityGetResultsResult = await site.read('mod_h5pactivity_get_results', params, preSets); @@ -235,9 +234,12 @@ export class AddonModH5PActivityProvider { } // Check if the full list of results is cached. If so, get the results from there. - options.forceCache = true; + const cacheOptions = { + ...options, // Include all the original options. + readingStrategy: CoreSitesReadingStrategy.OnlyCache, + }; - const attemptsResults = await AddonModH5PActivity.instance.getAllAttemptsResults(id, options); + const attemptsResults = await AddonModH5PActivity.instance.getAllAttemptsResults(id, cacheOptions); const attempt = attemptsResults.attempts.find((attempt) => { return attempt.id == attemptId; @@ -270,18 +272,14 @@ export class AddonModH5PActivityProvider { h5pactivityid: id, attemptids: attemptsIds, }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getAttemptResultsCommonCacheKey(id), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (options.forceCache) { - preSets.omitExpires = true; - } else if (options.ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - const response: AddonModH5PActivityGetResultsResult = await site.read('mod_h5pactivity_get_results', params, preSets); response.attempts = response.attempts.map((attempt) => { @@ -334,28 +332,24 @@ export class AddonModH5PActivityProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param moduleUrl Module URL. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the activity data. */ - protected async getH5PActivityByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string) + protected async getH5PActivityByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) : Promise { - const site = await CoreSites.instance.getSite(siteId); + const site = await CoreSites.instance.getSite(options.siteId); const params = { courseids: [courseId], }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getH5PActivityDataCacheKey(courseId), updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModH5PActivityProvider.COMPONENT, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (forceCache) { - preSets.omitExpires = true; - } - const response: AddonModH5PActivityGetByCoursesResult = await site.read('mod_h5pactivity_get_h5pactivities_by_courses', params, preSets); @@ -377,12 +371,11 @@ export class AddonModH5PActivityProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the activity data. */ - getH5PActivity(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { - return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId); + getH5PActivity(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getH5PActivityByField(courseId, 'coursemodule', cmId, options); } /** @@ -390,13 +383,12 @@ export class AddonModH5PActivityProvider { * * @param courseId Course ID. * @param contextId Context ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the activity data. */ - getH5PActivityByContextId(courseId: number, contextId: number, forceCache?: boolean, siteId?: string) + getH5PActivityByContextId(courseId: number, contextId: number, options: CoreSitesCommonWSOptions = {}) : Promise { - return this.getH5PActivityByField(courseId, 'context', contextId, forceCache, siteId); + return this.getH5PActivityByField(courseId, 'context', contextId, options); } /** @@ -404,12 +396,11 @@ export class AddonModH5PActivityProvider { * * @param courseId Course ID. * @param id Instance ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the activity data. */ - getH5PActivityById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { - return this.getH5PActivityByField(courseId, 'id', id, forceCache, siteId); + getH5PActivityById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getH5PActivityByField(courseId, 'id', id, options); } /** @@ -440,9 +431,8 @@ export class AddonModH5PActivityProvider { * @param options Other options. * @return Promise resolved with the attempts of the user. */ - async getUserAttempts(id: number, options?: AddonModH5PActivityGetAttemptsOptions): Promise { - - options = options || {}; + async getUserAttempts(id: number, options: AddonModH5PActivityGetAttemptsOptions = {}) + : Promise { const site = await CoreSites.instance.getSite(options.siteId); @@ -450,18 +440,14 @@ export class AddonModH5PActivityProvider { h5pactivityid: id, userids: [options.userId || site.getUserId()], }; - const preSets: CoreSiteWSPreSets = { + const preSets = { cacheKey: this.getUserAttemptsCacheKey(id, params.userids), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (options.forceCache) { - preSets.omitExpires = true; - } else if (options.ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - const response: AddonModH5PActivityGetAttemptsResult = await site.read('mod_h5pactivity_get_attempts', params, preSets); if (response.warnings[0]) { @@ -789,10 +775,7 @@ export type AddonModH5PActivityGetDeployedFileOptions = { /** * Options to pass to getAttemptResults function. */ -export type AddonModH5PActivityGetAttemptResultsOptions = { - forceCache?: boolean; // Whether to force cache. If not cached, it will call the WS. - ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down. - siteId?: string; // Site ID. If not defined, current site. +export type AddonModH5PActivityGetAttemptResultsOptions = CoreCourseCommonModWSOptions & { userId?: number; // User ID. If not defined, user of the site. }; diff --git a/src/addon/mod/h5pactivity/providers/prefetch-handler.ts b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts index 85a804baf..fd000532a 100644 --- a/src/addon/mod/h5pactivity/providers/prefetch-handler.ts +++ b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreWSExternalFile } from '@providers/ws'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -130,7 +130,10 @@ export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefet */ protected async prefetchActivity(module: any, courseId: number, single: boolean, siteId: string): Promise { - const h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(courseId, module.id, true, siteId); + const h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); const introFiles = this.getIntroFilesFromInstance(module, h5pActivity); @@ -171,14 +174,19 @@ export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefet */ protected async prefetchWSData(h5pActivity: AddonModH5PActivityData, siteId: string): Promise { - const accessInfo = await AddonModH5PActivity.instance.getAccessInformation(h5pActivity.id, true, siteId); + const accessInfo = await AddonModH5PActivity.instance.getAccessInformation(h5pActivity.id, { + cmId: h5pActivity.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }); if (!accessInfo.canreviewattempts) { // Not a teacher, prefetch user attempts and the current user profile. const site = await this.sitesProvider.getSite(siteId); const options = { - ignoreCache: true, + cmId: h5pActivity.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, siteId: siteId, }; diff --git a/src/addon/mod/h5pactivity/providers/sync.ts b/src/addon/mod/h5pactivity/providers/sync.ts index dafa44842..407f86d91 100644 --- a/src/addon/mod/h5pactivity/providers/sync.ts +++ b/src/addon/mod/h5pactivity/providers/sync.ts @@ -165,7 +165,7 @@ export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseP // Get the activity instance. const courseId = entries[0].courseid; - const h5pActivity = await AddonModH5PActivity.instance.getH5PActivityByContextId(courseId, contextId, false, siteId); + const h5pActivity = await AddonModH5PActivity.instance.getH5PActivityByContextId(courseId, contextId, {siteId}); // Sync offline logs. try { diff --git a/src/addon/mod/imscp/providers/imscp.ts b/src/addon/mod/imscp/providers/imscp.ts index bd7eb8cf4..3a0bd3c52 100644 --- a/src/addon/mod/imscp/providers/imscp.ts +++ b/src/addon/mod/imscp/providers/imscp.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -155,17 +155,21 @@ export class AddonModImscpProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the imscp is retrieved. */ - protected getImscpByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getImscpByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] + courseids: [courseId], }; const preSets = { cacheKey: this.getImscpDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModImscpProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_imscp_get_imscps_by_courses', params, preSets) @@ -188,11 +192,11 @@ export class AddonModImscpProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the imscp is retrieved. */ - getImscp(courseId: number, cmId: number, siteId?: string): Promise { - return this.getImscpByKey(courseId, 'coursemodule', cmId, siteId); + getImscp(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getImscpByKey(courseId, 'coursemodule', cmId, options); } /** diff --git a/src/addon/mod/imscp/providers/prefetch-handler.ts b/src/addon/mod/imscp/providers/prefetch-handler.ts index 6fe5ab2b9..899fdbe09 100644 --- a/src/addon/mod/imscp/providers/prefetch-handler.ts +++ b/src/addon/mod/imscp/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -67,7 +67,10 @@ export class AddonModImscpPrefetchHandler extends CoreCourseResourcePrefetchHand const promises = []; promises.push(super.downloadOrPrefetch(module, courseId, prefetch, dirPath)); - promises.push(this.imscpProvider.getImscp(courseId, module.id, siteId)); + promises.push(this.imscpProvider.getImscp(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + })); return Promise.all(promises); }); diff --git a/src/addon/mod/label/providers/label.ts b/src/addon/mod/label/providers/label.ts index ec924ebd0..b16a304fe 100644 --- a/src/addon/mod/label/providers/label.ts +++ b/src/addon/mod/label/providers/label.ts @@ -13,10 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; /** @@ -47,29 +47,22 @@ export class AddonModLabelProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache True to always get the value from cache, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not provided, current site. + * @param options Other options. * @return Promise resolved when the label is retrieved. */ - protected getLabelByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + protected getLabelByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getLabelDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getLabelDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModLabelProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_label_get_labels_by_courses', params, preSets) .then((response: AddonModLabelGetLabelsByCoursesResult): any => { @@ -91,14 +84,11 @@ export class AddonModLabelProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param forceCache True to always get the value from cache, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the label is retrieved. */ - getLabel(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { - return this.getLabelByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); + getLabel(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLabelByField(courseId, 'coursemodule', cmId, options); } /** @@ -106,14 +96,11 @@ export class AddonModLabelProvider { * * @param courseId Course ID. * @param labelId Label ID. - * @param forceCache True to always get the value from cache, false otherwise. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the label is retrieved. */ - getLabelById(courseId: number, labelId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { - return this.getLabelByField(courseId, 'id', labelId, forceCache, ignoreCache, siteId); + getLabelById(courseId: number, labelId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLabelByField(courseId, 'id', labelId, options); } /** diff --git a/src/addon/mod/label/providers/prefetch-handler.ts b/src/addon/mod/label/providers/prefetch-handler.ts index e05f9ac0c..b920ac7ba 100644 --- a/src/addon/mod/label/providers/prefetch-handler.ts +++ b/src/addon/mod/label/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -63,7 +63,9 @@ export class AddonModLabelPrefetchHandler extends CoreCourseResourcePrefetchHand let promise; if (this.labelProvider.isGetLabelAvailableForSite()) { - promise = this.labelProvider.getLabel(courseId, module.id, false, ignoreCache); + promise = this.labelProvider.getLabel(courseId, module.id, { + readingStrategy: ignoreCache ? CoreSitesReadingStrategy.OnlyNetwork : undefined + }); } else { promise = Promise.resolve(); } diff --git a/src/addon/mod/lesson/components/index/index.ts b/src/addon/mod/lesson/components/index/index.ts index c8c4fb1ed..4df72774b 100644 --- a/src/addon/mod/lesson/components/index/index.ts +++ b/src/addon/mod/lesson/components/index/index.ts @@ -118,6 +118,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo let lessonReady = true; this.askPassword = false; + const options = {cmId: this.module.id}; return this.lessonProvider.getLesson(this.courseId, this.module.id).then((lessonData) => { this.lesson = lessonData; @@ -130,7 +131,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo return this.syncActivity(showErrors); } }).then(() => { - return this.lessonProvider.getAccessInformation(this.lesson.id); + return this.lessonProvider.getAccessInformation(this.lesson.id, options); }).then((info) => { const promises = []; @@ -167,8 +168,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo })); // Update the list of content pages viewed and question attempts. - promises.push(this.lessonProvider.getContentPagesViewedOnline(this.lesson.id, info.attemptscount)); - promises.push(this.lessonProvider.getQuestionsAttemptsOnline(this.lesson.id, info.attemptscount)); + promises.push(this.lessonProvider.getContentPagesViewedOnline(this.lesson.id, info.attemptscount, options)); + promises.push(this.lessonProvider.getQuestionsAttemptsOnline(this.lesson.id, info.attemptscount, options)); } if (info.preventaccessreasons && info.preventaccessreasons.length) { @@ -364,7 +365,9 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo if (this.hasOffline) { if (continueLast) { - promise = this.lessonProvider.getLastPageSeen(this.lesson.id, this.accessInfo.attemptscount); + promise = this.lessonProvider.getLastPageSeen(this.lesson.id, this.accessInfo.attemptscount, { + cmId: this.module.id, + }); } else { promise = Promise.resolve(this.accessInfo.firstpageid); } @@ -445,7 +448,10 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo } // Get the overview of retakes for the group. - return this.lessonProvider.getRetakesOverview(this.lesson.id, groupId).then((data) => { + return this.lessonProvider.getRetakesOverview(this.lesson.id, { + groupId, + cmId: this.lesson.coursemodule, + }).then((data) => { const promises = []; // Format times and grades. @@ -617,7 +623,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * @return Promise resolved when done. */ protected validatePassword(password: string): Promise { - return this.lessonProvider.getLessonWithPassword(this.lesson.id, password).then((lessonData) => { + return this.lessonProvider.getLessonWithPassword(this.lesson.id, {password, cmId: this.module.id}).then((lessonData) => { this.lesson = lessonData; this.password = password; }).catch((error) => { diff --git a/src/addon/mod/lesson/pages/player/player.ts b/src/addon/mod/lesson/pages/player/player.ts index 2b2c1753b..b2a69cd5e 100644 --- a/src/addon/mod/lesson/pages/player/player.ts +++ b/src/addon/mod/lesson/pages/player/player.ts @@ -18,7 +18,7 @@ import { IonicPage, NavParams, Content, PopoverController, ModalController, Moda import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -172,11 +172,10 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { * * @param func Function to call. * @param args Arguments to pass to the function. - * @param offlineParamPos Position of the offline parameter in the args. - * @param jumpsParamPos Position of the jumps parameter in the args. + * @param options Options passed to the function (also included in args). * @return Promise resolved in success, rejected otherwise. */ - protected callFunction(func: Function, args: any[], offlineParamPos: number, jumpsParamPos?: number): Promise { + protected callFunction(func: Function, args: any[], options: any): Promise { return func.apply(func, args).catch((error) => { if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson) && !this.utils.isWebServiceError(error)) { @@ -184,14 +183,16 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { this.offline = true; // Get the possible jumps now. - return this.lessonProvider.getPagesPossibleJumps(this.lesson.id, true).then((jumpList) => { + return this.lessonProvider.getPagesPossibleJumps(this.lesson.id, { + cmId: this.lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((jumpList) => { this.jumps = jumpList; - // Call the function again with offline set to true and the new jumps. - args[offlineParamPos] = true; - if (typeof jumpsParamPos != 'undefined') { - args[jumpsParamPos] = this.jumps; - } + // Call the function again with offline mode and the new jumps. + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + options.jumps = this.jumps; + options.offline = true; return func.apply(func, args); }); @@ -246,8 +247,13 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { this.offline = true; } + const options = { + cmId: this.lesson.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; + return this.callFunction(this.lessonProvider.getAccessInformation.bind(this.lessonProvider), - [this.lesson.id, this.offline, true], 1); + [this.lesson.id, options], options); }).then((info) => { const promises = []; @@ -272,15 +278,23 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { if (this.password) { // Lesson uses password, get the whole lesson object. + const options = { + password: this.password, + cmId: this.lesson.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; promises.push(this.callFunction(this.lessonProvider.getLessonWithPassword.bind(this.lessonProvider), - [this.lesson.id, this.password, true, this.offline, true], 3).then((lesson) => { + [this.lesson.id, options], options).then((lesson) => { this.lesson = lesson; })); } if (this.offline) { // Offline mode, get the list of possible jumps to allow navigation. - promises.push(this.lessonProvider.getPagesPossibleJumps(this.lesson.id, true).then((jumpList) => { + promises.push(this.lessonProvider.getPagesPossibleJumps(this.lesson.id, { + cmId: this.lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((jumpList) => { this.jumps = jumpList; })); } @@ -334,7 +348,9 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { const error = result.warnings[0]; // Some data was deleted. Check if the retake has changed. - return this.lessonProvider.getAccessInformation(this.lesson.id).then((info) => { + return this.lessonProvider.getAccessInformation(this.lesson.id, { + cmId: this.lesson.coursemodule, + }).then((info) => { if (info.attemptscount != this.accessInfo.attemptscount) { // The retake has changed. Leave the view and show the error. this.forceLeave = true; @@ -359,9 +375,16 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { return promise.then(() => { // Now finish the retake. - const args = [this.lesson, this.courseId, this.password, outOfTime, this.review, this.offline, this.accessInfo]; + const options = { + password: this.password, + outOfTime, + review: this.review, + offline: this.offline, + accessInfo: this.accessInfo, + }; + const args = [this.lesson, this.courseId, options]; - return this.callFunction(this.lessonProvider.finishRetake.bind(this.lessonProvider), args, 5); + return this.callFunction(this.lessonProvider.finishRetake.bind(this.lessonProvider), args, options); }).then((data) => { this.title = this.lesson.name; this.eolData = data.data; @@ -447,7 +470,10 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { if (this.lesson.timelimit && !this.accessInfo.canmanage) { // Get the last lesson timer. - return this.lessonProvider.getTimers(this.lesson.id, false, true).then((timers) => { + return this.lessonProvider.getTimers(this.lesson.id, { + cmId: this.lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }).then((timers) => { this.endTime = timers[timers.length - 1].starttime + this.lesson.timelimit; }); } @@ -469,9 +495,14 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { this.loadingMenu = true; - const args = [this.lessonId, this.password, this.offline, true]; + const options = { + password: this.password, + cmId: this.lesson.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }; + const args = [this.lessonId, options]; - return this.callFunction(this.lessonProvider.getPages.bind(this.lessonProvider), args, 2).then((pages) => { + return this.callFunction(this.lessonProvider.getPages.bind(this.lessonProvider), args, options).then((pages) => { this.lessonPages = pages.map((entry) => { return entry.page; }); @@ -494,9 +525,18 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { return this.finishRetake(); } - const args = [this.lesson, pageId, this.password, this.review, true, this.offline, true, this.accessInfo, this.jumps]; + const options = { + password: this.password, + review: this.review, + inludeContents: true, + cmId: this.lesson.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + accessInfo: this.accessInfo, + jumps: this.jumps, + }; + const args = [this.lesson, pageId, options]; - return this.callFunction(this.lessonProvider.getPageData.bind(this.lessonProvider), args, 5, 8).then((data) => { + return this.callFunction(this.lessonProvider.getPageData.bind(this.lessonProvider), args, options).then((data) => { if (data.newpageid == AddonModLessonProvider.LESSON_EOL) { // End of lesson reached. return this.finishRetake(); @@ -548,10 +588,16 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { protected processPage(data: any, formSubmitted?: boolean): Promise { this.loaded = false; - const args = [this.lesson, this.courseId, this.pageData, data, this.password, this.review, this.offline, this.accessInfo, - this.jumps]; + const options = { + password: this.password, + review: this.review, + offline: this.offline, + accessInfo: this.accessInfo, + jumps: this.jumps, + }; + const args = [this.lesson, this.courseId, this.pageData, data, options]; - return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, 6, 8).then((result) => { + return this.callFunction(this.lessonProvider.processPage.bind(this.lessonProvider), args, options).then((result) => { if (formSubmitted) { this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.sitesProvider.getCurrentSiteId()); } @@ -559,11 +605,15 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { if (!this.offline && !this.review && this.lessonProvider.isLessonOffline(this.lesson)) { // Lesson allows offline and the user changed some data in server. Update cached data. const retake = this.accessInfo.attemptscount; + const options = { + cmId: this.lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }; if (this.lessonProvider.isQuestionPage(this.pageData.page.type)) { - this.lessonProvider.getQuestionsAttemptsOnline(this.lessonId, retake, false, undefined, false, true); + this.lessonProvider.getQuestionsAttemptsOnline(this.lessonId, retake, options); } else { - this.lessonProvider.getContentPagesViewedOnline(this.lessonId, retake, false, true); + this.lessonProvider.getContentPagesViewedOnline(this.lessonId, retake, options); } } diff --git a/src/addon/mod/lesson/pages/user-retake/user-retake.ts b/src/addon/mod/lesson/pages/user-retake/user-retake.ts index bbbb9e6c8..3d6418495 100644 --- a/src/addon/mod/lesson/pages/user-retake/user-retake.ts +++ b/src/addon/mod/lesson/pages/user-retake/user-retake.ts @@ -106,7 +106,9 @@ export class AddonModLessonUserRetakePage implements OnInit { this.lesson = lessonData; // Get the retakes overview for all participants. - return this.lessonProvider.getRetakesOverview(this.lesson.id); + return this.lessonProvider.getRetakesOverview(this.lesson.id, { + cmId: this.lesson.coursemodule, + }); }).then((data) => { // Search the student. let student; @@ -193,7 +195,10 @@ export class AddonModLessonUserRetakePage implements OnInit { protected setRetake(retakeNumber: number): Promise { this.selectedRetake = retakeNumber; - return this.lessonProvider.getUserRetake(this.lessonId, retakeNumber, this.userId).then((data) => { + return this.lessonProvider.getUserRetake(this.lessonId, retakeNumber, { + cmId: this.lesson.coursemodule, + userId: this.userId, + }).then((data) => { if (data && data.completed != -1) { // Completed. diff --git a/src/addon/mod/lesson/providers/grade-link-handler.ts b/src/addon/mod/lesson/providers/grade-link-handler.ts index c608c692e..7c0cc6c34 100644 --- a/src/addon/mod/lesson/providers/grade-link-handler.ts +++ b/src/addon/mod/lesson/providers/grade-link-handler.ts @@ -57,7 +57,7 @@ export class AddonModLessonGradeLinkHandler extends CoreContentLinksModuleGradeH courseId = module.course || courseId || params.courseid || params.cid; // Check if the user can see the user reports in the lesson. - return this.lessonProvider.getAccessInformation(module.instance); + return this.lessonProvider.getAccessInformation(module.instance, {cmId: module.id, siteId}); }).then((info) => { if (info.canviewreports) { // User can view reports, go to view the report. diff --git a/src/addon/mod/lesson/providers/lesson-sync.ts b/src/addon/mod/lesson/providers/lesson-sync.ts index f3c2d4303..a5375d352 100644 --- a/src/addon/mod/lesson/providers/lesson-sync.ts +++ b/src/addon/mod/lesson/providers/lesson-sync.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -292,10 +292,14 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid courseId = attempts[0].courseid; // Get the info, access info and the lesson password if needed. - return this.lessonProvider.getLessonById(courseId, lessonId, false, false, siteId).then((lessonData) => { + return this.lessonProvider.getLessonById(courseId, lessonId, {siteId}).then((lessonData) => { lesson = lessonData; - return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); + return this.prefetchHandler.getLessonPassword(lessonId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + askPassword, + siteId, + }); }).then((data) => { const attemptsLength = attempts.length, promises = []; @@ -368,10 +372,14 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid // Data already retrieved when syncing attempts. promise = Promise.resolve(); } else { - promise = this.lessonProvider.getLessonById(courseId, lessonId, false, false, siteId).then((lessonData) => { + promise = this.lessonProvider.getLessonById(courseId, lessonId, {siteId}).then((lessonData) => { lesson = lessonData; - return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId); + return this.prefetchHandler.getLessonPassword(lessonId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + askPassword, + siteId, + }); }).then((data) => { accessInfo = data.accessInfo; password = data.password; @@ -394,7 +402,7 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid } // All good, finish the retake. - return this.lessonProvider.finishRetakeOnline(lessonId, password, false, false, siteId).then((response) => { + return this.lessonProvider.finishRetakeOnline(lessonId, {password, siteId}).then((response) => { result.updated = true; if (!ignoreBlock) { @@ -466,7 +474,10 @@ export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvid protected sendAttempt(lesson: any, password: string, attempt: any, result: AddonModLessonSyncResult, siteId?: string) : Promise { - return this.lessonProvider.processPageOnline(lesson.id, attempt.pageid, attempt.data, password, false, siteId).then(() => { + return this.lessonProvider.processPageOnline(lesson.id, attempt.pageid, attempt.data, { + password, + siteId, + }).then(() => { result.updated = true; return this.lessonOfflineProvider.deleteAttempt(lesson.id, attempt.retake, attempt.pageid, attempt.timemodified, diff --git a/src/addon/mod/lesson/providers/lesson.ts b/src/addon/mod/lesson/providers/lesson.ts index a4db30c6f..148ad611c 100644 --- a/src/addon/mod/lesson/providers/lesson.ts +++ b/src/addon/mod/lesson/providers/lesson.ts @@ -16,14 +16,15 @@ 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 { CoreSitesProvider, CoreSiteSchema, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGradesProvider } from '@core/grades/providers/grades'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { AddonModLessonOfflineProvider } from './lesson-offline'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Result of check answer. @@ -314,32 +315,31 @@ export class AddonModLessonProvider { * Calculate some offline data like progress and ongoingscore. * * @param lesson Lesson. - * @param accessInfo Result of get access info. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param pageIndex Object containing all the pages indexed by ID. If not defined, it will be calculated. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the data. */ - protected calculateOfflineData(lesson: any, accessInfo?: any, password?: string, review?: boolean, pageIndex?: any, - siteId?: string): Promise<{reviewmode: boolean, progress: number, ongoingscore: string}> { + protected calculateOfflineData(lesson: any, options: AddonModLessonCalculateOfflineDataOptions = {}) + : Promise<{reviewmode: boolean, progress: number, ongoingscore: string}> { - accessInfo = accessInfo || {}; - - const reviewMode = review || accessInfo.reviewmode, + const accessInfo = options.accessInfo || {}; + const reviewMode = options.review || accessInfo.reviewmode, promises = []; let ongoingMessage = '', progress: number; if (!accessInfo.canmanage) { if (lesson.ongoing && !reviewMode) { - promises.push(this.getOngoingScoreMessage(lesson, accessInfo, password, review, pageIndex, siteId) - .then((message) => { + promises.push(this.getOngoingScoreMessage(lesson, accessInfo, options).then((message) => { ongoingMessage = message; })); } if (lesson.progressbar) { - promises.push(this.calculateProgress(lesson.id, accessInfo, password, review, pageIndex, siteId).then((p) => { + const modOptions = { + cmId: lesson.coursemodule, + ...options, // Include all options. + }; + + promises.push(this.calculateProgress(lesson.id, accessInfo, modOptions).then((p) => { progress = p; })); } @@ -366,37 +366,45 @@ export class AddonModLessonProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with a number: the progress (scale 0-100). */ - calculateProgress(lessonId: number, accessInfo: any, password?: string, review?: boolean, pageIndex?: any, siteId?: string) - : Promise { + calculateProgress(lessonId: number, accessInfo: any, options: AddonModLessonCalculateProgressOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); // Check if the user is reviewing the attempt. - if (review) { + if (options.review) { return Promise.resolve(100); } const retake = accessInfo.attemptscount; - let viewedPagesIds, - promise; + const commonOptions = { + cmId: options.cmId, + siteId: options.siteId, + }; + let viewedPagesIds; + let promise; - if (pageIndex) { + if (options.pageIndex) { promise = Promise.resolve(); } else { // Retrieve the index. - promise = this.getPages(lessonId, password, true, false, siteId).then((pages) => { - pageIndex = this.createPagesIndex(pages); + promise = this.getPages(lessonId, { + cmId: options.cmId, + password: options.password, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((pages) => { + options.pageIndex = this.createPagesIndex(pages); }); } return promise.then(() => { // Get the list of question pages attempted. - return this.getPagesIdsWithQuestionAttempts(lessonId, retake, false, siteId); + return this.getPagesIdsWithQuestionAttempts(lessonId, retake, commonOptions); }).then((ids) => { viewedPagesIds = ids; // Get the list of viewed content pages. - return this.getContentPagesViewedIds(lessonId, retake, siteId); + return this.getContentPagesViewedIds(lessonId, retake, commonOptions); }).then((viewedContentPagesIds) => { const validPages = {}; let pageId = accessInfo.firstpageid; @@ -410,7 +418,7 @@ export class AddonModLessonProvider { // Do not filter out Cluster Page(s) because we count a cluster as one. // By keeping the cluster page, we get our 1. while (pageId) { - pageId = this.validPageAndView(pageIndex, pageIndex[pageId], validPages, viewedPagesIds); + pageId = this.validPageAndView(options.pageIndex, options.pageIndex[pageId], validPages, viewedPagesIds); } // Progress calculation as a percent. @@ -998,23 +1006,24 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param courseId Course ID the lesson belongs to. - * @param password Lesson password (if any). - * @param outOfTime If the user ran out of time. - * @param review If the user wants to review just after finishing (1 hour margin). - * @param offline Whether it's offline mode. - * @param accessInfo Result of get access info. Required if offline is true. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved in success, rejected otherwise. */ - finishRetake(lesson: any, courseId: number, password?: string, outOfTime?: boolean, review?: boolean, offline?: boolean, - accessInfo?: any, siteId?: string): Promise { + finishRetake(lesson: any, courseId: number, options: AddonModLessonFinishRetakeOptions = {}): Promise { - if (offline) { - const retake = accessInfo.attemptscount; + if (options.offline) { + const retake = options.accessInfo.attemptscount; + const newOptions = { + cmId: lesson.coursemodule, + password: options.password, + review: options.review, + siteId: options.siteId, + }; - return this.lessonOfflineProvider.finishRetake(lesson.id, courseId, retake, true, outOfTime, siteId).then(() => { + return this.lessonOfflineProvider.finishRetake(lesson.id, courseId, retake, true, options.outOfTime, options.siteId) + .then(() => { // Get the lesson grade. - return this.lessonGrade(lesson, retake, password, review, undefined, siteId).catch(() => { + return this.lessonGrade(lesson, retake, newOptions).catch(() => { // Ignore errors. return {}; }); @@ -1034,7 +1043,7 @@ export class AddonModLessonProvider { this.addResultValueEolPage(result, 'offline', true); // Mark the result as offline. this.addResultValueEolPage(result, 'gradeinfo', gradeInfo); - if (lesson.custom && !accessInfo.canmanage) { + if (lesson.custom && !options.accessInfo.canmanage) { /* Before we calculate the custom score make sure they answered the minimum number of questions. We only need to do this for custom scoring as we can not get the miniumum score the user should achieve. If we are not using custom scoring (so all questions are valued as 1) then we simply check if they @@ -1052,10 +1061,9 @@ export class AddonModLessonProvider { } } - if (!accessInfo.canmanage) { + if (!options.accessInfo.canmanage) { if (gradeLesson) { - promises.push(this.calculateProgress(lesson.id, accessInfo, password, review, undefined, siteId) - .then((progress) => { + promises.push(this.calculateProgress(lesson.id, options.accessInfo, newOptions).then((progress) => { this.addResultValueEolPage(result, 'progresscompleted', progress); })); @@ -1094,7 +1102,7 @@ export class AddonModLessonProvider { } else { // User hasn't answered any question, only content pages. if (lesson.timelimit) { - if (outOfTime) { + if (options.outOfTime) { this.addResultValueEolPage(result, 'eolstudentoutoftimenoanswers', true, true); } } else { @@ -1109,7 +1117,7 @@ export class AddonModLessonProvider { } } - if (lesson.modattempts && accessInfo.canmanage) { + if (lesson.modattempts && options.accessInfo.canmanage) { this.addResultValueEolPage(result, 'modattemptsnoteacher', true, true); } @@ -1121,13 +1129,13 @@ export class AddonModLessonProvider { }); } - return this.finishRetakeOnline(lesson.id, password, outOfTime, review, siteId).then((response) => { + return this.finishRetakeOnline(lesson.id, options).then((response) => { this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { lessonId: lesson.id, type: 'finish', courseId: courseId, - outOfTime: outOfTime, - review: review + outOfTime: options.outOfTime, + review: options.review, }, this.sitesProvider.getCurrentSiteId()); return response; @@ -1138,23 +1146,20 @@ export class AddonModLessonProvider { * Finishes a retake. It will fail if offline or cannot connect. * * @param lessonId Lesson ID. - * @param password Lesson password (if any). - * @param outOfTime If the user ran out of time. - * @param review If the user wants to review just after finishing (1 hour margin). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved in success, rejected otherwise. */ - finishRetakeOnline(lessonId: number, password?: string, outOfTime?: boolean, review?: boolean, siteId?: string): Promise { + finishRetakeOnline(lessonId: number, options: AddonModLessonFinishRetakeOnlineOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { lessonid: lessonId, - outoftime: outOfTime ? 1 : 0, - review: review ? 1 : 0 + outoftime: options.outOfTime ? 1 : 0, + review: options.review ? 1 : 0, }; - if (typeof password == 'string') { - params.password = password; + if (typeof options.password == 'string') { + params.password = options.password; } return site.write('mod_lesson_finish_attempt', params).then((response) => { @@ -1180,26 +1185,21 @@ export class AddonModLessonProvider { * Get the access information of a certain lesson. * * @param lessonId Lesson ID. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the access information. */ - getAccessInformation(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAccessInformation(lessonId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - lessonid: lessonId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAccessInformationCacheKey(lessonId) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + }; + const preSets = { + cacheKey: this.getAccessInformationCacheKey(lessonId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_lesson_access_information', params, preSets); }); @@ -1220,10 +1220,11 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object with the online and offline viewed pages. */ - getContentPagesViewed(lessonId: number, retake: number, siteId?: string): Promise<{online: any[], offline: any[]}> { + getContentPagesViewed(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}) + : Promise<{online: any[], offline: any[]}> { const promises = [], type = AddonModLessonProvider.TYPE_STRUCTURE, result = { @@ -1232,12 +1233,12 @@ export class AddonModLessonProvider { }; // Get the online pages. - promises.push(this.getContentPagesViewedOnline(lessonId, retake, false, false, siteId).then((pages) => { + promises.push(this.getContentPagesViewedOnline(lessonId, retake, options).then((pages) => { result.online = pages; })); // Get the offline pages. - promises.push(this.lessonOfflineProvider.getRetakeAttemptsForType(lessonId, retake, type, siteId).catch(() => { + promises.push(this.lessonOfflineProvider.getRetakeAttemptsForType(lessonId, retake, type, options.siteId).catch(() => { return []; }).then((pages) => { result.offline = pages; @@ -1274,11 +1275,11 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with list of IDs. */ - getContentPagesViewedIds(lessonId: number, retake: number, siteId?: string): Promise { - return this.getContentPagesViewed(lessonId, retake, siteId).then((result) => { + getContentPagesViewedIds(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.getContentPagesViewed(lessonId, retake, options).then((result) => { const ids = {}, pages = result.online.concat(result.offline); @@ -1299,29 +1300,22 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the viewed pages. */ - getContentPagesViewedOnline(lessonId: number, retake: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getContentPagesViewedOnline(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - lessonid: lessonId, - lessonattempt: retake - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getContentPagesViewedCacheKey(lessonId, retake) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + lessonattempt: retake, + }; + const preSets = { + cacheKey: this.getContentPagesViewedCacheKey(lessonId, retake), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_content_pages_viewed', params, preSets).then((result) => { return result.pages; @@ -1334,11 +1328,11 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the last content page viewed. */ - getLastContentPageViewed(lessonId: number, retake: number, siteId?: string): Promise { - return this.getContentPagesViewed(lessonId, retake, siteId).then((data) => { + getLastContentPageViewed(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.getContentPagesViewed(lessonId, retake, options).then((data) => { let lastPage, maxTime = 0; @@ -1368,22 +1362,22 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the last page seen. */ - getLastPageSeen(lessonId: number, retake: number, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getLastPageSeen(lessonId: number, retake: number, options: CoreCourseCommonModWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); let lastPageSeen: number; // Get the last question answered. - return this.lessonOfflineProvider.getLastQuestionPageAttempt(lessonId, retake, siteId).then((answer) => { + return this.lessonOfflineProvider.getLastQuestionPageAttempt(lessonId, retake, options.siteId).then((answer) => { if (answer) { lastPageSeen = answer.newpageid; } // Now get the last content page viewed. - return this.getLastContentPageViewed(lessonId, retake, siteId).then((page) => { + return this.getLastContentPageViewed(lessonId, retake, options).then((page) => { if (page) { if (answer) { if (page.timemodified > answer.timemodified) { @@ -1406,13 +1400,11 @@ export class AddonModLessonProvider { * * @param courseId Course ID. * @param cmid Course module ID. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the lesson is retrieved. */ - getLesson(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.getLessonByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); + getLesson(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLessonByField(courseId, 'coursemodule', cmId, options); } /** @@ -1421,29 +1413,21 @@ export class AddonModLessonProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the lesson is retrieved. */ - protected getLessonByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + protected getLessonByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getLessonDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getLessonDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModLessonProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_lessons_by_courses', params, preSets).then((response) => { if (response && response.lessons) { @@ -1466,13 +1450,11 @@ export class AddonModLessonProvider { * * @param courseId Course ID. * @param id Lesson ID. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the lesson is retrieved. */ - getLessonById(courseId: number, id: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.getLessonByField(courseId, 'id', id, forceCache, ignoreCache, siteId); + getLessonById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getLessonByField(courseId, 'id', id, options); } /** @@ -1489,34 +1471,25 @@ export class AddonModLessonProvider { * Get a lesson protected with password. * * @param lessonId Lesson ID. - * @param password Password. - * @param validatePassword If true, the function will fail if the password is wrong. - * If false, it will return a lesson with the basic data if password is wrong. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the lesson. */ - getLessonWithPassword(lessonId: number, password?: string, validatePassword: boolean = true, forceCache?: boolean, - ignoreCache?: boolean, siteId?: string): Promise { + getLessonWithPassword(lessonId: number, options: AddonModLessonGetWithPasswordOptions = {}): Promise { + const validatePassword = typeof options.validatePassword == 'undefined' ? true : options.validatePassword; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - lessonid: lessonId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getLessonWithPasswordCacheKey(lessonId) - }; + lessonid: lessonId, + }; + const preSets = { + cacheKey: this.getLessonWithPasswordCacheKey(lessonId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof password == 'string') { - params.password = password; - } - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; + if (typeof options.password == 'string') { + params.password = options.password; } return site.read('mod_lesson_get_lesson', params, preSets).then((response) => { @@ -1574,24 +1547,20 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param accessInfo Result of get access info. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param pageIndex Object containing all the pages indexed by ID. If not provided, it will be calculated. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the ongoing score message. */ - getOngoingScoreMessage(lesson: any, accessInfo: any, password?: string, review?: boolean, pageIndex?: any, siteId?: string) - : Promise { + getOngoingScoreMessage(lesson: any, accessInfo: any, options: AddonModLessonGradeOptions = {}): Promise { if (accessInfo.canmanage) { return Promise.resolve(this.translate.instant('addon.mod_lesson.teacherongoingwarning')); } else { let retake = accessInfo.attemptscount; - if (review) { + if (options.review) { retake--; } - return this.lessonGrade(lesson, retake, password, review, pageIndex, siteId).then((gradeInfo) => { + return this.lessonGrade(lesson, retake, options).then((gradeInfo) => { const data: any = {}; if (lesson.custom) { @@ -1614,13 +1583,15 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param pageId Page ID. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of possible answers. */ - protected getPageAnswers(lesson: any, pageId: number, password?: string, review?: boolean, siteId?: string): Promise { - return this.getPageData(lesson, pageId, password, review, true, true, false, undefined, undefined, siteId).then((data) => { + protected getPageAnswers(lesson: any, pageId: number, options: AddonModLessonPwdReviewOptions = {}): Promise { + return this.getPageData(lesson, pageId, { + includeContents: true, + ...options, // Include all options. + readingStrategy: options.readingStrategy || CoreSitesReadingStrategy.PreferCache, + }).then((data) => { return data.answers; }); } @@ -1630,19 +1601,16 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param pageIds List of page IDs. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with an object containing the answers. */ - protected getPagesAnswers(lesson: any, pageIds: number[], password?: string, review?: boolean, siteId?: string) - : Promise { + protected getPagesAnswers(lesson: any, pageIds: number[], options: AddonModLessonPwdReviewOptions = {}): Promise { const answers = {}, promises = []; pageIds.forEach((pageId) => { - promises.push(this.getPageAnswers(lesson, pageId, password, review, siteId).then((pageAnswers) => { + promises.push(this.getPageAnswers(lesson, pageId, options).then((pageAnswers) => { pageAnswers.forEach((answer) => { // Include the pageid in each answer and add them to the final list. answer.pageid = pageId; @@ -1661,42 +1629,30 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param pageId Page ID. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param includeContents Include the page rendered contents. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param accessInfo Result of get access info. Required if offline is true. - * @param jumps Result of get pages possible jumps. Required if offline is true. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the page data. */ - getPageData(lesson: any, pageId: number, password?: string, review?: boolean, includeContents?: boolean, forceCache?: boolean, - ignoreCache?: boolean, accessInfo?: any, jumps?: any, siteId?: string): Promise { + getPageData(lesson: any, pageId: number, options: AddonModLessonGetPageDataOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - lessonid: lesson.id, - pageid: Number(pageId), - review: review ? 1 : 0, - returncontents: includeContents ? 1 : 0 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getPageDataCacheKey(lesson.id, pageId) - }; + lessonid: lesson.id, + pageid: Number(pageId), + review: options.review ? 1 : 0, + returncontents: options.includeContents ? 1 : 0, + }; + const preSets = { + cacheKey: this.getPageDataCacheKey(lesson.id, pageId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof password == 'string') { - params.password = password; + if (typeof options.password == 'string') { + params.password = options.password; } - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } - - if (review) { + if (options.review) { // Force online mode in review. preSets.getFromCache = false; preSets.saveToCache = false; @@ -1704,12 +1660,15 @@ export class AddonModLessonProvider { } return site.read('mod_lesson_get_page_data', params, preSets).then((data) => { - if (forceCache && accessInfo && data.page) { + if (preSets.omitExpires && options.accessInfo && data.page) { // Offline mode and valid page. Calculate the data that might be affected. - return this.calculateOfflineData(lesson, accessInfo, password, review, undefined, siteId).then((calcData) => { + return this.calculateOfflineData(lesson, options).then((calcData) => { Object.assign(data, calcData); - return this.getPageViewMessages(lesson, accessInfo, data.page, review, jumps, password, siteId); + return this.getPageViewMessages(lesson, options.accessInfo, data.page, options.jumps, { + password: options.password, + siteId: options.siteId, + }); }).then((messages) => { data.messages = messages; @@ -1747,32 +1706,25 @@ export class AddonModLessonProvider { * Get lesson pages. * * @param lessonId Lesson ID. - * @param password Lesson password (if any). - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the pages. */ - getPages(lessonId: number, password?: string, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + getPages(lessonId: number, options: AddonModLessonPwdReviewOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - lessonid: lessonId, - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getPagesCacheKey(lessonId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; + lessonid: lessonId, + }; + const preSets = { + cacheKey: this.getPagesCacheKey(lessonId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - if (typeof password == 'string') { - params.password = password; - } - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; + if (typeof options.password == 'string') { + params.password = options.password; } return site.read('mod_lesson_get_pages', params, preSets).then((response) => { @@ -1795,27 +1747,21 @@ export class AddonModLessonProvider { * Get possible jumps for a lesson. * * @param lessonId Lesson ID. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the jumps. */ - getPagesPossibleJumps(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + getPagesPossibleJumps(lessonId: number, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - lessonid: lessonId, - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getPagesPossibleJumpsCacheKey(lessonId) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + }; + const preSets = { + cacheKey: this.getPagesPossibleJumpsCacheKey(lessonId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_pages_possible_jumps', params, preSets).then((response) => { // Index the jumps by page and jumpto. @@ -1889,15 +1835,13 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param correct True to only fetch correct attempts, false to get them all. - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's user. + * @param options Other options. * @return Promise resolved with the IDs. */ - getPagesIdsWithQuestionAttempts(lessonId: number, retake: number, correct?: boolean, siteId?: string, userId?: number) + getPagesIdsWithQuestionAttempts(lessonId: number, retake: number, options: AddonModLessonGetPagesIdsWithAttemptsOptions = {}) : Promise { - return this.getQuestionsAttempts(lessonId, retake, correct, undefined, siteId, userId).then((result) => { + return this.getQuestionsAttempts(lessonId, retake, options).then((result) => { const ids = {}, attempts = result.online.concat(result.offline); @@ -1921,13 +1865,11 @@ export class AddonModLessonProvider { * @param lesson Lesson. * @param accessInfo Result of get access info. Required if offline is true. * @param page Page loaded. - * @param review If the user wants to review just after finishing (1 hour margin). * @param jumps Result of get pages possible jumps. - * @param password Lesson password (if any). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of messages. */ - getPageViewMessages(lesson: any, accessInfo: any, page: any, review: boolean, jumps: any, password?: string, siteId?: string) + getPageViewMessages(lesson: any, accessInfo: any, page: any, jumps: any, options: AddonModLessonGetPageViewMessagesOptions = {}) : Promise { const messages = []; @@ -1938,7 +1880,7 @@ export class AddonModLessonProvider { // Tell student how many questions they have seen, how many are required and their grade. const retake = accessInfo.attemptscount; - promise = this.lessonGrade(lesson, retake, password, review, undefined, siteId).then((gradeInfo) => { + promise = this.lessonGrade(lesson, retake, options).then((gradeInfo) => { if (gradeInfo.attempts) { if (gradeInfo.nquestions < lesson.minquestions) { this.addMessage(messages, 'addon.mod_lesson.numberofpagesviewednotice', {$a: { @@ -1947,7 +1889,7 @@ export class AddonModLessonProvider { }}); } - if (!review && !lesson.retake) { + if (!options.review && !lesson.retake) { this.addMessage(messages, 'addon.mod_lesson.numberofcorrectanswers', {$a: gradeInfo.earned}); if (lesson.grade != CoreGradesProvider.TYPE_NONE) { @@ -1986,13 +1928,10 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param correct True to only fetch correct attempts, false to get them all. - * @param pageId If defined, only get attempts on this page. - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's user. + * @param options Other options. * @return Promise resolved with the questions attempts. */ - getQuestionsAttempts(lessonId: number, retake: number, correct?: boolean, pageId?: number, siteId?: string, userId?: number) + getQuestionsAttempts(lessonId: number, retake: number, options: AddonModLessonGetQuestionsAttemptsOptions = {}) : Promise<{online: any[], offline: any[]}> { const promises = [], @@ -2001,12 +1940,12 @@ export class AddonModLessonProvider { offline: [] }; - promises.push(this.getQuestionsAttemptsOnline(lessonId, retake, correct, pageId, false, false, siteId, userId) - .then((attempts) => { + promises.push(this.getQuestionsAttemptsOnline(lessonId, retake, options).then((attempts) => { result.online = attempts; })); - promises.push(this.lessonOfflineProvider.getQuestionsAttempts(lessonId, retake, correct, pageId, siteId).catch(() => { + promises.push(this.lessonOfflineProvider.getQuestionsAttempts(lessonId, retake, options.correct, options.pageId, + options.siteId).catch(() => { // Error, assume no attempts. return []; }).then((attempts) => { @@ -2045,46 +1984,37 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number. - * @param correct True to only fetch correct attempts, false to get them all. - * @param pageId If defined, only get attempts on this page. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's user. + * @param options Other options. * @return Promise resolved with the questions attempts. */ - getQuestionsAttemptsOnline(lessonId: number, retake: number, correct?: boolean, pageId?: number, forceCache?: boolean, - ignoreCache?: boolean, siteId?: string, userId?: number): Promise { + getQuestionsAttemptsOnline(lessonId: number, retake: number, options: AddonModLessonGetQuestionsAttemptsOptions = {}) + : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); // Don't pass "pageId" and "correct" params, they will be filtered locally. const params = { - lessonid: lessonId, - attempt: retake, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuestionsAttemptsCacheKey(lessonId, retake, userId) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + attempt: retake, + userid: userId, + }; + const preSets = { + cacheKey: this.getQuestionsAttemptsCacheKey(lessonId, retake, userId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_questions_attempts', params, preSets).then((response) => { - if (pageId || correct) { + if (options.pageId || options.correct) { // Filter the attempts. return response.attempts.filter((attempt) => { - if (correct && !attempt.correct) { + if (options.correct && !attempt.correct) { return false; } - if (pageId && attempt.pageid != pageId) { + if (options.pageId && attempt.pageid != options.pageId) { return false; } @@ -2101,33 +2031,25 @@ export class AddonModLessonProvider { * Get the overview of retakes in a lesson (named "attempts overview" in Moodle). * * @param lessonId Lesson ID. - * @param groupId The group to get. If not defined, all participants. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the retakes overview. */ - getRetakesOverview(lessonId: number, groupId?: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getRetakesOverview(lessonId: number, options: AddonModLessonGroupOptions = {}): Promise { - groupId = groupId || 0; + const groupId = options.groupId || 0; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - lessonid: lessonId, - groupid: groupId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getRetakesOverviewCacheKey(lessonId, groupId), - updateFrequency: CoreSite.FREQUENCY_OFTEN - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + groupid: groupId, + }; + const preSets = { + cacheKey: this.getRetakesOverviewCacheKey(lessonId, groupId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_attempts_overview', params, preSets).then((response) => { return response.data; @@ -2204,30 +2126,23 @@ export class AddonModLessonProvider { * Get lesson timers. * * @param lessonId Lesson ID. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's current user. + * @param options Other options. * @return Promise resolved with the pages. */ - getTimers(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string, userId?: number): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + getTimers(lessonId: number, options: AddonModLessonUserOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - lessonid: lessonId, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getTimersCacheKey(lessonId, userId) - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + userid: userId, + }; + const preSets = { + cacheKey: this.getTimersCacheKey(lessonId, userId), + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_user_timers', params, preSets).then((response) => { return response.timers; @@ -2331,34 +2246,26 @@ export class AddonModLessonProvider { * * @param lessonId Lesson ID. * @param retake Retake number - * @param userId User ID. Undefined for current user. - * @param forceCache Whether it should always return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the retake data. */ - getUserRetake(lessonId: number, retake: number, userId?: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getUserRetake(lessonId: number, retake: number, options: AddonModLessonUserOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - lessonid: lessonId, - userid: userId, - lessonattempt: retake - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getUserRetakeCacheKey(lessonId, userId, retake), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + lessonid: lessonId, + userid: userId, + lessonattempt: retake, + }; + const preSets = { + cacheKey: this.getUserRetakeCacheKey(lessonId, userId, retake), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModLessonProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_lesson_get_user_attempt', params, preSets); }); @@ -2870,15 +2777,10 @@ export class AddonModLessonProvider { * * @param lesson Lesson. * @param retake Retake number. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param pageIndex Object containing all the pages indexed by ID. If not provided, it will be calculated. - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined, site's user. + * @param options Other options. * @return Promise resolved with the grade data. */ - lessonGrade(lesson: any, retake: number, password?: string, review?: boolean, pageIndex?: any, siteId?: string, - userId?: number): Promise { + lessonGrade(lesson: any, retake: number, options: AddonModLessonGradeOptions = {}): Promise { // Initialize all variables. let nViewed = 0, @@ -2890,7 +2792,11 @@ export class AddonModLessonProvider { earned = 0; // Get the questions attempts for the user. - return this.getQuestionsAttempts(lesson.id, retake, false, undefined, siteId, userId).then((attemptsData) => { + return this.getQuestionsAttempts(lesson.id, retake, { + cmId: lesson.coursemodule, + siteId: options.siteId, + userId: options.userId, + }).then((attemptsData) => { const attempts = attemptsData.online.concat(attemptsData.offline); if (!attempts.length) { @@ -2902,9 +2808,14 @@ export class AddonModLessonProvider { let promise; // Create the pageIndex if it isn't provided. - if (!pageIndex) { - promise = this.getPages(lesson.id, password, true, false, siteId).then((pages) => { - pageIndex = this.createPagesIndex(pages); + if (!options.pageIndex) { + promise = this.getPages(lesson.id, { + password: options.password, + cmId: lesson.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((pages) => { + options.pageIndex = this.createPagesIndex(pages); }); } else { promise = Promise.resolve(); @@ -2933,7 +2844,7 @@ export class AddonModLessonProvider { } // Get all the answers from the pages the user answered. - return this.getPagesAnswers(lesson, pageIds, password, review, siteId); + return this.getPagesAnswers(lesson, pageIds, options); }).then((answers) => { // Number of pages answered. nQuestions = Object.keys(attemptSet).length; @@ -2944,7 +2855,7 @@ export class AddonModLessonProvider { if (lesson.custom) { // If essay question, handle it, otherwise add to score. - if (pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) { + if (options.pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) { if (lastAttempt.useranswer && typeof lastAttempt.useranswer.score != 'undefined') { earned += lastAttempt.useranswer.score; } @@ -2959,7 +2870,7 @@ export class AddonModLessonProvider { }); // If essay question, increase numbers. - if (pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) { + if (options.pageIndex[lastAttempt.pageid].qtype == AddonModLessonProvider.LESSON_PAGE_ESSAY) { nManual++; manualPoints++; } @@ -3046,31 +2957,32 @@ export class AddonModLessonProvider { * @param courseId Course ID the lesson belongs to. * @param pageData Result of getPageData for the page to process. * @param data Data to save. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param offline Whether it's offline mode. - * @param accessInfo Result of get access info. Required if offline is true. - * @param jumps Result of get pages possible jumps. Required if offline is true. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - processPage(lesson: any, courseId: number, pageData: any, data: any, password?: string, review?: boolean, offline?: boolean, - accessInfo?: boolean, jumps?: any, siteId?: string): Promise { + processPage(lesson: any, courseId: number, pageData: any, data: any, options: AddonModLessonProcessPageOptions = {}) + : Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); const page = pageData.page, pageId = page.id; let result, pageIndex; - if (offline) { + if (options.offline) { // Get the list of pages of the lesson. - return this.getPages(lesson.id, password, true, false, siteId).then((pages) => { + return this.getPages(lesson.id, { + cmId: lesson.coursemodule, + password: options.password, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).then((pages) => { pageIndex = this.createPagesIndex(pages); if (pageData.answers.length) { - return this.recordAttempt(lesson, courseId, pageData, data, review, accessInfo, jumps, pageIndex, siteId); + return this.recordAttempt(lesson, courseId, pageData, data, options.review, options.accessInfo, options.jumps, + pageIndex, options.siteId); } else { // The page has no answers so we will just progress to the next page (as set by newpageid). return { @@ -3080,15 +2992,21 @@ export class AddonModLessonProvider { } }).then((res) => { result = res; - result.newpageid = this.getNewPageId(pageData.page.id, result.newpageid, jumps); + result.newpageid = this.getNewPageId(pageData.page.id, result.newpageid, options.jumps); // Calculate some needed offline data. - return this.calculateOfflineData(lesson, accessInfo, password, review, pageIndex, siteId); + return this.calculateOfflineData(lesson, { + accessInfo: options.accessInfo, + password: options.password, + review: options.review, + pageIndex, + siteId: options.siteId, + }); }).then((calculatedData) => { // Add some default data to match the WS response. result.warnings = []; result.displaymenu = pageData.displaymenu; // Keep the same value since we can't calculate it in offline. - result.messages = this.getPageProcessMessages(lesson, accessInfo, result, review, jumps); + result.messages = this.getPageProcessMessages(lesson, options.accessInfo, result, options.review, options.jumps); result.sent = false; Object.assign(result, calculatedData); @@ -3096,13 +3014,13 @@ export class AddonModLessonProvider { }); } - return this.processPageOnline(lesson.id, pageId, data, password, review, siteId).then((response) => { + return this.processPageOnline(lesson.id, pageId, data, options).then((response) => { this.eventsProvider.trigger(AddonModLessonProvider.DATA_SENT_EVENT, { lessonId: lesson.id, type: 'process', courseId: courseId, pageId: pageId, - review: review + review: options.review, }, this.sitesProvider.getCurrentSiteId()); response.sent = true; @@ -3117,24 +3035,22 @@ export class AddonModLessonProvider { * @param lessonId Lesson ID. * @param pageId Page ID. * @param data Data to save. - * @param password Lesson password (if any). - * @param review If the user wants to review just after finishing (1 hour margin). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved in success, rejected otherwise. */ - processPageOnline(lessonId: number, pageId: number, data: any, password?: string, review?: boolean, siteId?: string) + processPageOnline(lessonId: number, pageId: number, data: any, options: AddonModLessonProcessPageOnlineOptions = {}) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { lessonid: lessonId, pageid: pageId, data: this.utils.objectToArrayOfObjects(data, 'name', 'value', true), - review: review ? 1 : 0 + review: options.review ? 1 : 0, }; - if (typeof password == 'string') { - params.password = password; + if (typeof options.password == 'string') { + params.password = options.password; } return site.write('mod_lesson_process_page', params); @@ -3189,7 +3105,11 @@ export class AddonModLessonProvider { } else { if (!accessInfo.canmanage) { // Get the number of attempts that have been made on this question for this student and retake. - promise = this.getQuestionsAttempts(lesson.id, retake, false, pageData.page.id, siteId).then((attempts) => { + promise = this.getQuestionsAttempts(lesson.id, retake, { + cmId: lesson.coursemodule, + pageId: pageData.page.id, + siteId, + }).then((attempts) => { nAttempts = attempts.online.length + attempts.offline.length; // Check if they have reached (or exceeded) the maximum number of attempts allowed. @@ -3264,8 +3184,11 @@ export class AddonModLessonProvider { if (lesson.review && !result.correctanswer && !result.isessayquestion) { // Calculate the number of question attempt in the page if it isn't calculated already. if (typeof nAttempts == 'undefined') { - subPromise = this.getQuestionsAttempts(lesson.id, retake, false, pageData.page.id, siteId) - .then((result) => { + subPromise = this.getQuestionsAttempts(lesson.id, retake, { + cmId: lesson.coursemodule, + pageId: pageData.page.id, + siteId, + }).then((result) => { nAttempts = result.online.length + result.offline.length; }); } else { @@ -3399,3 +3322,141 @@ export class AddonModLessonProvider { return page.nextpageid; } } + +/** + * Common options including a group ID. + */ +export type AddonModLessonGroupOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // The group to get. If not defined, all participants. +}; + +/** + * Common options including a group ID. + */ +export type AddonModLessonUserOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User ID. If not defined, site's current user. +}; + +/** + * Common options including a password. + */ +export type AddonModLessonPasswordOptions = CoreCourseCommonModWSOptions & { + password?: string; // Lesson password (if any). +}; + +/** + * Common options including password and review. + */ +export type AddonModLessonPwdReviewOptions = AddonModLessonPasswordOptions & { + review?: boolean; // If the user wants to review just after finishing (1 hour margin). +}; + +/** + * Options to pass to get lesson with password. + */ +export type AddonModLessonGetWithPasswordOptions = AddonModLessonPasswordOptions & { + validatePassword?: boolean; // Defauls to true. If true, the function will fail if the password is wrong. + // If false, it will return a lesson with the basic data if password is wrong. +}; + +/** + * Options to pass to calculateProgress. + */ +export type AddonModLessonCalculateProgressBasicOptions = { + password?: string; // Lesson password (if any). + review?: boolean; // If the user wants to review just after finishing (1 hour margin). + pageIndex?: any; // Object containing all the pages indexed by ID. If not provided, it will be calculated. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to calculateProgress. + */ +export type AddonModLessonCalculateProgressOptions = AddonModLessonCalculateProgressBasicOptions & { + cmId?: number; // Module ID. +}; + +/** + * Options to pass to lessonGrade. + */ +export type AddonModLessonGradeOptions = AddonModLessonCalculateProgressBasicOptions & { + userId?: number; // User ID. If not defined, site's user. +}; + +/** + * Options to pass to calculateOfflineData. + */ +export type AddonModLessonCalculateOfflineDataOptions = AddonModLessonCalculateProgressBasicOptions & { + accessInfo?: any; // Result of get access info. +}; + +/** + * Options to pass to get page data. + */ +export type AddonModLessonGetPageDataOptions = AddonModLessonPwdReviewOptions & { + includeContents?: boolean; // Include the page rendered contents. + accessInfo?: any; // Result of get access info. Required if offline is true. + jumps?: any; // Result of get pages possible jumps. Required if offline is true. +}; + +/** + * Options to pass to get page data. + */ +export type AddonModLessonGetPageViewMessagesOptions = { + password?: string; // Lesson password (if any). + review?: boolean; // If the user wants to review just after finishing (1 hour margin). + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to get questions attempts. + */ +export type AddonModLessonGetQuestionsAttemptsOptions = CoreCourseCommonModWSOptions & { + correct?: boolean; // True to only fetch correct attempts, false to get them all. + pageId?: number; // If defined, only get attempts on this page. + userId?: number; // User ID. If not defined, site's user. +}; + +/** + * Options to pass to getPagesIdsWithQuestionAttempts. + */ +export type AddonModLessonGetPagesIdsWithAttemptsOptions = CoreCourseCommonModWSOptions & { + correct?: boolean; // True to only fetch correct attempts, false to get them all. + userId?: number; // User ID. If not defined, site's user. +}; + +/** + * Options to pass to processPageOnline. + */ +export type AddonModLessonProcessPageOnlineOptions = { + password?: string; // Lesson password (if any). + review?: boolean; // If the user wants to review just after finishing (1 hour margin). + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to processPage. + */ +export type AddonModLessonProcessPageOptions = AddonModLessonProcessPageOnlineOptions & { + offline?: boolean; // Whether it's offline mode. + accessInfo?: any; // Result of get access info. Required if offline is true. + jumps?: any; // Result of get pages possible jumps. Required if offline is true. +}; + +/** + * Options to pass to finishRetakeOnline. + */ +export type AddonModLessonFinishRetakeOnlineOptions = { + password?: string; // Lesson password (if any). + outOfTime?: boolean; // Whether the user ran out of time. + review?: boolean; // If the user wants to review just after finishing (1 hour margin). + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to finishRetake. + */ +export type AddonModLessonFinishRetakeOptions = AddonModLessonFinishRetakeOnlineOptions & { + offline?: boolean; // Whether it's offline mode. + accessInfo?: any; // Result of get access info. Required if offline is true. +}; diff --git a/src/addon/mod/lesson/providers/prefetch-handler.ts b/src/addon/mod/lesson/providers/prefetch-handler.ts index c8601ae58..1550e4143 100644 --- a/src/addon/mod/lesson/providers/prefetch-handler.ts +++ b/src/addon/mod/lesson/providers/prefetch-handler.ts @@ -17,10 +17,10 @@ import { ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { AddonModLessonProvider } from './lesson'; @@ -98,11 +98,15 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan password, result; - return this.lessonProvider.getLesson(courseId, module.id, false, false, siteId).then((lessonData) => { + return this.lessonProvider.getLesson(courseId, module.id, {siteId}).then((lessonData) => { lesson = lessonData; // Get the lesson password if it's needed. - return this.getLessonPassword(lesson.id, false, true, single, siteId); + return this.getLessonPassword(lesson.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + askPassword: single, + siteId, + }); }).then((data) => { password = data.password; lesson = data.lesson || lesson; @@ -116,7 +120,11 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan result = res; // Get the pages to calculate the size. - return this.lessonProvider.getPages(lesson.id, password, false, false, siteId); + return this.lessonProvider.getPages(lesson.id, { + cmId: module.id, + password, + siteId, + }); }).then((pages) => { pages.forEach((page) => { result.size += page.filessizetotal; @@ -130,19 +138,16 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan * Get the lesson password if needed. If not stored, it can ask the user to enter it. * * @param lessonId Lesson ID. - * @param forceCache Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param askPassword True if we should ask for password if needed, false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - getLessonPassword(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, askPassword?: boolean, siteId?: string) + getLessonPassword(lessonId: number, options: AddonModLessonGetPasswordOptions = {}) : Promise<{password?: string, lesson?: any, accessInfo: any}> { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); // Get access information to check if password is needed. - return this.lessonProvider.getAccessInformation(lessonId, forceCache, ignoreCache, siteId).then((info): any => { + return this.lessonProvider.getAccessInformation(lessonId, options).then((info): any => { if (info.preventaccessreasons && info.preventaccessreasons.length) { const passwordNeeded = info.preventaccessreasons.length == 1 && this.lessonProvider.isPasswordProtected(info); if (passwordNeeded) { @@ -152,15 +157,15 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan // No password found. }).then((password) => { if (password) { - return this.validatePassword(lessonId, info, password, forceCache, ignoreCache, siteId); + return this.validatePassword(lessonId, info, password, options); } else { return Promise.reject(null); } }).catch(() => { // No password or error validating it. Ask for it if allowed. - if (askPassword) { + if (options.askPassword) { return this.askUserPassword(info).then((password) => { - return this.validatePassword(lessonId, info, password, forceCache, ignoreCache, siteId); + return this.validatePassword(lessonId, info, password, options); }); } @@ -207,7 +212,10 @@ 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, true, false, siteId).then((lesson) => { + return this.lessonProvider.getLesson(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((lesson) => { const promises = []; promises.push(this.lessonProvider.invalidateLessonData(courseId, siteId)); @@ -227,9 +235,9 @@ 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, false, siteId).then((lesson) => { + return this.lessonProvider.getLesson(courseId, module.id, {siteId}).then((lesson) => { // Check if there is any prevent access reason. - return this.lessonProvider.getAccessInformation(lesson.id, false, false, siteId).then((info) => { + return this.lessonProvider.getAccessInformation(lesson.id, {cmId: module.id, siteId}).then((info) => { if (!info.canviewreports && !this.lessonProvider.isLessonOffline(lesson)) { return false; } @@ -273,15 +281,28 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when done. */ protected prefetchLesson(module: any, courseId: number, single: boolean, siteId: string): Promise { - let lesson, - password, - accessInfo; + let lesson; + let password; + let accessInfo; - return this.lessonProvider.getLesson(courseId, module.id, false, true, siteId).then((lessonData) => { + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + + return this.lessonProvider.getLesson(courseId, module.id, commonOptions).then((lessonData) => { lesson = lessonData; // Get the lesson password if it's needed. - return this.getLessonPassword(lesson.id, false, true, single, siteId); + return this.getLessonPassword(lesson.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + askPassword: single, + siteId, + }); }).then((data) => { password = data.password; lesson = data.lesson || lesson; @@ -297,7 +318,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan // Ignore errors. })); - promises.push(this.lessonProvider.getAccessInformation(lesson.id, false, true, siteId).then((info) => { + promises.push(this.lessonProvider.getAccessInformation(lesson.id, modOptions).then((info) => { accessInfo = info; })); @@ -316,7 +337,12 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan // Get the list of pages. if (this.lessonProvider.isLessonOffline(lesson)) { - promises.push(this.lessonProvider.getPages(lesson.id, password, false, true, siteId).then((pages) => { + const passwordOptions = { + password, + ...modOptions, // Include all mod options. + }; + + promises.push(this.lessonProvider.getPages(lesson.id, passwordOptions).then((pages) => { const subPromises = []; let hasRandomBranch = false; @@ -333,8 +359,10 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan } // Get the page data. We don't pass accessInfo because we don't need to calculate the offline data. - subPromises.push(this.lessonProvider.getPageData(lesson, data.page.id, password, false, true, false, - true, undefined, undefined, siteId).then((pageData) => { + subPromises.push(this.lessonProvider.getPageData(lesson, data.page.id, { + includeContents: true, + ...passwordOptions, // Include all options. + }).then((pageData) => { // Download the page files. let pageFiles = pageData.contentfiles || []; @@ -353,7 +381,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan }); // Prefetch the list of possible jumps for offline navigation. Do it here because we know hasRandomBranch. - subPromises.push(this.lessonProvider.getPagesPossibleJumps(lesson.id, false, true, siteId).catch((error) => { + subPromises.push(this.lessonProvider.getPagesPossibleJumps(lesson.id, modOptions).catch((error) => { if (hasRandomBranch) { // The WebSevice probably failed because RANDOMBRANCH aren't supported if the user hasn't seen any page. return Promise.reject(this.translate.instant('addon.mod_lesson.errorprefetchrandombranch')); @@ -366,16 +394,15 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan })); // Prefetch user timers to be able to calculate timemodified in offline. - promises.push(this.lessonProvider.getTimers(lesson.id, false, true, siteId).catch(() => { + promises.push(this.lessonProvider.getTimers(lesson.id, modOptions).catch(() => { // Ignore errors. })); // Prefetch viewed pages in last retake to calculate progress. - promises.push(this.lessonProvider.getContentPagesViewedOnline(lesson.id, retake, false, true, siteId)); + promises.push(this.lessonProvider.getContentPagesViewedOnline(lesson.id, retake, modOptions)); // Prefetch question attempts in last retake for offline calculations. - promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lesson.id, retake, false, undefined, false, true, - siteId)); + promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lesson.id, retake, modOptions)); } if (accessInfo.canviewreports) { @@ -384,11 +411,14 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan const subPromises = []; info.groups.forEach((group) => { - subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, group.id, false, true, siteId)); + subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, { + groupId: group.id, + ...modOptions, // Include all options. + })); }); - // Always get group 0, even if there are no groups. - subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, 0, false, true, siteId).then((data) => { + // Always get all participants, even if there are no groups. + subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, modOptions).then((data) => { if (!data || !data.students) { return; } @@ -406,8 +436,10 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan return; } - retakePromises.push(this.lessonProvider.getUserRetake(lesson.id, lastRetake.try, student.id, false, - true, siteId).then((attempt) => { + retakePromises.push(this.lessonProvider.getUserRetake(lesson.id, lastRetake.try, { + userId: student.id, + ...modOptions, // Include all options. + }).then((attempt) => { if (!attempt || !attempt.answerpages) { return; } @@ -445,19 +477,20 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan * @param lessonId Lesson ID. * @param info Lesson access info. * @param pwd Password to check. - * @param forceCache Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - protected validatePassword(lessonId: number, info: any, pwd: string, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise<{password: string, lesson: any, accessInfo: any}> { + protected validatePassword(lessonId: number, info: any, pwd: string, options: CoreCourseCommonModWSOptions = {}) + : Promise<{password: string, lesson: any, accessInfo: any}> { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.lessonProvider.getLessonWithPassword(lessonId, pwd, true, forceCache, ignoreCache, siteId).then((lesson) => { + return this.lessonProvider.getLessonWithPassword(lessonId, { + password: pwd, + ...options, // Include all options. + }).then((lesson) => { // Password is ok, store it and return the data. - return this.lessonProvider.storePassword(lesson.id, pwd, siteId).then(() => { + return this.lessonProvider.storePassword(lesson.id, pwd, options.siteId).then(() => { return { password: pwd, lesson: lesson, @@ -483,3 +516,10 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan return this.syncProvider.syncLesson(module.instance, false, false, siteId); } } + +/** + * Options to pass to get lesson password. + */ +export type AddonModLessonGetPasswordOptions = CoreCourseCommonModWSOptions & { + askPassword?: boolean; // True if we should ask for password if needed, false otherwise. +}; diff --git a/src/addon/mod/lti/providers/lti.ts b/src/addon/mod/lti/providers/lti.ts index acae1b745..b2d301078 100644 --- a/src/addon/mod/lti/providers/lti.ts +++ b/src/addon/mod/lti/providers/lti.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; @@ -100,29 +100,32 @@ export class AddonModLtiProvider { * * @param courseId Course ID. * @param cmId Course module ID. + * @param options Other options. * @return Promise resolved when the LTI is retrieved. */ - getLti(courseId: number, cmId: number): Promise { + async getLti(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { const params: any = { courseids: [courseId] }; - const preSets: any = { + const preSets = { cacheKey: this.getLtiCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModLtiProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - return this.sitesProvider.getCurrentSite().read('mod_lti_get_ltis_by_courses', params, preSets) - .then((response: AddonModLtiGetLtisByCoursesResult): any => { + const site = await this.sitesProvider.getSite(options.siteId); - if (response.ltis) { - const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId); - if (currentLti) { - return currentLti; - } + const response: AddonModLtiGetLtisByCoursesResult = await site.read('mod_lti_get_ltis_by_courses', params, preSets); + + if (response.ltis) { + const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId); + if (currentLti) { + return currentLti; } + } - return Promise.reject(null); - }); + throw new Error('Activity not found.'); } /** diff --git a/src/addon/mod/page/providers/page.ts b/src/addon/mod/page/providers/page.ts index 817e848b3..2fab4bcce 100644 --- a/src/addon/mod/page/providers/page.ts +++ b/src/addon/mod/page/providers/page.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -43,11 +43,11 @@ export class AddonModPageProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the page is retrieved. */ - getPageData(courseId: number, cmId: number, siteId?: string): Promise { - return this.getPageByKey(courseId, 'coursemodule', cmId, siteId); + getPageData(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getPageByKey(courseId, 'coursemodule', cmId, options); } /** @@ -56,18 +56,21 @@ export class AddonModPageProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the page is retrieved. */ - protected getPageByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getPageByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getPageCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getPageCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModPageProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_page_get_pages_by_courses', params, preSets) .then((response: AddonModPageGetPagesByCoursesResult): any => { diff --git a/src/addon/mod/quiz/components/index/index.ts b/src/addon/mod/quiz/components/index/index.ts index 3a2157138..d970c5e88 100644 --- a/src/addon/mod/quiz/components/index/index.ts +++ b/src/addon/mod/quiz/components/index/index.ts @@ -202,7 +202,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp } // Get quiz access info. - return this.quizProvider.getQuizAccessInformation(this.quizData.id).then((info) => { + return this.quizProvider.getQuizAccessInformation(this.quizData.id, {cmId: this.module.id}).then((info) => { this.quizAccessInfo = info; this.quizData.showReviewColumn = info.canreviewmyattempts; this.accessRules = info.accessrules; @@ -213,7 +213,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp } // Get question types in the quiz. - return this.quizProvider.getQuizRequiredQtypes(this.quizData.id).then((types) => { + return this.quizProvider.getQuizRequiredQtypes(this.quizData.id, {cmId: this.module.id}).then((types) => { this.unsupportedQuestions = this.quizProvider.getUnsupportedQuestions(types); this.hasSupportedQuestions = !!types.find((type) => { return type != 'random' && this.unsupportedQuestions.indexOf(type) == -1; @@ -239,11 +239,11 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp protected getAttempts(): Promise { // Get access information of last attempt (it also works if no attempts made). - return this.quizProvider.getAttemptAccessInformation(this.quizData.id, 0).then((info) => { + return this.quizProvider.getAttemptAccessInformation(this.quizData.id, 0, {cmId: this.module.id}).then((info) => { this.attemptAccessInfo = info; // Get attempts. - return this.quizProvider.getUserAttempts(this.quizData.id).then((atts) => { + return this.quizProvider.getUserAttempts(this.quizData.id, {cmId: this.module.id}).then((atts) => { return this.treatAttempts(atts).then((atts) => { this.attempts = atts; @@ -355,7 +355,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp if (this.quizData.showFeedbackColumn) { // Get the quiz overall feedback. - return this.quizProvider.getFeedbackForGrade(this.quizData.id, this.gradebookData.grade).then((response) => { + return this.quizProvider.getFeedbackForGrade(this.quizData.id, this.gradebookData.grade, { + cmId: this.module.id, + }).then((response) => { this.overallFeedback = response.feedbacktext; }); } @@ -379,7 +381,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp const attemptId = this.autoReview.attemptId; if (this.quizAccessInfo.canreviewmyattempts) { - return this.quizProvider.getAttemptReview(attemptId, -1).then(() => { + return this.quizProvider.getAttemptReview(attemptId, {page: -1, cmId: this.module.id}).then(() => { this.navCtrl.push('AddonModQuizReviewPage', {courseId: this.courseId, quizId: this.quizData.id, attemptId}); }).catch(() => { // Ignore errors. @@ -559,12 +561,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp promises.push(this.quizProvider.loadFinishedOfflineData(attempts)); // Get combined review options. - promises.push(this.quizProvider.getCombinedReviewOptions(this.quizData.id).then((result) => { + promises.push(this.quizProvider.getCombinedReviewOptions(this.quizData.id, {cmId: this.module.id}).then((result) => { this.options = result; })); // Get best grade. - promises.push(this.quizProvider.getUserBestGrade(this.quizData.id).then((best) => { + promises.push(this.quizProvider.getUserBestGrade(this.quizData.id, {cmId: this.module.id}).then((best) => { this.bestGrade = best; // Get gradebook grade. diff --git a/src/addon/mod/quiz/pages/attempt/attempt.ts b/src/addon/mod/quiz/pages/attempt/attempt.ts index dae185cc3..d4e3d1909 100644 --- a/src/addon/mod/quiz/pages/attempt/attempt.ts +++ b/src/addon/mod/quiz/pages/attempt/attempt.ts @@ -92,7 +92,7 @@ export class AddonModQuizAttemptPage implements OnInit { accessInfo; // Get all the attempts and search the one we want. - promises.push(this.quizProvider.getUserAttempts(this.quizId).then((attempts) => { + promises.push(this.quizProvider.getUserAttempts(this.quizId, {cmId: this.quiz.coursemodule}).then((attempts) => { for (let i = 0; i < attempts.length; i++) { const attempt = attempts[i]; if (attempt.id == this.attemptId) { @@ -110,12 +110,13 @@ export class AddonModQuizAttemptPage implements OnInit { return this.quizProvider.loadFinishedOfflineData([this.attempt]); })); - promises.push(this.quizProvider.getCombinedReviewOptions(this.quiz.id).then((opts) => { + promises.push(this.quizProvider.getCombinedReviewOptions(this.quiz.id, {cmId: this.quiz.coursemodule}).then((opts) => { options = opts; })); // Check if the user can review the attempt. - promises.push(this.quizProvider.getQuizAccessInformation(this.quiz.id).then((quizAccessInfo) => { + promises.push(this.quizProvider.getQuizAccessInformation(this.quiz.id, {cmId: this.quiz.coursemodule}) + .then((quizAccessInfo) => { accessInfo = quizAccessInfo; if (accessInfo.canreviewmyattempts) { @@ -123,7 +124,7 @@ export class AddonModQuizAttemptPage implements OnInit { return this.quizProvider.invalidateAttemptReviewForPage(this.attemptId, -1).catch(() => { // Ignore errors. }).then(() => { - return this.quizProvider.getAttemptReview(this.attemptId, -1); + return this.quizProvider.getAttemptReview(this.attemptId, {page: -1, cmId: this.quiz.coursemodule}); }).catch(() => { // Error getting the review, assume the user cannot review the attempt. accessInfo.canreviewmyattempts = false; @@ -146,7 +147,9 @@ export class AddonModQuizAttemptPage implements OnInit { options.someoptions.overallfeedback && !isNaN(grade)) { // Feedback should be displayed, get the feedback for the grade. - return this.quizProvider.getFeedbackForGrade(this.quiz.id, grade).then((response) => { + return this.quizProvider.getFeedbackForGrade(this.quiz.id, grade, { + cmId: this.quiz.coursemodule, + }).then((response) => { this.attempt.feedback = response.feedbacktext; }); } else { diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts index 653a9b89a..75fb54312 100644 --- a/src/addon/mod/quiz/pages/player/player.ts +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -17,7 +17,7 @@ import { IonicPage, NavParams, Content, PopoverController, ModalController, Moda import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -315,12 +315,18 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { } // Get access information for the quiz. - return this.quizProvider.getQuizAccessInformation(this.quiz.id, this.offline, true); + return this.quizProvider.getQuizAccessInformation(this.quiz.id, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }); }).then((info) => { this.quizAccessInfo = info; // Get user attempts to determine last attempt. - return this.quizProvider.getUserAttempts(this.quiz.id, 'all', true, this.offline, true); + return this.quizProvider.getUserAttempts(this.quiz.id, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }); }).then((attempts) => { if (!attempts.length) { // There are no attempts, start a new one. @@ -396,8 +402,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { */ protected fixSequenceChecks(): Promise { // Get current page data again to get the latest sequencechecks. - return this.quizProvider.getAttemptData(this.attempt.id, this.attempt.currentpage, this.preflightData, this.offline, true) - .then((data) => { + return this.quizProvider.getAttemptData(this.attempt.id, this.attempt.currentpage, this.preflightData, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((data) => { const newSequenceChecks = {}; @@ -443,7 +451,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ protected loadPage(page: number): Promise { - return this.quizProvider.getAttemptData(this.attempt.id, page, this.preflightData, this.offline, true).then((data) => { + return this.quizProvider.getAttemptData(this.attempt.id, page, this.preflightData, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((data) => { // Update attempt, status could change during the execution. this.attempt = data.attempt; this.attempt.currentpage = page; @@ -487,7 +498,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { protected loadSummary(): Promise { this.summaryQuestions = []; - return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline, true, true).then((qs) => { + return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, { + cmId: this.quiz.coursemodule, + loadLocal: this.offline, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((qs) => { this.showSummary = true; this.summaryQuestions = qs; @@ -511,8 +526,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { */ protected loadNavigation(): Promise { // We use the attempt summary to build the navigation because it contains all the questions. - return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline, true, true) - .then((questions) => { + return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, { + cmId: this.quiz.coursemodule, + loadLocal: this.offline, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((questions) => { questions.forEach((question) => { question.stateClass = this.questionHelper.getQuestionStateClass(question.state); @@ -652,7 +670,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { false, 'addon.mod_quiz.startattempt').then((attempt) => { // Re-fetch attempt access information with the right attempt (might have changed because a new attempt was created). - return this.quizProvider.getAttemptAccessInformation(this.quiz.id, attempt.id, this.offline, true).then((info) => { + return this.quizProvider.getAttemptAccessInformation(this.quiz.id, attempt.id, { + cmId: this.quiz.coursemodule, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + }).then((info) => { this.attemptAccessInfo = info; this.attempt = attempt; diff --git a/src/addon/mod/quiz/pages/review/review.ts b/src/addon/mod/quiz/pages/review/review.ts index 5c2ef225d..50dfd53b8 100644 --- a/src/addon/mod/quiz/pages/review/review.ts +++ b/src/addon/mod/quiz/pages/review/review.ts @@ -134,7 +134,7 @@ export class AddonModQuizReviewPage implements OnInit { this.quiz = quizData; this.componentId = this.quiz.coursemodule; - return this.quizProvider.getCombinedReviewOptions(this.quizId).then((result) => { + return this.quizProvider.getCombinedReviewOptions(this.quizId, {cmId: this.quiz.coursemodule}).then((result) => { this.options = result; // Load the navigation data. @@ -155,7 +155,7 @@ export class AddonModQuizReviewPage implements OnInit { * @return Promise resolved when done. */ protected loadPage(page: number): Promise { - return this.quizProvider.getAttemptReview(this.attemptId, page).then((data) => { + return this.quizProvider.getAttemptReview(this.attemptId, {page, cmId: this.quiz.coursemodule}).then((data) => { this.attempt = data.attempt; this.attempt.currentpage = page; this.currentPage = page; @@ -187,7 +187,7 @@ export class AddonModQuizReviewPage implements OnInit { */ protected loadNavigation(): Promise { // Get all questions in single page to retrieve all the questions. - return this.quizProvider.getAttemptReview(this.attemptId, -1).then((data) => { + return this.quizProvider.getAttemptReview(this.attemptId, {page: -1, cmId: this.quiz.coursemodule}).then((data) => { const lastQuestion = data.questions[data.questions.length - 1]; data.questions.forEach((question) => { diff --git a/src/addon/mod/quiz/providers/helper.ts b/src/addon/mod/quiz/providers/helper.ts index c83293381..084cb6303 100644 --- a/src/addon/mod/quiz/providers/helper.ts +++ b/src/addon/mod/quiz/providers/helper.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { ModalController, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonModQuizProvider } from './quiz'; @@ -166,12 +166,12 @@ export class AddonModQuizHelperProvider { * Get a quiz ID by attempt ID. * * @param attemptId Attempt ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the quiz ID. */ - getQuizIdByAttemptId(attemptId: number, siteId?: string): Promise { + getQuizIdByAttemptId(attemptId: number, options: {cmId?: number, siteId?: string} = {}): Promise { // Use getAttemptReview to retrieve the quiz ID. - return this.quizProvider.getAttemptReview(attemptId, undefined, false, siteId).then((reviewData) => { + return this.quizProvider.getAttemptReview(attemptId, options).then((reviewData) => { if (reviewData.attempt && reviewData.attempt.quiz) { return reviewData.attempt.quiz; } @@ -202,7 +202,7 @@ export class AddonModQuizHelperProvider { promise = Promise.resolve(quizId); } else { // Retrieve the quiz ID using the attempt ID. - promise = this.getQuizIdByAttemptId(attemptId); + promise = this.getQuizIdByAttemptId(attemptId, {siteId}); } return promise.then((id) => { @@ -298,6 +298,11 @@ export class AddonModQuizHelperProvider { siteId?: string): Promise { const rules = accessInfo && accessInfo.activerulenames; + const modOptions = { + cmId: quiz.coursemodule, + readingStrategy: offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; let promise; if (attempt) { @@ -305,7 +310,7 @@ export class AddonModQuizHelperProvider { // We're continuing an attempt. Call getAttemptData to validate the preflight data. const page = attempt.currentpage; - promise = this.quizProvider.getAttemptData(attempt.id, page, preflightData, offline, true, siteId).then(() => { + promise = this.quizProvider.getAttemptData(attempt.id, page, preflightData, modOptions).then(() => { if (offline) { // Get current page stored in local. return this.quizOfflineProvider.getAttemptById(attempt.id).then((localAttempt) => { @@ -318,7 +323,7 @@ export class AddonModQuizHelperProvider { } else { // Attempt is overdue or finished in offline, we can only see the summary. // Call getAttemptSummary to validate the preflight data. - promise = this.quizProvider.getAttemptSummary(attempt.id, preflightData, offline, true, false, siteId); + promise = this.quizProvider.getAttemptSummary(attempt.id, preflightData, modOptions); } } else { // We're starting a new attempt, call startAttempt. diff --git a/src/addon/mod/quiz/providers/prefetch-handler.ts b/src/addon/mod/quiz/providers/prefetch-handler.ts index f1832e11d..d0847f847 100644 --- a/src/addon/mod/quiz/providers/prefetch-handler.ts +++ b/src/addon/mod/quiz/providers/prefetch-handler.ts @@ -16,10 +16,10 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; @@ -90,7 +90,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl return this.quizProvider.getQuiz(courseId, module.id).then((quiz) => { const files = this.getIntroFilesFromInstance(module, quiz); - return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true).then((attempts) => { + return this.quizProvider.getUserAttempts(quiz.id, { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }).then((attempts) => { return this.getAttemptsFeedbackFiles(quiz, attempts).then((attemptFiles) => { return files.concat(attemptFiles); }); @@ -106,9 +109,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl * * @param quiz Quiz. * @param attempts Quiz user attempts. + * @param siteId Site ID. If not defined, current site. * @return List of Files. */ - protected getAttemptsFeedbackFiles(quiz: any, attempts: any[]): Promise { + protected getAttemptsFeedbackFiles(quiz: any, attempts: any[], siteId?: string): Promise { // We have quiz data, now we'll get specific data for each attempt. const promises = []; const getInlineFiles = this.sitesProvider.getCurrentSite() && @@ -121,8 +125,11 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl const attemptGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false); if (typeof attemptGrade != 'undefined') { - promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), true) - .then((feedback) => { + promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), { + cmId: quiz.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((feedback) => { if (getInlineFiles && feedback.feedbackinlinefiles && feedback.feedbackinlinefiles.length) { files = files.concat(feedback.feedbackinlinefiles); } else if (feedback.feedbacktext && !getInlineFiles) { @@ -219,13 +226,16 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl const siteId = this.sitesProvider.getCurrentSiteId(); - return this.quizProvider.getQuiz(courseId, module.id, false, false, siteId).then((quiz) => { + return this.quizProvider.getQuiz(courseId, module.id, {siteId}).then((quiz) => { if (quiz.allowofflineattempts !== 1 || quiz.hasquestions === 0) { return false; } // Not downloadable if we reached max attempts or the quiz has an unfinished attempt. - return this.quizProvider.getUserAttempts(quiz.id, undefined, true, false, false, siteId).then((attempts) => { + return this.quizProvider.getUserAttempts(quiz.id, { + cmId: module.id, + siteId, + }).then((attempts) => { const isLastFinished = !attempts.length || this.quizProvider.isAttemptFinished(attempts[attempts.length - 1].state); return quiz.attempts === 0 || quiz.attempts > attempts.length || !isLastFinished; @@ -283,26 +293,35 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl attemptAccessInfo, preflightData; + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + // Get quiz. - return this.quizProvider.getQuiz(courseId, module.id, false, true, siteId).then((quizData) => { + return this.quizProvider.getQuiz(courseId, module.id, commonOptions).then((quizData) => { quiz = quizData; const promises = [], introFiles = this.getIntroFilesFromInstance(module, quiz); // Prefetch some quiz data. - promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { + promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, modOptions).then((info) => { quizAccessInfo = info; })); - promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId)); - promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, modOptions)); + promises.push(this.quizProvider.getUserAttempts(quiz.id, modOptions).then((atts) => { attempts = atts; - return this.getAttemptsFeedbackFiles(quiz, attempts).then((attemptFiles) => { + return this.getAttemptsFeedbackFiles(quiz, attempts, siteId).then((attemptFiles) => { return this.filepoolProvider.addFilesToQueue(siteId, attemptFiles, AddonModQuizProvider.COMPONENT, module.id); }); })); - promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId).then((info) => { + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, modOptions).then((info) => { attemptAccessInfo = info; })); @@ -338,10 +357,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl if (startAttempt) { // Re-fetch user attempts since we created a new one. - promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + promises.push(this.quizProvider.getUserAttempts(quiz.id, modOptions).then((atts) => { attempts = atts; - return this.getAttemptsFeedbackFiles(quiz, attempts).then((attemptFiles) => { + return this.getAttemptsFeedbackFiles(quiz, attempts, siteId).then((attemptFiles) => { return this.filepoolProvider.addFilesToQueue(siteId, attemptFiles, AddonModQuizProvider.COMPONENT, module.id); }); @@ -355,16 +374,16 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl } // Fetch attempt related data. - promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId)); - promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId)); + promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, modOptions)); + promises.push(this.quizProvider.getUserBestGrade(quiz.id, modOptions)); promises.push(this.quizProvider.getGradeFromGradebook(courseId, module.id, true, siteId).then((gradebookData) => { if (typeof gradebookData.graderaw != 'undefined') { - return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); + return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, modOptions); } }).catch(() => { // Ignore errors. })); - promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt. + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, modOptions)); // Last attempt. return Promise.all(promises); }).then(() => { @@ -410,23 +429,35 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl promises = [], isSequential = this.quizProvider.isNavigationSequential(quiz); + const modOptions = { + cmId: quiz.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + if (this.quizProvider.isAttemptFinished(attempt.state)) { // Attempt is finished, get feedback and review data. const attemptGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false); if (typeof attemptGrade != 'undefined') { - promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), true, siteId)); + promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), modOptions)); } // Get the review for each page. pages.forEach((page) => { - promises.push(this.quizProvider.getAttemptReview(attempt.id, page, true, siteId).catch(() => { + promises.push(this.quizProvider.getAttemptReview(attempt.id, { + page, + ...modOptions, // Include all options. + }).catch(() => { // Ignore failures, maybe the user can't review the attempt. })); }); - // Get the review for all questions in same page. - promises.push(this.quizProvider.getAttemptReview(attempt.id, -1, true, siteId).then((data) => { + // Get the review for all questions in same page. + promises.push(this.quizProvider.getAttemptReview(attempt.id, { + page: -1, + ...modOptions, // Include all options. + }).then((data) => { // Download the files inside the questions. const questionPromises = []; @@ -442,8 +473,8 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl } else { // Attempt not finished, get data needed to continue the attempt. - promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, attempt.id, false, true, siteId)); - promises.push(this.quizProvider.getAttemptSummary(attempt.id, preflightData, false, true, false, siteId)); + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, attempt.id, modOptions)); + promises.push(this.quizProvider.getAttemptSummary(attempt.id, preflightData, modOptions)); if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { // Get data for each page. @@ -453,8 +484,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl return; } - promises.push(this.quizProvider.getAttemptData(attempt.id, page, preflightData, false, true, siteId) - .then((data) => { + promises.push(this.quizProvider.getAttemptData(attempt.id, page, preflightData, modOptions).then((data) => { // Download the files inside the questions. const questionPromises = []; @@ -485,30 +515,35 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl siteId = siteId || this.sitesProvider.getCurrentSiteId(); const promises = []; + const modOptions = { + cmId: quiz.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; let attempts, quizAccessInfo, preflightData, lastAttempt; // Get quiz data. - promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { + promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, modOptions).then((info) => { quizAccessInfo = info; })); - promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId)); - promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId)); - promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId)); - promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => { + promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, modOptions)); + promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, modOptions)); + promises.push(this.quizProvider.getUserBestGrade(quiz.id, modOptions)); + promises.push(this.quizProvider.getUserAttempts(quiz.id, modOptions).then((atts) => { attempts = atts; })); promises.push(this.quizProvider.getGradeFromGradebook(quiz.course, quiz.coursemodule, true, siteId) .then((gradebookData) => { if (typeof gradebookData.graderaw != 'undefined') { - return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId); + return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, modOptions); } }).catch(() => { // Ignore errors. })); - promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt. + promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, modOptions)); // Last attempt. return Promise.all(promises).then(() => { lastAttempt = attempts[attempts.length - 1]; @@ -529,7 +564,12 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl } }).then(() => { // Prefetch finished, set the right status. - return this.setStatusAfterPrefetch(quiz, attempts, true, false, siteId); + return this.setStatusAfterPrefetch(quiz, { + cmId: quiz.coursemodule, + attempts, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }); }); } @@ -538,29 +578,25 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl * If the last attempt is finished or there isn't one, set it as not downloaded to show download icon. * * @param quiz Quiz. - * @param attempts List of attempts. If not provided, they will be calculated. - * @param forceCache Whether it should always return cached data. Only if attempts is undefined. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). Only if - * attempts is undefined. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - setStatusAfterPrefetch(quiz: any, attempts?: any[], forceCache?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + setStatusAfterPrefetch(quiz: any, options: AddonModQuizSetStatusAfterPrefetchOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); const promises = []; let status; + let attempts = options.attempts; if (!attempts) { // Get the attempts. - promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, forceCache, ignoreCache, siteId).then((atts) => { + promises.push(this.quizProvider.getUserAttempts(quiz.id, options).then((atts) => { attempts = atts; })); } // Check the current status of the quiz. - promises.push(this.filepoolProvider.getPackageStatus(siteId, this.component, quiz.coursemodule).then((stat) => { + promises.push(this.filepoolProvider.getPackageStatus(options.siteId, this.component, quiz.coursemodule).then((stat) => { status = stat; })); @@ -573,7 +609,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl isLastFinished = !lastAttempt || this.quizProvider.isAttemptFinished(lastAttempt.state), newStatus = isLastFinished ? CoreConstants.NOT_DOWNLOADED : CoreConstants.DOWNLOADED; - return this.filepoolProvider.storePackageStatus(siteId, newStatus, this.component, quiz.coursemodule); + return this.filepoolProvider.storePackageStatus(options.siteId, newStatus, this.component, quiz.coursemodule); } }); } @@ -591,7 +627,7 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl this.syncProvider = this.injector.get(AddonModQuizSyncProvider); } - return this.quizProvider.getQuiz(courseId, module.id).then((quiz) => { + return this.quizProvider.getQuiz(courseId, module.id, {siteId}).then((quiz) => { return this.syncProvider.syncQuiz(quiz, false, siteId).then((results) => { module.attemptFinished = (results && results.attemptFinished) || false; @@ -604,3 +640,10 @@ export class AddonModQuizPrefetchHandler extends CoreCourseActivityPrefetchHandl }); } } + +/** + * Options to pass to setStatusAfterPrefetch. + */ +export type AddonModQuizSetStatusAfterPrefetchOptions = CoreCourseCommonModWSOptions & { + attempts?: any[]; // List of attempts. If not provided, they will be calculated. +}; diff --git a/src/addon/mod/quiz/providers/quiz-sync.ts b/src/addon/mod/quiz/providers/quiz-sync.ts index b49891a58..e13a013f5 100644 --- a/src/addon/mod/quiz/providers/quiz-sync.ts +++ b/src/addon/mod/quiz/providers/quiz-sync.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -111,7 +111,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider // Check if online attempt was finished because of the sync. if (onlineAttempt && !this.quizProvider.isAttemptFinished(onlineAttempt.state)) { // Attempt wasn't finished at start. Check if it's finished now. - return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => { + return this.quizProvider.getUserAttempts(quiz.id, {cmId: quiz.coursemodule, siteId}).then((attempts) => { // Search the attempt. for (const i in attempts) { const attempt = attempts[i]; @@ -180,7 +180,11 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider }).then(() => { // Prefetch finished or not needed, set the right status. - return this.prefetchHandler.setStatusAfterPrefetch(quiz, undefined, shouldDownload, false, siteId); + return this.prefetchHandler.setStatusAfterPrefetch(quiz, { + cmId: module.id, + readingStrategy: shouldDownload ? CoreSitesReadingStrategy.PreferCache : undefined, + siteId, + }); }); } @@ -226,7 +230,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider 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, false, siteId).then((quiz) => { + promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, {siteId}).then((quiz) => { const promise = force ? this.syncQuiz(quiz, false, siteId) : this.syncQuizIfNeeded(quiz, false, siteId); return promise.then((data) => { @@ -284,10 +288,15 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider syncQuiz(quiz: any, askPreflight?: boolean, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - const warnings = [], - courseId = quiz.course; - let syncPromise, - preflightData; + const warnings = []; + const courseId = quiz.course; + const modOptions = { + cmId: quiz.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + let syncPromise; + let preflightData; if (this.isSyncing(quiz.id, siteId)) { // There's already a sync ongoing for this quiz, return the promise. @@ -318,7 +327,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider const offlineAttempt = attempts.pop(); // Now get the list of online attempts to make sure this attempt exists and isn't finished. - return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((attempts) => { + return this.quizProvider.getUserAttempts(quiz.id, modOptions).then((attempts) => { const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined; let onlineAttempt; @@ -354,7 +363,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider let finish; // We're going to need preflightData, get it. - return this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => { + return this.quizProvider.getQuizAccessInformation(quiz.id, modOptions).then((info) => { return this.prefetchHandler.getPreflightData(quiz, info, onlineAttempt, askPreflight, 'core.settings.synchronization', siteId); @@ -364,8 +373,11 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider // Now get the online questions data. const pages = this.quizProvider.getPagesFromLayoutAndQuestions(onlineAttempt.layout, offlineQuestions); - return this.quizProvider.getAllQuestionsData(quiz, onlineAttempt, preflightData, pages, false, true, - siteId); + return this.quizProvider.getAllQuestionsData(quiz, onlineAttempt, preflightData, { + pages, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); }).then((onlineQuestions) => { // Validate questions, discarding the offline answers that can't be synchronized. diff --git a/src/addon/mod/quiz/providers/quiz.ts b/src/addon/mod/quiz/providers/quiz.ts index b4df1d58e..6caa9148e 100644 --- a/src/addon/mod/quiz/providers/quiz.ts +++ b/src/addon/mod/quiz/providers/quiz.ts @@ -16,18 +16,19 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreGradesHelperProvider } from '@core/grades/providers/helper'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate'; import { AddonModQuizOfflineProvider } from './quiz-offline'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for quiz. @@ -90,22 +91,16 @@ export class AddonModQuizProvider { * @param quiz Quiz. * @param attempt Attempt. * @param preflightData Preflight required data (like password). - * @param pages List of pages to get. If not defined, all pages. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the questions. */ - getAllQuestionsData(quiz: any, attempt: any, preflightData: any, pages?: number[], offline?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + getAllQuestionsData(quiz: any, attempt: any, preflightData: any, options: AddonModQuizAllQuestionsDataOptions = {}) + : Promise { - const promises = [], - questions = {}, - isSequential = this.isNavigationSequential(quiz); - - if (!pages) { - pages = this.getPagesFromLayout(attempt.layout); - } + const promises = []; + const questions = {}; + const isSequential = this.isNavigationSequential(quiz); + const pages = options.pages || this.getPagesFromLayout(attempt.layout); pages.forEach((page) => { if (isSequential && page < attempt.currentpage) { @@ -114,7 +109,7 @@ export class AddonModQuizProvider { } // Get the questions in the page. - promises.push(this.getAttemptData(attempt.id, page, preflightData, offline, ignoreCache, siteId).then((data) => { + promises.push(this.getAttemptData(attempt.id, page, preflightData, options).then((data) => { // Add the questions to the result object. data.questions.forEach((question) => { questions[question.slot] = question; @@ -153,29 +148,22 @@ export class AddonModQuizProvider { * * @param quizId Quiz ID. * @param attemptId Attempt ID. 0 for user's last attempt. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the access information. */ - getAttemptAccessInformation(quizId: number, attemptId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getAttemptAccessInformation(quizId: number, attemptId: number, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - quizid: quizId, - attemptid: attemptId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId) - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + attemptid: attemptId, + }; + const preSets = { + cacheKey: this.getAttemptAccessInformationCacheKey(quizId, attemptId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_attempt_access_information', params, preSets); }); @@ -208,30 +196,23 @@ export class AddonModQuizProvider { * @param attemptId Attempt ID. * @param page Page number. * @param preflightData Preflight required data (like password). - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the attempt data. */ - getAttemptData(attemptId: number, page: number, preflightData: any, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getAttemptData(attemptId: number, page: number, preflightData: any, options: CoreCourseCommonModWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - attemptid: attemptId, - page: page, - preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true) - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptDataCacheKey(attemptId, page) - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + attemptid: attemptId, + page: page, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true), + }; + const preSets = { + cacheKey: this.getAttemptDataCacheKey(attemptId, page), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_attempt_data', params, preSets); }); @@ -389,30 +370,24 @@ export class AddonModQuizProvider { * Get an attempt's review. * * @param attemptId Attempt ID. - * @param page Page number. If not defined, return all the questions in all the pages. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the attempt review. */ - getAttemptReview(attemptId: number, page?: number, ignoreCache?: boolean, siteId?: string): Promise { - if (typeof page == 'undefined') { - page = -1; - } + getAttemptReview(attemptId: number, options: AddonModQuizGetAttemptReviewOptions = {}): Promise { + const page = typeof options.page == 'undefined' ? -1 : options.page; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - attemptid: attemptId, - page: page - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptReviewCacheKey(attemptId, page), - cacheErrors: ['noreview'] - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + attemptid: attemptId, + page: page, + }; + const preSets = { + cacheKey: this.getAttemptReviewCacheKey(attemptId, page), + cacheErrors: ['noreview'], + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_attempt_review', params, preSets); }); @@ -433,34 +408,26 @@ export class AddonModQuizProvider { * * @param attemptId Attempt ID. * @param preflightData Preflight required data (like password). - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param loadLocal Whether it should load local state for each question. Only applicable if offline=true. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of questions for the attempt summary. */ - getAttemptSummary(attemptId: number, preflightData: any, offline?: boolean, ignoreCache?: boolean, loadLocal?: boolean, - siteId?: string): Promise { + getAttemptSummary(attemptId: number, preflightData: any, options: AddonModQuizGetAttemptSummaryOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - attemptid: attemptId, - preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true) - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptSummaryCacheKey(attemptId) - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + attemptid: attemptId, + preflightdata: this.utils.objectToArrayOfObjects(preflightData, 'name', 'value', true), + }; + const preSets = { + cacheKey: this.getAttemptSummaryCacheKey(attemptId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_attempt_summary', params, preSets).then((response) => { if (response && response.questions) { - if (offline && loadLocal) { + if (options.loadLocal) { return this.quizOfflineProvider.loadQuestionsLocalStates(attemptId, response.questions, site.getId()); } @@ -497,27 +464,23 @@ export class AddonModQuizProvider { * Get a quiz combined review options. * * @param quizId Quiz ID. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved with the combined review options. */ - getCombinedReviewOptions(quizId: number, ignoreCache?: boolean, siteId?: string, userId?: number): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + getCombinedReviewOptions(quizId: number, options: AddonModQuizUserOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - quizid: quizId, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + userid: userId, + }; + const preSets = { + cacheKey: this.getCombinedReviewOptionsCacheKey(quizId, userId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_combined_review_options', params, preSets).then((response) => { if (response && response.someoptions && response.alloptions) { @@ -559,25 +522,22 @@ export class AddonModQuizProvider { * * @param quizId Quiz ID. * @param grade Grade. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the feedback. */ - getFeedbackForGrade(quizId: number, grade: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getFeedbackForGrade(quizId: number, grade: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - quizid: quizId, - grade: grade - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + grade: grade, + }; + const preSets = { + cacheKey: this.getFeedbackForGradeCacheKey(quizId, grade), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_quiz_feedback_for_grade', params, preSets); }); @@ -683,29 +643,21 @@ export class AddonModQuizProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the Quiz is retrieved. */ - protected getQuizByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + protected getQuizByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuizDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getQuizDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModQuizProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_quizzes_by_courses', params, preSets).then((response) => { if (response && response.quizzes) { @@ -728,13 +680,11 @@ export class AddonModQuizProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the quiz is retrieved. */ - getQuiz(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.getQuizByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); + getQuiz(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getQuizByField(courseId, 'coursemodule', cmId, options); } /** @@ -742,13 +692,11 @@ export class AddonModQuizProvider { * * @param courseId Course ID. * @param id Quiz ID. - * @param forceCache Whether it should always return cached data. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the quiz is retrieved. */ - getQuizById(courseId: number, id: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.getQuizByField(courseId, 'id', id, forceCache, ignoreCache, siteId); + getQuizById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getQuizByField(courseId, 'id', id, options); } /** @@ -765,26 +713,20 @@ export class AddonModQuizProvider { * Get access information for an attempt. * * @param quizId Quiz ID. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the access information. */ - getQuizAccessInformation(quizId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getQuizAccessInformation(quizId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - quizid: quizId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuizAccessInformationCacheKey(quizId) - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + }; + const preSets = { + cacheKey: this.getQuizAccessInformationCacheKey(quizId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_quiz_access_information', params, preSets); }); @@ -829,24 +771,21 @@ export class AddonModQuizProvider { * Get the potential question types that would be required for a given quiz. * * @param quizId Quiz ID. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the access information. */ - getQuizRequiredQtypes(quizId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getQuizRequiredQtypes(quizId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - quizid: quizId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuizRequiredQtypesCacheKey(quizId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + }; + const preSets = { + cacheKey: this.getQuizRequiredQtypesCacheKey(quizId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_quiz_required_qtypes', params, preSets).then((response) => { if (response && response.questiontypes) { @@ -981,37 +920,29 @@ export class AddonModQuizProvider { * Get quiz attempts for a certain user. * * @param quizId Quiz ID. - * @param status Status of the attempts to get. By default, 'all'. - * @param includePreviews Whether to include previews. Defaults to true. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved with the attempts. */ - getUserAttempts(quizId: number, status: string = 'all', includePreviews: boolean = true, offline?: boolean, - ignoreCache?: boolean, siteId?: string, userId?: number): Promise { + getUserAttempts(quizId: number, options: AddonModQuizGetUserAttemptsOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + const status = options.status || 'all'; + const includePreviews = typeof options.includePreviews == 'undefined' ? true : options.includePreviews; + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - quizid: quizId, - userid: userId, - status: status, - includepreviews: includePreviews ? 1 : 0 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getUserAttemptsCacheKey(quizId, userId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + userid: userId, + status: status, + includepreviews: includePreviews ? 1 : 0, + }; + const preSets = { + cacheKey: this.getUserAttemptsCacheKey(quizId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_user_attempts', params, preSets).then((response) => { if (response && response.attempts) { @@ -1048,27 +979,23 @@ export class AddonModQuizProvider { * Get best grade in a quiz for a certain user. * * @param quizId Quiz ID. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved with the best grade data. */ - getUserBestGrade(quizId: number, ignoreCache?: boolean, siteId?: string, userId?: number): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + getUserBestGrade(quizId: number, options: AddonModQuizUserOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - quizid: quizId, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getUserBestGradeCacheKey(quizId, userId) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + quizid: quizId, + userid: userId, + }; + const preSets = { + cacheKey: this.getUserBestGradeCacheKey(quizId, userId), + component: AddonModQuizProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_quiz_get_user_best_grade', params, preSets); }); @@ -1246,8 +1173,11 @@ export class AddonModQuizProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Get required data to call the invalidate functions. - return this.getQuiz(courseId, moduleId, true, false, siteId).then((quiz) => { - return this.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => { + return this.getQuiz(courseId, moduleId, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((quiz) => { + return this.getUserAttempts(quiz.id, {cmId: quiz.coursemodule, siteId}).then((attempts) => { // Now invalidate it. const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined; @@ -1703,7 +1633,12 @@ export class AddonModQuizProvider { : Promise { // Get attempt summary to have the list of questions. - return this.getAttemptSummary(attempt.id, preflightData, true, false, true, siteId).then((questionArray) => { + return this.getAttemptSummary(attempt.id, preflightData, { + cmId: quiz.coursemodule, + loadLocal: true, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((questionArray) => { // Convert the question array to an object. const questions = this.utils.arrayToObject(questionArray, 'slot'); @@ -1860,3 +1795,40 @@ export class AddonModQuizProvider { }); } } + +/** + * Common options with user ID. + */ +export type AddonModQuizUserOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User ID. If not defined use site's current user. +}; + +/** + * Options to pass to getAllQuestionsData. + */ +export type AddonModQuizAllQuestionsDataOptions = CoreCourseCommonModWSOptions & { + pages?: number[]; // List of pages to get. If not defined, all pages. +}; + +/** + * Options to pass to getAttemptReview. + */ +export type AddonModQuizGetAttemptReviewOptions = CoreCourseCommonModWSOptions & { + page?: number; // List of pages to get. If not defined, all pages. +}; + +/** + * Options to pass to getAttemptSummary. + */ +export type AddonModQuizGetAttemptSummaryOptions = CoreCourseCommonModWSOptions & { + loadLocal?: boolean; // Whether it should load local state for each question. +}; + +/** + * Options to pass to getUserAttempts. + */ +export type AddonModQuizGetUserAttemptsOptions = CoreCourseCommonModWSOptions & { + status?: string; // Status of the attempts to get. By default, 'all'. + includePreviews?: boolean; // Whether to include previews. Defaults to true. + userId?: number; // User ID. If not defined use site's current user. +}; diff --git a/src/addon/mod/resource/providers/resource.ts b/src/addon/mod/resource/providers/resource.ts index 5445ec3b1..b8b5b2fd2 100644 --- a/src/addon/mod/resource/providers/resource.ts +++ b/src/addon/mod/resource/providers/resource.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; @@ -54,18 +54,22 @@ export class AddonModResourceProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the resource is retrieved. */ - protected getResourceDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getResourceDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getResourceCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getResourceCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModResourceProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_resource_get_resources_by_courses', params, preSets) .then((response: AddonModResourceGetResourcesByCoursesResult): any => { @@ -89,11 +93,11 @@ export class AddonModResourceProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the resource is retrieved. */ - getResourceData(courseId: number, cmId: number, siteId?: string): Promise { - return this.getResourceDataByKey(courseId, 'coursemodule', cmId, siteId); + getResourceData(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getResourceDataByKey(courseId, 'coursemodule', cmId, options); } /** diff --git a/src/addon/mod/scorm/components/index/index.ts b/src/addon/mod/scorm/components/index/index.ts index e16a6484a..f6f313360 100644 --- a/src/addon/mod/scorm/components/index/index.ts +++ b/src/addon/mod/scorm/components/index/index.ts @@ -146,7 +146,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { // Get the SCORM instance. - return this.scormProvider.getScorm(this.courseId, this.module.id, this.module.url).then((scormData) => { + return this.scormProvider.getScorm(this.courseId, this.module.id, {moduleUrl: this.module.url}).then((scormData) => { this.scorm = scormData; this.dataRetrieved.emit(this.scorm); @@ -185,12 +185,12 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom const promises = []; // Get access information. - promises.push(this.scormProvider.getAccessInformation(this.scorm.id).then((accessInfo) => { + promises.push(this.scormProvider.getAccessInformation(this.scorm.id, {cmId: this.module.id}).then((accessInfo) => { this.accessInfo = accessInfo; })); // Get the number of attempts. - promises.push(this.scormProvider.getAttemptCount(this.scorm.id).then((attemptsData) => { + promises.push(this.scormProvider.getAttemptCount(this.scorm.id, {cmId: this.module.id}).then((attemptsData) => { this.attempts = attemptsData; this.hasOffline = !!this.attempts.offline.length; @@ -207,7 +207,10 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom } // Check if the last attempt is incomplete. - return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.lastAttempt, this.lastIsOffline); + return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.lastAttempt, { + offline: this.lastIsOffline, + cmId: this.module.id, + }); }).then((incomplete) => { const promises = []; @@ -260,7 +263,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom * @return Promise resolved when done. */ protected fetchStructure(): Promise { - return this.scormProvider.getOrganizations(this.scorm.id).then((organizations) => { + return this.scormProvider.getOrganizations(this.scorm.id, {cmId: this.module.id}).then((organizations) => { this.organizations = organizations; if (!this.currentOrganization.identifier) { @@ -460,8 +463,11 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom this.loadingToc = true; - return this.scormProvider.getOrganizationToc(this.scorm.id, this.lastAttempt, organizationId, this.lastIsOffline) - .then((toc) => { + return this.scormProvider.getOrganizationToc(this.scorm.id, this.lastAttempt, { + organization: organizationId, + offline: this.lastIsOffline, + cmId: this.module.id, + }).then((toc) => { this.toc = this.scormProvider.formatTocToArray(toc); diff --git a/src/addon/mod/scorm/pages/player/player.ts b/src/addon/mod/scorm/pages/player/player.ts index a21bd9e58..ed6e32de7 100644 --- a/src/addon/mod/scorm/pages/player/player.ts +++ b/src/addon/mod/scorm/pages/player/player.ts @@ -15,7 +15,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { IonicPage, NavParams, ModalController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtils } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -201,7 +201,10 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { // Check if current attempt is incomplete. if (this.attempt > 0) { - return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, this.offline); + return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, { + offline: this.offline, + cmId: this.scorm.coursemodule, + }); } else { // User doesn't have attempts. Last attempt is not incomplete (since he doesn't have any). return false; @@ -217,7 +220,10 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { return this.scormHelper.createOfflineAttempt(this.scorm, result.attempt, attemptsData.online.length); } else { // Last attempt was online, verify that we can create a new online attempt. We ignore cache. - return this.scormProvider.getScormUserData(this.scorm.id, result.attempt, undefined, false, true).catch(() => { + return this.scormProvider.getScormUserData(this.scorm.id, result.attempt, { + cmId: this.scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }).catch(() => { // Cannot communicate with the server, create an offline attempt. this.offline = true; @@ -241,18 +247,22 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { // Wait for any ongoing sync to finish. We won't sync a SCORM while it's being played. return this.scormSyncProvider.waitForSync(this.scorm.id).then(() => { // Get attempts data. - return this.scormProvider.getAttemptCount(this.scorm.id).then((attemptsData) => { + return this.scormProvider.getAttemptCount(this.scorm.id, {cmId: this.scorm.coursemodule}).then((attemptsData) => { return this.determineAttemptAndMode(attemptsData).then(() => { // Fetch TOC and get user data. const promises = []; promises.push(this.fetchToc()); - promises.push(this.scormProvider.getScormUserData(this.scorm.id, this.attempt, undefined, this.offline) - .then((data) => { + promises.push(this.scormProvider.getScormUserData(this.scorm.id, this.attempt, { + cmId: this.scorm.coursemodule, + offline: this.offline, + }).then((data) => { this.userData = data; })); // Get access information. - promises.push(this.scormProvider.getAccessInformation(this.scorm.id).then((accessInfo) => { + promises.push(this.scormProvider.getAccessInformation(this.scorm.id, { + cmId: this.scorm.coursemodule, + }).then((accessInfo) => { this.accessInfo = accessInfo; })); @@ -273,11 +283,18 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.loadingToc = true; // We need to check incomplete again: attempt number or status might have changed. - return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, this.offline).then((incomplete) => { + return this.scormProvider.isAttemptIncomplete(this.scorm.id, this.attempt, { + offline: this.offline, + cmId: this.scorm.coursemodule, + }).then((incomplete) => { this.scorm.incomplete = incomplete; // Get TOC. - return this.scormProvider.getOrganizationToc(this.scorm.id, this.attempt, this.organizationId, this.offline); + return this.scormProvider.getOrganizationToc(this.scorm.id, this.attempt, { + organization: this.organizationId, + offline: this.offline, + cmId: this.scorm.coursemodule, + }); }).then((toc) => { this.toc = this.scormProvider.formatTocToArray(toc); @@ -300,8 +317,13 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { if (!this.currentSco) { // No SCO defined. Get the first valid one. - return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, this.toc, this.organizationId, this.mode, - this.offline).then((sco) => { + return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, { + toc: this.toc, + organization: this.organizationId, + mode: this.mode, + offline: this.offline, + cmId: this.scorm.coursemodule, + }).then((sco) => { if (sco) { this.currentSco = sco; @@ -374,7 +396,9 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.scormProvider.saveTracks(sco.id, this.attempt, tracks, this.scorm, this.offline).catch(() => { // Error saving data. We'll go offline if we're online and the asset is not marked as completed already. if (!this.offline) { - return this.scormProvider.getScormUserData(this.scorm.id, this.attempt, undefined, false).then((data) => { + return this.scormProvider.getScormUserData(this.scorm.id, this.attempt, { + cmId: this.scorm.coursemodule, + }).then((data) => { if (!data[sco.id] || data[sco.id].userdata['cmi.core.lesson_status'] != 'completed') { // Go offline. return this.scormHelper.convertAttemptToOffline(this.scorm, this.attempt).then(() => { @@ -462,7 +486,10 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { return this.scormProvider.saveTracks(scoId, this.attempt, tracks, this.scorm, this.offline).then(() => { if (!this.offline) { // New online attempt created, update cached data about online attempts. - this.scormProvider.getAttemptCount(this.scorm.id, false, true).catch(() => { + this.scormProvider.getAttemptCount(this.scorm.id, { + cmId: this.scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + }).catch(() => { // Ignore errors. }); } diff --git a/src/addon/mod/scorm/providers/helper.ts b/src/addon/mod/scorm/providers/helper.ts index 49fe8f6fd..c838894f1 100644 --- a/src/addon/mod/scorm/providers/helper.ts +++ b/src/addon/mod/scorm/providers/helper.ts @@ -19,6 +19,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonModScormProvider, AddonModScormAttemptCountResult } from './scorm'; import { AddonModScormOfflineProvider } from './scorm-offline'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Helper service that provides some features for SCORM. @@ -78,7 +79,7 @@ export class AddonModScormHelperProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Get data from the online attempt. - return this.scormProvider.getScormUserData(scorm.id, attempt, undefined, false, false, siteId).then((onlineData) => { + return this.scormProvider.getScormUserData(scorm.id, attempt, {cmId: scorm.coursemodule, siteId}).then((onlineData) => { // The SCORM API might have written some data to the offline attempt already. // We don't want to override it with cached online data. return this.scormOfflineProvider.getScormUserData(scorm.id, attempt, undefined, siteId).catch(() => { @@ -131,7 +132,7 @@ export class AddonModScormHelperProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Try to get data from online attempts. - return this.searchOnlineAttemptUserData(scorm.id, lastOnline, siteId).then((userData) => { + return this.searchOnlineAttemptUserData(scorm.id, lastOnline, {cmId: scorm.coursemodule, siteId}).then((userData) => { // We're creating a new attempt, remove all the user data that is not needed for a new attempt. for (const scoId in userData) { const sco = userData[scoId], @@ -177,7 +178,11 @@ export class AddonModScormHelperProvider { // Check if last online incomplete. const hasOffline = attempts.offline.indexOf(lastOnline) > -1; - return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, hasOffline, false, siteId).then((incomplete) => { + return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, { + offline: hasOffline, + cmId: scorm.coursemodule, + siteId, + }).then((incomplete) => { if (incomplete) { return { number: lastOnline, @@ -197,24 +202,19 @@ export class AddonModScormHelperProvider { * * @param scormId Scorm ID. * @param attempt Attempt number. - * @param toc SCORM's TOC. If not provided, it will be calculated. - * @param organization Organization to use. - * @param mode Mode. - * @param offline Whether the attempt is offline. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the first SCO. */ - getFirstSco(scormId: number, attempt: number, toc?: any[], organization?: string, mode?: string, offline?: boolean, - siteId?: string): Promise { + getFirstSco(scormId: number, attempt: number, options: AddonModScormGetFirstScoOptions = {}): Promise { - mode = mode || AddonModScormProvider.MODENORMAL; + const mode = options.mode || AddonModScormProvider.MODENORMAL; let promise; - if (toc && toc.length) { - promise = Promise.resolve(toc); + if (options.toc && options.toc.length) { + promise = Promise.resolve(options.toc); } else { // SCORM doesn't have a TOC. Get all the scos. - promise = this.scormProvider.getScosWithData(scormId, attempt, organization, offline, false, siteId); + promise = this.scormProvider.getScosWithData(scormId, attempt, options); } return promise.then((scos) => { @@ -319,16 +319,16 @@ export class AddonModScormHelperProvider { * * @param scormId SCORM ID. * @param attempt Online attempt to get the data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with user data. */ - searchOnlineAttemptUserData(scormId: number, attempt: number, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + searchOnlineAttemptUserData(scormId: number, attempt: number, options: CoreCourseCommonModWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, false, siteId).catch(() => { + return this.scormProvider.getScormUserData(scormId, attempt, options).catch(() => { if (attempt > 0) { // We couldn't retrieve the data. Try again with the previous online attempt. - return this.searchOnlineAttemptUserData(scormId, attempt - 1, siteId); + return this.searchOnlineAttemptUserData(scormId, attempt - 1, options); } else { // No more attempts to try. Reject return Promise.reject(null); @@ -336,3 +336,13 @@ export class AddonModScormHelperProvider { }); } } + +/** + * Options to pass to getFirstSco. + */ +export type AddonModScormGetFirstScoOptions = CoreCourseCommonModWSOptions & { + toc?: any[]; // SCORM's TOC. If not provided, it will be calculated. + organization?: string; // Organization to use. + mode?: string; // Mode. + offline?: boolean; // Whether the attempt is offline. +}; diff --git a/src/addon/mod/scorm/providers/prefetch-handler.ts b/src/addon/mod/scorm/providers/prefetch-handler.ts index d8263dbd7..dd34b8fe1 100644 --- a/src/addon/mod/scorm/providers/prefetch-handler.ts +++ b/src/addon/mod/scorm/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -111,7 +111,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand let scorm; - return this.scormProvider.getScorm(courseId, module.id, module.url, false, siteId).then((scormData) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url, siteId}).then((scormData) => { scorm = scormData; const promises = [], @@ -132,9 +132,6 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand // Ignore errors. })); - // Prefetch access information. - promises.push(this.scormProvider.getAccessInformation(scorm.id)); - return Promise.all(promises); }).then(() => { // Success, return the hash. @@ -246,9 +243,14 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand siteId = siteId || this.sitesProvider.getCurrentSiteId(); const promises = []; + const modOptions = { + cmId: scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; // Prefetch number of attempts (including not completed). - promises.push(this.scormProvider.getAttemptCountOnline(scorm.id, undefined, true, siteId).catch(() => { + promises.push(this.scormProvider.getAttemptCountOnline(scorm.id, modOptions).catch(() => { // If it fails, assume we have no attempts. return 0; }).then((numAttempts) => { @@ -257,7 +259,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand const dataPromises = []; for (let i = 1; i <= numAttempts; i++) { - dataPromises.push(this.scormProvider.getScormUserDataOnline(scorm.id, i, true, siteId).catch((err) => { + dataPromises.push(this.scormProvider.getScormUserDataOnline(scorm.id, i, modOptions).catch((err) => { // Ignore failures of all the attempts that aren't the last one. if (i == numAttempts) { return Promise.reject(err); @@ -268,12 +270,15 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand return Promise.all(dataPromises); } else { // No attempts. We'll still try to get user data to be able to identify SCOs not visible and so. - return this.scormProvider.getScormUserDataOnline(scorm.id, 0, true, siteId); + return this.scormProvider.getScormUserDataOnline(scorm.id, 0, modOptions); } })); // Prefetch SCOs. - promises.push(this.scormProvider.getScos(scorm.id, undefined, true, siteId)); + promises.push(this.scormProvider.getScos(scorm.id, modOptions)); + + // Prefetch access information. + promises.push(this.scormProvider.getAccessInformation(scorm.id, modOptions)); return Promise.all(promises); } @@ -288,7 +293,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand * to calculate the total size. */ getDownloadSize(module: any, courseId: any, single?: boolean): Promise<{ size: number, total: boolean }> { - return this.scormProvider.getScorm(courseId, module.id, module.url).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url}).then((scorm) => { if (this.scormProvider.isScormUnsupported(scorm)) { return {size: -1, total: false}; } else if (!scorm.packagesize) { @@ -310,7 +315,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Size, or promise resolved with the size. */ getDownloadedSize(module: any, courseId: number): number | Promise { - return this.scormProvider.getScorm(courseId, module.id, module.url).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url}).then((scorm) => { // Get the folder where SCORM should be unzipped. return this.scormProvider.getScormFolder(scorm.moduleurl); }).then((path) => { @@ -327,7 +332,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Promise resolved with the list of files. */ getFiles(module: any, courseId: number, single?: boolean): Promise { - return this.scormProvider.getScorm(courseId, module.id, module.url).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url}).then((scorm) => { return this.scormProvider.getScormFileList(scorm); }).catch(() => { // SCORM not found, return empty list. @@ -366,7 +371,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand * @return Whether the module can be downloaded. The promise should never be rejected. */ isDownloadable(module: any, courseId: number): boolean | Promise { - return this.scormProvider.getScorm(courseId, module.id, module.url).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url}).then((scorm) => { if (scorm.warningMessage) { // SCORM closed or not opened yet. return false; @@ -409,7 +414,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand const siteId = this.sitesProvider.getCurrentSiteId(); let scorm; - return this.scormProvider.getScorm(courseId, module.id, module.url, false, siteId).then((scormData) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url, siteId}).then((scormData) => { scorm = scormData; // Get the folder where SCORM should be unzipped. @@ -448,7 +453,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand this.syncProvider = this.injector.get(AddonModScormSyncProvider); } - return this.scormProvider.getScorm(courseId, module.id, module.url, false, siteId).then((scorm) => { + return this.scormProvider.getScorm(courseId, module.id, {moduleUrl: module.url, siteId}).then((scorm) => { return this.syncProvider.syncScorm(scorm, siteId); }); } diff --git a/src/addon/mod/scorm/providers/scorm-sync.ts b/src/addon/mod/scorm/providers/scorm-sync.ts index 59ecbb813..aba64e427 100644 --- a/src/addon/mod/scorm/providers/scorm-sync.ts +++ b/src/addon/mod/scorm/providers/scorm-sync.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -129,14 +129,20 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * @param scormId SCORM ID. * @param attempt Attempt number. * @param lastOnline Last online attempt number. + * @param cmId Module ID. * @param siteId Site ID. * @return Promise resolved if can retry the synchronization, rejected otherwise. */ - protected canRetrySync(scormId: number, attempt: number, lastOnline: number, siteId: string): Promise { + protected canRetrySync(scormId: number, attempt: number, lastOnline: number, cmId: number, siteId: string): Promise { + // If it's the last attempt we don't need to ignore cache because we already did it. const refresh = lastOnline != attempt; - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, refresh, siteId).then((siteData) => { + return this.scormProvider.getScormUserData(scormId, attempt, { + cmId, + readingStrategy: refresh ? CoreSitesReadingStrategy.OnlyNetwork : undefined, + siteId, + }).then((siteData) => { // Get synchronization snapshot (if sync fails it should store a snapshot). return this.scormOfflineProvider.getAttemptSnapshot(scormId, attempt, siteId).then((snapshot) => { if (!snapshot || !Object.keys(snapshot).length || !this.snapshotEquals(snapshot, siteData)) { @@ -209,12 +215,16 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide // Check if an attempt was finished in Moodle. if (initialCount) { // Get attempt count again to check if an attempt was finished. - return this.scormProvider.getAttemptCount(scorm.id, undefined, false, siteId).then((attemptsData) => { + return this.scormProvider.getAttemptCount(scorm.id, {cmId: scorm.coursemodule, siteId}).then((attemptsData) => { if (attemptsData.online.length > initialCount.online.length) { return true; } else if (!lastOnlineWasFinished && lastOnline > 0) { // Last online attempt wasn't finished, let's check if it is now. - return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, false, true, siteId).then((inc) => { + return this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, { + cmId: scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((inc) => { return !inc; }); } @@ -238,14 +248,19 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * * @param scormId SCORM ID. * @param attempt Attempt number. + * @param cmId Module ID. * @param siteId Site ID. * @return Promise resolved with the data. */ - protected getOfflineAttemptData(scormId: number, attempt: number, siteId: string) + protected getOfflineAttemptData(scormId: number, attempt: number, cmId: number, siteId: string) : Promise<{incomplete: boolean, timecreated: number}> { // Check if last offline attempt is incomplete. - return this.scormProvider.isAttemptIncomplete(scormId, attempt, true, false, siteId).then((incomplete) => { + return this.scormProvider.isAttemptIncomplete(scormId, attempt, { + offline: true, + cmId, + siteId, + }).then((incomplete) => { return this.scormOfflineProvider.getAttemptCreationTime(scormId, attempt, siteId).then((timecreated) => { return { incomplete: incomplete, @@ -363,17 +378,22 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * * @param scormId SCORM ID. * @param attempt Attemot number. + * @param cmId Module ID. * @param siteId Site ID. * @return Promise resolved when the snapshot is stored. */ - protected saveSyncSnapshot(scormId: number, attempt: number, siteId: string): Promise { + protected saveSyncSnapshot(scormId: number, attempt: number, cmId: number, siteId: string): Promise { // Try to get current state from the site. - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, true, siteId).then((data) => { + return this.scormProvider.getScormUserData(scormId, attempt, { + cmId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((data) => { return this.scormOfflineProvider.setAttemptSnapshot(scormId, attempt, data, siteId); }, () => { // Error getting user data from the site. We'll have to build it ourselves. // Let's try to get cached data about the attempt. - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, false, siteId).catch(() => { + return this.scormProvider.getScormUserData(scormId, attempt, {cmId, siteId}).catch(() => { // No cached data. return {}; }).then((data) => { @@ -479,7 +499,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide scorms.forEach((scorm) => { if (!this.syncProvider.isBlocked(AddonModScormProvider.COMPONENT, scorm.id, siteId)) { - promises.push(this.scormProvider.getScormById(scorm.courseId, scorm.id, '', false, siteId).then((scorm) => { + promises.push(this.scormProvider.getScormById(scorm.courseId, scorm.id, {siteId}).then((scorm) => { const promise = force ? this.syncScorm(scorm, siteId) : this.syncScormIfNeeded(scorm, siteId); return promise.then((data) => { @@ -506,10 +526,11 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * * @param scormId SCORM ID. * @param attempt Attempt number. + * @param cmId Module ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the attempt is successfully synced. */ - protected syncAttempt(scormId: number, attempt: number, siteId?: string): Promise { + protected syncAttempt(scormId: number, attempt: number, cmId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); this.logger.debug('Try to sync attempt ' + attempt + ' in SCORM ' + scormId + ' and site ' + siteId); @@ -565,7 +586,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide this.logger.error('Error synchronizing some SCOs for attempt ' + attempt + ' in SCORM ' + scormId + '. Saving snapshot.'); - return this.saveSyncSnapshot(scormId, attempt, siteId).then(() => { + return this.saveSyncSnapshot(scormId, attempt, cmId, siteId).then(() => { return Promise.reject(error); }); } else { @@ -629,7 +650,11 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide // Ignore errors. }).then(() => { // Get attempts data. We ignore cache for online attempts, so this call will fail if offline or server down. - return this.scormProvider.getAttemptCount(scorm.id, false, true, siteId); + return this.scormProvider.getAttemptCount(scorm.id, { + cmId: scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); }).then((attemptsData) => { if (!attemptsData.offline || !attemptsData.offline.length) { // Nothing to sync. @@ -649,8 +674,12 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide }); // Check if last online attempt is finished. Ignore cache. - const promise = lastOnline > 0 ? this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, false, true, siteId) : - Promise.resolve(false); + const promise = lastOnline <= 0 ? Promise.resolve(false) : + this.scormProvider.isAttemptIncomplete(scorm.id, lastOnline, { + cmId: scorm.coursemodule, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); return promise.then((incomplete) => { lastOnlineWasFinished = !incomplete; @@ -661,7 +690,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide attemptsData.offline.forEach((attempt) => { if (scorm.maxattempt == 0 || attempt <= scorm.maxattempt) { - promises.push(this.syncAttempt(scorm.id, attempt, siteId)); + promises.push(this.syncAttempt(scorm.id, attempt, scorm.coursemodule, siteId)); } }); @@ -672,7 +701,8 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide } else if (collisions.length) { // We have collisions, treat them. - return this.treatCollisions(scorm.id, collisions, lastOnline, attemptsData.offline, siteId).then((warns) => { + return this.treatCollisions(scorm.id, collisions, lastOnline, attemptsData.offline, scorm.coursemodule, siteId) + .then((warns) => { warnings = warnings.concat(warns); // The offline attempts might have changed since some collisions can be converted to new attempts. @@ -694,7 +724,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide // We'll only sync new attemps if last online attempt is completed. if (!incomplete || attempt <= lastOnline) { if (scorm.maxattempt == 0 || attempt <= scorm.maxattempt) { - promises.push(this.syncAttempt(scorm.id, attempt, siteId)); + promises.push(this.syncAttempt(scorm.id, attempt, scorm.coursemodule, siteId)); } } else { cannotSyncSome = true; @@ -730,6 +760,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * @param collisions Numbers of attempts that exist both in online and offline. * @param lastOnline Last online attempt. * @param offlineAttempts Numbers of offline attempts. + * @param cmId Module ID. * @param siteId Site ID. * @return Promise resolved when the collisions have been treated. It returns warnings array. * @description @@ -751,8 +782,8 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide * of the list if the last attempt is completed. If the last attempt is not completed then the offline data will de deleted * because we can't create a new attempt. */ - protected treatCollisions(scormId: number, collisions: number[], lastOnline: number, offlineAttempts: number[], siteId: string) - : Promise { + protected treatCollisions(scormId: number, collisions: number[], lastOnline: number, offlineAttempts: number[], cmId: number, + siteId: string): Promise { const warnings = [], newAttemptsSameOrder = [], // Attempts that will be created as new attempts but keeping the current order. @@ -761,7 +792,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide let lastOffline = Math.max.apply(Math, offlineAttempts); // Get needed data from the last offline attempt. - return this.getOfflineAttemptData(scormId, lastOffline, siteId).then((lastOfflineData) => { + return this.getOfflineAttemptData(scormId, lastOffline, cmId, siteId).then((lastOfflineData) => { const promises = []; collisions.forEach((attempt) => { @@ -785,7 +816,7 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide if (hasDataToSend) { // There are elements to sync. We need to check if it's possible to sync them or not. - return this.canRetrySync(scormId, attempt, lastOnline, siteId).catch(() => { + return this.canRetrySync(scormId, attempt, lastOnline, cmId, siteId).catch(() => { // Cannot retry sync, we'll create a new offline attempt if possible. return this.addToNewOrDelete(scormId, attempt, lastOffline, newAttemptsSameOrder, newAttemptsAtEnd, lastOfflineData.timecreated, lastOfflineData.incomplete, warnings, @@ -806,8 +837,11 @@ export class AddonModScormSyncProvider extends CoreCourseActivitySyncBaseProvide // If it's the last attempt we don't need to ignore cache because we already did it. const refresh = lastOnline != attempt; - return this.scormProvider.getScormUserData(scormId, attempt, undefined, false, refresh, siteId) - .then((data) => { + return this.scormProvider.getScormUserData(scormId, attempt, { + cmId, + readingStrategy: refresh ? CoreSitesReadingStrategy.OnlyNetwork : undefined, + siteId, + }).then((data) => { if (!this.snapshotEquals(snapshot, data)) { // Snapshot has diverged, it will be converted into a new attempt if possible. diff --git a/src/addon/mod/scorm/providers/scorm.ts b/src/addon/mod/scorm/providers/scorm.ts index 2cec079ec..da74ecc02 100644 --- a/src/addon/mod/scorm/providers/scorm.ts +++ b/src/addon/mod/scorm/providers/scorm.ts @@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreWSProvider } from '@providers/ws'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -25,9 +25,10 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUrlUtils } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonModScormOfflineProvider } from './scorm-offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreConstants } from '@core/constants'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Result of getAttemptCount. @@ -463,24 +464,25 @@ export class AddonModScormProvider { * Get access information for a given SCORM. * * @param scormId SCORM ID. - * @param forceCache True to always get the value from cache. false otherwise. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Object with access information. * @since 3.7 */ - getAccessInformation(scormId: number, forceCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAccessInformation(scormId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { if (!site.wsAvailable('mod_scorm_get_scorm_access_information')) { // Access information not available for 3.6 or older sites. return Promise.resolve({}); } const params = { - scormid: scormId + scormid: scormId, }; const preSets = { cacheKey: this.getAccessInformationCacheKey(scormId), - omitExpires: forceCache + component: AddonModScormProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_scorm_get_scorm_access_information', params, preSets); @@ -501,19 +503,15 @@ export class AddonModScormProvider { * Get the number of attempts done by a user in the given SCORM. * * @param scormId SCORM ID. - * @param ignoreMissing Whether it should ignore attempts without grade/completion. Only for online attempts. - * @param ignoreCache Whether it should ignore cached data for online attempts. - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved when done. */ - getAttemptCount(scormId: number, ignoreMissing?: boolean, ignoreCache?: boolean, siteId?: string, userId?: number) - : Promise { + getAttemptCount(scormId: number, options: AddonModScormGetAttemptCountOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const result: AddonModScormAttemptCountResult = { lastAttempt: { @@ -523,7 +521,7 @@ export class AddonModScormProvider { }, promises = []; - promises.push(this.getAttemptCountOnline(scormId, ignoreMissing, ignoreCache, siteId, userId).then((count) => { + promises.push(this.getAttemptCountOnline(scormId, options).then((count) => { // Calculate numbers of online attempts. result.online = []; @@ -539,7 +537,7 @@ export class AddonModScormProvider { } })); - promises.push(this.scormOfflineProvider.getAttempts(scormId, siteId, userId).then((attempts) => { + promises.push(this.scormOfflineProvider.getAttempts(scormId, options.siteId, userId).then((attempts) => { // Get only attempt numbers. result.offline = attempts.map((entry) => { // Calculate last attempt. We use >= to prioritize offline events if an attempt is both online and offline. @@ -584,32 +582,26 @@ export class AddonModScormProvider { * Get the number of attempts done by a user in the given SCORM in online. * * @param scormId SCORM ID. - * @param ignoreMissing Whether it should ignore attempts that haven't reported a grade/completion. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. - * @param userId User ID. If not defined use site's current user. + * @param options Other options. * @return Promise resolved when the attempt count is retrieved. */ - getAttemptCountOnline(scormId: number, ignoreMissing?: boolean, ignoreCache?: boolean, siteId?: string, userId?: number) - : Promise { + getAttemptCountOnline(scormId: number, options: AddonModScormGetAttemptCountOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + return this.sitesProvider.getSite(options.siteId).then((site) => { + const userId = options.userId || site.getUserId(); const params = { - scormid: scormId, - userid: userId, - ignoremissingcompletion: ignoreMissing ? 1 : 0 - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getAttemptCountCacheKey(scormId, userId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + scormid: scormId, + userid: userId, + ignoremissingcompletion: options.ignoreMissing ? 1 : 0, + }; + const preSets = { + cacheKey: this.getAttemptCountCacheKey(scormId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModScormProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_scorm_get_scorm_attempt_count', params, preSets).then((response) => { if (response && typeof response.attemptscount != 'undefined') { @@ -640,7 +632,7 @@ export class AddonModScormProvider { }; // Get the user data and use it to calculate the grade. - return this.getScormUserData(scorm.id, attempt, undefined, offline, false, siteId).then((data) => { + return this.getScormUserData(scorm.id, attempt, {offline, cmId: scorm.coursemodule, siteId}).then((data) => { for (const scoId in data) { const sco = data[scoId], userData = sco.userdata; @@ -694,11 +686,11 @@ export class AddonModScormProvider { * Get the list of a organizations defined in a SCORM package. * * @param scormId SCORM ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of organizations. */ - getOrganizations(scormId: number, siteId?: string): Promise { - return this.getScos(scormId, undefined, false, siteId).then((scos) => { + getOrganizations(scormId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.getScos(scormId, options).then((scos) => { const organizations = []; scos.forEach((sco) => { @@ -721,15 +713,12 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt The attempt number (to populate SCO track data). - * @param organization Organization identifier. - * @param offline Whether the attempt is offline. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the toc object. */ - getOrganizationToc(scormId: number, attempt: number, organization?: string, offline?: boolean, siteId?: string) - : Promise { + getOrganizationToc(scormId: number, attempt: number, options: AddonModScormGetScosWithDataOptions = {}): Promise { - return this.getScosWithData(scormId, attempt, organization, offline, false, siteId).then((scos) => { + return this.getScosWithData(scormId, attempt, options).then((scos) => { const map = {}, rootScos = []; @@ -738,7 +727,7 @@ export class AddonModScormProvider { map[sco.identifier] = index; if (sco.parent !== '/') { - if (sco.parent == organization) { + if (sco.parent == options.organization) { // It's a root SCO, add it to the root array. rootScos.push(sco); } else { @@ -774,26 +763,22 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt Attempt number. - * @param scos SCOs returned by getScos. Recommended if offline=true. - * @param offline Whether the attempt is offline. - * @param ignoreCache Whether it should ignore cached data for online attempts. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the user data is retrieved. */ - getScormUserData(scormId: number, attempt: number, scos?: any[], offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getScormUserData(scormId: number, attempt: number, options: AddonModScormGetUserDataOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - if (offline) { + if (options.offline) { // Get SCOs if not provided. - const promise = scos ? Promise.resolve(scos) : this.getScos(scormId, undefined, undefined, siteId); + const promise = options.scos ? Promise.resolve(options.scos) : this.getScos(scormId, options); return promise.then((scos) => { - return this.scormOfflineProvider.getScormUserData(scormId, attempt, scos, siteId); + return this.scormOfflineProvider.getScormUserData(scormId, attempt, scos, options.siteId); }); } else { - return this.getScormUserDataOnline(scormId, attempt, ignoreCache, siteId); + return this.getScormUserDataOnline(scormId, attempt, options); } } @@ -823,24 +808,21 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt Attempt number. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the user data is retrieved. */ - getScormUserDataOnline(scormId: number, attempt: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getScormUserDataOnline(scormId: number, attempt: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - scormid: scormId, - attempt: attempt - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getScormUserDataCacheKey(scormId, attempt) - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + scormid: scormId, + attempt: attempt, + }; + const preSets = { + cacheKey: this.getScormUserDataCacheKey(scormId, attempt), + component: AddonModScormProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_scorm_get_scorm_user_data', params, preSets).then((response) => { if (response && response.data) { @@ -876,37 +858,33 @@ export class AddonModScormProvider { * Retrieves the list of SCO objects for a given SCORM and organization. * * @param scormId SCORM ID. - * @param organization Organization. - * @param ignoreCache Whether it should ignore cached data (it will always fail if offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with a list of SCO. */ - getScos(scormId: number, organization?: string, ignoreCache?: boolean, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getScos(scormId: number, options: AddonModScormOrganizationOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { // Don't send the organization to the WS, we'll filter them locally. const params = { - scormid: scormId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getScosCacheKey(scormId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + scormid: scormId, + }; + const preSets = { + cacheKey: this.getScosCacheKey(scormId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModScormProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_scorm_get_scorm_scoes', params, preSets).then((response) => { if (response && response.scoes) { - if (organization) { + if (options.organization) { // Filter SCOs by organization. return response.scoes.filter((sco) => { - return sco.organization == organization; + return sco.organization == options.organization; }); } else { return response.scoes; @@ -924,20 +902,21 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt Attempt number. - * @param organization Organization ID. - * @param offline Whether the attempt is offline. - * @param ignoreCache Whether it should ignore cached data for online attempts. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with a list of SCO objects. */ - getScosWithData(scormId: number, attempt: number, organization?: string, offline?: boolean, ignoreCache?: boolean, - siteId?: string): Promise { + getScosWithData(scormId: number, attempt: number, options: AddonModScormGetScosWithDataOptions = {}): Promise { // Get organization SCOs. - return this.getScos(scormId, organization, ignoreCache, siteId).then((scos) => { + return this.getScos(scormId, options).then((scos) => { // Get the track data for all the SCOs in the organization for the given attempt. // We'll use this data to set SCO data like isvisible, status and so. - return this.getScormUserData(scormId, attempt, scos, offline, ignoreCache, siteId).then((data) => { + const userDataOptions = { + scos, + ...options, // Include all options. + }; + + return this.getScormUserData(scormId, attempt, userDataOptions).then((data) => { const trackDataBySCO = {}; @@ -1134,26 +1113,22 @@ export class AddonModScormProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param moduleUrl Module URL. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the SCORM is retrieved. */ - protected getScormByField(courseId: number, key: string, value: any, moduleUrl?: string, forceCache?: boolean, siteId?: string) + protected getScormByField(courseId: number, key: string, value: any, options: AddonModScormGetScormOptions = {}) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getScormDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (forceCache) { - preSets.omitExpires = true; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getScormDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModScormProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_scorm_get_scorms_by_courses', params, preSets).then((response) => { if (response && response.scorms) { @@ -1173,7 +1148,7 @@ export class AddonModScormProvider { } } - currentScorm.moduleurl = moduleUrl; + currentScorm.moduleurl = options.moduleUrl; return currentScorm; } @@ -1189,13 +1164,11 @@ export class AddonModScormProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param moduleUrl Module URL. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the SCORM is retrieved. */ - getScorm(courseId: number, cmId: number, moduleUrl?: string, forceCache?: boolean, siteId?: string): Promise { - return this.getScormByField(courseId, 'coursemodule', cmId, moduleUrl, forceCache, siteId); + getScorm(courseId: number, cmId: number, options: AddonModScormGetScormOptions = {}): Promise { + return this.getScormByField(courseId, 'coursemodule', cmId, options); } /** @@ -1203,13 +1176,11 @@ export class AddonModScormProvider { * * @param courseId Course ID. * @param id SCORM ID. - * @param moduleUrl Module URL. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the SCORM is retrieved. */ - getScormById(courseId: number, id: number, moduleUrl?: string, forceCache?: boolean, siteId?: string): Promise { - return this.getScormByField(courseId, 'id', id, moduleUrl, forceCache, siteId); + getScormById(courseId: number, id: number, options: AddonModScormGetScormOptions = {}): Promise { + return this.getScormByField(courseId, 'id', id, options); } /** @@ -1314,7 +1285,7 @@ export class AddonModScormProvider { invalidateContent(moduleId: number, courseId: number, siteId?: string, userId?: number): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.getScorm(courseId, moduleId, undefined, false, siteId).then((scorm) => { + return this.getScorm(courseId, moduleId, {siteId}).then((scorm) => { const promises = []; promises.push(this.invalidateAllScormData(scorm.id, siteId, userId)); @@ -1369,15 +1340,12 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param attempt Attempt. - * @param offline Whether the attempt is offline. - * @param ignoreCache Whether it should ignore cached data for online attempts. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with a boolean: true if incomplete, false otherwise. */ - isAttemptIncomplete(scormId: number, attempt: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + isAttemptIncomplete(scormId: number, attempt: number, options: AddonModScormOfflineOptions = {}): Promise { - return this.getScosWithData(scormId, attempt, undefined, offline, ignoreCache, siteId).then((scos) => { + return this.getScosWithData(scormId, attempt, options).then((scos) => { for (const i in scos) { const sco = scos[i]; @@ -1563,8 +1531,8 @@ export class AddonModScormProvider { siteId = siteId || this.sitesProvider.getCurrentSiteId(); if (offline) { - const promise = userData ? Promise.resolve(userData) : this.getScormUserData(scorm.id, attempt, undefined, offline, - false, siteId); + const promise = userData ? Promise.resolve(userData) : + this.getScormUserData(scorm.id, attempt, {offline, cmId: scorm.coursemodule, siteId}); return promise.then((userData) => { return this.scormOfflineProvider.saveTracks(scorm, scoId, attempt, tracks, userData, siteId); @@ -1572,7 +1540,7 @@ export class AddonModScormProvider { } else { return this.saveTracksOnline(scorm.id, scoId, attempt, tracks, siteId).then(() => { // Tracks have been saved, update cached user data. - this.updateUserDataAfterSave(scorm.id, attempt, tracks, siteId); + this.updateUserDataAfterSave(scorm.id, attempt, tracks, {cmId: scorm.coursemodule, siteId}); this.eventsProvider.trigger(AddonModScormProvider.DATA_SENT_EVENT, { scormId: scorm.id, @@ -1641,7 +1609,7 @@ export class AddonModScormProvider { if (success) { // Tracks have been saved, update cached user data. - this.updateUserDataAfterSave(scorm.id, attempt, tracks); + this.updateUserDataAfterSave(scorm.id, attempt, tracks, {cmId: scorm.coursemodule}); this.eventsProvider.trigger(AddonModScormProvider.DATA_SENT_EVENT, { scormId: scorm.id, @@ -1748,10 +1716,11 @@ export class AddonModScormProvider { * @param scormId SCORM ID. * @param attempt Attempt number. * @param tracks Tracking data saved. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when updated. */ - protected updateUserDataAfterSave(scormId: number, attempt: number, tracks: any[], siteId?: string): Promise { + protected updateUserDataAfterSave(scormId: number, attempt: number, tracks: any[], options: {cmId?: number, siteId?: string}) + : Promise { if (!tracks || !tracks.length) { return Promise.resolve(); } @@ -1767,9 +1736,54 @@ export class AddonModScormProvider { } if (needsUpdate) { - return this.getScormUserDataOnline(scormId, attempt, true, siteId); + return this.getScormUserDataOnline(scormId, attempt, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId: options.siteId, + }); } return Promise.resolve(); } } + +/** + * Options to pass to get SCORM. + */ +export type AddonModScormGetScormOptions = CoreSitesCommonWSOptions & { + moduleUrl?: string; // Module URL. +}; + +/** + * Common options with an organization ID. + */ +export type AddonModScormOrganizationOptions = CoreCourseCommonModWSOptions & { + organization?: string; // Organization ID. +}; + +/** + * Common options with offline boolean. + */ +export type AddonModScormOfflineOptions = CoreCourseCommonModWSOptions & { + offline?: boolean; // Whether the attempt is offline. +}; + +/** + * Options to pass to getAttemptCount. + */ +export type AddonModScormGetAttemptCountOptions = CoreCourseCommonModWSOptions & { + ignoreMissing?: boolean; // Whether it should ignore attempts that haven't reported a grade/completion. + userId?: number; // User ID. If not defined use site's current user. +}; + +/** + * Options to pass to getScormUserData. + */ +export type AddonModScormGetUserDataOptions = AddonModScormOfflineOptions & { + scos?: any[]; // SCOs returned by getScos. Recommended if offline=true. +}; + +/** + * Options to pass to getScosWithData. + */ +export type AddonModScormGetScosWithDataOptions = AddonModScormOfflineOptions & AddonModScormOrganizationOptions; diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index b7e71e144..4c375e42b 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -143,7 +143,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo * @return Promise resolved when done. */ protected fetchQuestions(): Promise { - return this.surveyProvider.getQuestions(this.survey.id).then((questions) => { + return this.surveyProvider.getQuestions(this.survey.id, {cmId: this.module.id}).then((questions) => { this.questions = this.surveyHelper.formatQuestions(questions); // Init answers object. diff --git a/src/addon/mod/survey/providers/prefetch-handler.ts b/src/addon/mod/survey/providers/prefetch-handler.ts index 026d00c0e..076da7258 100644 --- a/src/addon/mod/survey/providers/prefetch-handler.ts +++ b/src/addon/mod/survey/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -125,7 +125,10 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan * @return Promise resolved when done. */ protected prefetchSurvey(module: any, courseId: number, single: boolean, siteId: string): Promise { - return this.surveyProvider.getSurvey(courseId, module.id, true, siteId).then((survey) => { + return this.surveyProvider.getSurvey(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((survey) => { const promises = [], files = this.getIntroFilesFromInstance(module, survey); @@ -134,7 +137,11 @@ export class AddonModSurveyPrefetchHandler extends CoreCourseActivityPrefetchHan // If survey isn't answered, prefetch the questions. if (!survey.surveydone) { - promises.push(this.surveyProvider.getQuestions(survey.id, true, siteId)); + promises.push(this.surveyProvider.getQuestions(survey.id, { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + })); } return Promise.all(promises); diff --git a/src/addon/mod/survey/providers/survey.ts b/src/addon/mod/survey/providers/survey.ts index 3063c07e8..9e05db3f4 100644 --- a/src/addon/mod/survey/providers/survey.ts +++ b/src/addon/mod/survey/providers/survey.ts @@ -14,14 +14,15 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModSurveyOfflineProvider } from './offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for surveys. @@ -43,24 +44,21 @@ export class AddonModSurveyProvider { * Get a survey's questions. * * @param surveyId Survey ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the questions are retrieved. */ - getQuestions(surveyId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getQuestions(surveyId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - surveyid: surveyId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getQuestionsCacheKey(surveyId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + surveyid: surveyId, + }; + const preSets = { + cacheKey: this.getQuestionsCacheKey(surveyId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModSurveyProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_survey_get_questions', params, preSets) .then((response: AddonModSurveyGetQuestionsResult): any => { @@ -100,26 +98,22 @@ export class AddonModSurveyProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the survey is retrieved. */ - protected getSurveyDataByKey(courseId: number, key: string, value: any, ignoreCache?: boolean, siteId?: string) + protected getSurveyDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) : Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSurveyCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getSurveyCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModSurveyProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_survey_get_surveys_by_courses', params, preSets) .then((response: AddonModSurveyGetSurveysByCoursesResult): any => { @@ -143,12 +137,11 @@ export class AddonModSurveyProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the survey is retrieved. */ - getSurvey(courseId: number, cmId: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, ignoreCache, siteId); + getSurvey(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getSurveyDataByKey(courseId, 'coursemodule', cmId, options); } /** @@ -156,12 +149,11 @@ export class AddonModSurveyProvider { * * @param courseId Course ID. * @param id Survey ID. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the survey is retrieved. */ - getSurveyById(courseId: number, id: number, ignoreCache?: boolean, siteId?: string): Promise { - return this.getSurveyDataByKey(courseId, 'id', id, ignoreCache, siteId); + getSurveyById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getSurveyDataByKey(courseId, 'id', id, options); } /** diff --git a/src/addon/mod/url/providers/url.ts b/src/addon/mod/url/providers/url.ts index 49f2cbe7f..5019cf89b 100644 --- a/src/addon/mod/url/providers/url.ts +++ b/src/addon/mod/url/providers/url.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -107,18 +107,22 @@ export class AddonModUrlProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the url is retrieved. */ - protected getUrlDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getUrlDataByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}) + : Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getUrlCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getUrlCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModUrlProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_url_get_urls_by_courses', params, preSets) .then((response: AddonModUrlGetUrlsByCoursesResult): any => { @@ -142,11 +146,11 @@ export class AddonModUrlProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the url is retrieved. */ - getUrl(courseId: number, cmId: number, siteId?: string): Promise { - return this.getUrlDataByKey(courseId, 'coursemodule', cmId, siteId); + getUrl(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getUrlDataByKey(courseId, 'coursemodule', cmId, options); } /** diff --git a/src/addon/mod/wiki/components/index/index.ts b/src/addon/mod/wiki/components/index/index.ts index ca94dd980..1a52936bb 100644 --- a/src/addon/mod/wiki/components/index/index.ts +++ b/src/addon/mod/wiki/components/index/index.ts @@ -303,7 +303,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp this.pageIsOffline = false; - return this.wikiProvider.getPageContents(pageId); + return this.wikiProvider.getPageContents(pageId, {cmId: this.module.id}); } /** @@ -314,7 +314,11 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp protected fetchSubwikiPages(subwiki: any): Promise { let subwikiPages; - return this.wikiProvider.getSubwikiPages(subwiki.wikiid, subwiki.groupid, subwiki.userid).then((pages) => { + return this.wikiProvider.getSubwikiPages(subwiki.wikiid, { + groupId: subwiki.groupid, + userId: subwiki.userid, + cmId: this.module.id, + }).then((pages) => { subwikiPages = pages; // If no page specified, search first page. @@ -356,7 +360,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp * @param wikiId Wiki ID. */ protected fetchSubwikis(wikiId: number): Promise { - return this.wikiProvider.getSubwikis(wikiId).then((subwikis) => { + return this.wikiProvider.getSubwikis(wikiId, {cmId: this.module.id}).then((subwikis) => { this.loadedSubwikis = subwikis; return this.wikiOffline.subwikisHaveOfflineData(subwikis).then((hasOffline) => { diff --git a/src/addon/mod/wiki/pages/edit/edit.ts b/src/addon/mod/wiki/pages/edit/edit.ts index b0bf66db0..90210ca12 100644 --- a/src/addon/mod/wiki/pages/edit/edit.ts +++ b/src/addon/mod/wiki/pages/edit/edit.ts @@ -155,7 +155,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { this.editOffline = false; // Cannot edit pages in offline. // Get page contents to obtain title and editing permission - promise = this.wikiProvider.getPageContents(this.pageId).then((pageContents) => { + promise = this.wikiProvider.getPageContents(this.pageId, {cmId: this.module.id}).then((pageContents) => { this.pageForm.controls.title.setValue(pageContents.title); // Set the title in the form group. this.wikiId = pageContents.wikiid; this.subwikiId = pageContents.subwikiid; @@ -168,7 +168,11 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { return this.wikiSync.waitForSync(this.blockId); }).then(() => { // Get subwiki files, needed to replace URLs for rich text editor. - return this.wikiProvider.getSubwikiFiles(this.wikiId, this.groupId, this.userId); + return this.wikiProvider.getSubwikiFiles(this.wikiId, { + groupId: this.groupId, + userId: this.userId, + cmId: this.module.id, + }); }).then((files) => { this.subwikiFiles = files; @@ -460,7 +464,13 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { // Try to send the page. let wikiId = this.wikiId || (this.module && this.module.instance); - return this.wikiProvider.newPage(title, text, this.subwikiId, wikiId, this.userId, this.groupId).then((id) => { + return this.wikiProvider.newPage(title, text, { + subwikiId: this.subwikiId, + wikiId, + userId: this.userId, + groupId: this.groupId, + cmId: this.module.id, + }).then((id) => { this.domUtils.triggerFormSubmittedEvent(this.formElement, id > 0, this.sitesProvider.getCurrentSiteId()); @@ -470,7 +480,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { // Page was created, get its data and go to the page. this.pageId = id; - return this.wikiProvider.getPageContents(this.pageId).then((pageContents) => { + return this.wikiProvider.getPageContents(this.pageId, {cmId: this.module.id}).then((pageContents) => { const promises = []; wikiId = parseInt(pageContents.wikiid, 10); diff --git a/src/addon/mod/wiki/providers/create-link-handler.ts b/src/addon/mod/wiki/providers/create-link-handler.ts index f56b8ee8e..f592b7510 100644 --- a/src/addon/mod/wiki/providers/create-link-handler.ts +++ b/src/addon/mod/wiki/providers/create-link-handler.ts @@ -48,13 +48,15 @@ export class AddonModWikiCreateLinkHandler extends CoreContentLinksHandlerBase { protected currentStateIsSameWiki(activeView: ViewController, subwikiId: number, siteId: string): Promise { if (activeView && activeView.component.name == 'AddonModWikiIndexPage') { + const moduleId = activeView.data.module && activeView.data.module.id; + if (activeView.data.subwikiId == subwikiId) { // Same subwiki, so it's same wiki. return Promise.resolve(true); } else if (activeView.data.pageId) { // Get the page contents to check the subwiki. - return this.wikiProvider.getPageContents(activeView.data.pageId, false, false, siteId).then((page) => { + return this.wikiProvider.getPageContents(activeView.data.pageId, {cmId: moduleId, siteId}).then((page) => { return page.subwikiid == subwikiId; }).catch(() => { // Not found, return false. @@ -63,15 +65,14 @@ export class AddonModWikiCreateLinkHandler extends CoreContentLinksHandlerBase { } else if (activeView.data.wikiId) { // Check if the subwiki belongs to this wiki. - return this.wikiProvider.wikiHasSubwiki(activeView.data.wikiId, subwikiId, false, false, siteId); + return this.wikiProvider.wikiHasSubwiki(activeView.data.wikiId, subwikiId, {cmId: moduleId, siteId}); } else if (activeView.data.courseId && activeView.data.module) { - const moduleId = activeView.data.module && activeView.data.module.id; if (moduleId) { // Get the wiki. - return this.wikiProvider.getWiki(activeView.data.courseId, moduleId, false, siteId).then((wiki) => { + return this.wikiProvider.getWiki(activeView.data.courseId, moduleId, {siteId}).then((wiki) => { // Check if the subwiki belongs to this wiki. - return this.wikiProvider.wikiHasSubwiki(wiki.id, subwikiId, false, false, siteId); + return this.wikiProvider.wikiHasSubwiki(wiki.id, subwikiId, {cmId: moduleId, siteId}); }).catch(() => { // Not found, return false. return false; diff --git a/src/addon/mod/wiki/providers/page-or-map-link-handler.ts b/src/addon/mod/wiki/providers/page-or-map-link-handler.ts index cad304c40..d148e4a46 100644 --- a/src/addon/mod/wiki/providers/page-or-map-link-handler.ts +++ b/src/addon/mod/wiki/providers/page-or-map-link-handler.ts @@ -55,7 +55,7 @@ export class AddonModWikiPageOrMapLinkHandler extends CoreContentLinksHandlerBas action = url.indexOf('mod/wiki/map.php') != -1 ? 'map' : 'page'; // Get the page data to obtain wikiId, subwikiId, etc. - this.wikiProvider.getPageContents(pageId, false, false, siteId).then((page) => { + this.wikiProvider.getPageContents(pageId, {siteId}).then((page) => { let promise; if (courseId) { promise = Promise.resolve(courseId); diff --git a/src/addon/mod/wiki/providers/prefetch-handler.ts b/src/addon/mod/wiki/providers/prefetch-handler.ts index f9677e893..af4c3e15b 100644 --- a/src/addon/mod/wiki/providers/prefetch-handler.ts +++ b/src/addon/mod/wiki/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesReadingStrategy, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -65,18 +65,15 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl * * @param module The module object returned by WS. * @param courseId The course ID. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return List of pages. */ - protected getAllPages(module: any, courseId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + protected getAllPages(module: any, courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.wikiProvider.getWiki(courseId, module.id, offline, siteId).then((wiki) => { - return this.wikiProvider.getWikiPageList(wiki, offline, ignoreCache, siteId); + return this.wikiProvider.getWiki(courseId, module.id, options).then((wiki) => { + return this.wikiProvider.getWikiPageList(wiki, options); }).catch(() => { // Wiki not found, return empty list. return []; @@ -100,7 +97,10 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl return this.pluginFileDelegate.getFilesDownloadSize(files); })); - promises.push(this.getAllPages(module, courseId, false, true, siteId).then((pages) => { + promises.push(this.getAllPages(module, courseId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }).then((pages) => { let size = 0; pages.forEach((page) => { @@ -133,10 +133,10 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.wikiProvider.getWiki(courseId, module.id, false, siteId).then((wiki) => { + return this.wikiProvider.getWiki(courseId, module.id, {siteId}).then((wiki) => { const introFiles = this.getIntroFilesFromInstance(module, wiki); - return this.wikiProvider.getWikiFileList(wiki, false, false, siteId).then((files) => { + return this.wikiProvider.getWikiFileList(wiki, {siteId}).then((files) => { return introFiles.concat(files); }); }).catch(() => { @@ -191,14 +191,23 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl protected prefetchWiki(module: any, courseId: number, single: boolean, siteId: string, downloadTime: number): Promise { const userId = this.sitesProvider.getCurrentSiteUserId(); + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + // Get the list of pages. - return this.getAllPages(module, courseId, false, true, siteId).then((pages) => { + return this.getAllPages(module, courseId, commonOptions).then((pages) => { const promises = []; pages.forEach((page) => { // Fetch page contents if it needs to be fetched. if (page.timemodified > downloadTime) { - promises.push(this.wikiProvider.getPageContents(page.id, false, true, siteId)); + promises.push(this.wikiProvider.getPageContents(page.id, modOptions)); } }); @@ -206,7 +215,7 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl promises.push(this.groupsProvider.getActivityGroupInfo(module.id, false, userId, siteId)); // Fetch info to provide wiki links. - promises.push(this.wikiProvider.getWiki(courseId, module.id, false, siteId).then((wiki) => { + promises.push(this.wikiProvider.getWiki(courseId, module.id, {siteId}).then((wiki) => { return this.courseHelper.getModuleCourseIdByInstance(wiki.id, 'wiki', siteId); })); diff --git a/src/addon/mod/wiki/providers/wiki-sync.ts b/src/addon/mod/wiki/providers/wiki-sync.ts index 174fedcf6..4309c741c 100644 --- a/src/addon/mod/wiki/providers/wiki-sync.ts +++ b/src/addon/mod/wiki/providers/wiki-sync.ts @@ -266,8 +266,13 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { // Send the pages. pages.forEach((page) => { - promises.push(this.wikiProvider.newPageOnline(page.title, page.cachedcontent, subwikiId, wikiId, userId, groupId, - siteId).then((pageId) => { + promises.push(this.wikiProvider.newPageOnline(page.title, page.cachedcontent, { + subwikiId, + wikiId, + userId, + groupId, + siteId, + }).then((pageId) => { result.updated = true; @@ -339,7 +344,7 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { // Ignore errors. }).then(() => { // Sync is done at subwiki level, get all the subwikis. - return this.wikiProvider.getSubwikis(wikiId); + return this.wikiProvider.getSubwikis(wikiId, {cmId}); }).then((subwikis) => { const promises = [], result: AddonModWikiSyncWikiResult = { diff --git a/src/addon/mod/wiki/providers/wiki.ts b/src/addon/mod/wiki/providers/wiki.ts index ee9414c54..76c000570 100644 --- a/src/addon/mod/wiki/providers/wiki.ts +++ b/src/addon/mod/wiki/providers/wiki.ts @@ -16,13 +16,14 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModWikiOfflineProvider } from './wiki-offline'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSite } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; export interface AddonModWikiSubwikiListData { /** @@ -118,27 +119,21 @@ export class AddonModWikiProvider { * Get a wiki page contents. * * @param pageId Page ID. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the page data. */ - getPageContents(pageId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getPageContents(pageId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - pageid: pageId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getPageContentsCacheKey(pageId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + pageid: pageId, + }; + const preSets = { + cacheKey: this.getPageContentsCacheKey(pageId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_page_contents', params, preSets).then((response) => { return response.page || Promise.reject(null); @@ -191,36 +186,27 @@ export class AddonModWikiProvider { * Gets the list of files from a specific subwiki. * * @param wikiId Wiki ID. - * @param groupId Group to get files from. - * @param userId User to get files from. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with subwiki files. */ - getSubwikiFiles(wikiId: number, groupId?: number, userId?: number, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + getSubwikiFiles(wikiId: number, options: AddonModWikiGetSubwikiFilesOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - groupId = groupId || -1; - userId = userId || 0; + return this.sitesProvider.getSite(options.siteId).then((site) => { + const groupId = options.groupId || -1; + const userId = options.userId || 0; const params = { - wikiid: wikiId, - groupid: groupId, - userid: userId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubwikiFilesCacheKey(wikiId, groupId, userId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + wikiid: wikiId, + groupid: groupId, + userid: userId, + }; + const preSets = { + cacheKey: this.getSubwikiFilesCacheKey(wikiId, groupId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_subwiki_files', params, preSets).then((response) => { return response.files || Promise.reject(null); @@ -264,48 +250,34 @@ export class AddonModWikiProvider { * Get the list of Pages of a SubWiki. * * @param wikiId Wiki ID. - * @param groupId Group to get pages from. - * @param userId User to get pages from. - * @param sortBy The attribute to sort the returned list. - * @param sortDirection Direction to sort the returned list (ASC | DESC). - * @param includeContent Whether the pages have to include its content. Default: false. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with wiki subwiki pages. */ - getSubwikiPages(wikiId: number, groupId?: number, userId?: number, sortBy: string = 'title', sortDirection: string = 'ASC', - includeContent?: boolean, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + getSubwikiPages(wikiId: number, options: AddonModWikiGetSubwikiPagesOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - groupId = groupId || -1; - userId = userId || 0; - sortBy = sortBy || 'title'; - sortDirection = sortDirection || 'ASC'; - includeContent = includeContent || false; + return this.sitesProvider.getSite(options.siteId).then((site) => { + const groupId = options.groupId || -1; + const userId = options.userId || 0; + const sortBy = options.sortBy || 'title'; + const sortDirection = options.sortDirection || 'ASC'; const params = { - wikiid: wikiId, - groupid: groupId, - userid: userId, - options: { - sortby: sortBy, - sortdirection: sortDirection, - includecontent: includeContent ? 1 : 0 - } - + wikiid: wikiId, + groupid: groupId, + userid: userId, + options: { + sortby: sortBy, + sortdirection: sortDirection, + includecontent: options.includeContent ? 1 : 0, }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubwikiPagesCacheKey(wikiId, groupId, userId), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + }; + const preSets = { + cacheKey: this.getSubwikiPagesCacheKey(wikiId, groupId, userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_subwiki_pages', params, preSets).then((response) => { return response.pages || Promise.reject(null); @@ -339,27 +311,21 @@ export class AddonModWikiProvider { * Get all the subwikis of a wiki. * * @param wikiId Wiki ID. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with subwikis. */ - getSubwikis(wikiId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getSubwikis(wikiId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - wikiid: wikiId - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getSubwikisCacheKey(wikiId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + wikiid: wikiId, + }; + const preSets = { + cacheKey: this.getSubwikisCacheKey(wikiId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWikiProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_subwikis', params, preSets).then((response) => { return response.subwikis || Promise.reject(null); @@ -382,12 +348,11 @@ export class AddonModWikiProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the wiki is retrieved. */ - getWiki(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { - return this.getWikiByField(courseId, 'coursemodule', cmId, forceCache, siteId); + getWiki(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWikiByField(courseId, 'coursemodule', cmId, options); } /** @@ -396,20 +361,21 @@ export class AddonModWikiProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the wiki is retrieved. */ - protected getWikiByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string): Promise { + protected getWikiByField(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] - }, - preSets = { - cacheKey: this.getWikiDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + courseids: [courseId], + }; + const preSets = { + cacheKey: this.getWikiDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWikiProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; return site.read('mod_wiki_get_wikis_by_courses', params, preSets).then((response) => { if (response.wikis) { @@ -432,12 +398,11 @@ export class AddonModWikiProvider { * * @param courseId Course ID. * @param id Wiki ID. - * @param forceCache Whether it should always return cached data. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the wiki is retrieved. */ - getWikiById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { - return this.getWikiByField(courseId, 'id', id, forceCache, siteId); + getWikiById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWikiByField(courseId, 'id', id, options); } /** @@ -454,22 +419,29 @@ export class AddonModWikiProvider { * Gets a list of files to download for a wiki, using a format similar to module.contents from get_course_contents. * * @param wiki Wiki. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the list of files. */ - getWikiFileList(wiki: any, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getWikiFileList(wiki: any, options: CoreSitesCommonWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); let files = []; + const modOptions = { + cmId: wiki.coursemodule, + ...options, // Include all options. + }; - return this.getSubwikis(wiki.id, offline, ignoreCache, siteId).then((subwikis) => { + return this.getSubwikis(wiki.id, modOptions).then((subwikis) => { const promises = []; subwikis.forEach((subwiki) => { - promises.push(this.getSubwikiFiles(subwiki.wikiid, subwiki.groupid, subwiki.userid, offline, ignoreCache, siteId) - .then((swFiles) => { + const subwikiOptions = { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...modOptions, // Include all options. + }; + + promises.push(this.getSubwikiFiles(subwiki.wikiid, subwikiOptions).then((swFiles) => { files = files.concat(swFiles); })); }); @@ -484,22 +456,27 @@ export class AddonModWikiProvider { * Gets a list of all pages for a Wiki. * * @param wiki Wiki. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Page list. */ - getWikiPageList(wiki: any, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getWikiPageList(wiki: any, options: CoreSitesCommonWSOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); let pages = []; + const modOptions = { + cmId: wiki.coursemodule, + ...options, // Include all options. + }; - return this.getSubwikis(wiki.id, offline, ignoreCache, siteId).then((subwikis) => { + return this.getSubwikis(wiki.id, modOptions).then((subwikis) => { const promises = []; subwikis.forEach((subwiki) => { - promises.push(this.getSubwikiPages(subwiki.wikiid, subwiki.groupid, subwiki.userid, undefined, undefined, - undefined, offline, ignoreCache, siteId).then((subwikiPages) => { + promises.push(this.getSubwikiPages(subwiki.wikiid, { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...modOptions, // Include all options. + }).then((subwikiPages) => { pages = pages.concat(subwikiPages); })); }); @@ -522,7 +499,7 @@ export class AddonModWikiProvider { invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.getWiki(courseId, moduleId, false, siteId).then((wiki) => { + return this.getWiki(courseId, moduleId, {siteId}).then((wiki) => { const promises = []; promises.push(this.invalidateWikiData(courseId, siteId)); @@ -618,16 +595,13 @@ export class AddonModWikiProvider { * @param wikiId Wiki ID. * @param subwikiId Subwiki ID. * @param title Page title. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with true if used, resolved with false if not used or cannot determine. */ - isTitleUsed(wikiId: number, subwikiId: number, title: string, offline?: boolean, ignoreCache?: boolean, siteId?: string) - : Promise { + isTitleUsed(wikiId: number, subwikiId: number, title: string, options: CoreCourseCommonModWSOptions = {}): Promise { // First get the subwiki. - return this.getSubwikis(wikiId, offline, ignoreCache, siteId).then((subwikis) => { + return this.getSubwikis(wikiId, options).then((subwikis) => { // Search the subwiki. const subwiki = subwikis.find((subwiki) => { return subwiki.id == subwikiId; @@ -636,8 +610,11 @@ export class AddonModWikiProvider { return subwiki || Promise.reject(null); }).then((subwiki) => { // Now get all the pages of the subwiki. - return this.getSubwikiPages(wikiId, subwiki.groupid, subwiki.userid, undefined, undefined, false, offline, - ignoreCache, siteId); + return this.getSubwikiPages(wikiId, { + groupId: subwiki.groupid, + userId: subwiki.userid, + ...options, // Include all options. + }); }).then((pages) => { // Check if there's any page with the same title. const page = pages.find((page) => { @@ -690,25 +667,24 @@ export class AddonModWikiProvider { * * @param title Title to create the page. * @param content Content to save on the page. - * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. - * @param wikiId Wiki ID. Optional, will be used to create a new subwiki if subwikiId not supplied. - * @param userId User ID. Optional, will be used to create a new subwiki if subwikiId not supplied. - * @param groupId Group ID. Optional, will be used to create a new subwiki if subwikiId not supplied. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with page ID if page was created in server, -1 if stored in device. */ - newPage(title: string, content: string, subwikiId?: number, wikiId?: number, userId?: number, groupId?: number, - siteId?: string): Promise { + newPage(title: string, content: string, options: AddonModWikiNewPageOptions = {}): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a new page to be synchronized later. const storeOffline = (): Promise => { let promise; - if (wikiId) { + if (options.wikiId) { // We have wiki ID, check if there's already an online page with this title and subwiki. - promise = this.isTitleUsed(wikiId, subwikiId, title, true, false, siteId).catch(() => { + promise = this.isTitleUsed(options.wikiId, options.subwikiId, title, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }).catch(() => { // Error, assume not used. return false; }).then((used) => { @@ -721,7 +697,8 @@ export class AddonModWikiProvider { } return promise.then(() => { - return this.wikiOffline.saveNewPage(title, content, subwikiId, wikiId, userId, groupId, siteId).then(() => { + return this.wikiOffline.saveNewPage(title, content, options.subwikiId, options.wikiId, options.userId, + options.groupId, options.siteId).then(() => { return -1; }); }); @@ -733,9 +710,10 @@ export class AddonModWikiProvider { } // Discard stored content for this page. If it exists it means the user is editing it. - return this.wikiOffline.deleteNewPage(title, subwikiId, wikiId, userId, groupId, siteId).then(() => { + return this.wikiOffline.deleteNewPage(title, options.subwikiId, options.wikiId, options.userId, options.groupId, + options.siteId).then(() => { // Try to create it in online. - return this.newPageOnline(title, content, subwikiId, wikiId, userId, groupId, siteId).catch((error) => { + return this.newPageOnline(title, content, options).catch((error) => { if (this.utils.isWebServiceError(error)) { // The WebService has thrown an error, this means that the page cannot be added. return Promise.reject(error); @@ -752,32 +730,27 @@ export class AddonModWikiProvider { * * @param title Title to create the page. * @param content Content to save on the page. - * @param subwikiId Subwiki ID. If not defined, wikiId, userId and groupId should be defined. - * @param wikiId Wiki ID. Optional, will be used create subwiki if not informed. - * @param userId User ID. Optional, will be used create subwiki if not informed. - * @param groupId Group ID. Optional, will be used create subwiki if not informed. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the page ID if created, rejected otherwise. */ - newPageOnline(title: string, content: string, subwikiId?: number, wikiId?: number, userId?: number, groupId?: number, - siteId?: string): Promise { + newPageOnline(title: string, content: string, options: AddonModWikiNewPageOnlineOptions = {}): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - title: title, - content: content, - contentformat: 'html' - }; + title: title, + content: content, + contentformat: 'html', + }; - subwikiId = this.wikiOffline.convertToPositiveNumber(subwikiId); - wikiId = this.wikiOffline.convertToPositiveNumber(wikiId); + const subwikiId = this.wikiOffline.convertToPositiveNumber(options.subwikiId); + const wikiId = this.wikiOffline.convertToPositiveNumber(options.wikiId); if (subwikiId && subwikiId > 0) { params.subwikiid = subwikiId; } else if (wikiId) { params.wikiid = wikiId; - params.userid = this.wikiOffline.convertToPositiveNumber(userId); - params.groupid = this.wikiOffline.convertToPositiveNumber(groupId); + params.userid = this.wikiOffline.convertToPositiveNumber(options.userId); + params.groupid = this.wikiOffline.convertToPositiveNumber(options.groupId); } return site.write('mod_wiki_new_page', params).then((response) => { @@ -830,14 +803,12 @@ export class AddonModWikiProvider { * * @param wikiId Wiki ID. * @param subwikiId Subwiki ID to search. - * @param offline Whether it should return cached data. Has priority over ignoreCache. - * @param ignoreCache Whether it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with true if it has subwiki, resolved with false otherwise. */ - wikiHasSubwiki(wikiId: number, subwikiId: number, offline?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + wikiHasSubwiki(wikiId: number, subwikiId: number, options: CoreCourseCommonModWSOptions = {}): Promise { // Get the subwikis to check if any of them matches the one passed as param. - return this.getSubwikis(wikiId, offline, ignoreCache, siteId).then((subwikis) => { + return this.getSubwikis(wikiId, options).then((subwikis) => { const subwiki = subwikis.find((subwiki) => { return subwiki.id == subwikiId; }); @@ -849,3 +820,40 @@ export class AddonModWikiProvider { }); } } + +/** + * Options to pass to getSubwikiFiles. + */ +export type AddonModWikiGetSubwikiFilesOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User to get files from. + groupId?: number; // Group to get files from. +}; + +/** + * Options to pass to getSubwikiPages. + */ +export type AddonModWikiGetSubwikiPagesOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User to get pages from. + groupId?: number; // Group to get pages from. + sortBy?: string; // The attribute to sort the returned list. Defaults to 'title'. + sortDirection?: string; // Direction to sort the returned list (ASC | DESC). Defaults to 'ASC'. + includeContent?: boolean; // Whether the pages have to include their content. +}; + +/** + * Options to pass to newPageOnline. + */ +export type AddonModWikiNewPageOnlineOptions = { + subwikiId?: number; // Subwiki ID. If not defined, wikiId, userId and groupId should be defined. + wikiId?: number; // Wiki ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + userId?: number; // User ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + groupId?: number; // Group ID. Optional, will be used to create a new subwiki if subwikiId not supplied. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to newPage. + */ +export type AddonModWikiNewPageOptions = AddonModWikiNewPageOnlineOptions & { + cmId?: number; // Module ID. +}; diff --git a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts index 0eda6bceb..7418e92b5 100644 --- a/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts +++ b/src/addon/mod/workshop/components/assessment-strategy/assessment-strategy.ts @@ -147,8 +147,10 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit { * @return Promised resvoled when data is loaded. */ protected load(): Promise { - return this.workshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, this.userId) - .then((assessmentData) => { + return this.workshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, { + userId: this.userId, + cmId: this.workshop.coursemodule, + }).then((assessmentData) => { this.data.assessment = assessmentData; let promise; diff --git a/src/addon/mod/workshop/components/assessment/assessment.ts b/src/addon/mod/workshop/components/assessment/assessment.ts index a565bf5a6..c80eed725 100644 --- a/src/addon/mod/workshop/components/assessment/assessment.ts +++ b/src/addon/mod/workshop/components/assessment/assessment.ts @@ -112,8 +112,8 @@ export class AddonModWorkshopAssessmentComponent implements OnInit { if (!this.submission) { const modal = this.domUtils.showModalLoading('core.sending', true); - this.workshopHelper.getSubmissionById(this.workshop.id, this.assessment.submissionid) - .then((submissionData) => { + this.workshopHelper.getSubmissionById(this.workshop.id, this.assessment.submissionid, + {cmId: this.workshop.coursemodule}).then((submissionData) => { params.submission = submissionData; this.navCtrl.push('AddonModWorkshopAssessmentPage', params); diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts index d14b2b04e..d2e7b7903 100644 --- a/src/addon/mod/workshop/components/index/index.ts +++ b/src/addon/mod/workshop/components/index/index.ts @@ -198,7 +198,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity } }).then(() => { // Check if there are answers stored in offline. - return this.workshopProvider.getWorkshopAccessInformation(this.workshop.id); + return this.workshopProvider.getWorkshopAccessInformation(this.workshop.id, {cmId: this.module.id}); }).then((accessData) => { this.access = accessData; @@ -209,7 +209,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity }); } }).then(() => { - return this.workshopProvider.getUserPlanPhases(this.workshop.id); + return this.workshopProvider.getUserPlanPhases(this.workshop.id, {cmId: this.module.id}); }).then((phases) => { this.phases = phases; @@ -245,7 +245,11 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity * @return Resolved when done. */ gotoSubmissionsPage(page: number): Promise { - return this.workshopProvider.getGradesReport(this.workshop.id, this.group, page).then((report) => { + return this.workshopProvider.getGradesReport(this.workshop.id, { + groupId: this.group, + page, + cmId: this.module.id, + }).then((report) => { const numEntries = (report && report.grades && report.grades.length) || 0; this.page = page; @@ -348,7 +352,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity const promises = []; if (this.canSubmit) { - promises.push(this.workshopHelper.getUserSubmission(this.workshop.id).then((submission) => { + promises.push(this.workshopHelper.getUserSubmission(this.workshop.id, {cmId: this.module.id}).then((submission) => { const actions = this.workshopHelper.filterSubmissionActions(this.offlineSubmissions, submission.id || false); return this.workshopHelper.applyOfflineData(submission, actions).then((submission) => { @@ -366,7 +370,9 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity if (this.workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT) { this.canAssess = this.workshopHelper.canAssess(this.workshop, this.access); if (this.canAssess) { - assessPromise = this.workshopHelper.getReviewerAssessments(this.workshop.id).then((assessments) => { + assessPromise = this.workshopHelper.getReviewerAssessments(this.workshop.id, { + cmId: this.module.id, + }).then((assessments) => { const p2 = []; assessments.forEach((assessment) => { @@ -391,13 +397,13 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity } if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { - promises.push(this.workshopProvider.getGrades(this.workshop.id).then((grades) => { + promises.push(this.workshopProvider.getGrades(this.workshop.id, {cmId: this.module.id}).then((grades) => { this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : false; })); if (this.access.canviewpublishedsubmissions) { promises.push(assessPromise.then(() => { - return this.workshopProvider.getSubmissions(this.workshop.id).then((submissions) => { + return this.workshopProvider.getSubmissions(this.workshop.id, {cmId: this.module.id}).then((submissions) => { this.publishedSubmissions = submissions.filter((submission) => { if (submission.published) { this.assessments.forEach((assessment) => { diff --git a/src/addon/mod/workshop/pages/assessment/assessment.ts b/src/addon/mod/workshop/pages/assessment/assessment.ts index 5cc584349..337bd1a63 100644 --- a/src/addon/mod/workshop/pages/assessment/assessment.ts +++ b/src/addon/mod/workshop/pages/assessment/assessment.ts @@ -150,7 +150,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { }).then((gradeInfo) => { this.maxGrade = gradeInfo.grade; - return this.workshopProvider.getWorkshopAccessInformation(this.workshopId); + return this.workshopProvider.getWorkshopAccessInformation(this.workshopId, {cmId: this.workshop.coursemodule}); }).then((accessData) => { this.access = accessData; @@ -168,8 +168,10 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy { if (this.evaluating || this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { // Get all info of the assessment. - return this.workshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, - this.profile && this.profile.id).then((assessment) => { + return this.workshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, { + userId: this.profile && this.profile.id, + cmId: this.workshop.coursemodule, + }).then((assessment) => { let defaultGrade, promise; this.assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts index 6d4f6d704..ea8e03f45 100644 --- a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts @@ -149,7 +149,8 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { if (this.submissionId > 0) { this.editing = true; - return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId).then((submissionData) => { + return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId, {cmId: this.module.id}) + .then((submissionData) => { this.submission = submissionData; const canEdit = (this.userId == submissionData.authorid && this.access.cansubmit && diff --git a/src/addon/mod/workshop/pages/submission/submission.ts b/src/addon/mod/workshop/pages/submission/submission.ts index 245d320ac..f1d0ec97b 100644 --- a/src/addon/mod/workshop/pages/submission/submission.ts +++ b/src/addon/mod/workshop/pages/submission/submission.ts @@ -190,7 +190,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { * @return Resolved when done. */ protected fetchSubmissionData(): Promise { - return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId).then((submissionData) => { + return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId, { + cmId: this.module.id, + }).then((submissionData) => { const promises = []; this.submission = submissionData; @@ -207,8 +209,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { if (this.access.canviewallassessments) { // Get new data, different that came from stateParams. - promises.push(this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId) - .then((subAssessments) => { + promises.push(this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId, { + cmId: this.module.id, + }).then((subAssessments) => { // Only allow the student to delete their own submission if it's still editable and hasn't been assessed. if (this.canDelete) { this.canDelete = !subAssessments.length; @@ -228,7 +231,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { })); } else if (this.currentUserId == this.userId && this.assessmentId) { // Get new data, different that came from stateParams. - promises.push(this.workshopProvider.getAssessment(this.workshopId, this.assessmentId).then((assessment) => { + promises.push(this.workshopProvider.getAssessment(this.workshopId, this.assessmentId, { + cmId: this.module.id, + }).then((assessment) => { // Only allow the student to delete their own submission if it's still editable and hasn't been assessed. if (this.canDelete) { this.canDelete = !assessment; @@ -239,7 +244,9 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { this.submissionInfo.reviewedby = [assessment]; })); } else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && this.userId == this.currentUserId) { - this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId).then((assessments) => { + this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId, { + cmId: this.module.id, + }).then((assessments) => { this.submissionInfo.reviewedby = assessments.map((assessment) => { return this.parseAssessment(assessment); }); diff --git a/src/addon/mod/workshop/providers/helper.ts b/src/addon/mod/workshop/providers/helper.ts index ad82e99e9..7adf38209 100644 --- a/src/addon/mod/workshop/providers/helper.ts +++ b/src/addon/mod/workshop/providers/helper.ts @@ -19,7 +19,7 @@ import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploa import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { AddonModWorkshopProvider } from './workshop'; +import { AddonModWorkshopProvider, AddonModWorkshopUserOptions } from './workshop'; import { AddonModWorkshopOfflineProvider } from './offline'; import { AddonWorkshopAssessmentStrategyDelegate } from './assessment-strategy-delegate'; @@ -109,12 +109,13 @@ export class AddonModWorkshopHelperProvider { * Return a particular user submission from the submission list. * * @param workshopId Workshop ID. - * @param userId User ID. If not defined current user Id. + * @param options Other options. * @return Resolved with the submission, resolved with false if not found. */ - getUserSubmission(workshopId: number, userId: number = 0): Promise { - return this.workshopProvider.getSubmissions(workshopId).then((submissions) => { - userId = userId || this.sitesProvider.getCurrentSiteUserId(); + getUserSubmission(workshopId: number, options: AddonModWorkshopUserOptions = {}): Promise { + const userId = options.userId || this.sitesProvider.getCurrentSiteUserId(); + + return this.workshopProvider.getSubmissions(workshopId, options).then((submissions) => { for (const x in submissions) { if (submissions[x].authorid == userId) { @@ -131,13 +132,12 @@ export class AddonModWorkshopHelperProvider { * * @param workshopId Workshop ID. * @param submissionId Submission ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the submission, resolved with false if not found. */ - getSubmissionById(workshopId: number, submissionId: number, siteId?: string): Promise { - return this.workshopProvider.getSubmission(workshopId, submissionId, siteId).catch(() => { - return this.workshopProvider.getSubmissions(workshopId, undefined, undefined, undefined, undefined, siteId) - .then((submissions) => { + getSubmissionById(workshopId: number, submissionId: number, options: {cmId?: number, siteId?: string} = {}): Promise { + return this.workshopProvider.getSubmission(workshopId, submissionId, options).catch(() => { + return this.workshopProvider.getSubmissions(workshopId, options).then((submissions) => { for (const x in submissions) { if (submissions[x].id == submissionId) { return submissions[x]; @@ -154,14 +154,12 @@ export class AddonModWorkshopHelperProvider { * * @param workshopId Workshop ID. * @param assessmentId Assessment ID. - * @param userId User ID. If not defined, current user. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Resolved with the assessment. */ - getReviewerAssessmentById(workshopId: number, assessmentId: number, userId: number = 0, siteId?: string): Promise { - return this.workshopProvider.getAssessment(workshopId, assessmentId, siteId).catch((error) => { - return this.workshopProvider.getReviewerAssessments(workshopId, userId, undefined, undefined, siteId) - .then((assessments) => { + getReviewerAssessmentById(workshopId: number, assessmentId: number, options: AddonModWorkshopUserOptions = {}): Promise { + return this.workshopProvider.getAssessment(workshopId, assessmentId, options).catch((error) => { + return this.workshopProvider.getReviewerAssessments(workshopId, options).then((assessments) => { for (const x in assessments) { if (assessments[x].id == assessmentId) { return assessments[x]; @@ -172,8 +170,7 @@ export class AddonModWorkshopHelperProvider { return Promise.reject(error); }); }).then((assessment) => { - return this.workshopProvider.getAssessmentForm(workshopId, assessmentId, undefined, undefined, undefined, siteId) - .then((assessmentForm) => { + return this.workshopProvider.getAssessmentForm(workshopId, assessmentId, options).then((assessmentForm) => { assessment.form = assessmentForm; return assessment; @@ -185,20 +182,15 @@ export class AddonModWorkshopHelperProvider { * Retrieves the assessment of the given user and all the related data. * * @param workshopId Workshop ID. - * @param userId User ID. If not defined, current user. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getReviewerAssessments(workshopId: number, userId: number = 0, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); + getReviewerAssessments(workshopId: number, options: AddonModWorkshopUserOptions = {}): Promise { + options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); - return this.workshopProvider.getReviewerAssessments(workshopId, userId, offline, ignoreCache, siteId) - .then((assessments) => { + return this.workshopProvider.getReviewerAssessments(workshopId, options).then((assessments) => { const promises = assessments.map((assessment) => { - return this.getSubmissionById(workshopId, assessment.submissionid, siteId).then((submission) => { + return this.getSubmissionById(workshopId, assessment.submissionid, options).then((submission) => { assessment.submission = submission; }); }); diff --git a/src/addon/mod/workshop/providers/prefetch-handler.ts b/src/addon/mod/workshop/providers/prefetch-handler.ts index 1213804c6..9f69bb7d0 100644 --- a/src/addon/mod/workshop/providers/prefetch-handler.ts +++ b/src/addon/mod/workshop/providers/prefetch-handler.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -68,7 +68,7 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * @return Promise resolved with the list of files. */ getFiles(module: any, courseId: number, single?: boolean): Promise { - return this.getWorkshopInfoHelper(module, courseId, true).then((info) => { + return this.getWorkshopInfoHelper(module, courseId, {omitFail: true}).then((info) => { return info.files; }); } @@ -78,31 +78,32 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * * @param module Module to get the files. * @param courseId Course ID the module belongs to. - * @param omitFail True to always return even if fails. Default false. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved with the info fetched. */ - protected getWorkshopInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - let workshop, - groups = [], - files = [], - access; + protected getWorkshopInfoHelper(module: any, courseId: number, options: AddonModWorkshopGetInfoOptions = {}): Promise { + let workshop; + let groups = []; + let files = []; + let access; + const modOptions = { + cmId: module.id, + ...options, // Include all options. + }; - return this.sitesProvider.getSite(siteId).then((site) => { + return this.sitesProvider.getSite(options.siteId).then((site) => { const userId = site.getUserId(); - return this.workshopProvider.getWorkshop(courseId, module.id, siteId, forceCache).then((data) => { + return this.workshopProvider.getWorkshop(courseId, module.id, options).then((data) => { files = this.getIntroFilesFromInstance(module, data); files = files.concat(data.instructauthorsfiles).concat(data.instructreviewersfiles); workshop = data; - return this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId).then((accessData) => { + return this.workshopProvider.getWorkshopAccessInformation(workshop.id, modOptions).then((accessData) => { access = accessData; if (access.canviewallsubmissions) { - return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => { + return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, options.siteId) + .then((groupInfo) => { if (!groupInfo.groups || groupInfo.groups.length == 0) { groupInfo.groups = [{id: 0}]; } @@ -111,7 +112,7 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH } }); }).then(() => { - return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => { + return this.workshopProvider.getUserPlanPhases(workshop.id, modOptions).then((phases) => { // Get submission phase info. const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION], canSubmit = this.workshopHelper.canSubmit(workshop, access, submissionPhase.tasks), @@ -119,7 +120,10 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH promises = []; if (canSubmit) { - promises.push(this.workshopHelper.getUserSubmission(workshop.id, userId).then((submission) => { + promises.push(this.workshopHelper.getUserSubmission(workshop.id, { + userId, + cmId: module.id, + }).then((submission) => { if (submission) { files = files.concat(submission.contentfiles).concat(submission.attachmentfiles); } @@ -127,12 +131,13 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH } if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { - promises.push(this.workshopProvider.getSubmissions(workshop.id).then((submissions) => { + promises.push(this.workshopProvider.getSubmissions(workshop.id, modOptions).then((submissions) => { const promises2 = []; submissions.forEach((submission) => { files = files.concat(submission.contentfiles).concat(submission.attachmentfiles); - promises2.push(this.workshopProvider.getSubmissionAssessments(workshop.id, submission.id) - .then((assessments) => { + promises2.push(this.workshopProvider.getSubmissionAssessments(workshop.id, submission.id, { + cmId: module.id, + }).then((assessments) => { assessments.forEach((assessment) => { files = files.concat(assessment.feedbackattachmentfiles) .concat(assessment.feedbackcontentfiles); @@ -146,7 +151,7 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH // Get assessment files. if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { - promises.push(this.workshopHelper.getReviewerAssessments(workshop.id).then((assessments) => { + promises.push(this.workshopHelper.getReviewerAssessments(workshop.id, modOptions).then((assessments) => { assessments.forEach((assessment) => { files = files.concat(assessment.feedbackattachmentfiles).concat(assessment.feedbackcontentfiles); }); @@ -163,7 +168,7 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH files: files.filter((file) => typeof file !== 'undefined') }; }).catch((message): any => { - if (omitFail) { + if (options.omitFail) { // Any error, return the info we have. return { workshop: workshop, @@ -195,8 +200,10 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * @return Whether the module can be downloaded. The promise should never be rejected. */ isDownloadable(module: any, courseId: number): boolean | Promise { - return this.workshopProvider.getWorkshop(courseId, module.id, undefined, true).then((workshop) => { - return this.workshopProvider.getWorkshopAccessInformation(workshop.id).then((accessData) => { + return this.workshopProvider.getWorkshop(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }).then((workshop) => { + return this.workshopProvider.getWorkshopAccessInformation(workshop.id, {cmId: module.id}).then((accessData) => { // Check if workshop is setup by phase. return accessData.canswitchphase || workshop.phase > AddonModWorkshopProvider.PHASE_SETUP; }); @@ -230,15 +237,15 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * * @param workshopId Workshop ID. * @param groups Array of groups in the activity. + * @param cmId Module ID. * @param siteId Site ID. If not defined, current site. * @return All unique entries. */ - protected getAllGradesReport(workshopId: number, groups: any[], siteId: string): Promise { + protected getAllGradesReport(workshopId: number, groups: any[], cmId: number, siteId: string): Promise { const promises = []; groups.forEach((group) => { - promises.push(this.workshopProvider.fetchAllGradeReports( - workshopId, group.id, undefined, false, false, siteId)); + promises.push(this.workshopProvider.fetchAllGradeReports(workshopId, {groupId: group.id, cmId, siteId})); }); return Promise.all(promises).then((grades) => { @@ -266,23 +273,31 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH * @return Promise resolved when done. */ protected prefetchWorkshop(module: any, courseId: number, single: boolean, siteId: string): Promise { - const userIds = []; siteId = siteId || this.sitesProvider.getCurrentSiteId(); + const userIds = []; + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + return this.sitesProvider.getSite(siteId).then((site) => { const currentUserId = site.getUserId(); // Prefetch the workshop data. - return this.getWorkshopInfoHelper(module, courseId, false, false, true, siteId).then((info) => { + return this.getWorkshopInfoHelper(module, courseId, commonOptions).then((info) => { const workshop = info.workshop, promises = [], assessments = []; promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id)); - promises.push(this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId) - .then((access) => { - return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => { + promises.push(this.workshopProvider.getWorkshopAccessInformation(workshop.id, modOptions).then((access) => { + return this.workshopProvider.getUserPlanPhases(workshop.id, modOptions).then((phases) => { // Get submission phase info. const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION], @@ -291,14 +306,14 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH promises2 = []; if (canSubmit) { - promises2.push(this.workshopProvider.getSubmissions(workshop.id)); + promises2.push(this.workshopProvider.getSubmissions(workshop.id, modOptions)); // Add userId to the profiles to prefetch. userIds.push(currentUserId); } let reportPromise = Promise.resolve(); if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) { - reportPromise = this.getAllGradesReport(workshop.id, info.groups, siteId) + reportPromise = this.getAllGradesReport(workshop.id, info.groups, module.id, siteId) .then((grades) => { grades.forEach((grade) => { userIds.push(grade.userid); @@ -322,15 +337,19 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { // Wait the report promise to finish to override assessments array if needed. reportPromise = reportPromise.finally(() => { - return this.workshopHelper.getReviewerAssessments(workshop.id, currentUserId, undefined, - undefined, siteId).then((revAssessments) => { + return this.workshopHelper.getReviewerAssessments(workshop.id, { + userId: currentUserId, + cmId: module.id, + siteId, + }).then((revAssessments) => { const promises = []; let files = []; // Files in each submission. revAssessments.forEach((assessment) => { if (assessment.submission.authorid == currentUserId) { - promises.push(this.workshopProvider.getAssessment(workshop.id, assessment.id)); + promises.push(this.workshopProvider.getAssessment(workshop.id, assessment.id, + modOptions)); } userIds.push(assessment.reviewerid); userIds.push(assessment.gradinggradeoverby); @@ -350,17 +369,16 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH reportPromise = reportPromise.finally(() => { if (assessments.length > 0) { return Promise.all(assessments.map((assessment, id) => { - return this.workshopProvider.getAssessmentForm(workshop.id, id, undefined, undefined, undefined, - siteId); + return this.workshopProvider.getAssessmentForm(workshop.id, id, modOptions); })); } }); promises2.push(reportPromise); if (workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { - promises2.push(this.workshopProvider.getGrades(workshop.id)); + promises2.push(this.workshopProvider.getGrades(workshop.id, modOptions)); if (access.canviewpublishedsubmissions) { - promises2.push(this.workshopProvider.getSubmissions(workshop.id)); + promises2.push(this.workshopProvider.getSubmissions(workshop.id, modOptions)); } } @@ -391,3 +409,10 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH return this.syncProvider.syncWorkshop(module.instance, siteId); } } + +/** + * Options to pass to getWorkshopInfoHelper. + */ +export type AddonModWorkshopGetInfoOptions = CoreSitesCommonWSOptions & { + omitFail?: boolean; // True to always return even if fails. +}; diff --git a/src/addon/mod/workshop/providers/sync.ts b/src/addon/mod/workshop/providers/sync.ts index a7c87e2b4..905f21675 100644 --- a/src/addon/mod/workshop/providers/sync.ts +++ b/src/addon/mod/workshop/providers/sync.ts @@ -204,7 +204,7 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { return Promise.reject(null); } - return this.workshopProvider.getWorkshopById(courseId, workshopId, siteId).then((workshop) => { + return this.workshopProvider.getWorkshopById(courseId, workshopId, {siteId}).then((workshop) => { const submissionsActions = syncs[0], assessments = syncs[1], submissionEvaluations = syncs[2], @@ -289,7 +289,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { if (submissionId > 0) { editing = true; - timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => { + timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, { + cmId: workshop.coursemodule, + siteId, + }).then((submission) => { return submission.timemodified; }).catch(() => { return -1; @@ -403,7 +406,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { let discardError; const assessmentId = assessmentData.assessmentid; - const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => { + const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, { + cmId: workshop.coursemodule, + siteId, + }).then((assessment) => { return assessment.timemodified; }).catch(() => { return -1; @@ -481,7 +487,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { let discardError; const submissionId = evaluate.submissionid; - const timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => { + const timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, { + cmId: workshop.coursemodule, + siteId, + }).then((submission) => { return submission.timemodified; }).catch(() => { return -1; @@ -540,7 +549,10 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { let discardError; const assessmentId = evaluate.assessmentid; - const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => { + const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, { + cmId: workshop.coursemodule, + siteId, + }).then((assessment) => { return assessment.timemodified; }).catch(() => { return -1; diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index ec8fc292e..cf373a796 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -15,11 +15,12 @@ import { Injectable } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; +import { CoreSitesProvider, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModWorkshopOfflineProvider } from './offline'; import { CoreSite } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; /** * Service that provides some features for workshops. @@ -202,25 +203,21 @@ export class AddonModWorkshopProvider { * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the workshop is retrieved. */ - protected getWorkshopByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false): - Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + protected getWorkshopByKey(courseId: number, key: string, value: any, options: CoreSitesCommonWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - courseids: [courseId] + courseids: [courseId], }; - const preSets: any = { + const preSets = { cacheKey: this.getWorkshopDataCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWorkshopProvider.COMPONENT, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (forceCache) { - preSets.omitExpires = true; - } - return site.read('mod_workshop_get_workshops_by_courses', params, preSets).then((response) => { if (response && response.workshops) { const workshopFound = response.workshops.find((workshop) => workshop[key] == value); @@ -252,12 +249,11 @@ export class AddonModWorkshopProvider { * * @param courseId Course ID. * @param cmId Course module ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the workshop is retrieved. */ - getWorkshop(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getWorkshopByKey(courseId, 'coursemodule', cmId, siteId, forceCache); + getWorkshop(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWorkshopByKey(courseId, 'coursemodule', cmId, options); } /** @@ -265,12 +261,11 @@ export class AddonModWorkshopProvider { * * @param courseId Course ID. * @param id Workshop ID. - * @param siteId Site ID. If not defined, current site. - * @param forceCache True to always get the value from cache, false otherwise. Default false. + * @param options Other options. * @return Promise resolved when the workshop is retrieved. */ - getWorkshopById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise { - return this.getWorkshopByKey(courseId, 'id', id, siteId, forceCache); + getWorkshopById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getWorkshopByKey(courseId, 'id', id, options); } /** @@ -303,28 +298,21 @@ export class AddonModWorkshopProvider { * Get access information for a given workshop. * * @param workshopId Workshop ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop is retrieved. */ - getWorkshopAccessInformation(workshopId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): - Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getWorkshopAccessInformation(workshopId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - workshopid: workshopId + workshopid: workshopId, }; - const preSets: any = { - cacheKey: this.getWorkshopAccessInformationDataCacheKey(workshopId) + const preSets = { + cacheKey: this.getWorkshopAccessInformationDataCacheKey(workshopId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_workshop_access_information', params, preSets); }); } @@ -346,28 +334,22 @@ export class AddonModWorkshopProvider { * Return the planner information for the given user. * * @param workshopId Workshop ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getUserPlanPhases(workshopId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getUserPlanPhases(workshopId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { workshopid: workshopId }; - const preSets: any = { + const preSets = { cacheKey: this.getUserPlanDataCacheKey(workshopId), - updateFrequency: CoreSite.FREQUENCY_OFTEN + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_user_plan', params, preSets).then((response) => { if (response && response.userplan && response.userplan.phases) { return this.utils.arrayToObject(response.userplan.phases, 'code'); @@ -395,33 +377,27 @@ export class AddonModWorkshopProvider { * Retrieves all the workshop submissions visible by the current user or the one done by the given user. * * @param workshopId Workshop ID. - * @param userId User ID, 0 means the current user. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop submissions are retrieved. */ - getSubmissions(workshopId: number, userId: number = 0, groupId: number = 0, offline: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getSubmissions(workshopId: number, options: AddonModWorkshopGetSubmissionsOptions = {}): Promise { + const userId = options.userId || 0; + const groupId = options.groupId || 0; + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { workshopid: workshopId, userid: userId, - groupid: groupId + groupid: groupId, }; - const preSets: any = { + const preSets = { cacheKey: this.getSubmissionsDataCacheKey(workshopId, userId, groupId), - updateFrequency: CoreSite.FREQUENCY_OFTEN + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_submissions', params, preSets).then((response) => { if (response && response.submissions) { return response.submissions; @@ -452,16 +428,19 @@ export class AddonModWorkshopProvider { * * @param workshopId Workshop ID. * @param submissionId Submission ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop submission data is retrieved. */ - getSubmission(workshopId: number, submissionId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getSubmission(workshopId: number, submissionId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - submissionid: submissionId + submissionid: submissionId, }; const preSets = { - cacheKey: this.getSubmissionDataCacheKey(workshopId, submissionId) + cacheKey: this.getSubmissionDataCacheKey(workshopId, submissionId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_workshop_get_submission', params, preSets).then((response) => { @@ -492,16 +471,19 @@ export class AddonModWorkshopProvider { * Returns the grades information for the given workshop and user. * * @param workshopId Workshop ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop grades data is retrieved. */ - getGrades(workshopId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getGrades(workshopId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { workshopid: workshopId }; const preSets = { - cacheKey: this.getGradesDataCacheKey(workshopId) + cacheKey: this.getGradesDataCacheKey(workshopId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_workshop_get_grades', params, preSets); @@ -525,35 +507,26 @@ export class AddonModWorkshopProvider { * Retrieves the assessment grades report. * * @param workshopId Workshop ID. - * @param groupId Group id, 0 means that the function will determine the user group. - * @param page Page of records to return. Default 0. - * @param perPage Records per page to return. Default AddonModWorkshopProvider.PER_PAGE. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getGradesReport(workshopId: number, groupId: number = 0, page: number = 0, perPage: number = 0, offline: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getGradesReport(workshopId: number, options: AddonModWorkshopGetGradesReportOptions = {}): Promise { + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { workshopid: workshopId, - groupid: groupId, - page: page, - perpage: perPage || AddonModWorkshopProvider.PER_PAGE + groupid: options.groupId, + page: options.page || 0, + perpage: options.perPage || AddonModWorkshopProvider.PER_PAGE }; - const preSets: any = { - cacheKey: this.getGradesReportDataCacheKey(workshopId, groupId), - updateFrequency: CoreSite.FREQUENCY_OFTEN + const preSets = { + cacheKey: this.getGradesReportDataCacheKey(workshopId, options.groupId), + updateFrequency: CoreSite.FREQUENCY_OFTEN, + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_grades_report', params, preSets).then((response) => { if (response && response.report) { return response.report; @@ -568,44 +541,37 @@ export class AddonModWorkshopProvider { * Performs the whole fetch of the grade reports in the workshop. * * @param workshopId Workshop ID. - * @param groupId Group ID. - * @param perPage Records per page to fetch. It has to match with the prefetch. - * Default on AddonModWorkshopProvider.PER_PAGE. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when done. */ - fetchAllGradeReports(workshopId: number, groupId: number = 0, perPage: number = 0, forceCache: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - perPage = perPage || AddonModWorkshopProvider.PER_PAGE; - - return this.fetchGradeReportsRecursive(workshopId, groupId, perPage, forceCache, ignoreCache, [], 0, siteId); + fetchAllGradeReports(workshopId: number, options: AddonModWorkshopFetchAllGradesReportOptions = {}): Promise { + return this.fetchGradeReportsRecursive(workshopId, [], { + ...options, // Include all options. + page: 0, + perPage: options.perPage || AddonModWorkshopProvider.PER_PAGE, + siteId: options.siteId || this.sitesProvider.getCurrentSiteId(), + }); } /** * Recursive call on fetch all grade reports. * * @param workshopId Workshop ID. - * @param groupId Group ID. - * @param perPage Records per page to fetch. It has to match with the prefetch. - * @param forceCache True to always get the value from cache, false otherwise. Default false. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @param grades Grades already fetched (just to concatenate them). - * @param page Page of records to return. - * @param siteId Site ID. + * @param options Other options. * @return Promise resolved when done. */ - protected fetchGradeReportsRecursive(workshopId: number, groupId: number, perPage: number, forceCache: boolean, - ignoreCache: boolean, grades: any[], page: number, siteId: string): Promise { - return this.getGradesReport(workshopId, groupId, page, perPage, forceCache, ignoreCache, siteId).then((report) => { + protected fetchGradeReportsRecursive(workshopId: number, grades: any[], options: AddonModWorkshopGetGradesReportOptions = {}) + : Promise { + + return this.getGradesReport(workshopId, options).then((report) => { Array.prototype.push.apply(grades, report.grades); - const canLoadMore = ((page + 1) * perPage) < report.totalcount; + const canLoadMore = ((options.page + 1) * options.perPage) < report.totalcount; if (canLoadMore) { - return this.fetchGradeReportsRecursive( - workshopId, groupId, perPage, forceCache, ignoreCache, grades, page + 1, siteId); + options.page++; + + return this.fetchGradeReportsRecursive(workshopId, grades, options); } return grades; @@ -631,28 +597,21 @@ export class AddonModWorkshopProvider { * * @param workshopId Workshop ID. * @param submissionId Submission ID. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getSubmissionAssessments(workshopId: number, submissionId: number, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getSubmissionAssessments(workshopId: number, submissionId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - submissionid: submissionId + submissionid: submissionId, }; - const preSets: any = { - cacheKey: this.getSubmissionAssessmentsDataCacheKey(workshopId, submissionId) + const preSets = { + cacheKey: this.getSubmissionAssessmentsDataCacheKey(workshopId, submissionId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_submission_assessments', params, preSets).then((response) => { if (response && response.assessments) { return response.assessments; @@ -898,31 +857,23 @@ export class AddonModWorkshopProvider { * Retrieves all the assessments reviewed by the given user. * * @param workshopId Workshop ID. - * @param userId User ID. If not defined, current user. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getReviewerAssessments(workshopId: number, userId?: number, offline: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getReviewerAssessments(workshopId: number, options: AddonModWorkshopUserOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params: any = { - workshopid: workshopId + workshopid: workshopId, }; - const preSets: any = { - cacheKey: this.getReviewerAssessmentsDataCacheKey(workshopId, userId) + const preSets = { + cacheKey: this.getReviewerAssessmentsDataCacheKey(workshopId, options.userId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (userId) { - params.userid = userId; - } - - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; + if (options.userId) { + params.userid = options.userId; } return site.read('mod_workshop_get_reviewer_assessments', params, preSets).then((response) => { @@ -954,16 +905,19 @@ export class AddonModWorkshopProvider { * * @param workshopId Workshop ID. * @param assessmentId Assessment ID. - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAssessment(workshopId: number, assessmentId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { - assessmentid: assessmentId + assessmentid: assessmentId, }; const preSets = { - cacheKey: this.getAssessmentDataCacheKey(workshopId, assessmentId) + cacheKey: this.getAssessmentDataCacheKey(workshopId, assessmentId), + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_workshop_get_assessment', params, preSets).then((response) => { @@ -995,31 +949,26 @@ export class AddonModWorkshopProvider { * * @param workshopId Workshop ID. * @param assessmentId Assessment ID. - * @param mode Mode assessment (default) or preview. - * @param offline True if it should return cached data. Has priority over ignoreCache. - * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). - * @param siteId Site ID. If not defined, current site. + * @param options Other options. * @return Promise resolved when the workshop data is retrieved. */ - getAssessmentForm(workshopId: number, assessmentId: number, mode: string = 'assessment', offline: boolean = false, - ignoreCache: boolean = false, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { + getAssessmentForm(workshopId: number, assessmentId: number, options: AddonModWorkshopGetAssessmentFormOptions = {}) + : Promise { + const mode = options.mode || 'assessment'; + + return this.sitesProvider.getSite(options.siteId).then((site) => { const params = { assessmentid: assessmentId, - mode: mode || 'assessment' + mode: mode, }; - const preSets: any = { + const preSets = { cacheKey: this.getAssessmentFormDataCacheKey(workshopId, assessmentId, mode), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModWorkshopProvider.COMPONENT, + componentId: options.cmId, + ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (offline) { - preSets.omitExpires = true; - } else if (ignoreCache) { - preSets.getFromCache = 0; - preSets.emergencyCache = 0; - } - return site.read('mod_workshop_get_assessment_form_definition', params, preSets).then((response) => { if (response) { response.fields = this.parseFields(response.fields); @@ -1327,7 +1276,10 @@ export class AddonModWorkshopProvider { invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - return this.getWorkshop(courseId, moduleId, siteId, true).then((workshop) => { + return this.getWorkshop(courseId, moduleId, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((workshop) => { return this.invalidateContentById(workshop.id, courseId, siteId); }); } @@ -1399,3 +1351,43 @@ export class AddonModWorkshopProvider { name, 'workshop', params, siteId); } } + +/** + * Common options with a user ID. + */ +export type AddonModWorkshopUserOptions = CoreCourseCommonModWSOptions & { + userId?: number; // User ID. If not defined, current user. +}; + +/** + * Common options with a group ID. + */ +export type AddonModWorkshopGroupOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group id, 0 or not defined means that the function will determine the user group. +}; + +/** + * Options to pass to getSubmissions. + */ +export type AddonModWorkshopGetSubmissionsOptions = AddonModWorkshopUserOptions & AddonModWorkshopGroupOptions; + +/** + * Options to pass to fetchAllGradeReports. + */ +export type AddonModWorkshopFetchAllGradesReportOptions = AddonModWorkshopGroupOptions & { + perPage?: number; // Records per page to return. Default AddonModWorkshopProvider.PER_PAGE. +}; + +/** + * Options to pass to getGradesReport. + */ +export type AddonModWorkshopGetGradesReportOptions = AddonModWorkshopFetchAllGradesReportOptions & { + page?: number; // Page of records to return. Default 0. +}; + +/** + * Options to pass to getAssessmentForm. + */ +export type AddonModWorkshopGetAssessmentFormOptions = CoreCourseCommonModWSOptions & { + mode?: string; // Mode assessment (default) or preview. Defaults to 'assessment'. +}; diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 5a734cd76..e4de0e08e 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -18,7 +18,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteSchema, CoreSitesCommonWSOptions } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -1162,6 +1162,13 @@ export class CoreCourseProvider { } } +/** + * Common options used by modules when calling a WS through CoreSite. + */ +export type CoreCourseCommonModWSOptions = CoreSitesCommonWSOptions & { + cmId?: number; // Module ID. +}; + /** * Data returned by course_summary_exporter. */ diff --git a/src/providers/sites.ts b/src/providers/sites.ts index aed300a25..99e60cb42 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -210,12 +210,24 @@ export interface CoreRegisteredSiteSchema extends CoreSiteSchema { siteId?: string; } +/** + * Possible reading strategies (for cache). + */ export const enum CoreSitesReadingStrategy { OnlyCache, PreferCache, + OnlyNetwork, PreferNetwork, } +/** + * Common options used when calling a WS through CoreSite. + */ +export type CoreSitesCommonWSOptions = { + readingStrategy?: CoreSitesReadingStrategy; // Reading strategy. + siteId?: string; // Site ID. If not defined, current site. +}; + /* * Service to manage and interact with sites. * It allows creating tables in the databases of all sites. Each service or component should be responsible of creating @@ -2008,6 +2020,14 @@ export class CoreSitesProvider { forceOffline: true, }; case CoreSitesReadingStrategy.PreferNetwork: + return { + getFromCache: false, + }; + case CoreSitesReadingStrategy.OnlyNetwork: + return { + getFromCache: false, + emergencyCache: false, + }; default: return {}; } From 02da06a0fec64fb7bd31b5fc9e1a6d02fe78f532 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 10 Sep 2020 10:12:45 +0200 Subject: [PATCH 106/182] MOBILE-3338 course: Always get stored data and adapt strings --- scripts/langindex.json | 3 +- src/addon/block/myoverview/lang/en.json | 1 - .../index/addon-mod-assign-index.html | 2 +- .../index/addon-mod-book-index.html | 2 +- .../index/addon-mod-choice-index.html | 2 +- .../index/addon-mod-data-index.html | 2 +- .../index/addon-mod-feedback-index.html | 2 +- .../index/addon-mod-folder-index.html | 2 +- .../index/addon-mod-forum-index.html | 2 +- .../index/addon-mod-glossary-index.html | 2 +- .../index/addon-mod-h5pactivity-index.html | 2 +- .../index/addon-mod-imscp-index.html | 2 +- .../index/addon-mod-lesson-index.html | 2 +- .../index/addon-mod-page-index.html | 2 +- .../index/addon-mod-quiz-index.html | 2 +- .../index/addon-mod-resource-index.html | 2 +- .../index/addon-mod-scorm-index.html | 2 +- .../index/addon-mod-survey-index.html | 2 +- .../index/addon-mod-wiki-index.html | 2 +- .../index/addon-mod-workshop-index.html | 2 +- .../pages/course-storage/course-storage.ts | 16 +++------ .../pages/courses-storage/courses-storage.ts | 6 ++-- src/assets/lang/en.json | 3 +- src/classes/site.ts | 8 ++--- src/core/course/lang/en.json | 1 + src/core/course/providers/helper.ts | 33 ++++++++++++++----- .../course-progress/course-progress.ts | 2 +- .../core-siteplugins-module-index.html | 2 +- src/lang/en.json | 1 + 29 files changed, 61 insertions(+), 51 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index db78fc2ed..b0020c991 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -45,7 +45,6 @@ "addon.block_myoverview.hiddencourses": "block_myoverview", "addon.block_myoverview.inprogress": "block_myoverview", "addon.block_myoverview.lastaccessed": "block_myoverview", - "addon.block_myoverview.morecourses": "block_myoverview", "addon.block_myoverview.nocourses": "block_myoverview", "addon.block_myoverview.past": "block_myoverview", "addon.block_myoverview.pluginname": "block_myoverview", @@ -1383,6 +1382,7 @@ "core.choose": "moodle", "core.choosedots": "moodle", "core.clearsearch": "local_moodlemobileapp", + "core.clearstoreddata": "local_moodlemobileapp", "core.clicktohideshow": "moodle", "core.clicktoseefull": "local_moodlemobileapp", "core.close": "repository", @@ -1436,6 +1436,7 @@ "core.course.availablespace": "local_moodlemobileapp", "core.course.cannotdeletewhiledownloading": "local_moodlemobileapp", "core.course.confirmdeletemodulefiles": "local_moodlemobileapp", + "core.course.confirmdeletestoreddata": "local_moodlemobileapp", "core.course.confirmdownload": "local_moodlemobileapp", "core.course.confirmdownloadunknownsize": "local_moodlemobileapp", "core.course.confirmdownloadzerosize": "local_moodlemobileapp", diff --git a/src/addon/block/myoverview/lang/en.json b/src/addon/block/myoverview/lang/en.json index 9d38164d8..7bca82636 100644 --- a/src/addon/block/myoverview/lang/en.json +++ b/src/addon/block/myoverview/lang/en.json @@ -6,7 +6,6 @@ "hiddencourses": "Removed from view", "inprogress": "In progress", "lastaccessed": "Last accessed", - "morecourses": "More courses", "nocourses": "No courses", "past": "Past", "pluginname": "Course overview", diff --git a/src/addon/mod/assign/components/index/addon-mod-assign-index.html b/src/addon/mod/assign/components/index/addon-mod-assign-index.html index ee087a9df..18284862e 100644 --- a/src/addon/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addon/mod/assign/components/index/addon-mod-assign-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/book/components/index/addon-mod-book-index.html b/src/addon/mod/book/components/index/addon-mod-book-index.html index ee1e5bbad..74ad51a52 100644 --- a/src/addon/mod/book/components/index/addon-mod-book-index.html +++ b/src/addon/mod/book/components/index/addon-mod-book-index.html @@ -9,7 +9,7 @@ - + diff --git a/src/addon/mod/choice/components/index/addon-mod-choice-index.html b/src/addon/mod/choice/components/index/addon-mod-choice-index.html index 5cc0b88b7..bdadfd1f2 100644 --- a/src/addon/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addon/mod/choice/components/index/addon-mod-choice-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/data/components/index/addon-mod-data-index.html b/src/addon/mod/data/components/index/addon-mod-data-index.html index af4a82ad6..c0a92a4ad 100644 --- a/src/addon/mod/data/components/index/addon-mod-data-index.html +++ b/src/addon/mod/data/components/index/addon-mod-data-index.html @@ -12,7 +12,7 @@ - + diff --git a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html index c7edc940d..d4f10a815 100644 --- a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/folder/components/index/addon-mod-folder-index.html b/src/addon/mod/folder/components/index/addon-mod-folder-index.html index 2aa7976f3..e56091080 100644 --- a/src/addon/mod/folder/components/index/addon-mod-folder-index.html +++ b/src/addon/mod/folder/components/index/addon-mod-folder-index.html @@ -6,7 +6,7 @@ - + diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html index 31e15b3e0..4f00d77b0 100644 --- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html +++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html index 0c01a1288..475d5e969 100644 --- a/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html @@ -14,7 +14,7 @@ - + diff --git a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html index 23df4f6ad..cd0c7f9e8 100644 --- a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html +++ b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html @@ -8,7 +8,7 @@ - + diff --git a/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html index 9f767d27c..7d14a8a56 100644 --- a/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html @@ -9,7 +9,7 @@ - + diff --git a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html index 834abcf38..ebe107144 100644 --- a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/page/components/index/addon-mod-page-index.html b/src/addon/mod/page/components/index/addon-mod-page-index.html index d3d6c7726..783080214 100644 --- a/src/addon/mod/page/components/index/addon-mod-page-index.html +++ b/src/addon/mod/page/components/index/addon-mod-page-index.html @@ -6,7 +6,7 @@ - + diff --git a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html index ec8685448..d7586c3ac 100644 --- a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html +++ b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/resource/components/index/addon-mod-resource-index.html b/src/addon/mod/resource/components/index/addon-mod-resource-index.html index e6aa1efff..f910fabfb 100644 --- a/src/addon/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addon/mod/resource/components/index/addon-mod-resource-index.html @@ -6,7 +6,7 @@ - + diff --git a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html index cc16da2c2..b3e5d565e 100644 --- a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html +++ b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/survey/components/index/addon-mod-survey-index.html b/src/addon/mod/survey/components/index/addon-mod-survey-index.html index eb101c594..9b3531df3 100644 --- a/src/addon/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addon/mod/survey/components/index/addon-mod-survey-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html index 44a57c1ca..f9a3da731 100644 --- a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html @@ -19,7 +19,7 @@ - + diff --git a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html index f662c02ae..7e8daa461 100644 --- a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html +++ b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.ts b/src/addon/storagemanager/pages/course-storage/course-storage.ts index f3ec571e1..7dace4b7b 100644 --- a/src/addon/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addon/storagemanager/pages/course-storage/course-storage.ts @@ -20,7 +20,6 @@ import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { TranslateService } from '@ngx-translate/core'; import { CoreConstants } from '@core/constants'; -import { CoreSitesProvider } from '@providers/sites'; /** * Page that displays the amount of file storage used by each activity on the course, and allows @@ -40,7 +39,6 @@ export class AddonStorageManagerCourseStoragePage { totalSize: number; constructor(navParams: NavParams, - private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, private prefetchDelegate: CoreCourseModulePrefetchDelegate, private courseHelperProvider: CoreCourseHelperProvider, @@ -102,7 +100,7 @@ export class AddonStorageManagerCourseStoragePage { */ async deleteForCourse(): Promise { try { - await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await this.domUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -132,7 +130,7 @@ export class AddonStorageManagerCourseStoragePage { */ async deleteForSection(section: any): Promise { try { - await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await this.domUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -162,7 +160,7 @@ export class AddonStorageManagerCourseStoragePage { } try { - await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await this.domUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -186,13 +184,7 @@ export class AddonStorageManagerCourseStoragePage { const promises = []; modules.forEach((module) => { // Remove the files. - const promise = this.prefetchDelegate.removeModuleFiles(module, this.course.id).then(() => { - const handler = this.prefetchDelegate.getPrefetchHandlerFor(module); - if (handler) { - - return this.sitesProvider.getCurrentSite().deleteComponentFromCache(handler.component, module.id); - } - }).then(() => { + const promise = this.courseHelperProvider.removeModuleStoredData(module, this.course.id).then(() => { // When the files and cache are removed, update the size. module.parentSection.totalSize -= module.totalSize; this.totalSize -= module.totalSize; diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.ts b/src/addon/storagemanager/pages/courses-storage/courses-storage.ts index 05b5aa709..5beb27b09 100644 --- a/src/addon/storagemanager/pages/courses-storage/courses-storage.ts +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.ts @@ -92,7 +92,7 @@ export class AddonStorageManagerCoursesStoragePage { */ async deleteCompletelyDownloadedCourses(): Promise { try { - await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -122,7 +122,7 @@ export class AddonStorageManagerCoursesStoragePage { */ async deleteCourse(course: DownloadedCourse): Promise { try { - await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; @@ -206,7 +206,7 @@ export class AddonStorageManagerCoursesStoragePage { const sections = await CoreCourse.instance.getSections(courseId); const modules = CoreArray.flatten(sections.map((section) => section.modules)); const promisedModuleSizes = modules.map(async (module) => { - const size = await CoreCourseModulePrefetch.instance.getModuleDownloadedSize(module, courseId); + const size = await CoreCourseModulePrefetch.instance.getModuleStoredSize(module, courseId); return isNaN(size) ? 0 : size; }); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 5be7a28c6..d5fd154d5 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -45,7 +45,6 @@ "addon.block_myoverview.hiddencourses": "Removed from view", "addon.block_myoverview.inprogress": "In progress", "addon.block_myoverview.lastaccessed": "Last accessed", - "addon.block_myoverview.morecourses": "More courses", "addon.block_myoverview.nocourses": "No courses", "addon.block_myoverview.past": "Past", "addon.block_myoverview.pluginname": "Course overview", @@ -1384,6 +1383,7 @@ "core.choose": "Choose", "core.choosedots": "Choose...", "core.clearsearch": "Clear search", + "core.clearstoreddata": "Clear storage {{$a}}", "core.clicktohideshow": "Click to expand or collapse", "core.clicktoseefull": "Click to see full contents.", "core.close": "Close", @@ -1437,6 +1437,7 @@ "core.course.availablespace": " You currently have about {{available}} free space.", "core.course.cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", "core.course.confirmdeletemodulefiles": "Are you sure you want to delete these files?", + "core.course.confirmdeletestoreddata": "Are you sure you want to delete the stored data?", "core.course.confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", "core.course.confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?", "core.course.confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?", diff --git a/src/classes/site.ts b/src/classes/site.ts index 14bbf40fb..10c9c3e27 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -1107,10 +1107,10 @@ export class CoreSite { * @param componentId Optional component id (if not included, returns sum for whole component) * @return Promise resolved when we have calculated the size */ - getComponentCacheSize(component: string, componentId?: string): Promise { - const params = [component]; + getComponentCacheSize(component: string, componentId?: number): Promise { + const params: any[] = [component]; let extraClause = ''; - if (componentId) { + if (componentId !== undefined && componentId !== null) { params.push(componentId); extraClause = ' AND componentId = ?'; } @@ -1200,7 +1200,7 @@ export class CoreSite { * @param componentId Component id. * @return Promise resolved when the entries are deleted. */ - async deleteComponentFromCache(component: string, componentId?: string): Promise { + async deleteComponentFromCache(component: string, componentId?: number): Promise { if (!component) { return; } diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json index 69cbe5cf9..2a74a13a0 100644 --- a/src/core/course/lang/en.json +++ b/src/core/course/lang/en.json @@ -7,6 +7,7 @@ "availablespace": " You currently have about {{available}} free space.", "cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", "confirmdeletemodulefiles": "Are you sure you want to delete these files?", + "confirmdeletestoreddata": "Are you sure you want to delete the stored data?", "confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", "confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?", "confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?", diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 99e181b96..bc1d52fef 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -419,16 +419,11 @@ export class CoreCourseHelperProvider { try { - await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await this.domUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); modal = this.domUtils.showModalLoading(); - await this.prefetchDelegate.removeModuleFiles(module, courseId); - - const handler = this.prefetchDelegate.getPrefetchHandlerFor(module); - if (handler) { - await this.sitesProvider.getCurrentSite().deleteComponentFromCache(handler.component, String(module.id)); - } + await this.removeModuleStoredData(module, courseId); done && done(); @@ -859,7 +854,7 @@ export class CoreCourseHelperProvider { if (typeof instance.contextFileStatusObserver == 'undefined' && component) { // Debounce the update size function to prevent too many calls when downloading or deleting a whole activity. const debouncedUpdateSize = this.utils.debounce(() => { - this.prefetchDelegate.getModuleDownloadedSize(module, courseId).then((moduleSize) => { + this.prefetchDelegate.getModuleStoredSize(module, courseId).then((moduleSize) => { instance.size = moduleSize > 0 ? this.textUtils.bytesToSize(moduleSize, 2) : 0; }); }, 1000); @@ -1630,12 +1625,32 @@ export class CoreCourseHelperProvider { const modules = CoreArray.flatten(sections.map((section) => section.modules)); await Promise.all( - modules.map((module) => this.prefetchDelegate.removeModuleFiles(module, courseId)), + modules.map((module) => this.removeModuleStoredData(module, courseId)), ); await this.courseProvider.setCourseStatus(courseId, CoreConstants.NOT_DOWNLOADED); } + /** + * Remove module stored data. + * + * @param module Module to remove the files. + * @param courseId Course ID the module belongs to. + * @return Promise resolved when done. + */ + async removeModuleStoredData(module: any, courseId: number): Promise { + const promises = []; + + promises.push(this.prefetchDelegate.removeModuleFiles(module, courseId)); + + const handler = this.prefetchDelegate.getPrefetchHandlerFor(module); + if (handler) { + promises.push(this.sitesProvider.getCurrentSite().deleteComponentFromCache(handler.component, module.id)); + } + + await Promise.all(promises); + } + } export class CoreCourseHelper extends makeSingleton(CoreCourseHelperProvider) {} diff --git a/src/core/courses/components/course-progress/course-progress.ts b/src/core/courses/components/course-progress/course-progress.ts index 88923c044..1d30aa267 100644 --- a/src/core/courses/components/course-progress/course-progress.ts +++ b/src/core/courses/components/course-progress/course-progress.ts @@ -163,7 +163,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { */ async deleteCourse(): Promise { try { - await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + await this.domUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { if (!error.coreCanceled) { throw error; diff --git a/src/core/siteplugins/components/module-index/core-siteplugins-module-index.html b/src/core/siteplugins/components/module-index/core-siteplugins-module-index.html index a601f05c3..54836ba2e 100644 --- a/src/core/siteplugins/components/module-index/core-siteplugins-module-index.html +++ b/src/core/siteplugins/components/module-index/core-siteplugins-module-index.html @@ -5,7 +5,7 @@ - + diff --git a/src/lang/en.json b/src/lang/en.json index f00f084d3..23d49ca72 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -26,6 +26,7 @@ "choose": "Choose", "choosedots": "Choose...", "clearsearch": "Clear search", + "clearstoreddata": "Clear storage {{$a}}", "clicktohideshow": "Click to expand or collapse", "clicktoseefull": "Click to see full contents.", "close": "Close", From b7f4b3e12dbf5fba6ab2acc71c8c2b691b2746e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 23 Sep 2020 11:00:26 +0200 Subject: [PATCH 107/182] MOBILE-3469 course: Fix course format block loading order --- src/core/course/components/format/core-course-format.html | 2 +- src/core/course/pages/section/section.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/course/components/format/core-course-format.html b/src/core/course/components/format/core-course-format.html index 638720e4e..abdccb785 100644 --- a/src/core/course/components/format/core-course-format.html +++ b/src/core/course/components/format/core-course-format.html @@ -5,7 +5,7 @@ - + diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html index 6d2411153..6dff54147 100644 --- a/src/core/course/pages/section/section.html +++ b/src/core/course/pages/section/section.html @@ -24,7 +24,7 @@ - + From 99044b72316df7a28a31f15db00a01de64dc9f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 24 Sep 2020 14:04:06 +0200 Subject: [PATCH 108/182] MOBILE-3558 calendar: Fix fab button to be stuck at the corner --- src/addon/calendar/pages/day/day.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index a48f1d51f..1f36db633 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -64,12 +64,12 @@ - - - - - + + + + + From f0a13c9a93c06e088ad4be3cfa575eb5cc25c9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 24 Sep 2020 16:35:40 +0200 Subject: [PATCH 109/182] MOBILE-3519 forum: Fix offline replies new format --- .../addon-forum-post-options-menu.html | 8 ++-- .../post-options-menu/post-options-menu.ts | 34 +++++++++++++--- .../components/post/addon-mod-forum-post.html | 4 +- src/addon/mod/forum/components/post/post.ts | 4 +- .../mod/forum/pages/discussion/discussion.ts | 2 +- src/addon/mod/forum/providers/forum.ts | 9 ++++- src/addon/mod/forum/providers/helper.ts | 39 +++++++++---------- src/providers/utils/utils.ts | 8 ++++ 8 files changed, 71 insertions(+), 37 deletions(-) diff --git a/src/addon/mod/forum/components/post-options-menu/addon-forum-post-options-menu.html b/src/addon/mod/forum/components/post-options-menu/addon-forum-post-options-menu.html index 9b7e34dd6..d66529bc8 100644 --- a/src/addon/mod/forum/components/post-options-menu/addon-forum-post-options-menu.html +++ b/src/addon/mod/forum/components/post-options-menu/addon-forum-post-options-menu.html @@ -1,12 +1,12 @@ - +

{{ 'addon.mod_forum.edit' | translate }}

- + -

{{ 'addon.mod_forum.delete' | translate }}

-

{{ 'core.discard' | translate }}

+

{{ 'addon.mod_forum.delete' | translate }}

+

{{ 'core.discard' | translate }}

{{ 'core.numwords' | translate: {'$a': wordCount} }}

diff --git a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts index 7ddaf862f..8b9f2c69c 100644 --- a/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts +++ b/src/addon/mod/forum/components/post-options-menu/post-options-menu.ts @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, NgZone } from '@angular/core'; import { NavParams, ViewController } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { AddonModForumProvider } from '../../providers/forum'; +import { CoreApp } from '@providers/app'; +import { Network } from '@ionic-native/network'; /** * This component is meant to display a popover with the post options. @@ -34,10 +36,15 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { canDelete = false; loaded = false; url: string; + isOnline: boolean; + offlinePost: boolean; protected cmId: number; + protected onlineObserver: any; constructor(navParams: NavParams, + network: Network, + zone: NgZone, protected viewCtrl: ViewController, protected domUtils: CoreDomUtilsProvider, protected forumProvider: AddonModForumProvider, @@ -45,20 +52,28 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { this.post = navParams.get('post'); this.forumId = navParams.get('forumId'); this.cmId = navParams.get('cmId'); + + this.isOnline = CoreApp.instance.isOnline(); + this.onlineObserver = network.onchange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = CoreApp.instance.isOnline(); + }); + }); } /** * Component being initialized. */ async ngOnInit(): Promise { - if (this.post.id) { + if (this.post.id > 0) { const site: CoreSite = this.sitesProvider.getCurrentSite(); this.url = site.createSiteUrl('/mod/forum/discuss.php', {d: this.post.discussionid}, 'p' + this.post.id); + this.offlinePost = false; } else { // Offline post, you can edit or discard the post. - this.canEdit = true; - this.canDelete = true; this.loaded = true; + this.offlinePost = true; return; } @@ -98,7 +113,7 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { * Delete a post. */ deletePost(): void { - if (this.post.id) { + if (!this.offlinePost) { this.viewCtrl.dismiss({action: 'delete'}); } else { this.viewCtrl.dismiss({action: 'deleteoffline'}); @@ -109,10 +124,17 @@ export class AddonForumPostOptionsMenuComponent implements OnInit { * Edit a post. */ editPost(): void { - if (this.post.id) { + if (!this.offlinePost) { this.viewCtrl.dismiss({action: 'edit'}); } else { this.viewCtrl.dismiss({action: 'editoffline'}); } } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.onlineObserver && this.onlineObserver.unsubscribe(); + } } diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html index c1a08da81..6a620c98e 100644 --- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html +++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html @@ -50,14 +50,14 @@ - +
- + {{ 'addon.mod_forum.subject' | translate }} diff --git a/src/addon/mod/forum/components/post/post.ts b/src/addon/mod/forum/components/post/post.ts index 48d0fc8e4..f9402ece9 100644 --- a/src/addon/mod/forum/components/post/post.ts +++ b/src/addon/mod/forum/components/post/post.ts @@ -93,7 +93,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges * Component being initialized. */ ngOnInit(): void { - this.uniqueId = this.post.id ? 'reply' + this.post.id : 'edit' + this.post.parentid; + this.uniqueId = this.post.id > 0 ? 'reply' + this.post.id : 'edit' + this.post.parentid; const reTranslated = this.translate.instant('addon.mod_forum.re'); this.displaySubject = !this.parentSubject || @@ -102,7 +102,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges this.defaultReplySubject = this.post.replysubject || ((this.post.subject.startsWith('Re: ') || this.post.subject.startsWith(reTranslated)) ? this.post.subject : `${reTranslated} ${this.post.subject}`); - this.optionsMenuEnabled = !this.post.id || (this.forumProvider.isGetDiscussionPostAvailable() && + this.optionsMenuEnabled = this.post.id < 0 || (this.forumProvider.isGetDiscussionPostAvailable() && (this.forumProvider.isDeletePostAvailable() || this.forumProvider.isUpdatePostAvailable())); } diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts index cee6fa087..14d83c207 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.ts +++ b/src/addon/mod/forum/pages/discussion/discussion.ts @@ -360,7 +360,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { offlineReplies.push(reply); // Disable reply of the parent. Reply in offline to the same post is not allowed, edit instead. - posts[reply.parent].canreply = false; + posts[reply.parentid].capabilities.reply = false; })); }); diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index cdb8b6b5b..4118db0f1 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -589,8 +589,13 @@ export class AddonModForumProvider { sortDiscussionPosts(posts: any[], direction: string): void { // @todo: Check children when sorting. posts.sort((a, b) => { - a = parseInt(a.created, 10); - b = parseInt(b.created, 10); + a = parseInt(a.timecreated, 10) || 0; + b = parseInt(b.timecreated, 10) || 0; + if (a == 0 || b == 0) { + // Leave 0 at the end. + return b - a; + } + if (direction == 'ASC') { return a - b; } else { diff --git a/src/addon/mod/forum/providers/helper.ts b/src/addon/mod/forum/providers/helper.ts index a3d72dccd..63aa04019 100644 --- a/src/addon/mod/forum/providers/helper.ts +++ b/src/addon/mod/forum/providers/helper.ts @@ -161,24 +161,23 @@ export class AddonModForumHelperProvider { */ convertOfflineReplyToOnline(offlineReply: any, siteId?: string): Promise { const reply: any = { - attachments: [], - canreply: false, - children: [], - created: offlineReply.timecreated, - discussion: offlineReply.discussionid, - id: false, - mailed: 0, - mailnow: 0, - message: offlineReply.message, - messageformat: 1, - messagetrust: 0, - modified: false, - parent: offlineReply.postid, - postread: false, + id: -offlineReply.timecreated, + discussionid: offlineReply.discussionid, + parentid: offlineReply.postid, + hasparent: !!offlineReply.postid, + author: { + id: offlineReply.userid, + }, + timecreated: false, subject: offlineReply.subject, - totalscore: 0, - userid: offlineReply.userid, - isprivatereply: offlineReply.options && offlineReply.options.private + message: offlineReply.message, + attachments: [], + capabilities: { + reply: false, + }, + unread: false, + isprivatereply: offlineReply.options && offlineReply.options.private, + tags: null }, promises = []; @@ -187,7 +186,7 @@ export class AddonModForumHelperProvider { reply.attachments = offlineReply.options.attachmentsid.online || []; if (offlineReply.options.attachmentsid.offline) { - promises.push(this.getReplyStoredFiles(offlineReply.forumid, reply.parent, siteId, reply.userid) + promises.push(this.getReplyStoredFiles(offlineReply.forumid, reply.parentid, siteId, offlineReply.userid) .then((files) => { reply.attachments = reply.attachments.concat(files); })); @@ -196,8 +195,8 @@ export class AddonModForumHelperProvider { // Get user data. promises.push(this.userProvider.getProfile(offlineReply.userid, offlineReply.courseid, true).then((user) => { - reply.userfullname = user.fullname; - reply.userpictureurl = user.profileimageurl; + reply.author.fullname = user.fullname; + reply.author.urls = { profileimage: user.profileimageurl }; }).catch(() => { // Ignore errors. })); diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 8b87c1b93..f1a582a11 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -581,6 +581,10 @@ export class CoreUtilsProvider { parent = node[parentFieldName]; node.children = []; + if (!id || !parent) { + this.logger.error(`Node with incorrect ${idFieldName}:${id} or ${parentFieldName}:${parent} found on formatTree`); + } + // Use map to look-up the parents. map[id] = index; if (parent != rootParentId) { @@ -597,12 +601,16 @@ export class CoreUtilsProvider { mapDepth[id] = mapDepth[parent]; // Change the parent to be the one that is two levels up the hierarchy. node.parent = parentOfParent; + } else { + this.logger.error(`Node parent of parent:${parentOfParent} not found on formatTree`); } } else { parentNode.children.push(node); // Increase the depth level. mapDepth[id] = mapDepth[parent] + 1; } + } else { + this.logger.error(`Node parent:${parent} not found on formatTree`); } } else { tree.push(node); From 1e6d72f0ff99f4c7795f9bc32cd8aa25708927b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 25 Sep 2020 15:02:23 +0200 Subject: [PATCH 110/182] MOBILE-3562 course: Remove course overview file listing on preview --- src/core/courses/pages/course-preview/course-preview.html | 1 - src/core/courses/pages/course-preview/course-preview.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/core/courses/pages/course-preview/course-preview.html b/src/core/courses/pages/course-preview/course-preview.html index f84814ab6..b77f02dba 100644 --- a/src/core/courses/pages/course-preview/course-preview.html +++ b/src/core/courses/pages/course-preview/course-preview.html @@ -42,7 +42,6 @@ -

{{ instance.name }}

diff --git a/src/core/courses/pages/course-preview/course-preview.ts b/src/core/courses/pages/course-preview/course-preview.ts index 28f6d62ee..a6ff05ca4 100644 --- a/src/core/courses/pages/course-preview/course-preview.ts +++ b/src/core/courses/pages/course-preview/course-preview.ts @@ -38,7 +38,6 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { course: any; isEnrolled: boolean; canAccessCourse = true; - component = 'CoreCoursesCoursePreview'; selfEnrolInstances: any[] = []; paypalEnabled: boolean; dataLoaded: boolean; From 42ba687aa2989846bbe569b9e9a8a6237cf7ff45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 28 Sep 2020 10:45:21 +0200 Subject: [PATCH 111/182] MOBILE-3523 gulp: Fix config gulp task escaping single quotes in values --- gulp/task-build-config.js | 67 +++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/gulp/task-build-config.js b/gulp/task-build-config.js index 98d380140..69bf71aa3 100644 --- a/gulp/task-build-config.js +++ b/gulp/task-build-config.js @@ -45,6 +45,8 @@ class BuildConfigTask { * @param done Function to call when done. */ run(path, done) { + const self = this; + // Get the last commit. exec('git log -1 --pretty=format:"%H"', (err, commit, stderr) => { if (err) { @@ -61,28 +63,9 @@ class BuildConfigTask { let contents = LICENSE + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n'; for (let key in config) { - let value = config[key]; + let value = self.transformValue(config[key]); - if (typeof value == 'string') { - // Wrap the string in ' and escape them. - value = "'" + value.replace(/([^\\])'/g, "$1\\'") + "'"; - } else if (typeof value != 'number' && typeof value != 'boolean') { - // Stringify with 4 spaces of indentation, and then add 4 more spaces in each line. - value = JSON.stringify(value, null, 4).replace(/^(?: )/gm, ' ').replace(/^(?:})/gm, ' }'); - // Replace " by ' in values. - value = value.replace(/: "([^"]*)"/g, ": '$1'"); - - // Check if the keys have "-" in it. - const matches = value.match(/"([^"]*\-[^"]*)":/g); - if (matches) { - // Replace " by ' in keys. We cannot remove them because keys have chars like '-'. - value = value.replace(/"([^"]*)":/g, "'$1':"); - } else { - // Remove ' in keys. - value = value.replace(/"([^"]*)":/g, "$1:"); - } - - // Add type any to the key. + if (typeof config[key] != 'number' && typeof config[key] != 'boolean' && typeof config[key] != 'string') { key = key + ': any'; } @@ -108,6 +91,48 @@ class BuildConfigTask { .on('end', done); }); } + + + /** + * Recursively transform a config value into personalized TS. + * + * @param value Value to convert + * @return Converted value. + */ + transformValue(value) { + if (typeof value == 'string') { + // Wrap the string in ' and escape them. + return "'" + value.replace(/([^\\])'/g, "$1\\'") + "'"; + } + + if (typeof value != 'number' && typeof value != 'boolean') { + const isArray = Array.isArray(value); + let contents = ''; + + let quoteKeys = false; + if (!isArray) { + for (let key in value) { + if (key.indexOf('-') >= 0) { + quoteKeys = true; + break; + } + } + } + + for (let key in value) { + value[key] = this.transformValue(value[key]); + + const quotedKey = quoteKeys ? "'" + key + "'" : key; + contents += ' ' + (isArray ? '' : quotedKey + ': ') + value[key] + ",\n"; + } + + contents += (isArray ? ']' : '}'); + + return (isArray ? '[' : '{') + "\n" + contents.replace(/^/gm, ' '); + } + + return value; + } } module.exports = BuildConfigTask; From 0badecfc8a2dc8c72c76ec0b4253b2f0641af0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 28 Sep 2020 14:40:40 +0200 Subject: [PATCH 112/182] MOBILE-3469 course: Get course data when entering from search --- src/core/course/providers/course.ts | 92 +++++++++++-------- src/core/course/providers/default-format.ts | 2 +- src/core/course/providers/format-delegate.ts | 6 +- src/core/course/providers/helper.ts | 2 +- .../pages/course-preview/course-preview.ts | 2 +- 5 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index e4de0e08e..c40e5c7ba 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -28,7 +28,7 @@ import { CoreCourseOfflineProvider } from './course-offline'; import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; import { CoreCourseFormatDelegate } from './format-delegate'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesProvider, CoreCourses } from '@core/courses/providers/courses'; import { makeSingleton } from '@singletons/core.singletons'; /** @@ -978,49 +978,69 @@ export class CoreCourseProvider { * @param params Other params to pass to the course page. * @return Promise resolved when done. */ - openCourse(navCtrl: NavController, course: any, params?: any): Promise { + async openCourse(navCtrl: NavController, course: any, params?: any): Promise { const loading = this.domUtils.showModalLoading(); // Wait for site plugins to be fetched. - return this.sitePluginsProvider.waitFetchPlugins().then(() => { - if (this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) { - // This course uses a custom format plugin, wait for the format plugin to finish loading. + await this.sitePluginsProvider.waitFetchPlugins(); - return this.sitePluginsProvider.sitePluginLoaded('format_' + course.format).then(() => { - // The format loaded successfully, but the handlers wont be registered until all site plugins have loaded. - if (this.sitePluginsProvider.sitePluginsFinishedLoading) { - return this.courseFormatDelegate.openCourse(navCtrl, course, params); + if (typeof course.format == 'undefined') { + const coursesProvider = CoreCourses.instance; + try { + course = await coursesProvider.getUserCourse(course.id, true); + } catch (error) { + // Not enrolled or an error happened. Try to use another WebService. + const available = coursesProvider.isGetCoursesByFieldAvailableInSite(); + try { + if (available) { + course = await CoreCourses.instance.getCourseByField('id', course.id); } else { - // Wait for plugins to be loaded. - const deferred = this.utils.promiseDefer(), - observer = this.eventsProvider.on(CoreEventsProvider.SITE_PLUGINS_LOADED, () => { - observer && observer.off(); - - this.courseFormatDelegate.openCourse(navCtrl, course, params).then((response) => { - deferred.resolve(response); - }).catch((error) => { - deferred.reject(error); - }); - }); - - return deferred.promise; + course = await CoreCourses.instance.getCourse(course.id); } - }).catch(() => { - // The site plugin failed to load. The user needs to restart the app to try loading it again. - const message = this.translate.instant('core.courses.errorloadplugins'); - const reload = this.translate.instant('core.courses.reload'); - const ignore = this.translate.instant('core.courses.ignore'); - this.domUtils.showConfirm(message, '', reload, ignore).then(() => { - window.location.reload(); - }); - }); - } else { - // No custom format plugin. We don't need to wait for anything. + } catch (error) { + // Ignore errors. + } + } + } + + if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) { + // No custom format plugin. We don't need to wait for anything. + await this.courseFormatDelegate.openCourse(navCtrl, course, params); + loading.dismiss(); + + return; + } + + // This course uses a custom format plugin, wait for the format plugin to finish loading. + try { + await this.sitePluginsProvider.sitePluginLoaded('format_' + course.format); + // The format loaded successfully, but the handlers wont be registered until all site plugins have loaded. + if (this.sitePluginsProvider.sitePluginsFinishedLoading) { return this.courseFormatDelegate.openCourse(navCtrl, course, params); } - }).finally(() => { - loading.dismiss(); - }); + + // Wait for plugins to be loaded. + const deferred = this.utils.promiseDefer(), + observer = this.eventsProvider.on(CoreEventsProvider.SITE_PLUGINS_LOADED, () => { + observer && observer.off(); + + this.courseFormatDelegate.openCourse(navCtrl, course, params).then((response) => { + deferred.resolve(response); + }).catch((error) => { + deferred.reject(error); + }); + }); + + return deferred.promise; + } catch (error) { + // The site plugin failed to load. The user needs to restart the app to try loading it again. + const message = this.translate.instant('core.courses.errorloadplugins'); + const reload = this.translate.instant('core.courses.reload'); + const ignore = this.translate.instant('core.courses.ignore'); + this.domUtils.showConfirm(message, '', reload, ignore).then(() => { + window.location.reload(); + }); + } } /** diff --git a/src/core/course/providers/default-format.ts b/src/core/course/providers/default-format.ts index bbf673151..7b5390a9c 100644 --- a/src/core/course/providers/default-format.ts +++ b/src/core/course/providers/default-format.ts @@ -172,7 +172,7 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { * @param params Params to pass to the course page. * @return Promise resolved when done. */ - openCourse(navCtrl: NavController, course: any, params?: any): Promise { + openCourse(navCtrl: NavController, course: any, params?: any): Promise { params = params || {}; Object.assign(params, { course: course }); diff --git a/src/core/course/providers/format-delegate.ts b/src/core/course/providers/format-delegate.ts index ceee5af13..aafbdb462 100644 --- a/src/core/course/providers/format-delegate.ts +++ b/src/core/course/providers/format-delegate.ts @@ -102,7 +102,7 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { * @param params Params to pass to the course page. * @return Promise resolved when done. */ - openCourse?(navCtrl: NavController, course: any, params?: any): Promise; + openCourse?(navCtrl: NavController, course: any, params?: any): Promise; /** * Return the Component to use to display the course format instead of using the default one. @@ -351,14 +351,14 @@ export class CoreCourseFormatDelegate extends CoreDelegate { } /** - * Open a course. + * Open a course. Should not be called directly. Call CoreCourseHelper.openCourse instead. * * @param navCtrl The NavController instance to use. * @param course The course to open. It should contain a "format" attribute. * @param params Params to pass to the course page. * @return Promise resolved when done. */ - openCourse(navCtrl: NavController, course: any, params?: any): Promise { + openCourse(navCtrl: NavController, course: any, params?: any): Promise { return this.executeFunctionOnEnabled(course.format, 'openCourse', [navCtrl, course, params]); } diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index bc1d52fef..87fcd202e 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -1601,7 +1601,7 @@ export class CoreCourseHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - openCourse(navCtrl: NavController, course: any, params?: any, siteId?: string): Promise { + openCourse(navCtrl: NavController, course: any, params?: any, siteId?: string): Promise { if (!siteId || siteId == this.sitesProvider.getCurrentSiteId()) { // Current site, we can open the course. return this.courseProvider.openCourse(navCtrl, course, params); diff --git a/src/core/courses/pages/course-preview/course-preview.ts b/src/core/courses/pages/course-preview/course-preview.ts index a6ff05ca4..664960ed4 100644 --- a/src/core/courses/pages/course-preview/course-preview.ts +++ b/src/core/courses/pages/course-preview/course-preview.ts @@ -259,7 +259,7 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { return; } - this.courseFormatDelegate.openCourse(this.navCtrl, this.course); + this.courseHelper.openCourse(this.navCtrl, this.course); } /** From ce9ec993c51b1c3a172df58bf4bfa8e53ca0840b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 29 Sep 2020 09:44:28 +0200 Subject: [PATCH 113/182] MOBILE-3469 course: Remove unused property --- src/core/course/providers/course.ts | 1 + .../pages/course-preview/course-preview.ts | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index c40e5c7ba..7e6d02696 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -985,6 +985,7 @@ export class CoreCourseProvider { await this.sitePluginsProvider.waitFetchPlugins(); if (typeof course.format == 'undefined') { + // This block can be replaced by a call to CourseHelper.getCourse(), but it is circular dependant. const coursesProvider = CoreCourses.instance; try { course = await coursesProvider.getUserCourse(course.id, true); diff --git a/src/core/courses/pages/course-preview/course-preview.ts b/src/core/courses/pages/course-preview/course-preview.ts index 664960ed4..fd9131a0f 100644 --- a/src/core/courses/pages/course-preview/course-preview.ts +++ b/src/core/courses/pages/course-preview/course-preview.ts @@ -24,7 +24,6 @@ import { CoreCoursesProvider } from '../../providers/courses'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; -import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; /** * Page that allows "previewing" a course and enrolling in it if enabled and not enrolled. @@ -64,13 +63,23 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy { protected currentInstanceId: number; protected courseStatusObserver; - constructor(private navCtrl: NavController, navParams: NavParams, private sitesProvider: CoreSitesProvider, - private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, appProvider: CoreAppProvider, - private coursesProvider: CoreCoursesProvider, private platform: Platform, private modalCtrl: ModalController, - private translate: TranslateService, private eventsProvider: CoreEventsProvider, - private courseOptionsDelegate: CoreCourseOptionsDelegate, private courseHelper: CoreCourseHelperProvider, - private courseProvider: CoreCourseProvider, private courseFormatDelegate: CoreCourseFormatDelegate, - private zone: NgZone) { + constructor( + protected navCtrl: NavController, + navParams: NavParams, + protected sitesProvider: CoreSitesProvider, + protected domUtils: CoreDomUtilsProvider, + protected textUtils: CoreTextUtilsProvider, + appProvider: CoreAppProvider, + protected coursesProvider: CoreCoursesProvider, + protected platform: Platform, + protected modalCtrl: ModalController, + protected translate: TranslateService, + protected eventsProvider: CoreEventsProvider, + protected courseOptionsDelegate: CoreCourseOptionsDelegate, + protected courseHelper: CoreCourseHelperProvider, + protected courseProvider: CoreCourseProvider, + protected zone: NgZone + ) { this.course = navParams.get('course'); this.avoidOpenCourse = navParams.get('avoidOpenCourse'); From 1652e4d8f2e713eec23e050c7c53e67d6049bb03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 29 Sep 2020 14:21:47 +0200 Subject: [PATCH 114/182] MOBILE-3200 database: Fix update access data on group change --- src/addon/mod/data/components/index/index.ts | 137 ++++++++++--------- 1 file changed, 69 insertions(+), 68 deletions(-) diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index 34feeef8f..1df731973 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -180,69 +180,67 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * @param showErrors If show errors to the user of hide them. * @return Promise resolved when done. */ - protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { let canAdd = false, canSearch = false; - return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { - this.data = data; - this.hasComments = data.comments; + this.data = await this.dataProvider.getDatabase(this.courseId, this.module.id); + this.hasComments = this.data.comments; - this.description = data.intro || data.description; - this.dataRetrieved.emit(data); + this.description = this.data.intro || this.data.description; + this.dataRetrieved.emit(this.data); - if (sync) { + if (sync) { + try { // Try to synchronize the data. - return this.syncActivity(showErrors).catch(() => { - // Ignore errors. - }); + await this.syncActivity(showErrors); + } catch (error) { + // Ignore errors. } - }).then(() => { - return this.dataProvider.getDatabaseAccessInformation(this.data.id, {cmId: this.module.id}); - }).then((accessData) => { - this.access = accessData; + } - if (!accessData.timeavailable) { - const time = this.timeUtils.timestamp(); + this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.data.coursemodule); + this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, this.groupInfo); - this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ? - parseInt(this.data.timeavailablefrom, 10) * 1000 : false; - this.timeAvailableFromReadable = this.timeAvailableFrom ? this.timeUtils.userDate(this.timeAvailableFrom) : false; - this.timeAvailableTo = this.data.timeavailableto && time > this.data.timeavailableto ? - parseInt(this.data.timeavailableto, 10) * 1000 : false; - this.timeAvailableToReadable = this.timeAvailableTo ? this.timeUtils.userDate(this.timeAvailableTo) : false; + this.access = await this.dataProvider.getDatabaseAccessInformation(this.data.id, { + cmId: this.module.id, + groupId: this.selectedGroup || undefined + }); - this.isEmpty = true; - this.groupInfo = null; + if (!this.access.timeavailable) { + const time = this.timeUtils.timestamp(); - return; - } + this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ? + parseInt(this.data.timeavailablefrom, 10) * 1000 : false; + this.timeAvailableFromReadable = this.timeAvailableFrom ? this.timeUtils.userDate(this.timeAvailableFrom) : false; + this.timeAvailableTo = this.data.timeavailableto && time > this.data.timeavailableto ? + parseInt(this.data.timeavailableto, 10) * 1000 : false; + this.timeAvailableToReadable = this.timeAvailableTo ? this.timeUtils.userDate(this.timeAvailableTo) : false; + this.isEmpty = true; + this.groupInfo = null; + } else { canSearch = true; - canAdd = accessData.canaddentry; + canAdd = this.access.canaddentry; + } - return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => { - this.groupInfo = groupInfo; - this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo); - }); - }).then(() => { - return this.dataProvider.getFields(this.data.id, {cmId: this.module.id}).then((fields) => { - if (fields.length == 0) { - canSearch = false; - canAdd = false; - } - this.search.advanced = []; + const fields = await this.dataProvider.getFields(this.data.id, {cmId: this.module.id}); + this.search.advanced = []; - this.fields = this.utils.arrayToObject(fields, 'id'); - this.fieldsArray = this.utils.objectToArray(this.fields); + this.fields = this.utils.arrayToObject(fields, 'id'); + this.fieldsArray = this.utils.objectToArray(this.fields); + if (this.fieldsArray.length == 0) { + canSearch = false; + canAdd = false; + } - return this.fetchEntriesData(); - }); - }).finally(() => { + try { + await this.fetchEntriesData(); + } finally { this.canAdd = canAdd; this.canSearch = canSearch; this.fillContextMenu(refresh); - }); + } } /** @@ -252,24 +250,16 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp */ protected fetchEntriesData(): Promise { - return this.dataProvider.getDatabaseAccessInformation(this.data.id, { + const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined; + const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined; + + return this.dataHelper.fetchEntries(this.data, this.fieldsArray, { groupId: this.selectedGroup, - cmId: this.module.id, - }).then((accessData) => { - // Update values for current group. - this.access.canaddentry = accessData.canaddentry; - - const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined; - const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined; - - return this.dataHelper.fetchEntries(this.data, this.fieldsArray, { - groupId: this.selectedGroup, - search, - advSearch, - sort: Number(this.search.sortBy), - order: this.search.sortDirection, - page: this.search.page, - }); + search, + advSearch, + sort: Number(this.search.sortBy), + order: this.search.sortDirection, + page: this.search.page, }).then((entries) => { const numEntries = entries.entries.length; const numOfflineEntries = entries.offlineEntries.length; @@ -390,18 +380,29 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * @param groupId Group ID. * @return Resolved when new group is selected or rejected if not. */ - setGroup(groupId: number): Promise { + async setGroup(groupId: number): Promise { this.selectedGroup = groupId; this.search.page = 0; - return this.fetchEntriesData().then(() => { + // Only update canAdd if there's any field, otheerwise, canAdd will remain false. + if (this.fieldsArray.length > 0) { + // Update values for current group. + this.access = await this.dataProvider.getDatabaseAccessInformation(this.data.id, { + groupId: this.selectedGroup, + cmId: this.module.id, + }); + + this.canAdd = this.access.canaddentry; + } + + try { + await this.fetchEntriesData(); + // Log activity view for coherence with Moodle web. return this.logView(); - }).catch((message) => { - this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); - - return Promise.reject(null); - }); + } catch (error) { + this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + } } /** From 6d7a96d8dfb3006bd0e102b01ac4f88f00380768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 29 Sep 2020 14:40:51 +0200 Subject: [PATCH 115/182] MOBILE-3200 database: Do not need courseId to delete an offline entry --- src/addon/mod/data/providers/helper.ts | 73 ++++++++++++++++---------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index ea686e422..eaf9a2b5d 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -18,7 +18,6 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { AddonModDataFieldsDelegate } from './fields-delegate'; @@ -35,12 +34,19 @@ import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; @Injectable() export class AddonModDataHelperProvider { - constructor(private sitesProvider: CoreSitesProvider, protected dataProvider: AddonModDataProvider, - private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate, - private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider, - private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private utils: CoreUtilsProvider, - private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, - private ratingOffline: CoreRatingOfflineProvider) {} + constructor( + protected sitesProvider: CoreSitesProvider, + protected dataProvider: AddonModDataProvider, + protected translate: TranslateService, + protected fieldsDelegate: AddonModDataFieldsDelegate, + protected dataOffline: AddonModDataOfflineProvider, + protected fileUploaderProvider: CoreFileUploaderProvider, + protected textUtils: CoreTextUtilsProvider, + protected eventsProvider: CoreEventsProvider, + protected domUtils: CoreDomUtilsProvider, + protected courseProvider: CoreCourseProvider, + protected ratingOffline: CoreRatingOfflineProvider + ) {} /** * Returns the record with the offline actions applied. @@ -632,35 +638,44 @@ export class AddonModDataHelperProvider { * @param courseId Course ID. It not defined, it will be fetched. * @param siteId Site ID. If not defined, current site. */ - showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): void { + async showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - this.domUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord').then(() => { - const modal = this.domUtils.showModalLoading(); + let modal; + try { + await this.domUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord'); - return this.getActivityCourseIdIfNotSet(dataId, courseId, siteId).then((courseId) => { - return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId); - }).catch((message) => { + modal = this.domUtils.showModalLoading(); + + try { + if (entryId > 0) { + courseId = await this.getActivityCourseIdIfNotSet(dataId, courseId, siteId); + } + + this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId); + } catch (message) { this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true); - return Promise.reject(null); - }).then(() => { - return this.utils.allPromises([ - this.dataProvider.invalidateEntryData(dataId, entryId, siteId), - this.dataProvider.invalidateEntriesData(dataId, siteId) - ]).catch(() => { - // Ignore errors. - }); - }).then(() => { - this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId, deleted: true}, siteId); + modal && modal.dismiss(); - this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); - }).finally(() => { - modal.dismiss(); - }); - }).catch(() => { + return; + } + + try { + await this.dataProvider.invalidateEntryData(dataId, entryId, siteId); + await this.dataProvider.invalidateEntriesData(dataId, siteId); + } catch (error) { + // Ignore errors. + } + + this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId, deleted: true}, siteId); + + this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); + } catch (error) { // Ignore error, it was already displayed. - }); + } + + modal && modal.dismiss(); } /** From b0b806280b8fd1de62db0ff6f53473dfba92c974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 29 Sep 2020 15:39:39 +0200 Subject: [PATCH 116/182] MOBILE-3200 database: Improve add/edit offline action handling --- src/addon/mod/data/pages/edit/edit.ts | 33 +++++++++------ src/addon/mod/data/providers/data.ts | 61 ++++++++++++++------------- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index b3542eebf..72e56a5ac 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -52,6 +52,8 @@ export class AddonModDataEditPage { protected siteId: string; protected offline: boolean; protected forceLeave = false; // To allow leaving the page without checking for changes. + protected initialSelectedGroup = null; + protected isEditing = false; title = ''; component = AddonModDataProvider.COMPONENT; @@ -75,7 +77,10 @@ export class AddonModDataEditPage { this.module = params.get('module') || {}; this.entryId = params.get('entryId') || null; this.courseId = params.get('courseId'); - this.selectedGroup = params.get('group') || 0; + this.selectedGroup = this.entryId ? null : (params.get('group') || 0); + + // If entryId is lower than 0 or null, it is a new entry or an offline entry. + this.isEditing = this.entryId && this.entryId > 0; this.siteId = sitesProvider.getCurrentSiteId(); @@ -103,7 +108,8 @@ export class AddonModDataEditPage { const inputData = this.editForm.value; - const changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents); + let changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents); + changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup); if (changed) { // Show confirmation if some data has been modified. @@ -169,6 +175,7 @@ export class AddonModDataEditPage { return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents).then((changed) => { + changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup); if (!changed) { if (this.entryId) { return this.returnToEntryList(); @@ -196,7 +203,7 @@ export class AddonModDataEditPage { return Promise.reject(e); }).then((editData) => { if (editData.length > 0) { - if (this.entryId) { + if (this.isEditing) { return this.dataProvider.editEntry(this.data.id, this.entryId, this.courseId, editData, this.fields, undefined, this.offline); } @@ -213,20 +220,20 @@ export class AddonModDataEditPage { } // This is done if entry is updated when editing or creating if not. - if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) { + if ((this.isEditing && result.updated) || (!this.isEditing && result.newentryid)) { this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.siteId); - if (result.sent) { - this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' }); - } - const promises = []; - this.entryId = this.entryId || result.newentryid; + if (result.sent) { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' }); - promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId)); - promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId)); + if (this.isEditing) { + promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId)); + } + promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId)); + } return Promise.all(promises).then(() => { this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, @@ -264,7 +271,7 @@ export class AddonModDataEditPage { * @param groupId Group identifier to set. * @return Resolved when done. */ - setGroup(groupId: number): Promise { + setGroup(groupId: number): Promise { this.selectedGroup = groupId; this.loaded = false; @@ -322,7 +329,7 @@ export class AddonModDataEditPage { * * @return Resolved when done. */ - protected returnToEntryList(): Promise { + protected returnToEntryList(): Promise { const inputData = this.editForm.value; return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index cde86d294..6fd85d2db 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -133,6 +133,7 @@ export class AddonModDataProvider { }); }; + // Checks to store offline. if (!this.appProvider.isOnline() || forceOffline) { const notifications = this.checkFields(fields, contents); if (notifications) { @@ -140,22 +141,40 @@ export class AddonModDataProvider { fieldnotifications: notifications }); } - - return storeOffline(); } - return this.addEntryOnline(dataId, contents, groupId, siteId).then((result) => { - result.sent = true; + // Get other not synced actions. + return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { + if (entries && entries.length) { + // Found. Delete add and edit actions first. + const proms = []; + entries.forEach((entry) => { + if (entry.action == 'add') { + proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); + } + }); - return result; - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); + return Promise.all(proms); + } + }).then(() => { + // App is offline, store the action. + if (!this.appProvider.isOnline() || forceOffline) { + return storeOffline(); } - // Couldn't connect to server, store in offline. - return storeOffline(); + return this.addEntryOnline(dataId, contents, groupId, siteId).then((result) => { + result.sent = true; + + return result; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + return Promise.reject(error); + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + }); }); } @@ -398,9 +417,6 @@ export class AddonModDataProvider { }); }; - let justAdded = false, - groupId; - if (!this.appProvider.isOnline() || forceOffline) { const notifications = this.checkFields(fields, contents); if (notifications) { @@ -416,11 +432,7 @@ export class AddonModDataProvider { // Found. Delete add and edit actions first. const proms = []; entries.forEach((entry) => { - if (entry.action == 'add') { - justAdded = true; - groupId = entry.groupid; - proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); - } else if (entry.action == 'edit') { + if (entry.action == 'edit') { proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); } }); @@ -428,17 +440,6 @@ export class AddonModDataProvider { return Promise.all(proms); } }).then(() => { - if (justAdded) { - // The field was added offline, add again and stop. - return this.addEntry(dataId, entryId, courseId, contents, groupId, fields, siteId, forceOffline) - .then((result) => { - result.updated = true; - result.sent = true; - - return result; - }); - } - if (!this.appProvider.isOnline() || forceOffline) { // App is offline, store the action. return storeOffline(); From cb4eac9a17c50dbdb3cb78dd3c7c5bb04e367ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 29 Sep 2020 15:40:27 +0200 Subject: [PATCH 117/182] MOBILE-3200 database: Check permissions on add and edit entry --- scripts/langindex.json | 2 + src/addon/mod/data/lang/en.json | 1 + src/addon/mod/data/pages/edit/edit.html | 4 +- src/addon/mod/data/pages/edit/edit.ts | 92 ++++++++++++++++++------- src/assets/lang/en.json | 1 + 5 files changed, 72 insertions(+), 28 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 2c1b0fd02..1e1606b32 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -507,11 +507,13 @@ "addon.mod_data.foundrecords": "data", "addon.mod_data.gettinglocation": "local_moodlemobileapp", "addon.mod_data.latlongboth": "data", + "addon.mod_data.locationnotenabled": "local_moodlemobileapp", "addon.mod_data.locationpermissiondenied": "local_moodlemobileapp", "addon.mod_data.menuchoose": "data", "addon.mod_data.modulenameplural": "data", "addon.mod_data.more": "data", "addon.mod_data.mylocation": "local_moodlemobileapp", + "addon.mod_data.noaccess": "data", "addon.mod_data.nomatch": "data", "addon.mod_data.norecords": "data", "addon.mod_data.notapproved": "data", diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json index 106896f4f..920d6f014 100644 --- a/src/addon/mod/data/lang/en.json +++ b/src/addon/mod/data/lang/en.json @@ -28,6 +28,7 @@ "modulenameplural": "Databases", "more": "More", "mylocation": "My location", + "noaccess": "You do not have access to this page", "nomatch": "No matching entries found!", "norecords": "No entries in database", "notapproved": "Entry is not approved yet.", diff --git a/src/addon/mod/data/pages/edit/edit.html b/src/addon/mod/data/pages/edit/edit.html index 7b5418da5..557f9a175 100644 --- a/src/addon/mod/data/pages/edit/edit.html +++ b/src/addon/mod/data/pages/edit/edit.html @@ -18,8 +18,8 @@
-
- +
+ diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index 72e56a5ac..2a911e615 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -93,7 +93,7 @@ export class AddonModDataEditPage { * View loaded. */ ionViewDidLoad(): void { - this.fetchEntryData(); + this.fetchEntryData(true); } /** @@ -126,38 +126,78 @@ export class AddonModDataEditPage { /** * Fetch the entry data. * + * @param [refresh] To refresh all downloaded data. * @return Resolved when done. */ - protected fetchEntryData(): Promise { - return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { - this.title = data.name || this.title; - this.data = data; - this.cssClass = 'addon-data-entries-' + data.id; + protected async fetchEntryData(refresh: boolean = false): Promise { + try { + this.data = await this.dataProvider.getDatabase(this.courseId, this.module.id); + this.title = this.data.name || this.title; + this.cssClass = 'addon-data-entries-' + this.data.id; - return this.dataProvider.getDatabaseAccessInformation(data.id, {cmId: this.module.id}); - }).then((accessData) => { - if (this.entryId) { - return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => { - this.groupInfo = groupInfo; - this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo); - }); - } - }).then(() => { - return this.dataProvider.getFields(this.data.id, {cmId: this.module.id}); - }).then((fieldsData) => { - this.fieldsArray = fieldsData; - this.fields = this.utils.arrayToObject(fieldsData, 'id'); + this.fieldsArray = await this.dataProvider.getFields(this.data.id, {cmId: this.module.id}); + this.fields = this.utils.arrayToObject(this.fieldsArray, 'id'); + + const entry = await this.dataHelper.fetchEntry(this.data, this.fieldsArray, this.entryId); - return this.dataHelper.fetchEntry(this.data, fieldsData, this.entryId); - }).then((entry) => { this.entry = entry.entry; + // Load correct group. + this.selectedGroup = this.selectedGroup == null ? this.entry.groupid : this.selectedGroup; + + // Check permissions when adding a new entry or offline entry. + if (!this.isEditing) { + let haveAccess = false; + + if (refresh) { + this.groupInfo = await this.groupsProvider.getActivityGroupInfo(this.data.coursemodule); + this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, this.groupInfo); + this.initialSelectedGroup = this.selectedGroup; + } + + if (this.groupInfo.groups.length > 0) { + if (refresh) { + const canAddGroup = {}; + + await Promise.all(this.groupInfo.groups.map(async (group) => { + const accessData = await this.dataProvider.getDatabaseAccessInformation(this.data.id, { + cmId: this.module.id, groupId: group.id}); + + canAddGroup[group.id] = accessData.canaddentry; + })); + + this.groupInfo.groups = this.groupInfo.groups.filter((group) => { + return !!canAddGroup[group.id]; + }); + + haveAccess = canAddGroup[this.selectedGroup]; + } else { + // Groups already filtered, so it have access. + haveAccess = true; + } + } else { + const accessData = await this.dataProvider.getDatabaseAccessInformation(this.data.id, {cmId: this.module.id}); + haveAccess = accessData.canaddentry; + } + + if (!haveAccess) { + // You shall not pass, go back. + this.domUtils.showErrorModal('addon.mod_data.noaccess', true); + + // Go back to entry list. + this.forceLeave = true; + this.navCtrl.pop(); + + return; + } + } + this.editFormRender = this.displayEditFields(); - }).catch((message) => { + } catch (message) { this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); - }).finally(() => { - this.loaded = true; - }); + } + + this.loaded = true; } /** @@ -166,7 +206,7 @@ export class AddonModDataEditPage { * @param e Event. * @return Resolved when done. */ - save(e: Event): Promise { + save(e: Event): Promise { e.preventDefault(); e.stopPropagation(); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index dedb0a4f6..dfc7d26d2 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -513,6 +513,7 @@ "addon.mod_data.modulenameplural": "Databases", "addon.mod_data.more": "More", "addon.mod_data.mylocation": "My location", + "addon.mod_data.noaccess": "You do not have access to this page", "addon.mod_data.nomatch": "No matching entries found!", "addon.mod_data.norecords": "No entries in database", "addon.mod_data.notapproved": "Entry is not approved yet.", From 572e907ac1e44d6828e1b4690c07f458d860fe94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 2 Oct 2020 10:26:08 +0200 Subject: [PATCH 118/182] MOBILE-3200 database: Translate some async functions --- src/addon/mod/data/providers/data.ts | 301 +++++++++++++-------------- 1 file changed, 141 insertions(+), 160 deletions(-) diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index 6fd85d2db..414249033 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -117,65 +117,51 @@ export class AddonModDataProvider { * @param forceOffline Force editing entry in offline. * @return Promise resolved when the action is done. */ - addEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], groupId: number = 0, + async addEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], groupId: number = 0, fields: any, siteId?: string, forceOffline: boolean = false): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. - const storeOffline = (): Promise => { - return this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId) - .then((entry) => { - return { - // Return provissional entry Id. - newentryid: entry, - sent: false, - }; - }); + const storeOffline = async (): Promise => { + const entry = await this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId); + + return { + // Return provissional entry Id. + newentryid: entry, + sent: false, + }; }; // Checks to store offline. if (!this.appProvider.isOnline() || forceOffline) { const notifications = this.checkFields(fields, contents); if (notifications) { - return Promise.resolve({ - fieldnotifications: notifications - }); + return { fieldnotifications: notifications }; } } - // Get other not synced actions. - return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { - if (entries && entries.length) { - // Found. Delete add and edit actions first. - const proms = []; - entries.forEach((entry) => { - if (entry.action == 'add') { - proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); - } - }); + // Remove unnecessary not synced actions. + await this.deleteEntryOfflineAction(dataId, entryId, 'add', siteId); - return Promise.all(proms); - } - }).then(() => { - // App is offline, store the action. - if (!this.appProvider.isOnline() || forceOffline) { - return storeOffline(); + // App is offline, store the action. + if (!this.appProvider.isOnline() || forceOffline) { + return storeOffline(); + } + + try { + const result = await this.addEntryOnline(dataId, contents, groupId, siteId); + result.sent = true; + + return result; + } catch (error) { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; } - return this.addEntryOnline(dataId, contents, groupId, siteId).then((result) => { - result.sent = true; - - return result; - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); - } - - // Couldn't connect to server, store in offline. - return storeOffline(); - }); - }); + // Couldn't connect to server, store in offline. + return storeOffline(); + } } /** @@ -212,48 +198,49 @@ export class AddonModDataProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ - approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise { + async approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. - const storeOffline = (): Promise => { + const storeOffline = async (): Promise => { const action = approve ? 'approve' : 'disapprove'; - return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId) - .then(() => { - return { - sent: false, - }; - }); + await this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId); + + return { + sent: false, + }; }; // Get if the opposite action is not synced. const oppositeAction = approve ? 'disapprove' : 'approve'; - return this.dataOffline.getEntry(dataId, entryId, oppositeAction, siteId).then(() => { - // Found. Just delete the action. - return this.dataOffline.deleteEntry(dataId, entryId, oppositeAction, siteId); - }).catch(() => { + const found = await this.deleteEntryOfflineAction(dataId, entryId, oppositeAction, siteId); + if (found) { + // Offline action has been found and deleted. Stop here. + return; + } - if (!this.appProvider.isOnline()) { - // App is offline, store the action. - return storeOffline(); + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + try { + await this.approveEntryOnline(entryId, approve, siteId); + + return { + sent: true, + }; + } catch (error) { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; } - return this.approveEntryOnline(entryId, approve, siteId).then(() => { - return { - sent: true, - }; - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); - } - - // Couldn't connect to server, store in offline. - return storeOffline(); - }); - }); + // Couldn't connect to server, store in offline. + return storeOffline(); + } } /** @@ -317,60 +304,45 @@ export class AddonModDataProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ - deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise { + async deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. - const storeOffline = (): Promise => { - return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId) - .then(() => { - return { - sent: false, - }; - }); + const storeOffline = async (): Promise => { + await this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId); + + return { + sent: false, + }; }; - let justAdded = false; - // Check if the opposite action is not synced and just delete it. - return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { - if (entries && entries.length) { - // Found. Delete other actions first. - const proms = entries.map((entry) => { - if (entry.action == 'add') { - justAdded = true; - } + const addedOffline = await this.deleteEntryOfflineAction(dataId, entryId, 'add', siteId); + if (addedOffline) { + // Offline add action found and deleted. Stop here. + return; + } - return this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId); - }); + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } - return Promise.all(proms); - } - }).then(() => { - if (justAdded) { - // The field was added offline, delete and stop. - return; + try { + await this.deleteEntryOnline(entryId, siteId); + + return { + sent: true, + }; + } catch (error) { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; } - if (!this.appProvider.isOnline()) { - // App is offline, store the action. - return storeOffline(); - } - - return this.deleteEntryOnline(entryId, siteId).then(() => { - return { - sent: true, - }; - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); - } - - // Couldn't connect to server, store in offline. - return storeOffline(); - }); - }); + // Couldn't connect to server, store in offline. + return storeOffline(); + } } /** @@ -390,6 +362,29 @@ export class AddonModDataProvider { }); } + /** + * Delete entry offline action. + * + * @param dataId Database ID. + * @param entryId Entry ID. + * @param action Action name to delete. + * @param siteId Site ID. + * @return Resolved with true if the action has been found and deleted. + */ + protected async deleteEntryOfflineAction(dataId: number, entryId: number, action: string, siteId: string): Promise { + // Get other not not synced actions. + try { + await this.dataOffline.getEntry(dataId, entryId, action, siteId); + + await this.dataOffline.deleteEntry(dataId, entryId, action, siteId); + + return true; + } catch (error) { + // Not found. + return false; + } + } + /** * Updates an existing entry. * @@ -402,64 +397,50 @@ export class AddonModDataProvider { * @param forceOffline Force editing entry in offline. * @return Promise resolved when the action is done. */ - editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, siteId?: string, - forceOffline: boolean = false): Promise { + async editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, + siteId?: string, forceOffline: boolean = false): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. - const storeOffline = (): Promise => { - return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId) - .then(() => { - return { - updated: true, - sent: false, - }; - }); + const storeOffline = async (): Promise => { + await this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId); + + return { + updated: true, + sent: false, + }; }; if (!this.appProvider.isOnline() || forceOffline) { const notifications = this.checkFields(fields, contents); if (notifications) { - return Promise.resolve({ - fieldnotifications: notifications - }); + return { fieldnotifications: notifications }; } } - // Get other not not synced actions. - return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { - if (entries && entries.length) { - // Found. Delete add and edit actions first. - const proms = []; - entries.forEach((entry) => { - if (entry.action == 'edit') { - proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); - } - }); + // Remove unnecessary not synced actions. + await this.deleteEntryOfflineAction(dataId, entryId, 'edit', siteId); - return Promise.all(proms); - } - }).then(() => { - if (!this.appProvider.isOnline() || forceOffline) { - // App is offline, store the action. - return storeOffline(); + if (!this.appProvider.isOnline() || forceOffline) { + // App is offline, store the action. + return storeOffline(); + } + + try { + const result = await this.editEntryOnline(entryId, contents, siteId); + result.sent = true; + + return result; + } catch (error) { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; } - return this.editEntryOnline(entryId, contents, siteId).then((result) => { - result.sent = true; - - return result; - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that responses cannot be submitted. - return Promise.reject(error); - } - - // Couldn't connect to server, store in offline. - return storeOffline(); - }); - }); - } + // Couldn't connect to server, store in offline. + return storeOffline(); + } +} /** * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. From 94b215816e352cb31e8a4139ef37816c5fba6e88 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 5 Oct 2020 11:42:21 +0200 Subject: [PATCH 119/182] MOBILE-3523 core: Fix handlers order if one hasn't priority --- src/core/contentlinks/providers/delegate.ts | 2 +- src/core/course/providers/options-delegate.ts | 2 +- src/core/fileuploader/providers/delegate.ts | 2 +- src/core/mainmenu/providers/delegate.ts | 2 +- src/core/pushnotifications/providers/delegate.ts | 1 + src/core/settings/providers/delegate.ts | 2 +- src/core/siteplugins/classes/handlers/course-option-handler.ts | 2 +- src/core/siteplugins/classes/handlers/main-menu-handler.ts | 2 +- src/core/siteplugins/classes/handlers/message-output-handler.ts | 2 +- src/core/siteplugins/classes/handlers/settings-handler.ts | 2 +- src/core/siteplugins/classes/handlers/user-handler.ts | 2 +- src/core/user/providers/user-delegate.ts | 2 +- 12 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/core/contentlinks/providers/delegate.ts b/src/core/contentlinks/providers/delegate.ts index b323f60be..eb9c526e1 100644 --- a/src/core/contentlinks/providers/delegate.ts +++ b/src/core/contentlinks/providers/delegate.ts @@ -191,7 +191,7 @@ export class CoreContentLinksDelegate { // Add them to the list. linkActions.push({ - priority: handler.priority, + priority: handler.priority || 0, actions: actions }); } diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index 815be116e..cf46e95de 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -405,7 +405,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { promises.push(Promise.resolve(getFunction.call(handler, injector, course)).then((data) => { handlersToDisplay.push({ data: data, - priority: handler.priority, + priority: handler.priority || 0, prefetch: handler.prefetch && handler.prefetch.bind(handler), name: handler.name }); diff --git a/src/core/fileuploader/providers/delegate.ts b/src/core/fileuploader/providers/delegate.ts index a0de9d939..77b8be619 100644 --- a/src/core/fileuploader/providers/delegate.ts +++ b/src/core/fileuploader/providers/delegate.ts @@ -177,7 +177,7 @@ export class CoreFileUploaderDelegate extends CoreDelegate { } const data: CoreFileUploaderHandlerDataToReturn = handler.getData(); - data.priority = handler.priority; + data.priority = handler.priority || 0; data.mimetypes = supportedMimetypes; handlers.push(data); } diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts index 9f07edc13..e044b6dd3 100644 --- a/src/core/mainmenu/providers/delegate.ts +++ b/src/core/mainmenu/providers/delegate.ts @@ -162,7 +162,7 @@ export class CoreMainMenuDelegate extends CoreDelegate { handlersData.push({ name: name, data: data, - priority: handler.priority + priority: handler.priority || 0, }); } diff --git a/src/core/pushnotifications/providers/delegate.ts b/src/core/pushnotifications/providers/delegate.ts index b8b10cbd7..94297132c 100644 --- a/src/core/pushnotifications/providers/delegate.ts +++ b/src/core/pushnotifications/providers/delegate.ts @@ -177,6 +177,7 @@ export class CorePushNotificationsDelegate { this.logger.log(`Registered addon '${handler.name}'`); this.clickHandlers[handler.name] = handler; + handler.priority = handler.priority || 0; return true; } diff --git a/src/core/settings/providers/delegate.ts b/src/core/settings/providers/delegate.ts index ea5557024..c6a892d0a 100644 --- a/src/core/settings/providers/delegate.ts +++ b/src/core/settings/providers/delegate.ts @@ -108,7 +108,7 @@ export class CoreSettingsDelegate extends CoreDelegate { handlersData.push({ data: data, - priority: handler.priority + priority: handler.priority || 0, }); } diff --git a/src/core/siteplugins/classes/handlers/course-option-handler.ts b/src/core/siteplugins/classes/handlers/course-option-handler.ts index e1a572172..90205d756 100644 --- a/src/core/siteplugins/classes/handlers/course-option-handler.ts +++ b/src/core/siteplugins/classes/handlers/course-option-handler.ts @@ -35,7 +35,7 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl protected utils: CoreUtilsProvider) { super(name); - this.priority = handlerSchema.priority; + this.priority = handlerSchema.priority || 0; this.isMenuHandler = !!handlerSchema.ismenuhandler; } diff --git a/src/core/siteplugins/classes/handlers/main-menu-handler.ts b/src/core/siteplugins/classes/handlers/main-menu-handler.ts index 5ff904abf..448c95672 100644 --- a/src/core/siteplugins/classes/handlers/main-menu-handler.ts +++ b/src/core/siteplugins/classes/handlers/main-menu-handler.ts @@ -25,7 +25,7 @@ export class CoreSitePluginsMainMenuHandler extends CoreSitePluginsBaseHandler i protected initResult: any) { super(name); - this.priority = handlerSchema.priority; + this.priority = handlerSchema.priority || 0; } /** diff --git a/src/core/siteplugins/classes/handlers/message-output-handler.ts b/src/core/siteplugins/classes/handlers/message-output-handler.ts index 1e0985ccc..569c1f3c5 100644 --- a/src/core/siteplugins/classes/handlers/message-output-handler.ts +++ b/src/core/siteplugins/classes/handlers/message-output-handler.ts @@ -32,7 +32,7 @@ export class CoreSitePluginsMessageOutputHandler extends CoreSitePluginsBaseHand */ getDisplayData(): AddonMessageOutputHandlerData { return { - priority: this.handlerSchema.priority, + priority: this.handlerSchema.priority || 0, label: this.title, icon: this.handlerSchema.displaydata.icon, page: 'CoreSitePluginsPluginPage', diff --git a/src/core/siteplugins/classes/handlers/settings-handler.ts b/src/core/siteplugins/classes/handlers/settings-handler.ts index 1267faa07..94c99c963 100644 --- a/src/core/siteplugins/classes/handlers/settings-handler.ts +++ b/src/core/siteplugins/classes/handlers/settings-handler.ts @@ -25,7 +25,7 @@ export class CoreSitePluginsSettingsHandler extends CoreSitePluginsBaseHandler i protected initResult: any) { super(name); - this.priority = handlerSchema.priority; + this.priority = handlerSchema.priority || 0; } /** diff --git a/src/core/siteplugins/classes/handlers/user-handler.ts b/src/core/siteplugins/classes/handlers/user-handler.ts index ae381c477..690c9fbc9 100644 --- a/src/core/siteplugins/classes/handlers/user-handler.ts +++ b/src/core/siteplugins/classes/handlers/user-handler.ts @@ -43,7 +43,7 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle protected utils: CoreUtilsProvider) { super(name); - this.priority = handlerSchema.priority; + this.priority = handlerSchema.priority || 0; // Only support TYPE_COMMUNICATION and TYPE_NEW_PAGE. this.type = handlerSchema.type != CoreUserDelegate.TYPE_COMMUNICATION ? diff --git a/src/core/user/providers/user-delegate.ts b/src/core/user/providers/user-delegate.ts index 588bb817d..b78ed122d 100644 --- a/src/core/user/providers/user-delegate.ts +++ b/src/core/user/providers/user-delegate.ts @@ -260,7 +260,7 @@ export class CoreUserDelegate extends CoreDelegate { userData.handlers.push({ name: name, data: handler.getDisplayData(user, courseId), - priority: handler.priority, + priority: handler.priority || 0, type: handler.type || CoreUserDelegate.TYPE_NEW_PAGE }); } From 0f968c6986e1101816db33a6f9d4c45ddb6e3464 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 15 Oct 2020 09:02:09 +0200 Subject: [PATCH 120/182] MOBILE-3523 login: Fix consecutive logins if logout+SSO --- src/core/mainmenu/pages/menu/menu.ts | 6 ++++-- src/providers/sites.ts | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts index bdf1cdbd4..bc2d6e39d 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -46,6 +46,7 @@ export class CoreMainMenuPage implements OnDestroy { protected urlToOpen: string; protected mainMenuId: number; protected keyboardObserver: any; + protected resizeFunction; @ViewChild('mainTabs') mainTabs: CoreIonTabsComponent; @@ -115,7 +116,8 @@ export class CoreMainMenuPage implements OnDestroy { } }); - window.addEventListener('resize', this.initHandlers.bind(this)); + this.resizeFunction = this.initHandlers.bind(this); + window.addEventListener('resize', this.resizeFunction); if (CoreApp.instance.isIOS()) { // In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done. @@ -241,7 +243,7 @@ export class CoreMainMenuPage implements OnDestroy { ngOnDestroy(): void { this.subscription && this.subscription.unsubscribe(); this.redirectObs && this.redirectObs.off(); - window.removeEventListener('resize', this.initHandlers.bind(this)); + window.removeEventListener('resize', this.resizeFunction); CoreApp.instance.setMainMenuOpen(this.mainMenuId, false); this.keyboardObserver && this.keyboardObserver.off(); } diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 99e60cb42..124b68e71 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -1549,11 +1549,16 @@ export class CoreSitesProvider { await this.dbReady; const site = await this.getSite(siteId); - const newValues = { - token: '', // Erase the token for security. + const newValues: any = { loggedOut: loggedOut ? 1 : 0 }; + if (loggedOut) { + // Erase the token for security. + newValues.token = ''; + site.token = ''; + } + site.setLoggedOut(loggedOut); return this.appDB.updateRecords(CoreSitesProvider.SITES_TABLE, newValues, { id: siteId }); From d986401aefa9c82e76268878dfdf74ffce4be906 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 15 Oct 2020 09:07:02 +0200 Subject: [PATCH 121/182] MOBILE-3523 config: Lock plugins and libraries for 3.9.3 --- package.json | 152 +++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 75c1227a1..a2049cb93 100644 --- a/package.json +++ b/package.json @@ -47,96 +47,96 @@ "windows.store": "npx electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop" }, "dependencies": { - "@angular/animations": "^5.2.11", - "@angular/common": "^5.2.11", - "@angular/compiler": "^5.2.11", - "@angular/compiler-cli": "^5.2.11", - "@angular/core": "^5.2.11", - "@angular/forms": "^5.2.11", - "@angular/platform-browser": "^5.2.11", - "@angular/platform-browser-dynamic": "^5.2.11", - "@ionic-native/badge": "^4.20.0", - "@ionic-native/camera": "^4.20.0", - "@ionic-native/chooser": "^4.20.0", - "@ionic-native/clipboard": "^4.20.0", - "@ionic-native/core": "^4.20.0", - "@ionic-native/device": "^4.20.0", - "@ionic-native/diagnostic": "^4.2.0", - "@ionic-native/file": "^4.20.0", - "@ionic-native/file-opener": "^4.20.0", - "@ionic-native/file-transfer": "^4.20.0", - "@ionic-native/geolocation": "^4.20.0", - "@ionic-native/globalization": "^4.20.0", - "@ionic-native/http": "^4.20.0", - "@ionic-native/in-app-browser": "^4.20.0", - "@ionic-native/keyboard": "^4.20.0", - "@ionic-native/local-notifications": "^4.20.0", - "@ionic-native/media": "^4.20.0", - "@ionic-native/media-capture": "^4.20.0", - "@ionic-native/network": "^4.20.0", - "@ionic-native/push": "^4.20.0", - "@ionic-native/qr-scanner": "^4.20.0", - "@ionic-native/screen-orientation": "^4.20.0", - "@ionic-native/splash-screen": "^4.20.0", - "@ionic-native/sqlite": "^4.20.0", - "@ionic-native/status-bar": "^4.20.0", - "@ionic-native/web-intent": "^4.20.0", - "@ionic-native/zip": "^4.20.0", - "@ngx-translate/core": "^8.0.0", - "@ngx-translate/http-loader": "^2.0.1", - "ajv": "^6.11.0", - "chart.js": "^2.9.3", - "com-darryncampbell-cordova-plugin-intent": "^1.3.0", - "cordova": "^10.0.0", - "cordova-android": "^8.1.0", - "cordova-android-support-gradle-release": "^3.0.1", - "cordova-clipboard": "^1.3.0", - "cordova-ios": "^5.1.1", - "cordova-plugin-advanced-http": "^2.4.1", - "cordova-plugin-badge": "^0.8.8", - "cordova-plugin-camera": "^4.1.0", - "cordova-plugin-chooser": "^1.3.2", - "cordova-plugin-customurlscheme": "^5.0.1", - "cordova-plugin-device": "^2.0.3", - "cordova-plugin-file": "^6.0.2", - "cordova-plugin-file-opener2": "^3.0.4", - "cordova-plugin-file-transfer": "^1.7.1", + "@angular/animations": "5.2.11", + "@angular/common": "5.2.11", + "@angular/compiler": "5.2.11", + "@angular/compiler-cli": "5.2.11", + "@angular/core": "5.2.11", + "@angular/forms": "5.2.11", + "@angular/platform-browser": "5.2.11", + "@angular/platform-browser-dynamic": "5.2.11", + "@ionic-native/badge": "4.20.0", + "@ionic-native/camera": "4.20.0", + "@ionic-native/chooser": "4.20.0", + "@ionic-native/clipboard": "4.20.0", + "@ionic-native/core": "4.20.0", + "@ionic-native/device": "4.20.0", + "@ionic-native/diagnostic": "4.2.0", + "@ionic-native/file": "4.20.0", + "@ionic-native/file-opener": "4.20.0", + "@ionic-native/file-transfer": "4.20.0", + "@ionic-native/geolocation": "4.20.0", + "@ionic-native/globalization": "4.20.0", + "@ionic-native/http": "4.20.0", + "@ionic-native/in-app-browser": "4.20.0", + "@ionic-native/keyboard": "4.20.0", + "@ionic-native/local-notifications": "4.20.0", + "@ionic-native/media": "4.20.0", + "@ionic-native/media-capture": "4.20.0", + "@ionic-native/network": "4.20.0", + "@ionic-native/push": "4.20.0", + "@ionic-native/qr-scanner": "4.20.0", + "@ionic-native/screen-orientation": "4.20.0", + "@ionic-native/splash-screen": "4.20.0", + "@ionic-native/sqlite": "4.20.0", + "@ionic-native/status-bar": "4.20.0", + "@ionic-native/web-intent": "4.20.0", + "@ionic-native/zip": "4.20.0", + "@ngx-translate/core": "8.0.0", + "@ngx-translate/http-loader": "2.0.1", + "ajv": "6.11.0", + "chart.js": "2.9.3", + "com-darryncampbell-cordova-plugin-intent": "1.3.0", + "cordova": "10.0.0", + "cordova-android": "8.1.0", + "cordova-android-support-gradle-release": "3.0.1", + "cordova-clipboard": "1.3.0", + "cordova-ios": "5.1.1", + "cordova-plugin-advanced-http": "2.4.1", + "cordova-plugin-badge": "0.8.8", + "cordova-plugin-camera": "4.1.0", + "cordova-plugin-chooser": "1.3.2", + "cordova-plugin-customurlscheme": "5.0.1", + "cordova-plugin-device": "2.0.3", + "cordova-plugin-file": "6.0.2", + "cordova-plugin-file-opener2": "3.0.4", + "cordova-plugin-file-transfer": "1.7.1", "cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", - "cordova-plugin-globalization": "^1.11.0", + "cordova-plugin-globalization": "1.11.0", "cordova-plugin-inappbrowser": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle", "cordova-plugin-ionic-keyboard": "2.1.3", "cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle", "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", - "cordova-plugin-media": "^5.0.3", - "cordova-plugin-media-capture": "^3.0.3", - "cordova-plugin-network-information": "^2.0.2", + "cordova-plugin-media": "5.0.3", + "cordova-plugin-media-capture": "3.0.3", + "cordova-plugin-network-information": "2.0.2", "cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist", - "cordova-plugin-screen-orientation": "^3.0.2", - "cordova-plugin-splashscreen": "^6.0.0", - "cordova-plugin-statusbar": "^2.4.3", - "cordova-plugin-whitelist": "^1.3.4", + "cordova-plugin-screen-orientation": "3.0.2", + "cordova-plugin-splashscreen": "6.0.0", + "cordova-plugin-statusbar": "2.4.3", + "cordova-plugin-whitelist": "1.3.4", "cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git", "cordova-plugin-wkwebview-cookies": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git", - "cordova-plugin-zip": "^3.1.0", - "cordova-sqlite-storage": "^4.0.0", - "cordova-support-google-services": "^1.3.2", - "es6-promise-plugin": "^4.2.2", - "font-awesome": "^4.7.0", + "cordova-plugin-zip": "3.1.0", + "cordova-sqlite-storage": "4.0.0", + "cordova-support-google-services": "1.3.2", + "es6-promise-plugin": "4.2.2", + "font-awesome": "4.7.0", "inquirer": "^7.3.2", "ionic-angular": "3.9.9", "ionicons": "3.0.0", - "jszip": "^3.1.5", + "jszip": "3.1.5", "mathjax": "2.7.7", - "moment": "^2.24.0", - "nl.kingsquare.cordova.background-audio": "^1.0.1", - "phonegap-plugin-multidex": "^1.0.0", + "moment": "2.24.0", + "nl.kingsquare.cordova.background-audio": "1.0.1", + "phonegap-plugin-multidex": "1.0.0", "phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v3", "promise.prototype.finally": "3.1.0", - "rxjs": "^5.5.12", - "sw-toolbox": "^3.6.0", - "ts-md5": "^1.2.7", - "web-animations-js": "^2.3.2", - "zone.js": "^0.8.29" + "rxjs": "5.5.12", + "sw-toolbox": "3.6.0", + "ts-md5": "1.2.7", + "web-animations-js": "2.3.2", + "zone.js": "0.8.29" }, "devDependencies": { "@ionic/app-scripts": "3.2.3", From 514ccda794da0fcb3c245e06829099ce3c1b715d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 15 Oct 2020 09:27:09 +0200 Subject: [PATCH 122/182] MOBILE-3523 ios: Fix error calculating platform version --- .../editor/components/rich-text-editor/rich-text-editor.ts | 6 ++++-- src/core/fileuploader/providers/file-handler.ts | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/editor/components/rich-text-editor/rich-text-editor.ts index 2bc3fb491..4defb0b7e 100644 --- a/src/core/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/editor/components/rich-text-editor/rich-text-editor.ts @@ -15,6 +15,7 @@ import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy, Optional } from '@angular/core'; import { TextInput, Content, Platform, Slides } from 'ionic-angular'; +import { Device } from '@ionic-native/device'; import { CoreApp } from '@providers/app'; import { CoreSitesProvider } from '@providers/sites'; import { CoreFilepoolProvider } from '@providers/filepool'; @@ -118,7 +119,8 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe protected events: CoreEventsProvider, protected utils: CoreUtilsProvider, protected platform: Platform, - protected editorOffline: CoreEditorOfflineProvider) { + protected editorOffline: CoreEditorOfflineProvider, + protected device: Device) { this.contentChanged = new EventEmitter(); this.element = elementRef.nativeElement as HTMLDivElement; this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp. @@ -236,7 +238,7 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe if (CoreApp.instance.isAndroid()) { // In Android we ignore the keyboard height because it is not part of the web view. height = this.domUtils.getContentHeight(this.content) - this.getSurroundingHeight(this.element); - } else if (CoreApp.instance.isIOS() && this.kbHeight > 0 && this.platform.version().major < 12) { + } else if (CoreApp.instance.isIOS() && this.kbHeight > 0 && Number(this.device.version.split('.')[0]) < 12) { // Keyboard open in iOS 11 or previous. The window height changes when the keyboard is open. height = window.innerHeight - this.getSurroundingHeight(this.element); diff --git a/src/core/fileuploader/providers/file-handler.ts b/src/core/fileuploader/providers/file-handler.ts index 01f93288b..5127a8b8c 100644 --- a/src/core/fileuploader/providers/file-handler.ts +++ b/src/core/fileuploader/providers/file-handler.ts @@ -44,8 +44,7 @@ export class CoreFileUploaderFileHandler implements CoreFileUploaderHandler { * @return True or promise resolved with true if enabled. */ isEnabled(): boolean | Promise { - return CoreApp.instance.isAndroid() || !CoreApp.instance.isMobile() || - (CoreApp.instance.isIOS() && this.platform.version().major >= 9); + return true; } /** From 0124b4d567500e558b9d19c0c452466269443c2e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 15 Oct 2020 10:27:42 +0200 Subject: [PATCH 123/182] MOBILE-3523 ios: Fix embedded iframes cookies in iOS --- src/components/iframe/iframe.ts | 22 +--------------------- src/directives/format-text.ts | 30 ++++++++++++++++++------------ src/providers/utils/iframe.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index 0bc3cabde..e1c8dd430 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -24,9 +24,6 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreIframeUtilsProvider } from '@providers/utils/iframe'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { CoreUrl } from '@singletons/url'; -import { CoreApp } from '@providers/app'; -import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies'; @Component({ selector: 'core-iframe', @@ -107,24 +104,7 @@ export class CoreIframeComponent implements OnChanges { if (changes.src) { const url = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue; - if (CoreApp.instance.isIOS() && url && !this.urlUtils.isLocalFileUrl(url)) { - // Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView. - try { - const win = window; - const urlParts = CoreUrl.parse(url); - - if (urlParts.domain) { - await win.WKWebViewCookies.setCookie({ - name: 'MoodleAppCookieForWKWebView', - value: '1', - domain: urlParts.domain, - }); - } - } catch (err) { - // Ignore errors. - this.logger.error('Error setting cookie', err); - } - } + await this.iframeUtils.fixIframeCookies(url); this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(CoreFile.instance.convertFileSrc(url)); diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 6cc11c983..79f0f85e3 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -450,6 +450,7 @@ export class CoreFormatTextDirective implements OnChanges { const div = document.createElement('div'), canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']), navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + const promises = []; div.innerHTML = formatted; @@ -504,7 +505,7 @@ export class CoreFormatTextDirective implements OnChanges { }); iframes.forEach((iframe) => { - this.treatIframe(iframe, site, canTreatVimeo, navCtrl); + promises.push(this.treatIframe(iframe, site, canTreatVimeo, navCtrl)); }); svgImages.forEach((image) => { @@ -543,10 +544,9 @@ export class CoreFormatTextDirective implements OnChanges { this.domUtils.handleBootstrapTooltips(div); // Wait for images to load. - let promise: Promise = null; if (externalImages.length) { // Automatically reject the promise after 5 seconds to prevent blocking the user forever. - promise = this.utils.timeoutPromise(this.utils.allPromises(externalImages.map((externalImage): any => { + promises.push(this.utils.timeoutPromise(this.utils.allPromises(externalImages.map((externalImage): any => { if (externalImage.loaded) { // Image has already been loaded, no need to wait. return Promise.resolve(); @@ -558,12 +558,10 @@ export class CoreFormatTextDirective implements OnChanges { resolve(); }); }); - })), 5000); - } else { - promise = Promise.resolve(); + })), 5000)); } - return promise.catch(() => { + return Promise.all(promises).catch(() => { // Ignore errors. So content gets always shown. }).then(() => { result.div = div; @@ -665,7 +663,8 @@ export class CoreFormatTextDirective implements OnChanges { * @param canTreatVimeo Whether Vimeo videos can be treated in the site. * @param navCtrl NavController to use. */ - protected treatIframe(iframe: HTMLIFrameElement, site: CoreSite, canTreatVimeo: boolean, navCtrl: NavController): void { + protected async treatIframe(iframe: HTMLIFrameElement, site: CoreSite, canTreatVimeo: boolean, navCtrl: NavController) + : Promise { const src = iframe.src, currentSite = this.sitesProvider.getCurrentSite(); @@ -673,15 +672,19 @@ export class CoreFormatTextDirective implements OnChanges { if (currentSite && currentSite.containsUrl(src)) { // URL points to current site, try to use auto-login. - currentSite.getAutoLoginUrl(src, false).then((finalUrl) => { - iframe.src = finalUrl; + const finalUrl = await currentSite.getAutoLoginUrl(src, false); - this.iframeUtils.treatFrame(iframe, false, navCtrl); - }); + await this.iframeUtils.fixIframeCookies(finalUrl); + + iframe.src = finalUrl; + + this.iframeUtils.treatFrame(iframe, false, navCtrl); return; } + await this.iframeUtils.fixIframeCookies(src); + if (src && canTreatVimeo) { // Check if it's a Vimeo video. If it is, use the wsplayer script instead to make restricted videos work. const matches = iframe.src.match(/https?:\/\/player\.vimeo\.com\/video\/([0-9]+)/); @@ -714,6 +717,9 @@ export class CoreFormatTextDirective implements OnChanges { if (site && !site.isVersionGreaterEqualThan('3.7')) { newUrl += '&width=' + width + '&height=' + height; } + + await this.iframeUtils.fixIframeCookies(newUrl); + iframe.src = newUrl; if (!iframe.width) { diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index 0fdc5af74..56c5e11c8 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -30,6 +30,7 @@ import { makeSingleton } from '@singletons/core.singletons'; import { CoreUrl } from '@singletons/url'; import { CoreWindow } from '@singletons/window'; import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript'; +import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies'; /* * "Utils" service with helper functions for iframes, embed and similar. @@ -488,6 +489,36 @@ export class CoreIframeUtilsProvider { } } } + + /** + * Fix cookies for an iframe URL. + * + * @param url URL of the iframe. + * @return Promise resolved when done. + */ + async fixIframeCookies(url: string): Promise { + if (!CoreApp.instance.isIOS() || !url || this.urlUtils.isLocalFileUrl(url)) { + // No need to fix cookies. + return; + } + + // Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView. + try { + const win = window; + const urlParts = CoreUrl.parse(url); + + if (urlParts.domain) { + await win.WKWebViewCookies.setCookie({ + name: 'MoodleAppCookieForWKWebView', + value: '1', + domain: urlParts.domain, + }); + } + } catch (err) { + // Ignore errors. + this.logger.error('Error setting cookie', err); + } + } } export class CoreIframeUtils extends makeSingleton(CoreIframeUtilsProvider) {} From e0413f8ebe96c7ca034e47c7997da57dd2534b94 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Wed, 14 Oct 2020 10:05:28 +0700 Subject: [PATCH 124/182] MOBILE-3569 resource: Allow IFRAME to use XHTML --- src/addon/mod/resource/providers/helper.ts | 2 +- src/assets/js/iframe-treat-links.js | 2 +- src/providers/utils/iframe.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/addon/mod/resource/providers/helper.ts b/src/addon/mod/resource/providers/helper.ts index 62a00e511..c1566e931 100644 --- a/src/addon/mod/resource/providers/helper.ts +++ b/src/addon/mod/resource/providers/helper.ts @@ -142,7 +142,7 @@ export class AddonModResourceHelperProvider { mimetype = this.mimetypeUtils.getMimeType(ext); } - return mimetype == 'text/html'; + return mimetype == 'text/html' || mimetype == 'application/xhtml+xml'; } /** diff --git a/src/assets/js/iframe-treat-links.js b/src/assets/js/iframe-treat-links.js index 68f83571b..c585c96fe 100644 --- a/src/assets/js/iframe-treat-links.js +++ b/src/assets/js/iframe-treat-links.js @@ -49,7 +49,7 @@ // Find the link being clicked. var el = event.target; - while (el && el.tagName !== 'A') { + while (el && (el.tagName !== 'A' && el.tagName !== 'a')) { el = el.parentElement; } diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts index 0fdc5af74..6a044b558 100644 --- a/src/providers/utils/iframe.ts +++ b/src/providers/utils/iframe.ts @@ -329,7 +329,7 @@ export class CoreIframeUtilsProvider { // Find the link being clicked. let el = event.target; - while (el && el.tagName !== 'A') { + while (el && el.tagName !== 'A' && el.tagName !== 'a') { el = el.parentElement; } From b6cb6439784346b464f832421616e5d18c1bd308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 15 Oct 2020 22:30:09 +0200 Subject: [PATCH 125/182] MOBILE-3572 settings: Set light mode as default scheme mode --- src/core/settings/pages/general/general.ts | 12 +----- src/core/settings/providers/helper.ts | 48 ++++++++++++++++++---- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/core/settings/pages/general/general.ts b/src/core/settings/pages/general/general.ts index 89836bfd9..ccba76780 100644 --- a/src/core/settings/pages/general/general.ts +++ b/src/core/settings/pages/general/general.ts @@ -71,17 +71,9 @@ export class CoreSettingsGeneralPage { this.colorSchemes.push('light'); this.selectedScheme = this.colorSchemes[0]; } else { - let defaultColorScheme = 'light'; + this.colorSchemes = this.settingsHelper.getAllowedColorSchemes(); - if (window.matchMedia('(prefers-color-scheme: dark)').matches || - window.matchMedia('(prefers-color-scheme: light)').matches) { - this.colorSchemes.push('auto'); - defaultColorScheme = 'auto'; - } - this.colorSchemes.push('light'); - this.colorSchemes.push('dark'); - - this.configProvider.get(CoreConstants.SETTINGS_COLOR_SCHEME, defaultColorScheme).then((scheme) => { + this.configProvider.get(CoreConstants.SETTINGS_COLOR_SCHEME, 'light').then((scheme) => { this.selectedScheme = scheme; }); } diff --git a/src/core/settings/providers/helper.ts b/src/core/settings/providers/helper.ts index b2cb12589..acc335e05 100644 --- a/src/core/settings/providers/helper.ts +++ b/src/core/settings/providers/helper.ts @@ -44,6 +44,7 @@ export interface CoreSiteSpaceUsage { export class CoreSettingsHelper { protected logger; protected syncPromises = {}; + protected colorSchemes: string[] = []; constructor(loggerProvider: CoreLoggerProvider, protected appProvider: CoreAppProvider, @@ -342,14 +343,7 @@ export class CoreSettingsHelper { if (!!CoreConfigConstants.forceColorScheme) { this.setColorScheme(CoreConfigConstants.forceColorScheme); } else { - let defaultColorScheme = 'light'; - - if (window.matchMedia('(prefers-color-scheme: dark)').matches || - window.matchMedia('(prefers-color-scheme: light)').matches) { - defaultColorScheme = 'auto'; - } - - this.configProvider.get(CoreConstants.SETTINGS_COLOR_SCHEME, defaultColorScheme).then((scheme) => { + this.configProvider.get(CoreConstants.SETTINGS_COLOR_SCHEME, 'light').then((scheme) => { this.setColorScheme(scheme); }); } @@ -388,6 +382,40 @@ export class CoreSettingsHelper { document.documentElement.style.fontSize = fontSize + '%'; } + /** + * Get system allowed color schemes. + * + * @return Allowed color schemes. + */ + getAllowedColorSchemes(): string[] { + if (this.colorSchemes.length > 0) { + return this.colorSchemes; + } + + if (!CoreConfigConstants.forceColorScheme) { + this.colorSchemes.push('light'); + this.colorSchemes.push('dark'); + if (window.matchMedia('(prefers-color-scheme)').media !== 'not all') { + this.colorSchemes.push('auto'); + } + } else { + this.colorSchemes = [CoreConfigConstants.forceColorScheme]; + } + + return this.colorSchemes; + } + + /** + * Toggle Dark on auto mode. + * + * @param dark If dark scheme should be set or removed. + */ + protected toggleDarkTheme(dark: boolean): void { + if (document.body.classList.contains('scheme-auto')) { + document.body.classList.toggle('scheme-dark', dark); + } + } + /** * Set body color scheme. * @@ -397,6 +425,10 @@ export class CoreSettingsHelper { document.body.classList.remove('scheme-light'); document.body.classList.remove('scheme-dark'); document.body.classList.remove('scheme-auto'); + + const colorSchemes = this.getAllowedColorSchemes(); + + colorScheme = colorSchemes.indexOf(colorScheme) >= 0 ? colorScheme : colorSchemes[0]; document.body.classList.add('scheme-' + colorScheme); } } From 5cf0204240e1d03151538d9387685030eaea1e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 16 Oct 2020 11:03:01 +0200 Subject: [PATCH 126/182] MOBILE-3572 settings: Add notice when auto color scheme is selected --- src/assets/lang/en.json | 1 + src/core/settings/lang/en.json | 1 + src/core/settings/pages/general/general.html | 5 ++++- src/core/settings/pages/general/general.ts | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index dfc7d26d2..391cb9b50 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -2001,6 +2001,7 @@ "core.settings.cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", "core.settings.colorscheme": "Color Scheme", "core.settings.colorscheme-auto": "Auto (based on system settings)", + "core.settings.colorscheme-auto-notice": "Auto mode may not work in some Android devices.", "core.settings.colorscheme-dark": "Dark", "core.settings.colorscheme-light": "Light", "core.settings.compilationinfo": "Compilation info", diff --git a/src/core/settings/lang/en.json b/src/core/settings/lang/en.json index d87510a7d..33c24a3e4 100644 --- a/src/core/settings/lang/en.json +++ b/src/core/settings/lang/en.json @@ -7,6 +7,7 @@ "cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", "colorscheme": "Color Scheme", "colorscheme-auto": "Auto (based on system settings)", + "colorscheme-auto-notice": "Auto mode may not work in some Android devices.", "colorscheme-dark": "Dark", "colorscheme-light": "Light", "compilationinfo": "Compilation info", diff --git a/src/core/settings/pages/general/general.html b/src/core/settings/pages/general/general.html index 2cd97f99f..aa271537d 100644 --- a/src/core/settings/pages/general/general.html +++ b/src/core/settings/pages/general/general.html @@ -20,7 +20,7 @@ - +

{{ 'core.settings.colorscheme' | translate }}

{{ 'core.settings.forcedsetting' | translate }}

@@ -29,6 +29,9 @@ {{ 'core.settings.colorscheme-' + scheme | translate }}
+ +

{{ 'core.settings.colorscheme-auto-notice' | translate }}

+

{{ 'core.settings.enablerichtexteditor' | translate }}

diff --git a/src/core/settings/pages/general/general.ts b/src/core/settings/pages/general/general.ts index ccba76780..b0b03a40d 100644 --- a/src/core/settings/pages/general/general.ts +++ b/src/core/settings/pages/general/general.ts @@ -15,6 +15,7 @@ import { Component, ViewChild } from '@angular/core'; import { IonicPage, Segment } from 'ionic-angular'; import { CoreConstants } from '@core/constants'; +import { CoreApp } from '@providers/app'; import { CoreConfigProvider } from '@providers/config'; import { CoreFileProvider } from '@providers/file'; import { CoreEventsProvider } from '@providers/events'; @@ -46,6 +47,7 @@ export class CoreSettingsGeneralPage { colorSchemes = []; selectedScheme: string; colorSchemeDisabled: boolean; + isAndroid: boolean; constructor(protected configProvider: CoreConfigProvider, fileProvider: CoreFileProvider, @@ -71,6 +73,8 @@ export class CoreSettingsGeneralPage { this.colorSchemes.push('light'); this.selectedScheme = this.colorSchemes[0]; } else { + this.isAndroid = CoreApp.instance.isAndroid(); + this.colorSchemes = this.settingsHelper.getAllowedColorSchemes(); this.configProvider.get(CoreConstants.SETTINGS_COLOR_SCHEME, 'light').then((scheme) => { From 5452ce6cffd9501531949e17988c202d8ce26a35 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 16 Oct 2020 12:18:42 +0200 Subject: [PATCH 127/182] MOBILE-3571 core: Split calls with too many parameters --- src/classes/site.ts | 13 ++- .../providers/module-prefetch-delegate.ts | 6 +- src/core/filter/providers/filter.ts | 6 +- src/providers/ws.ts | 86 ++++++++++++++++++- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/src/classes/site.ts b/src/classes/site.ts index 10c9c3e27..b3285842f 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -20,7 +20,9 @@ import { CoreDbProvider } from '@providers/db'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets } from '@providers/ws'; +import { + CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets, CoreWSPreSetsSplitRequest +} from '@providers/ws'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -138,6 +140,12 @@ export interface CoreSiteWSPreSets { * Component id. Optionally included when 'component' is set. */ componentId?: number; + + /** + * Whether to split a request if it has too many parameters. Sending too many parameters to the site + * can cause the request to fail (see PHP's max_input_vars). + */ + splitRequest?: CoreWSPreSetsSplitRequest; } /** @@ -650,7 +658,8 @@ export class CoreSite { siteUrl: this.siteUrl, cleanUnicode: this.cleanUnicode, typeExpected: preSets.typeExpected, - responseExpected: preSets.responseExpected + responseExpected: preSets.responseExpected, + splitRequest: preSets.splitRequest, }; if (wsPreSets.cleanUnicode && this.textUtils.hasUnicodeData(data)) { diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 53a236b09..888725bce 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -471,7 +471,11 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { preSets: CoreSiteWSPreSets = { cacheKey: this.getCourseUpdatesCacheKey(courseId), emergencyCache: false, // If downloaded data has changed and offline, just fail. See MOBILE-2085. - uniqueCacheKey: true + uniqueCacheKey: true, + splitRequest: { + param: 'tocheck', + maxLength: 10, + }, }; return site.read('core_course_check_updates', params, preSets).then((response) => { diff --git a/src/core/filter/providers/filter.ts b/src/core/filter/providers/filter.ts index cfc5bc66d..c23f7a912 100644 --- a/src/core/filter/providers/filter.ts +++ b/src/core/filter/providers/filter.ts @@ -272,7 +272,11 @@ export class CoreFilterProvider { }, preSets = { cacheKey: this.getAvailableInContextsCacheKey(contextsToSend), - updateFrequency: CoreSite.FREQUENCY_RARELY + updateFrequency: CoreSite.FREQUENCY_RARELY, + splitRequest: { + param: 'contexts', + maxLength: 300, + }, }; return site.read('core_filters_get_available_in_context', data, preSets) diff --git a/src/providers/ws.ts b/src/providers/ws.ts index 86fd9070a..9a11d4ee4 100644 --- a/src/providers/ws.ts +++ b/src/providers/ws.ts @@ -57,8 +57,34 @@ export interface CoreWSPreSets { * Defaults to false. Clean multibyte Unicode chars from data. */ cleanUnicode?: boolean; + + /** + * Whether to split a request if it has too many parameters. Sending too many parameters to the site + * can cause the request to fail (see PHP's max_input_vars). + */ + splitRequest?: CoreWSPreSetsSplitRequest; } +/** + * Options to split a request. + */ +export type CoreWSPreSetsSplitRequest = { + /** + * Name of the parameter used to split the request if too big. Must be an array parameter. + */ + param: string; + + /** + * Max number of entries sent per request. + */ + maxLength: number; + + /** + * Callback to combine the results. If not supplied, arrays in the result will be concatenated. + */ + combineCallback?: (previousValue: any, currentValue: any, currentIndex: number, array: any[]) => any; +}; + /** * PreSets accepted by AJAX WS calls. */ @@ -622,7 +648,7 @@ export class CoreWSProvider { } /** - * Perform the post call and save the promise while waiting to be resolved. + * Perform the post call. It can be split into several requests. * * @param method The WebService method to be called. * @param siteUrl Complete site url to perform the call. @@ -639,6 +665,64 @@ export class CoreWSProvider { options['responseType'] = 'text'; } + if (!preSets.splitRequest || !ajaxData[preSets.splitRequest.param]) { + return this.performSinglePost(method, siteUrl, ajaxData, preSets, options); + } + + // Split the request into several requests if needed. + const promises: Promise[] = []; + + for (let i = 0; i < ajaxData[preSets.splitRequest.param].length; i += preSets.splitRequest.maxLength) { + // Limit the array sent. + const limitedData = Object.assign({}, ajaxData); + limitedData[preSets.splitRequest.param] = + ajaxData[preSets.splitRequest.param].slice(i, i + preSets.splitRequest.maxLength); + + promises.push(this.performSinglePost(method, siteUrl, limitedData, preSets, options)); + } + + return Promise.all(promises).then((results) => { + // Combine the results. + const firstResult = results.shift(); + + if (preSets.splitRequest.combineCallback) { + return results.reduce(preSets.splitRequest.combineCallback, firstResult); + } + + return results.reduce(this.combineObjectsArrays, firstResult); + }); + } + + /** + * Combine the arrays of two objects. + * + * @param object1 First object. + * @param object2 Second object. + * @return Combined object. + */ + protected combineObjectsArrays(object1: any, object2: any): any { + for (const name in object2) { + if (Array.isArray(object2[name])) { + object1[name] = object1[name].concat(object2[name]); + } + } + + return object1; + } + + /** + * Perform a single post request. + * + * @param method The WebService method to be called. + * @param siteUrl Complete site url to perform the call. + * @param ajaxData Arguments to pass to the method. + * @param preSets Extra settings and information. + * @param options Request options. + * @return Promise resolved with the response data in success and rejected with CoreWSError if it fails. + */ + protected performSinglePost(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets, options: any) + : Promise { + // We add the method name to the URL purely to help with debugging. // This duplicates what is in the ajaxData, but that does no harm. // POST variables take precedence over GET. From e76f0d6cc0bf3931c73ea209804eb332683f3643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 2 Oct 2020 16:08:22 +0200 Subject: [PATCH 128/182] MOBILE-3523 login: Fix and simplify login style personalization --- src/core/login/login.scss | 33 ++++++++++++++++--- .../login/pages/credentials/credentials.html | 2 +- src/core/login/pages/reconnect/reconnect.html | 2 +- src/core/login/pages/reconnect/reconnect.scss | 25 +++++--------- src/core/login/pages/site/site.html | 4 +-- src/core/login/pages/site/site.scss | 20 ++++++++++- 6 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/core/login/login.scss b/src/core/login/login.scss index 8206b3750..a91ae89c3 100644 --- a/src/core/login/login.scss +++ b/src/core/login/login.scss @@ -2,16 +2,16 @@ $core-login-page-background-color: $white !default; $core-login-page-text-color: $text-color !default; $core-login-button-outline: false !default; $core-login-loading-color: false !default; -$core-login-item-inner-background-color: $white !default; -$core-login-item-background-color: $white !default; +$core-login-input-item-background-color: $core-login-page-background-color !default; +$core-login-input-item-text-color: $core-login-page-text-color !default; // Dark. $core-dark-login-page-background-color: $black !default; $core-dark-login-page-text-color: $core-dark-text-color !default; -$core-dark-login-item-inner-background-color: $core-dark-login-page-background-color !default; -$core-dark-login-item-background-color: $core-dark-login-page-background-color !default; $core-dark-login-button-outline: $core-login-button-outline !default; $core-dark-login-loading-color: $core-dark-text-color !default; +$core-dark-input-login-item-background-color: $core-dark-login-page-background-color !default; +$core-dark-input-login-item-text-color: $core-dark-login-page-text-color !default; ion-app.app-root page-core-login-credentials, ion-app.app-root page-core-login-reconnect, @@ -26,6 +26,13 @@ ion-app.app-root page-core-login-site { } } + .item p, .item h2, .item h3, .list .item.core-oauth-icon .label, .text-input { + color: $core-login-page-text-color; + @include darkmode() { + color: $core-dark-login-page-text-color; + } + } + img { max-width: 100%; } @@ -78,6 +85,22 @@ ion-app.app-root page-core-login-site { margin-bottom: 20px; } + + .core-login-form .item, + .core-login-form .item-inner { + background-color: $core-login-input-item-background-color; + @include darkmode() { + background-color: $core-dark-input-login-item-background-color; + } + + p, input { + color: $core-login-input-item-text-color; + @include darkmode() { + color: $core-dark-input-login-item-text-color; + } + } + } + ion-list.core-login-forgotten-password { margin-top: 0; margin-bottom: 0; @@ -89,6 +112,8 @@ ion-app.app-root page-core-login-site { @include darkmode() { background: transparent; } + + color: $core-login-page-text-color; } } diff --git a/src/core/login/pages/credentials/credentials.html b/src/core/login/pages/credentials/credentials.html index 690b3d93b..1d501a496 100644 --- a/src/core/login/pages/credentials/credentials.html +++ b/src/core/login/pages/credentials/credentials.html @@ -47,7 +47,7 @@ -
diff --git a/src/addon/qtype/essay/component/addon-qtype-essay.html b/src/addon/qtype/essay/component/addon-qtype-essay.html index 4876c04e0..0564f70b1 100644 --- a/src/addon/qtype/essay/component/addon-qtype-essay.html +++ b/src/addon/qtype/essay/component/addon-qtype-essay.html @@ -4,49 +4,50 @@

- - - - - - - - - - + + + + + + + + + + + + - - - -

{{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }}

-
- -

-
+ + + +

{{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }}

+
+ +

+
+
+ + + + + + + + + +

{{ 'core.question.errorattachmentsnotsupportedinsite' | translate }}

+
+
- - - - - - - - - - -

{{ 'core.question.errorattachmentsnotsupportedinsite' | translate }}

+ + + + +

+ + +
- - - -

-
- - -
- -
-
diff --git a/src/addon/qtype/essay/component/essay.ts b/src/addon/qtype/essay/component/essay.ts index 6fc1dd19c..aee330d49 100644 --- a/src/addon/qtype/essay/component/essay.ts +++ b/src/addon/qtype/essay/component/essay.ts @@ -44,11 +44,11 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen */ ngOnInit(): void { this.uploadFilesSupported = typeof this.question.responsefileareas != 'undefined'; - this.initEssayComponent(); + this.initEssayComponent(this.review); this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text); - if (this.question.allowsAttachments && this.uploadFilesSupported) { + if (this.question.allowsAttachments && this.uploadFilesSupported && !this.review) { this.loadAttachments(); } } diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts index 92820067c..91a238153 100644 --- a/src/core/question/classes/base-question-component.ts +++ b/src/core/question/classes/base-question-component.ts @@ -32,6 +32,7 @@ export class CoreQuestionBaseComponent { @Input() contextLevel?: string; // The context level. @Input() contextInstanceId?: number; // The instance ID related to the context. @Input() courseId?: number; // The course the question belongs to (if any). + @Input() review?: boolean; // Whether the user is in review mode. @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. @@ -207,99 +208,118 @@ export class CoreQuestionBaseComponent { /** * Initialize a question component of type essay. * + * @param review Whether we're in review mode. * @return Element containing the question HTML, void if the data is not valid. */ - initEssayComponent(): void | HTMLElement { + initEssayComponent(review?: boolean): void | HTMLElement { const questionEl = this.initComponent(); - if (questionEl) { - const textarea = questionEl.querySelector('textarea[name*=_answer]'); - const answerDraftIdInput = questionEl.querySelector('input[name*="_answer:itemid"]'); + if (!questionEl) { + return; + } + + const answerDraftIdInput = questionEl.querySelector('input[name*="_answer:itemid"]'); + + if (this.question.settings) { + this.question.allowsAttachments = this.question.settings.attachments != '0'; + this.question.allowsAnswerFiles = this.question.settings.responseformat == 'editorfilepicker'; + this.question.isMonospaced = this.question.settings.responseformat == 'monospaced'; + this.question.isPlainText = this.question.isMonospaced || this.question.settings.responseformat == 'plain'; + this.question.hasInlineText = this.question.settings.responseformat != 'noinline'; + } else { + this.question.allowsAttachments = !!questionEl.querySelector('div[id*=filemanager]'); + this.question.allowsAnswerFiles = !!answerDraftIdInput; + this.question.isMonospaced = !!questionEl.querySelector('.qtype_essay_monospaced'); + this.question.isPlainText = this.question.isMonospaced || !!questionEl.querySelector('.qtype_essay_plain'); + } + + if (review) { + // Search the answer and the attachments. + this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); if (this.question.settings) { - this.question.allowsAttachments = this.question.settings.attachments != '0'; - this.question.allowsAnswerFiles = this.question.settings.responseformat == 'editorfilepicker'; - this.question.isMonospaced = this.question.settings.responseformat == 'monospaced'; - this.question.isPlainText = this.question.isMonospaced || this.question.settings.responseformat == 'plain'; + this.question.attachments = Array.from(this.questionHelper.getResponseFileAreaFiles(this.question, 'attachments')); } else { - this.question.allowsAttachments = !!questionEl.querySelector('div[id*=filemanager]'); - this.question.allowsAnswerFiles = !!answerDraftIdInput; - this.question.isMonospaced = !!questionEl.querySelector('.qtype_essay_monospaced'); - this.question.isPlainText = this.question.isMonospaced || !!questionEl.querySelector('.qtype_essay_plain'); - } - - this.question.hasDraftFiles = this.question.allowsAnswerFiles && - this.questionHelper.hasDraftFileUrls(questionEl.innerHTML); - - if (!textarea && !this.question.allowsAttachments) { - // Textarea and filemanager not found, we might be in review. Search the answer and the attachments. - this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); this.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml( this.domUtils.getContentsOfElement(questionEl, '.attachments')); - - return questionEl; } - if (textarea) { - const input = questionEl.querySelector('input[type="hidden"][name*=answerformat]'); - let content = this.textUtils.decodeHTML(textarea.innerHTML || ''); + return; + } - if (this.question.hasDraftFiles && this.question.responsefileareas) { - content = this.textUtils.replaceDraftfileUrls(CoreSites.instance.getCurrentSite().getURL(), content, - this.questionHelper.getResponseFileAreaFiles(this.question, 'answer')).text; - } + const textarea = questionEl.querySelector('textarea[name*=_answer]'); - this.question.textarea = { - id: textarea.id, - name: textarea.name, - text: content, - }; + this.question.hasDraftFiles = this.question.allowsAnswerFiles && + this.questionHelper.hasDraftFileUrls(questionEl.innerHTML); - if (input) { - this.question.formatInput = { - name: input.name, - value: input.value - }; - } - } - - if (answerDraftIdInput) { - this.question.answerDraftIdInput = { - name: answerDraftIdInput.name, - value: Number(answerDraftIdInput.value), - }; - } - - if (this.question.allowsAttachments) { - const attachmentsInput = questionEl.querySelector('.attachments input[name*=_attachments]'); - const objectElement = questionEl.querySelector('.attachments object'); - const fileManagerUrl = objectElement && objectElement.data; - - if (attachmentsInput) { - this.question.attachmentsDraftIdInput = { - name: attachmentsInput.name, - value: Number(attachmentsInput.value), - }; - } - - if (this.question.settings) { - this.question.attachmentsMaxFiles = Number(this.question.settings.attachments); - this.question.attachmentsAcceptedTypes = this.question.settings.filetypeslist && - this.question.settings.filetypeslist.join(','); - } - - if (fileManagerUrl) { - const params = CoreUrlUtils.instance.extractUrlParams(fileManagerUrl); - const maxBytes = Number(params.maxbytes); - const areaMaxBytes = Number(params.areamaxbytes); - - this.question.attachmentsMaxBytes = maxBytes === -1 || areaMaxBytes === -1 ? - Math.max(maxBytes, areaMaxBytes) : Math.min(maxBytes, areaMaxBytes); - } - } + if (!textarea && (this.question.hasInlineText || !this.question.allowsAttachments)) { + // Textarea not found, we might be in review. Search the answer and the attachments. + this.question.answer = this.domUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); + this.question.attachments = this.questionHelper.getQuestionAttachmentsFromHtml( + this.domUtils.getContentsOfElement(questionEl, '.attachments')); return questionEl; } + + if (textarea) { + const input = questionEl.querySelector('input[type="hidden"][name*=answerformat]'); + let content = this.textUtils.decodeHTML(textarea.innerHTML || ''); + + if (this.question.hasDraftFiles && this.question.responsefileareas) { + content = this.textUtils.replaceDraftfileUrls(CoreSites.instance.getCurrentSite().getURL(), content, + this.questionHelper.getResponseFileAreaFiles(this.question, 'answer')).text; + } + + this.question.textarea = { + id: textarea.id, + name: textarea.name, + text: content, + }; + + if (input) { + this.question.formatInput = { + name: input.name, + value: input.value + }; + } + } + + if (answerDraftIdInput) { + this.question.answerDraftIdInput = { + name: answerDraftIdInput.name, + value: Number(answerDraftIdInput.value), + }; + } + + if (this.question.allowsAttachments) { + const attachmentsInput = questionEl.querySelector('.attachments input[name*=_attachments]'); + const objectElement = questionEl.querySelector('.attachments object'); + const fileManagerUrl = objectElement && objectElement.data; + + if (attachmentsInput) { + this.question.attachmentsDraftIdInput = { + name: attachmentsInput.name, + value: Number(attachmentsInput.value), + }; + } + + if (this.question.settings) { + this.question.attachmentsMaxFiles = Number(this.question.settings.attachments); + this.question.attachmentsAcceptedTypes = this.question.settings.filetypeslist && + this.question.settings.filetypeslist.join(','); + } + + if (fileManagerUrl) { + const params = CoreUrlUtils.instance.extractUrlParams(fileManagerUrl); + const maxBytes = Number(params.maxbytes); + const areaMaxBytes = Number(params.areamaxbytes); + + this.question.attachmentsMaxBytes = maxBytes === -1 || areaMaxBytes === -1 ? + Math.max(maxBytes, areaMaxBytes) : Math.min(maxBytes, areaMaxBytes); + } + } + + return questionEl; } /** diff --git a/src/core/question/components/question/question.ts b/src/core/question/components/question/question.ts index ce43dbf13..58bea6f95 100644 --- a/src/core/question/components/question/question.ts +++ b/src/core/question/components/question/question.ts @@ -39,6 +39,7 @@ export class CoreQuestionComponent implements OnInit { @Input() contextLevel?: string; // The context level. @Input() contextInstanceId?: number; // The instance ID related to the context. @Input() courseId?: number; // Course ID the question belongs to (if any). It can be used to improve performance with filters. + @Input() review?: boolean; // Whether the user is in review mode. @Output() buttonClicked: EventEmitter; // Will emit an event when a behaviour button is clicked. @Output() onAbort: EventEmitter; // Will emit an event if the question should be aborted. @@ -88,8 +89,9 @@ export class CoreQuestionComponent implements OnInit { contextLevel: this.contextLevel, contextInstanceId: this.contextInstanceId, courseId: this.courseId, + review: this.review, buttonClicked: this.buttonClicked, - onAbort: this.onAbort + onAbort: this.onAbort, }; // Treat the question. From 185ed17cdbb9599388fd218738baf441ce8c6f9a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 24 Nov 2020 11:46:37 +0100 Subject: [PATCH 166/182] MOBILE-3523 quiz: Fix download ddmarker files --- src/addon/qtype/ddmarker/providers/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/qtype/ddmarker/providers/handler.ts b/src/addon/qtype/ddmarker/providers/handler.ts index e4fa425b8..3971da112 100644 --- a/src/addon/qtype/ddmarker/providers/handler.ts +++ b/src/addon/qtype/ddmarker/providers/handler.ts @@ -124,7 +124,7 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { getAdditionalDownloadableFiles(question: any, usageId: number): string[] { this.questionHelper.extractQuestionScripts(question, usageId); - if (question.amdArgs && typeof question.amdArgs[1] !== 'undefined') { + if (question.amdArgs && typeof question.amdArgs[1] == 'string') { // Moodle 3.6+. return [question.amdArgs[1]]; } From bc7602abcde724284b1b8a4b98e5a9866923195e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 24 Nov 2020 14:27:32 +0100 Subject: [PATCH 167/182] MOBILE-3523 essay: Fix attachments in quiz submitted offline --- src/addon/mod/quiz/pages/player/player.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts index 0ccbb17cf..fe33b00d9 100644 --- a/src/addon/mod/quiz/pages/player/player.ts +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -583,7 +583,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { */ protected processAttempt(userFinish?: boolean, timeUp?: boolean, retrying?: boolean): Promise { // Get the answers to send. - return this.prepareAnswers().then((answers) => { + let promise = Promise.resolve({}); + + if (!this.showSummary) { + promise = this.prepareAnswers(); + } + + return promise.then((answers) => { // Send the answers. return this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData, userFinish, timeUp, this.offline).catch((error) => { From 289fcd599bb887aa9648e050864acb933782813b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 24 Nov 2020 14:28:09 +0100 Subject: [PATCH 168/182] MOBILE-3523 essay: Fix offline status when required attachments --- src/addon/qtype/essay/providers/handler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/addon/qtype/essay/providers/handler.ts b/src/addon/qtype/essay/providers/handler.ts index 527b8505f..54c45ebd0 100644 --- a/src/addon/qtype/essay/providers/handler.ts +++ b/src/addon/qtype/essay/providers/handler.ts @@ -165,8 +165,8 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { return attachments && attachments.length >= Number(question.settings.attachmentsrequired) ? 1 : 0; } - return (hasTextAnswer || question.settings.responserequired == '0') && - (attachments && attachments.length > Number(question.settings.attachmentsrequired)) ? 1 : 0; + return ((hasTextAnswer || question.settings.responserequired == '0') && + (attachments && attachments.length >= Number(question.settings.attachmentsrequired))) ? 1 : 0; } /** From 6631488ec3e71f950364516bde63fb71197b8ec3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 24 Nov 2020 16:10:53 +0100 Subject: [PATCH 169/182] MOBILE-3523 login: Fix iOS auto-fill in credentials --- .../login/pages/credentials/credentials.ts | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index c40ad2703..d8993b267 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild, ElementRef } from '@angular/core'; +import { Component, ViewChild, ElementRef, OnDestroy } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; @@ -25,6 +25,7 @@ import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CoreConfigConstants } from '../../../../configconstants'; import { CoreCustomURLSchemes } from '@providers/urlschemes'; +import { Subscription } from 'rxjs'; /** * Page to enter the user credentials. @@ -34,7 +35,7 @@ import { CoreCustomURLSchemes } from '@providers/urlschemes'; selector: 'page-core-login-credentials', templateUrl: 'credentials.html', }) -export class CoreLoginCredentialsPage { +export class CoreLoginCredentialsPage implements OnDestroy { @ViewChild('credentialsForm') formElement: ElementRef; @@ -57,6 +58,7 @@ export class CoreLoginCredentialsPage { protected viewLeft = false; protected siteId: string; protected urlToOpen: string; + protected valueChangeSubscription: Subscription; constructor(private navCtrl: NavController, navParams: NavParams, @@ -89,6 +91,28 @@ export class CoreLoginCredentialsPage { } else { this.showScanQR = false; } + + if (appProvider.isIOS()) { + // Make iOS auto-fill work. The field that isn't focused doesn't get updated, do it manually. + // Debounce it to prevent triggering this function too often when the user is typing. + this.valueChangeSubscription = this.credForm.valueChanges.debounceTime(1000).subscribe((changes) => { + if (!this.formElement || !this.formElement.nativeElement) { + return; + } + + const usernameInput = this.formElement.nativeElement.querySelector('input[name="username"]'); + const passwordInput = this.formElement.nativeElement.querySelector('input[name="password"]'); + const usernameValue = usernameInput && usernameInput.value; + const passwordValue = passwordInput && passwordInput.value; + + if (typeof usernameValue != 'undefined' && usernameValue != changes.username) { + this.credForm.get('username').setValue(usernameValue); + } + if (typeof passwordValue != 'undefined' && passwordValue != changes.password) { + this.credForm.get('password').setValue(passwordValue); + } + }); + } } /** @@ -336,4 +360,11 @@ export class CoreLoginCredentialsPage { } } } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe(); + } } From c1411c3ed46a3e8cae8e6e950df8334f68ab5d69 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 25 Nov 2020 09:14:32 +0100 Subject: [PATCH 170/182] MOBILE-3523 essay: Improve essay files prefetch --- src/addon/qtype/ddmarker/providers/handler.ts | 5 +++-- src/addon/qtype/essay/providers/handler.ts | 18 +++++++++++++++ src/core/question/providers/delegate.ts | 9 ++++---- src/core/question/providers/helper.ts | 22 ++++++++++++++----- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/addon/qtype/ddmarker/providers/handler.ts b/src/addon/qtype/ddmarker/providers/handler.ts index 3971da112..a3c2bd01e 100644 --- a/src/addon/qtype/ddmarker/providers/handler.ts +++ b/src/addon/qtype/ddmarker/providers/handler.ts @@ -18,6 +18,7 @@ import { CoreQuestionProvider } from '@core/question/providers/question'; import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { AddonQtypeDdMarkerComponent } from '../component/ddmarker'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Handler to support drag-and-drop markers question type. @@ -119,9 +120,9 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles(question: any, usageId: number): string[] { + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { this.questionHelper.extractQuestionScripts(question, usageId); if (question.amdArgs && typeof question.amdArgs[1] == 'string') { diff --git a/src/addon/qtype/essay/providers/handler.ts b/src/addon/qtype/essay/providers/handler.ts index 54c45ebd0..834850ca4 100644 --- a/src/addon/qtype/essay/providers/handler.ts +++ b/src/addon/qtype/essay/providers/handler.ts @@ -24,6 +24,7 @@ import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { CoreQuestion } from '@core/question/providers/question'; import { AddonQtypeEssayComponent } from '../component/essay'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Handler to support essay question type. @@ -67,6 +68,23 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { return this.questionHelper.deleteStoredQuestionFiles(question, component, componentId, siteId); } + /** + * Get the list of files that needs to be downloaded in addition to the files embedded in the HTML. + * + * @param question Question. + * @param usageId Usage ID. + * @return List of files or URLs. + */ + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { + if (!question.responsefileareas) { + return []; + } + + return question.responsefileareas.reduce((urlsList, area) => { + return urlsList.concat(area.files || []); + }, []); + } + /** * Check whether the question allows text and/or attachments. * diff --git a/src/core/question/providers/delegate.ts b/src/core/question/providers/delegate.ts index 6caa7eb2b..7a0cd7675 100644 --- a/src/core/question/providers/delegate.ts +++ b/src/core/question/providers/delegate.ts @@ -18,6 +18,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreQuestionDefaultHandler } from './default-question-handler'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Interface that all question type handlers must implement. @@ -119,9 +120,9 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles?(question: any, usageId: number): string[]; + getAdditionalDownloadableFiles?(question: any, usageId: number): (string | CoreWSExternalFile)[]; /** * Clear temporary data after the data has been saved. @@ -324,9 +325,9 @@ export class CoreQuestionDelegate extends CoreDelegate { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles(question: any, usageId: number): string[] { + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { const type = this.getTypeName(question); return this.executeFunctionOnEnabled(type, 'getAdditionalDownloadableFiles', [question, usageId]) || []; diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index 9c6570858..b90274929 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -590,29 +590,39 @@ export class CoreQuestionHelperProvider { */ prefetchQuestionFiles(question: any, component?: string, componentId?: string | number, siteId?: string, usageId?: number) : Promise { - const urls = this.filepoolProvider.extractDownloadableFilesFromHtml(question.html); if (!component) { component = CoreQuestionProvider.COMPONENT; componentId = question.number; } - urls.push(...this.questionDelegate.getAdditionalDownloadableFiles(question, usageId)); + const files = this.questionDelegate.getAdditionalDownloadableFiles(question, usageId) || []; + + files.push(...this.filepoolProvider.extractDownloadableFilesFromHtml(question.html)); return this.sitesProvider.getSite(siteId).then((site) => { const promises = []; + const treated = {}; - urls.forEach((url) => { - if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) { + files.forEach((file) => { + const fileUrl = typeof file == 'string' ? file : file.fileurl; + const timemodified = (typeof file != 'string' && file.timemodified) || 0; + + if (treated[fileUrl]) { + return; + } + treated[fileUrl] = true; + + if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(fileUrl)) { return; } - if (url.indexOf('theme/image.php') > -1 && url.indexOf('flagged') > -1) { + if (fileUrl.indexOf('theme/image.php') > -1 && fileUrl.indexOf('flagged') > -1) { // Ignore flag images. return; } - promises.push(this.filepoolProvider.addToQueueByUrl(siteId, url, component, componentId)); + promises.push(this.filepoolProvider.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified)); }); return Promise.all(promises); From 042ee24d0a6dc7fc3a272cc84a387ac7eb1ee6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 25 Nov 2020 13:47:23 +0100 Subject: [PATCH 171/182] MOBILE-3523 data: Fix database prefetch --- src/addon/mod/data/components/index/index.ts | 1 + src/addon/mod/data/providers/data.ts | 9 ++++----- src/addon/mod/data/providers/prefetch-handler.ts | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index 1df731973..3f79e9972 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -260,6 +260,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp sort: Number(this.search.sortBy), order: this.search.sortDirection, page: this.search.page, + cmId: this.module.id, }).then((entries) => { const numEntries = entries.entries.length; const numOfflineEntries = entries.offlineEntries.length; diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index 553f37b54..26d55fbc3 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -608,8 +608,11 @@ export class AddonModDataProvider { */ getDatabaseAccessInformation(dataId: number, options: AddonModDataAccessInfoOptions = {}): Promise { return this.sitesProvider.getSite(options.siteId).then((site) => { + options.groupId = options.groupId || 0; + const params = { databaseid: dataId, + groupid: options.groupId, }; const preSets = { cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, options.groupId), @@ -618,10 +621,6 @@ export class AddonModDataProvider { ...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; - if (typeof options.groupId !== 'undefined') { - params['groupid'] = options.groupId; - } - return site.read('mod_data_get_data_access_information', params, preSets); }); } @@ -636,7 +635,7 @@ export class AddonModDataProvider { getEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise { options.groupId = options.groupId || 0; options.sort = options.sort || 0; - options.order || options.order || 'DESC'; + options.order = options.order || 'DESC'; options.page = options.page || 0; options.perPage = options.perPage || AddonModDataProvider.PER_PAGE; diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts index 4355a8fe6..a2aded865 100644 --- a/src/addon/mod/data/providers/prefetch-handler.ts +++ b/src/addon/mod/data/providers/prefetch-handler.ts @@ -22,7 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; -import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseProvider, CoreCourseCommonModWSOptions } from '@core/course/providers/course'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { AddonModDataProvider, AddonModDataEntry } from './data'; import { AddonModDataSyncProvider } from './sync'; @@ -100,13 +100,14 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param options Other options. * @return Promise resolved with the info fetched. */ - protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean, options: CoreSitesCommonWSOptions = {}) + protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean, options: CoreCourseCommonModWSOptions = {}) : Promise { let database, groups = [], entries = [], files = []; + options.cmId = options.cmId || module.id; options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); return this.dataProvider.getDatabase(courseId, module.id, options).then((data) => { From 1be4bc02d6c10937a37475f23c9ce383e89e4e03 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 25 Nov 2020 14:59:32 +0100 Subject: [PATCH 172/182] MOBILE-3523 ddmarker: Fix ddmarker offline if no background image --- src/addon/qtype/ddmarker/classes/ddmarker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addon/qtype/ddmarker/classes/ddmarker.ts b/src/addon/qtype/ddmarker/classes/ddmarker.ts index 90b5686b1..0379c7322 100644 --- a/src/addon/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addon/qtype/ddmarker/classes/ddmarker.ts @@ -751,7 +751,7 @@ export class AddonQtypeDdMarkerQuestion { this.question.loaded = true; }; - if (bgImg.complete && bgImg.naturalWidth) { + if (!bgImg.src || (bgImg.complete && bgImg.naturalWidth)) { imgLoaded(); return; From 1a87eb309459bf25c5d73f92f331be3e3276a5bd Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 25 Nov 2020 15:33:02 +0100 Subject: [PATCH 173/182] MOBILE-3523 qtype_calculated: Fallback if unitsleft is null --- src/addon/qtype/calculated/providers/handler.ts | 2 +- src/core/question/classes/base-question-component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/addon/qtype/calculated/providers/handler.ts b/src/addon/qtype/calculated/providers/handler.ts index 22423edb4..0e432658f 100644 --- a/src/addon/qtype/calculated/providers/handler.ts +++ b/src/addon/qtype/calculated/providers/handler.ts @@ -191,7 +191,7 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { let unitsLeft = false; let match = null; - if (!question.settings) { + if (!question.settings || question.settings.unitsleft === null) { // We don't know if units should be before or after so we check both. match = answer.match(new RegExp('^' + regexString)); if (!match) { diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts index 91a238153..0dbd9df63 100644 --- a/src/core/question/classes/base-question-component.ts +++ b/src/core/question/classes/base-question-component.ts @@ -108,7 +108,7 @@ export class CoreQuestionBaseComponent { this.question.select = selectModel; // Check which one should be displayed first: the select or the input. - if (this.question.settings) { + if (this.question.settings && this.question.settings.unitsleft !== null) { this.question.selectFirst = this.question.settings.unitsleft == '1'; } else { const input = questionEl.querySelector('input[type="text"][name*=answer]'); @@ -166,7 +166,7 @@ export class CoreQuestionBaseComponent { } // Check which one should be displayed first: the options or the input. - if (this.question.settings) { + if (this.question.settings && this.question.settings.unitsleft !== null) { this.question.optionsFirst = this.question.settings.unitsleft == '1'; } else { const input = questionEl.querySelector('input[type="text"][name*=answer]'); From 95e2fc2a4d94e4e0a11ad5d9c5beaf624b416c7d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 26 Nov 2020 09:17:14 +0100 Subject: [PATCH 174/182] MOBILE-3523 courses: Fix course name in manage storage --- .../storagemanager/pages/course-storage/course-storage.html | 5 ++++- .../pages/courses-storage/courses-storage.html | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.html b/src/addon/storagemanager/pages/course-storage/course-storage.html index 88dffd229..b489def78 100644 --- a/src/addon/storagemanager/pages/course-storage/course-storage.html +++ b/src/addon/storagemanager/pages/course-storage/course-storage.html @@ -7,7 +7,10 @@ -

{{ course.displayname }}

+

+ {{ course.displayname }} + {{ course.fullname }} +

{{ 'addon.storagemanager.info' | translate }}

diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.html b/src/addon/storagemanager/pages/courses-storage/courses-storage.html index 937ddd2be..285510235 100644 --- a/src/addon/storagemanager/pages/courses-storage/courses-storage.html +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.html @@ -24,7 +24,10 @@ -

{{ course.displayname }}

+

+ {{ course.displayname }} + {{ course.fullname }} +

{{ 'core.downloading' | translate }}

From baa2a6432251f789ae272b81830f0f3c3703845f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 26 Nov 2020 12:24:39 +0100 Subject: [PATCH 175/182] MOBILE-3523 course: Fix wrong download icon after delete files --- .../mod/scorm/providers/prefetch-handler.ts | 8 +++----- .../providers/module-prefetch-delegate.ts | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/addon/mod/scorm/providers/prefetch-handler.ts b/src/addon/mod/scorm/providers/prefetch-handler.ts index dd34b8fe1..376329f9d 100644 --- a/src/addon/mod/scorm/providers/prefetch-handler.ts +++ b/src/addon/mod/scorm/providers/prefetch-handler.ts @@ -424,17 +424,15 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand // Remove the unzipped folder. promises.push(this.fileProvider.removeDir(path).catch((error) => { - if (error && error.code == 1) { + if (error && (error.code == 1 || !this.appProvider.isMobile())) { // Not found, ignore error. } else { return Promise.reject(error); } })); - // Maybe the ZIP wasn't deleted for some reason. Try to delete it too. - promises.push(this.filepoolProvider.removeFileByUrl(siteId, this.scormProvider.getPackageUrl(scorm)).catch(() => { - // Ignore errors. - })); + // Delete other files. + promises.push(this.filepoolProvider.removeFilesByComponent(siteId, this.component, module.id)); return Promise.all(promises); }); diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 888725bce..81c988081 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -1384,13 +1384,22 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { } return promise.then(() => { - if (handler) { - // Update status of the module. - const packageId = this.filepoolProvider.getPackageId(handler.component, module.id); - this.statusCache.setValue(packageId, 'downloadedSize', 0); + if (!handler) { + return; + } + + // Update downloaded size. + const packageId = this.filepoolProvider.getPackageId(handler.component, module.id); + this.statusCache.setValue(packageId, 'downloadedSize', 0); + + // If module is downloadable, set not dowloaded status. + return this.isModuleDownloadable(module, courseId).then((downloadable) => { + if (!downloadable) { + return; + } return this.filepoolProvider.storePackageStatus(siteId, CoreConstants.NOT_DOWNLOADED, handler.component, module.id); - } + }); }); } From da9d90e65053a8d73142632b4d6864197bc96118 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 27 Nov 2020 09:39:56 +0100 Subject: [PATCH 176/182] MOBILE-3523 forum: Fix tags not displayed in 3.7+ --- src/addon/mod/forum/providers/forum.ts | 10 +++++++++- src/core/tag/components/list/core-tag-list.html | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index 4118db0f1..a758f22f2 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -539,7 +539,15 @@ export class AddonModForumProvider { unread: !post.postread, isprivatereply: !!post.isprivatereply, - tags: post.tags + tags: (post.tags || []).map((tag) => { + return { + id: tag.taginstanceid, + tagid: tag.id, + isstandard: tag.isstandard, + displayname: tag.rawname, + flag: !!tag.flag, + }; + }), }; if (post.groupname) { diff --git a/src/core/tag/components/list/core-tag-list.html b/src/core/tag/components/list/core-tag-list.html index 7e6372e20..60035d009 100644 --- a/src/core/tag/components/list/core-tag-list.html +++ b/src/core/tag/components/list/core-tag-list.html @@ -1,3 +1,3 @@ - {{ tag.rawname }} + {{ tag.displayname }} From 2dabb6e1b2403b90acf305d5add639bcaab5c5d2 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 27 Nov 2020 11:38:32 +0100 Subject: [PATCH 177/182] MOBILE-3523 tags: Fix click tags in forum --- src/addon/mod/forum/providers/forum.ts | 36 ++++++++++++++----- .../tag/components/list/core-tag-list.html | 2 +- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts index a758f22f2..a9d92f53a 100644 --- a/src/addon/mod/forum/providers/forum.ts +++ b/src/addon/mod/forum/providers/forum.ts @@ -25,6 +25,7 @@ import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModForumOfflineProvider } from './offline'; import { CoreRatingInfo } from '@core/rating/providers/rating'; import { CoreCourseCommonModWSOptions } from '@core/course/providers/course'; +import { CoreUrlUtils } from '@providers/utils/url'; /** * Service that provides some features for forums. @@ -539,15 +540,7 @@ export class AddonModForumProvider { unread: !post.postread, isprivatereply: !!post.isprivatereply, - tags: (post.tags || []).map((tag) => { - return { - id: tag.taginstanceid, - tagid: tag.id, - isstandard: tag.isstandard, - displayname: tag.rawname, - flag: !!tag.flag, - }; - }), + tags: post.tags, }; if (post.groupname) { @@ -557,6 +550,29 @@ export class AddonModForumProvider { return newPost; }); }; + // For some reason, the new WS doesn't use the tags exporter so it returns a different format than other WebServices. + // Convert the new format to the exporter one so it's the same as in other WebServices. + const translateTagsFormatToLegacy = (posts: any[]): any[] => { + posts.forEach((post) => { + post.tags = post.tags.map((tag) => { + const viewUrl = (tag.urls && tag.urls.view) || ''; + const params = CoreUrlUtils.instance.extractUrlParams(viewUrl); + + return { + id: tag.tagid, + taginstanceid: tag.id, + flag: tag.flag ? 1 : 0, + isstandard: tag.isstandard, + rawname: tag.displayname, + name: tag.displayname, + tagcollid: params.tc ? Number(params.tc) : undefined, + taginstancecontextid: params.from ? Number(params.from) : undefined, + }; + }); + }); + + return posts; + }; const params = { discussionid: discussionId, @@ -577,6 +593,8 @@ export class AddonModForumProvider { if (wsName == 'mod_forum_get_forum_discussion_posts') { response.posts = translateLegacyPostsFormat(response.posts); + } else { + response.posts = translateTagsFormatToLegacy(response.posts); } this.storeUserData(response.posts); diff --git a/src/core/tag/components/list/core-tag-list.html b/src/core/tag/components/list/core-tag-list.html index 60035d009..7e6372e20 100644 --- a/src/core/tag/components/list/core-tag-list.html +++ b/src/core/tag/components/list/core-tag-list.html @@ -1,3 +1,3 @@ - {{ tag.displayname }} + {{ tag.rawname }} From eab3a622baea128f9787d164effd2ac6450f997d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 27 Nov 2020 12:25:06 +0100 Subject: [PATCH 178/182] MOBILE-3523 workshop: Prefetch assessments forms --- .../mod/workshop/components/assessment/assessment.ts | 2 +- src/addon/mod/workshop/providers/helper.ts | 10 +++++++--- src/addon/mod/workshop/providers/prefetch-handler.ts | 5 +++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/addon/mod/workshop/components/assessment/assessment.ts b/src/addon/mod/workshop/components/assessment/assessment.ts index 4aafdd625..b7a13569f 100644 --- a/src/addon/mod/workshop/components/assessment/assessment.ts +++ b/src/addon/mod/workshop/components/assessment/assessment.ts @@ -113,7 +113,7 @@ export class AddonModWorkshopAssessmentComponent implements OnInit { }; if (!this.submission) { - const modal = this.domUtils.showModalLoading('core.sending', true); + const modal = this.domUtils.showModalLoading(); this.workshopHelper.getSubmissionById(this.workshop.id, this.assessment.submissionid, {cmId: this.workshop.coursemodule}).then((submissionData) => { diff --git a/src/addon/mod/workshop/providers/helper.ts b/src/addon/mod/workshop/providers/helper.ts index 7adf38209..5ef4d8f8f 100644 --- a/src/addon/mod/workshop/providers/helper.ts +++ b/src/addon/mod/workshop/providers/helper.ts @@ -189,10 +189,14 @@ export class AddonModWorkshopHelperProvider { options.siteId = options.siteId || this.sitesProvider.getCurrentSiteId(); return this.workshopProvider.getReviewerAssessments(workshopId, options).then((assessments) => { - const promises = assessments.map((assessment) => { - return this.getSubmissionById(workshopId, assessment.submissionid, options).then((submission) => { + const promises = []; + assessments.forEach((assessment) => { + promises.push(this.getSubmissionById(workshopId, assessment.submissionid, options).then((submission) => { assessment.submission = submission; - }); + })); + promises.push(this.workshopProvider.getAssessmentForm(workshopId, assessment.id, options).then((assessmentForm) => { + assessment.form = assessmentForm; + })); }); return Promise.all(promises).then(() => { diff --git a/src/addon/mod/workshop/providers/prefetch-handler.ts b/src/addon/mod/workshop/providers/prefetch-handler.ts index 9f69bb7d0..c189a9f60 100644 --- a/src/addon/mod/workshop/providers/prefetch-handler.ts +++ b/src/addon/mod/workshop/providers/prefetch-handler.ts @@ -142,6 +142,11 @@ export class AddonModWorkshopPrefetchHandler extends CoreCourseActivityPrefetchH files = files.concat(assessment.feedbackattachmentfiles) .concat(assessment.feedbackcontentfiles); }); + if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) { + return Promise.all(assessments.map((assessment) => { + return this.workshopHelper.getReviewerAssessmentById(workshop.id, assessment.id); + })); + } })); }); From b03eae2f5fa1b30de6741c3fefd127d2384addbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 30 Nov 2020 12:41:56 +0100 Subject: [PATCH 179/182] MOBILE-3523 core: Add 3gpp extension file --- src/assets/exttomime.json | 1 + src/assets/mimetoext.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/assets/exttomime.json b/src/assets/exttomime.json index 2728d8b3d..c89ea0c24 100644 --- a/src/assets/exttomime.json +++ b/src/assets/exttomime.json @@ -6,6 +6,7 @@ "3ds": {"type":"image/x-3ds"}, "3g2": {"type":"video/3gpp2"}, "3gp": {"type":"video/quicktime","icon":"quicktime","string":"video","groups":["video"]}, +"3gpp": {"type":"video/quicktime","icon":"quicktime","string":"video","groups":["video"]}, "7z": {"type":"application/x-7z-compressed","icon":"archive","string":"archive","groups":["archive"]}, "a": {"type":"application/octet-stream"}, "aab": {"type":"application/x-authorware-bin"}, diff --git a/src/assets/mimetoext.json b/src/assets/mimetoext.json index 438731f8b..6e8770637 100644 --- a/src/assets/mimetoext.json +++ b/src/assets/mimetoext.json @@ -1018,7 +1018,7 @@ "text/x-vcard": ["vcf"], "text/xml": ["resx","jcb","jcw","jmt","jmx","jcl","xsl","rhb","sqt","xml","jqz"], "text/yaml": ["yaml","yml"], -"video/3gpp": ["3gp"], +"video/3gpp": ["3gp", "3gpp"], "video/3gpp2": ["3g2"], "video/animaflex": ["afl"], "video/avi": ["avi"], From 2662f22942b46f2187244b9566fcb158561761c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 30 Nov 2020 14:16:16 +0100 Subject: [PATCH 180/182] MOBILE-3523 core: Fix 3gpp extension mimetype --- src/assets/exttomime.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/exttomime.json b/src/assets/exttomime.json index c89ea0c24..7720dc36e 100644 --- a/src/assets/exttomime.json +++ b/src/assets/exttomime.json @@ -5,8 +5,8 @@ "3dml": {"type":"text/vnd.in3d.3dml"}, "3ds": {"type":"image/x-3ds"}, "3g2": {"type":"video/3gpp2"}, -"3gp": {"type":"video/quicktime","icon":"quicktime","string":"video","groups":["video"]}, -"3gpp": {"type":"video/quicktime","icon":"quicktime","string":"video","groups":["video"]}, +"3gp": {"type":"video/3gpp","icon":"quicktime","string":"video","groups":["video"]}, +"3gpp": {"type":"video/3gpp","icon":"quicktime","string":"video","groups":["video"]}, "7z": {"type":"application/x-7z-compressed","icon":"archive","string":"archive","groups":["archive"]}, "a": {"type":"application/octet-stream"}, "aab": {"type":"application/x-authorware-bin"}, From 4128757acd4ba41cab23778382ae9e21445fd25c Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Mon, 30 Nov 2020 15:19:07 +0100 Subject: [PATCH 181/182] MOBILE-3523 release: Definitive version for 3.9.3 --- config.xml | 4 ++-- src/config.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.xml b/config.xml index b5c78097b..d093952b0 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + Moodle Moodle official app Moodle Mobile team @@ -241,7 +241,7 @@ - 3.9.3-dev + 3.9.3 YES diff --git a/src/config.json b/src/config.json index 4749c6bb2..cac489641 100644 --- a/src/config.json +++ b/src/config.json @@ -3,7 +3,7 @@ "appname": "Moodle Mobile", "desktopappname": "Moodle Desktop", "versioncode": 3930, - "versionname": "3.9.3-dev", + "versionname": "3.9.3", "cache_update_frequency_usually": 420000, "cache_update_frequency_often": 1200000, "cache_update_frequency_sometimes": 3600000, From 646a4f4188132e2498176686f4d67239a014df29 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Mon, 30 Nov 2020 19:17:44 +0100 Subject: [PATCH 182/182] MOBILE-3523 release: Do not require bluetooh in Android --- config.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.xml b/config.xml index d093952b0..01aeaa0c1 100644 --- a/config.xml +++ b/config.xml @@ -219,6 +219,9 @@ + + +