From affe05a9d5762ca7c36368859e2023059285d2fb Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 10 Apr 2019 16:28:25 +0200 Subject: [PATCH 1/7] MOBILE-2856 data: Interfaces for WS and offline data --- src/addon/mod/data/components/index/index.ts | 3 - src/addon/mod/data/pages/edit/edit.ts | 3 - src/addon/mod/data/pages/entry/entry.ts | 3 - src/addon/mod/data/providers/data.ts | 126 ++++++++++++++---- src/addon/mod/data/providers/helper.ts | 83 +++++++----- src/addon/mod/data/providers/offline.ts | 46 ++++--- .../mod/data/providers/prefetch-handler.ts | 14 +- src/addon/mod/data/providers/sync.ts | 16 +-- 8 files changed, 193 insertions(+), 101 deletions(-) diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index 54c6d61cd..ac4d95546 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -327,9 +327,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp }); entries.entries.forEach((entry) => { - // Index contents by fieldid. - entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); - if (typeof this.offlineActions[entry.id] != 'undefined') { promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fieldsArray)); } else { diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index 4ca834934..ce8ccd06a 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -158,9 +158,6 @@ export class AddonModDataEditPage { }).then((entry) => { if (entry) { entry = entry.entry; - - // Index contents by fieldid. - entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); } else { entry = { contents: {} diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 2c568ab8d..0ae32b2be 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -171,9 +171,6 @@ export class AddonModDataEntryPage implements OnDestroy { this.ratingInfo = entry.ratinginfo; entry = entry.entry; - // Index contents by fieldid. - entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); - fieldsArray = this.utils.objectToArray(this.fields); return this.dataHelper.applyOfflineActions(entry, this.offlineActions, fieldsArray); diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index a50cf5156..31caaf80f 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -21,6 +21,67 @@ import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { AddonModDataOfflineProvider } from './offline'; import { AddonModDataFieldsDelegate } from './fields-delegate'; +import { CoreRatingInfo } from '@core/rating/providers/rating'; + +/** + * Database entry (online or offline). + */ +export interface AddonModDataEntry { + id: number; // Negative for offline entries. + userid: number; + groupid: number; + dataid: number; + timecreated: number; + timemodified: number; + approved: boolean; + canmanageentry: boolean; + fullname: string; + contents: AddonModDataEntryFields; + deleted?: boolean; // Entry is deleted offline. + hasOffline?: boolean; // Entry has offline actions. +} + +/** + * Entry field content. + */ +export interface AddonModDataEntryField { + fieldid: number; + content: string; + content1: string; + content2: string; + content3: string; + content4: string; + files: any[]; +} + +/** + * Entry contents indexed by field id. + */ +export interface AddonModDataEntryFields { + [fieldid: number]: AddonModDataEntryField; +} + +/** + * List of entries returned by web service and helper functions. + */ +export interface AddonModDataEntries { + entries: AddonModDataEntry[]; // Online entries. + totalcount: number; // Total count of online entries or found entries. + maxcount?: number; // Total count of online entries. Only returned when searching. + offlineEntries?: AddonModDataEntry[]; // Offline entries. + hasOfflineActions?: boolean; // Whether the database has offline data. + hasOfflineRatings?: boolean; // Whether the database has offline ratings. +} + +/** + * Subfield form data. + */ +export interface AddonModDataSubfieldData { + fieldid: number; + subfield?: string; + value?: string; // Value encoded in JSON. + files?: any[]; +} /** * Service that provides some features for databases. @@ -49,13 +110,13 @@ export class AddonModDataProvider { * @param {number} courseId Course ID. * @param {any} contents The fields data to be created. * @param {number} [groupId] Group id, 0 means that the function will determine the user group. - * @param {any} fields The fields that define the contents. + * @param {any[]} fields The fields that define the contents. * @param {string} [siteId] Site ID. If not defined, current site. * @param {boolean} [forceOffline] Force editing entry in offline. * @return {Promise} Promise resolved when the action is done. */ - addEntry(dataId: number, entryId: number, courseId: number, contents: any, groupId: number = 0, fields: any, siteId?: string, - forceOffline: boolean = false): Promise { + 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. @@ -93,12 +154,12 @@ export class AddonModDataProvider { * Adds a new entry to a database. It does not cache calls. It will fail if offline or cannot connect. * * @param {number} dataId Database ID. - * @param {any} data The fields data to be created. + * @param {any[]} data The fields data to be created. * @param {number} [groupId] Group id, 0 means that the function will determine the user group. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the action is done. */ - addEntryOnline(dataId: number, data: any, groupId?: number, siteId?: string): Promise { + addEntryOnline(dataId: number, data: AddonModDataSubfieldData[], groupId?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { databaseid: dataId, @@ -184,7 +245,7 @@ export class AddonModDataProvider { * @param {any} contents The contents data of the fields. * @return {any} Array of notifications if any or false. */ - protected checkFields(fields: any, contents: any): any { + protected checkFields(fields: any, contents: AddonModDataSubfieldData[]): any[] | false { const notifications = [], contentsIndexed = {}; @@ -289,13 +350,13 @@ export class AddonModDataProvider { * @param {number} dataId Database ID. * @param {number} entryId Entry ID. * @param {number} courseId Course ID. - * @param {any} contents The contents data to be updated. + * @param {any[]} contents The contents data to be updated. * @param {any} fields The fields that define the contents. * @param {string} [siteId] Site ID. If not defined, current site. * @param {boolean} forceOffline Force editing entry in offline. * @return {Promise} Promise resolved when the action is done. */ - editEntry(dataId: number, entryId: number, courseId: number, contents: any, fields: any, siteId?: string, + editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, siteId?: string, forceOffline: boolean = false): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); @@ -370,11 +431,11 @@ export class AddonModDataProvider { * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. * * @param {number} entryId Entry ID. - * @param {any} data The fields data to be updated. + * @param {any[]} data The fields data to be updated. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the action is done. */ - editEntryOnline(entryId: number, data: number, siteId?: string): Promise { + editEntryOnline(entryId: number, data: AddonModDataSubfieldData[], siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { entryid: entryId, @@ -397,11 +458,11 @@ export class AddonModDataProvider { * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when done. + * @return {Promise} 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?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId); @@ -420,10 +481,10 @@ export class AddonModDataProvider { * @param {any} entries Entries already fetch (just to concatenate them). * @param {number} page Page of records to return. * @param {string} siteId Site ID. - * @return {Promise} Promise resolved when done. + * @return {Promise} 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 { + 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) => { entries = entries.concat(result.entries); @@ -595,11 +656,11 @@ export class AddonModDataProvider { * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when the database is retrieved. + * @return {Promise} 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 { + siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { // Always use sort and order params to improve cache usage (entries are identified by params). const params = { @@ -622,7 +683,13 @@ export class AddonModDataProvider { preSets['emergencyCache'] = false; } - return site.read('mod_data_get_entries', params, preSets); + return site.read('mod_data_get_entries', params, preSets).then((response) => { + response.entries.forEach((entry) => { + entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); + }); + + return response; + }); }); } @@ -654,9 +721,10 @@ export class AddonModDataProvider { * @param {number} entryId Entry ID. * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when the database entry is retrieved. + * @return {Promise<{entry: AddonModDataEntry, ratinginfo: CoreRatingInfo}>} Promise resolved when the entry is retrieved. */ - getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string): Promise { + getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string): + Promise<{entry: AddonModDataEntry, ratinginfo: CoreRatingInfo}> { return this.sitesProvider.getSite(siteId).then((site) => { const params = { entryid: entryId, @@ -671,7 +739,11 @@ export class AddonModDataProvider { preSets['emergencyCache'] = false; } - return site.read('mod_data_get_entry', params, preSets); + return site.read('mod_data_get_entry', params, preSets).then((response) => { + response.entry.contents = this.utils.arrayToObject(response.entry.contents, 'fieldid'); + + return response; + }); }); } @@ -871,16 +943,16 @@ export class AddonModDataProvider { * @param {number} dataId The data instance id. * @param {number} [groupId=0] Group id, 0 means that the function will determine the user group. * @param {string} [search] Search text. It will be used if advSearch is not defined. - * @param {any} [advSearch] Advanced search data. + * @param {any[]} [advSearch] Advanced search data. * @param {string} [sort] Sort by this field. * @param {string} [order] The direction of the sorting. * @param {number} [page=0] Page of records to return. * @param {number} [perPage=PER_PAGE] Records per page to return. Default on AddonModDataProvider.PER_PAGE. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when the action is done. + * @return {Promise} 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 { + page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { databaseid: dataId, @@ -911,7 +983,13 @@ export class AddonModDataProvider { params['advsearch'] = advSearch; } - return site.read('mod_data_search_entries', params, preSets); + return site.read('mod_data_search_entries', params, preSets).then((response) => { + response.entries.forEach((entry) => { + entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); + }); + + return response; + }); }); } } diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index c713f3674..930d63e15 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -18,8 +18,8 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { AddonModDataFieldsDelegate } from './fields-delegate'; -import { AddonModDataOfflineProvider } from './offline'; -import { AddonModDataProvider } from './data'; +import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; +import { AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields } from './data'; /** * Service that provides helper functions for datas. @@ -35,15 +35,19 @@ export class AddonModDataHelperProvider { /** * Returns the record with the offline actions applied. * - * @param {any} record Entry to modify. - * @param {any} offlineActions Offline data with the actions done. - * @param {any} fields Entry defined fields indexed by fieldid. - * @return {any} Modified entry. + * @param {AddonModDataEntry} record Entry to modify. + * @param {AddonModDataOfflineAction[]} offlineActions Offline data with the actions done. + * @param {any[]} fields Entry defined fields indexed by fieldid. + * @return {Promise} Promise resolved when done. */ - applyOfflineActions(record: any, offlineActions: any[], fields: any[]): any { + applyOfflineActions(record: AddonModDataEntry, offlineActions: AddonModDataOfflineAction[], fields: any[]): + Promise { const promises = []; offlineActions.forEach((action) => { + record.timemodified = action.timemodified; + record.hasOffline = true; + switch (action.action) { case 'approve': record.approved = true; @@ -56,6 +60,8 @@ export class AddonModDataHelperProvider { break; case 'add': case 'edit': + record.groupid = action.groupid; + const offlineContents = {}; action.fields.forEach((offlineContent) => { @@ -77,10 +83,12 @@ export class AddonModDataHelperProvider { promises.push(this.getStoredFiles(record.dataid, record.id, field.id).then((offlineFiles) => { record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], offlineContents[field.id], offlineFiles); + record.contents[field.id].fieldid = field.id; })); } else { record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], offlineContents[field.id]); + record.contents[field.id].fieldid = field.id; } }); break; @@ -97,15 +105,16 @@ export class AddonModDataHelperProvider { /** * Displays fields for being shown. * - * @param {string} template Template HMTL. - * @param {any[]} fields Fields that defines every content in the entry. - * @param {any} entry Entry. - * @param {number} offset Entry offset. - * @param {string} mode Mode list or show. - * @param {any} actions Actions that can be performed to the record. - * @return {string} Generated HTML. + * @param {string} template Template HMTL. + * @param {any[]} fields Fields that defines every content in the entry. + * @param {any} entry Entry. + * @param {number} offset Entry offset. + * @param {string} mode Mode list or show. + * @param {AddonModDataOfflineAction[]} actions Actions that can be performed to the record. + * @return {string} Generated HTML. */ - displayShowFields(template: string, fields: any[], entry: any, offset: number, mode: string, actions: any): string { + displayShowFields(template: string, fields: any[], entry: any, offset: number, mode: string, + actions: AddonModDataOfflineAction[]): string { if (!template) { return ''; } @@ -256,17 +265,17 @@ export class AddonModDataHelperProvider { * Retrieve the entered data in the edit form. * We don't use ng-model because it doesn't detect changes done by JavaScript. * - * @param {any} inputData Array with the entered form values. - * @param {Array} fields Fields that defines every content in the entry. - * @param {number} [dataId] Database Id. If set, files will be uploaded and itemId set. - * @param {number} entryId Entry Id. - * @param {any} entryContents Original entry contents indexed by field id. - * @param {boolean} offline True to prepare the data for an offline uploading, false otherwise. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} That contains object with the answers. + * @param {any} inputData Array with the entered form values. + * @param {Array} fields Fields that defines every content in the entry. + * @param {number} [dataId] Database Id. If set, files will be uploaded and itemId set. + * @param {number} entryId Entry Id. + * @param {AddonModDataEntryFields} entryContents Original entry contents. + * @param {boolean} offline True to prepare the data for an offline uploading, false otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} That contains object with the answers. */ - getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: any, offline: boolean = false, - siteId?: string): Promise { + getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: AddonModDataEntryFields, + offline: boolean = false, siteId?: string): Promise { if (!inputData) { return Promise.resolve({}); } @@ -322,13 +331,13 @@ export class AddonModDataHelperProvider { /** * Retrieve the temp files to be updated. * - * @param {any} inputData Array with the entered form values. - * @param {Array} fields Fields that defines every content in the entry. - * @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set. - * @param {any} entryContents Original entry contents indexed by field id. - * @return {Promise} That contains object with the files. + * @param {any} inputData Array with the entered form values. + * @param {any[]} fields Fields that defines every content in the entry. + * @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set. + * @param {AddonModDataEntryFields} entryContents Original entry contents indexed by field id. + * @return {Promise} That contains object with the files. */ - getEditTmpFiles(inputData: any, fields: any, dataId: number, entryContents: any): Promise { + getEditTmpFiles(inputData: any, fields: any[], dataId: number, entryContents: AddonModDataEntryFields): Promise { if (!inputData) { return Promise.resolve([]); } @@ -403,13 +412,13 @@ export class AddonModDataHelperProvider { /** * Check if data has been changed by the user. * - * @param {any} inputData Array with the entered form values. - * @param {any} fields Fields that defines every content in the entry. - * @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set. - * @param {any} entryContents Original entry contents indexed by field id. - * @return {Promise} True if changed, false if not. + * @param {any} inputData Object with the entered form values. + * @param {any[]} fields Fields that defines every content in the entry. + * @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set. + * @param {AddonModDataEntryFields} entryContents Original entry contents indexed by field id. + * @return {Promise} True if changed, false if not. */ - hasEditDataChanged(inputData: any, fields: any, dataId: number, entryContents: any): Promise { + hasEditDataChanged(inputData: any, fields: any[], dataId: number, entryContents: AddonModDataEntryFields): Promise { const promises = fields.map((field) => { return this.fieldsDelegate.hasFieldDataChanged(field, inputData, entryContents[field.id]); }); diff --git a/src/addon/mod/data/providers/offline.ts b/src/addon/mod/data/providers/offline.ts index 0c773f978..92e38cb25 100644 --- a/src/addon/mod/data/providers/offline.ts +++ b/src/addon/mod/data/providers/offline.ts @@ -19,6 +19,20 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreFileProvider } from '@providers/file'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { SQLiteDB } from '@classes/sqlitedb'; +import { AddonModDataSubfieldData } from './data'; + +/** + * Entry action stored offline. + */ +export interface AddonModDataOfflineAction { + dataid: number; + courseid: number; + groupid: number; + action: string; + entryid: number; // Negative for offline entries. + fields: AddonModDataSubfieldData[]; + timemodified: number; +} /** * Service to handle Offline data. @@ -175,10 +189,10 @@ export class AddonModDataOfflineProvider { /** * Get all the stored entry data from all the databases. * - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with entries. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with entries. */ - getAllEntries(siteId?: string): Promise { + getAllEntries(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getAllRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE); }).then((entries) => { @@ -189,11 +203,11 @@ export class AddonModDataOfflineProvider { /** * Get all the stored entry data from a certain database. * - * @param {number} dataId Database ID. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with entries. + * @param {number} dataId Database ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with entries. */ - getDatabaseEntries(dataId: number, siteId?: string): Promise { + getDatabaseEntries(dataId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}); }).then((entries) => { @@ -208,9 +222,9 @@ export class AddonModDataOfflineProvider { * @param {number} entryId Database entry Id. * @param {string} action Action to be done * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with entry. + * @return {Promise} Promise resolved with entry. */ - getEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { + getEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecord(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, action: action}); @@ -225,9 +239,9 @@ export class AddonModDataOfflineProvider { * @param {number} dataId Database ID. * @param {number} entryId Database entry Id. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with entry actions. + * @return {Promise} Promise resolved with entry actions. */ - getEntryActions(dataId: number, entryId: number, siteId?: string): Promise { + getEntryActions(dataId: number, entryId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId}); }).then((entries) => { @@ -286,10 +300,10 @@ export class AddonModDataOfflineProvider { /** * Parse "fields" of an offline record. * - * @param {any} record Record object - * @return {any} Record object with columns parsed. + * @param {any} record Record object + * @return {AddonModDataOfflineAction} Record object with columns parsed. */ - protected parseRecord(record: any): any { + protected parseRecord(record: any): AddonModDataOfflineAction { record.fields = this.textUtils.parseJSON(record.fields); return record; @@ -308,8 +322,8 @@ export class AddonModDataOfflineProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if stored, rejected if failure. */ - saveEntry(dataId: number, entryId: number, action: string, courseId: number, groupId?: number, fields?: any[], - timemodified?: number, siteId?: string): Promise { + saveEntry(dataId: number, entryId: number, action: string, courseId: number, groupId?: number, + fields?: AddonModDataSubfieldData[], timemodified?: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { timemodified = timemodified || new Date().getTime(); diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts index 5586ef558..a0a0d297a 100644 --- a/src/addon/mod/data/providers/prefetch-handler.ts +++ b/src/addon/mod/data/providers/prefetch-handler.ts @@ -25,7 +25,7 @@ import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreRatingProvider } from '@core/rating/providers/rating'; -import { AddonModDataProvider } from './data'; +import { AddonModDataProvider, AddonModDataEntry } from './data'; import { AddonModDataSyncProvider } from './sync'; import { AddonModDataHelperProvider } from './helper'; @@ -57,10 +57,10 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {string} [siteId] Site ID. - * @return {Promise} All unique entries. + * @return {Promise} All unique entries. */ protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false, - siteId?: string): Promise { + siteId?: string): Promise { const promises = groups.map((group) => { return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache, siteId); @@ -139,14 +139,14 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl /** * Returns the file contained in the entries. * - * @param {any[]} entries List of entries to get files from. - * @return {any[]} List of files. + * @param {AddonModDataEntry[]} entries List of entries to get files from. + * @return {any[]} List of files. */ - protected getEntriesFiles(entries: any[]): any[] { + protected getEntriesFiles(entries: AddonModDataEntry[]): any[] { let files = []; entries.forEach((entry) => { - entry.contents.forEach((content) => { + this.utils.objectToArray(entry.contents).forEach((content) => { files = files.concat(content.files); }); }); diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts index 5bfe991bd..03590485a 100644 --- a/src/addon/mod/data/providers/sync.ts +++ b/src/addon/mod/data/providers/sync.ts @@ -20,7 +20,7 @@ import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; -import { AddonModDataOfflineProvider } from './offline'; +import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; import { AddonModDataProvider } from './data'; import { AddonModDataHelperProvider } from './helper'; import { CoreEventsProvider } from '@providers/events'; @@ -174,7 +174,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { // No offline data found, return empty object. return []; }); - }).then((offlineActions) => { + }).then((offlineActions: AddonModDataOfflineAction[]) => { if (!offlineActions.length) { // Nothing to sync. return; @@ -226,13 +226,13 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { /** * Synchronize an entry. * - * @param {any} data Database. - * @param {any} entryActions Entry actions. - * @param {any} result Object with the result of the sync. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved if success, rejected otherwise. + * @param {any} data Database. + * @param {AddonModDataOfflineAction[]} entryActions Entry actions. + * @param {any} result Object with the result of the sync. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected otherwise. */ - protected syncEntry(data: any, entryActions: any[], result: any, siteId?: string): Promise { + protected syncEntry(data: any, entryActions: AddonModDataOfflineAction[], result: any, siteId?: string): Promise { let discardError, timePromise, entryId = 0, From 3d0e7d14e101b03fe7ccea89fe6fc33f39f6821d Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 10 Apr 2019 17:24:25 +0200 Subject: [PATCH 2/7] MOBILE-2856 data: Fix error when adding entry offline --- src/addon/mod/data/providers/data.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index 31caaf80f..f79ab69a2 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -137,6 +137,8 @@ export class AddonModDataProvider { fieldnotifications: notifications }); } + + return storeOffline(); } return this.addEntryOnline(dataId, contents, groupId, siteId).catch((error) => { From 6b0f08695fe1cd4f25a965d94fdd6a52bf9dcd7a Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 10 Apr 2019 17:39:26 +0200 Subject: [PATCH 3/7] MOBILE-2856 data: Helper functions for fetching entries --- src/addon/mod/data/components/index/index.ts | 173 +++++------------ src/addon/mod/data/pages/edit/edit.ts | 19 +- src/addon/mod/data/pages/entry/entry.html | 4 +- src/addon/mod/data/pages/entry/entry.ts | 30 +-- src/addon/mod/data/providers/helper.ts | 192 +++++++++++++++---- src/addon/mod/data/providers/offline.ts | 17 +- 6 files changed, 222 insertions(+), 213 deletions(-) diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index ac4d95546..3236fbd37 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -20,11 +20,9 @@ import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { CoreRatingProvider } from '@core/rating/providers/rating'; -import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; import { CoreRatingSyncProvider } from '@core/rating/providers/sync'; import { AddonModDataProvider } from '../../providers/data'; import { AddonModDataHelperProvider } from '../../providers/helper'; -import { AddonModDataOfflineProvider } from '../../providers/offline'; import { AddonModDataSyncProvider } from '../../providers/sync'; import { AddonModDataComponentsModule } from '../components.module'; import { AddonModDataPrefetchHandler } from '../../providers/prefetch-handler'; @@ -65,8 +63,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp advanced: [] }; hasNextPage = false; - offlineActions: any; - offlineEntries: any; entriesRendered = ''; extraImports = [AddonModDataComponentsModule]; jsData; @@ -81,12 +77,19 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp protected ratingOfflineObserver: any; protected ratingSyncObserver: any; - constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider, - private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content, - private prefetchHandler: AddonModDataPrefetchHandler, private timeUtils: CoreTimeUtilsProvider, - private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, - private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController, - private ratingOffline: CoreRatingOfflineProvider) { + constructor( + injector: Injector, + @Optional() content: Content, + private dataProvider: AddonModDataProvider, + private dataHelper: AddonModDataHelperProvider, + private prefetchHandler: AddonModDataPrefetchHandler, + private timeUtils: CoreTimeUtilsProvider, + private groupsProvider: CoreGroupsProvider, + private commentsProvider: CoreCommentsProvider, + private modalCtrl: ModalController, + private utils: CoreUtilsProvider, + protected navCtrl: NavController) { + super(injector, content); // Refresh entries on change. @@ -233,8 +236,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.selectedGroup = groupInfo.groups[0].id; } } - - return this.fetchOfflineEntries(); }); }).then(() => { return this.dataProvider.getFields(this.data.id).then((fields) => { @@ -270,21 +271,19 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp // Update values for current group. this.access.canaddentry = accessData.canaddentry; - if (this.search.searching) { - const text = this.search.searchingAdvanced ? undefined : this.search.text, - advanced = this.search.searchingAdvanced ? this.search.advanced : undefined; + 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.dataProvider.searchEntries(this.data.id, this.selectedGroup, text, advanced, this.search.sortBy, - this.search.sortDirection, this.search.page); - } else { - return this.dataProvider.getEntries(this.data.id, this.selectedGroup, this.search.sortBy, this.search.sortDirection, - this.search.page); - } + return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, search, advSearch, + this.search.sortBy, this.search.sortDirection, this.search.page); }).then((entries) => { - const numEntries = (entries && entries.entries && entries.entries.length) || 0; - this.isEmpty = !numEntries && !Object.keys(this.offlineActions).length && !Object.keys(this.offlineEntries).length; + const numEntries = entries.entries.length; + const numOfflineEntries = entries.offlineEntries.length; + this.isEmpty = !numEntries && !entries.offlineEntries.length; this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) * AddonModDataProvider.PER_PAGE) < entries.totalcount; + this.hasOffline = entries.hasOfflineActions; + this.hasOfflineRatings = entries.hasOfflineRatings; this.entriesRendered = ''; if (typeof entries.maxcount != 'undefined') { @@ -298,76 +297,40 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp } if (!this.isEmpty) { - const siteInfo = this.sitesProvider.getCurrentSite().getInfo(), - promises = []; + this.entries = entries.offlineEntries.concat(entries.entries); - this.utils.objectToArray(this.offlineEntries).forEach((offlineActions) => { - const offlineEntry = offlineActions.find((offlineEntry) => offlineEntry.action == 'add'); + let entriesHTML = this.data.listtemplateheader || ''; - if (offlineEntry) { - const entry = { - id: offlineEntry.entryid, - canmanageentry: true, - approved: !this.data.approval || this.data.manageapproved, - dataid: offlineEntry.dataid, - groupid: offlineEntry.groupid, - timecreated: -offlineEntry.entryid, - timemodified: -offlineEntry.entryid, - userid: siteInfo.userid, - fullname: siteInfo.fullname, - contents: {} - }; + // Get first entry from the whole list. + if (!this.search.searching || !this.firstEntry) { + this.firstEntry = this.entries[0].id; + } - if (offlineActions.length > 0) { - promises.push(this.dataHelper.applyOfflineActions(entry, offlineActions, this.fieldsArray)); - } else { - promises.push(Promise.resolve(entry)); - } - } + const template = this.data.listtemplate || this.dataHelper.getDefaultTemplate('list', this.fieldsArray); + + const entriesById = {}; + this.entries.forEach((entry, index) => { + entriesById[entry.id] = entry; + + const actions = this.dataHelper.getActions(this.data, this.access, entry); + const offset = this.search.searching ? undefined : + this.search.page * AddonModDataProvider.PER_PAGE + index - numOfflineEntries; + + entriesHTML += this.dataHelper.displayShowFields(template, this.fieldsArray, entry, offset, 'list', actions); }); + entriesHTML += this.data.listtemplatefooter || ''; - entries.entries.forEach((entry) => { - if (typeof this.offlineActions[entry.id] != 'undefined') { - promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fieldsArray)); - } else { - promises.push(Promise.resolve(entry)); - } - }); + this.entriesRendered = entriesHTML; - return Promise.all(promises).then((entries) => { - this.entries = entries; - - let entriesHTML = this.data.listtemplateheader || ''; - - // Get first entry from the whole list. - if (entries && entries[0] && (!this.search.searching || !this.firstEntry)) { - this.firstEntry = entries[0].id; - } - - const template = this.data.listtemplate || this.dataHelper.getDefaultTemplate('list', this.fieldsArray); - - const entriesById = {}; - entries.forEach((entry, index) => { - entriesById[entry.id] = entry; - - const actions = this.dataHelper.getActions(this.data, this.access, entry); - const offset = this.search.page * AddonModDataProvider.PER_PAGE + index; - - entriesHTML += this.dataHelper.displayShowFields(template, this.fieldsArray, entry, offset, 'list', - actions); - }); - entriesHTML += this.data.listtemplatefooter || ''; - - this.entriesRendered = entriesHTML; - - // Pass the input data to the component. - this.jsData = { - fields: this.fields, - entries: entriesById, - data: this.data, - gotoEntry: this.gotoEntry.bind(this) - }; - }); + // Pass the input data to the component. + this.jsData = { + fields: this.fields, + entries: entriesById, + data: this.data, + module: this.module, + group: this.selectedGroup, + gotoEntry: this.gotoEntry.bind(this) + }; } else if (!this.search.searching) { // Empty and no searching. this.canSearch = false; @@ -476,42 +439,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.navCtrl.push('AddonModDataEntryPage', params); } - /** - * Fetch offline entries. - * - * @return {Promise} Resolved then done. - */ - protected fetchOfflineEntries(): Promise { - // Check if there are entries stored in offline. - return this.dataOffline.getDatabaseEntries(this.data.id).then((offlineEntries) => { - this.hasOffline = !!offlineEntries.length; - - this.offlineActions = {}; - this.offlineEntries = {}; - - // Only show offline entries on first page. - if (this.search.page == 0 && this.hasOffline) { - offlineEntries.forEach((entry) => { - if (entry.entryid > 0) { - if (typeof this.offlineActions[entry.entryid] == 'undefined') { - this.offlineActions[entry.entryid] = []; - } - this.offlineActions[entry.entryid].push(entry); - } else { - if (typeof this.offlineActions[entry.entryid] == 'undefined') { - this.offlineEntries[entry.entryid] = []; - } - this.offlineEntries[entry.entryid].push(entry); - } - }); - } - }).then(() => { - return this.ratingOffline.hasRatings('mod_data', 'entry', 'module', this.data.coursemodule).then((hasRatings) => { - this.hasOfflineRatings = hasRatings; - }); - }); - } - /** * Performs the sync of the activity. * diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index ce8ccd06a..bed268873 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -45,7 +45,6 @@ export class AddonModDataEditPage { protected data: any; protected entryId: number; protected entry: any; - protected offlineActions = []; protected fields = {}; protected fieldsArray = []; protected siteId: string; @@ -145,28 +144,14 @@ export class AddonModDataEditPage { }); } }).then(() => { - return this.dataOffline.getEntryActions(this.data.id, this.entryId); - }).then((actions) => { - this.offlineActions = actions; - return this.dataProvider.getFields(this.data.id); }).then((fieldsData) => { this.fieldsArray = fieldsData; this.fields = this.utils.arrayToObject(fieldsData, 'id'); - return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions); + return this.dataHelper.fetchEntry(this.data, fieldsData, this.entryId); }).then((entry) => { - if (entry) { - entry = entry.entry; - } else { - entry = { - contents: {} - }; - } - - return this.dataHelper.applyOfflineActions(entry, this.offlineActions, this.fieldsArray); - }).then((entryData) => { - this.entry = entryData; + this.entry = entry.entry; this.editFormRender = this.displayEditFields(); }).catch((message) => { diff --git a/src/addon/mod/data/pages/entry/entry.html b/src/addon/mod/data/pages/entry/entry.html index aef6d0da0..20e8d31d0 100644 --- a/src/addon/mod/data/pages/entry/entry.html +++ b/src/addon/mod/data/pages/entry/entry.html @@ -9,7 +9,7 @@ -
+
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
@@ -31,7 +31,7 @@ - + diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 0ae32b2be..07b3ee48f 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -23,7 +23,6 @@ import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreRatingInfo } from '@core/rating/providers/rating'; import { AddonModDataProvider } from '../../providers/data'; import { AddonModDataHelperProvider } from '../../providers/helper'; -import { AddonModDataOfflineProvider } from '../../providers/offline'; import { AddonModDataSyncProvider } from '../../providers/sync'; import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; import { AddonModDataComponentsModule } from '../../components/components.module'; @@ -46,6 +45,7 @@ export class AddonModDataEntryPage implements OnDestroy { protected syncObserver: any; // It will observe the sync auto event. protected entryChangedObserver: any; // It will observe the changed entry event. protected fields = {}; + protected fieldsArray = []; title = ''; moduleName = 'data'; @@ -56,8 +56,6 @@ export class AddonModDataEntryPage implements OnDestroy { loadingRating = false; selectedGroup = 0; entry: any; - offlineActions = []; - hasOffline = false; previousOffset: number; nextOffset: number; access: any; @@ -74,7 +72,7 @@ export class AddonModDataEntryPage implements OnDestroy { constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider, - protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider, + protected dataHelper: AddonModDataHelperProvider, sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected eventsProvider: CoreEventsProvider, private cdr: ChangeDetectorRef) { this.module = params.get('module') || {}; @@ -131,8 +129,6 @@ export class AddonModDataEntryPage implements OnDestroy { * @return {Promise} Resolved when done. */ protected fetchEntryData(refresh?: boolean, isPtr?: boolean): Promise { - let fieldsArray; - this.isPullingToRefresh = isPtr; return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { @@ -155,32 +151,22 @@ export class AddonModDataEntryPage implements OnDestroy { this.selectedGroup = groupInfo.groups[0].id; } } - - return this.dataOffline.getEntryActions(this.data.id, this.entryId); }); - }).then((actions) => { - this.offlineActions = actions; - this.hasOffline = !!actions.length; - + }).then(() => { return this.dataProvider.getFields(this.data.id).then((fieldsData) => { this.fields = this.utils.arrayToObject(fieldsData, 'id'); + this.fieldsArray = fieldsData; - return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions); + return this.dataHelper.fetchEntry(this.data, fieldsData, this.entryId); }); }).then((entry) => { + this.entry = entry.entry; this.ratingInfo = entry.ratinginfo; - entry = entry.entry; - - fieldsArray = this.utils.objectToArray(this.fields); - - return this.dataHelper.applyOfflineActions(entry, this.offlineActions, fieldsArray); - }).then((entryData) => { - this.entry = entryData; const actions = this.dataHelper.getActions(this.data, this.access, this.entry); - const templte = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', fieldsArray); - this.entryHtml = this.dataHelper.displayShowFields(templte, fieldsArray, this.entry, this.offset, 'show', actions); + const templte = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', this.fieldsArray); + this.entryHtml = this.dataHelper.displayShowFields(templte, this.fieldsArray, this.entry, this.offset, 'show', actions); this.showComments = actions.comments; const entries = {}; diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 930d63e15..c2428fabb 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -19,7 +19,9 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { AddonModDataFieldsDelegate } from './fields-delegate'; import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; -import { AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields } from './data'; +import { AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields, AddonModDataEntries } from './data'; +import { CoreRatingInfo } from '@core/rating/providers/rating'; +import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; /** * Service that provides helper functions for datas. @@ -30,7 +32,7 @@ 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 textUtils: CoreTextUtilsProvider, private ratingOffline: CoreRatingOfflineProvider) { } /** * Returns the record with the offline actions applied. @@ -156,6 +158,153 @@ export class AddonModDataHelperProvider { return template; } + /** + * Get online and offline entries, or search entries. + * + * @param {any} data Database object. + * @param {any[]} fields The fields that define the contents. + * @param {number} [groupId=0] Group ID. + * @param {string} [search] Search text. It will be used if advSearch is not defined. + * @param {any[]} [advSearch] Advanced search data. + * @param {string} [sort=0] 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 {string} [order=DESC] The direction of the sorting: 'ASC' or 'DESC'. + * Empty for using the default database setting. + * @param {number} [page=0] Page of records to return. + * @param {number} [perPage=PER_PAGE] Records per page to return. Default on PER_PAGE. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} 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) => { + const offlineActions = {}; + const result: AddonModDataEntries = { + entries: [], + totalcount: 0, + offlineEntries: [] + }; + + const offlinePromise = this.dataOffline.getDatabaseEntries(data.id, site.id).then((actions) => { + result.hasOfflineActions = !!actions.length; + + actions.forEach((action) => { + if (typeof offlineActions[action.entryid] == 'undefined') { + offlineActions[action.entryid] = []; + } + 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)) { + result.offlineEntries.push({ + id: action.entryid, + canmanageentry: true, + approved: !data.approval || data.manageapproved, + dataid: data.id, + groupid: action.groupid, + timecreated: -action.entryid, + timemodified: -action.entryid, + userid: site.getUserId(), + fullname: site.getInfo().fullname, + contents: {} + }); + } + }); + + // Sort offline entries by creation time. + result.offlineEntries.sort((entry1, entry2) => entry2.timecreated - entry1.timecreated); + }); + + const ratingsPromise = this.ratingOffline.hasRatings('mod_data', 'entry', 'module', data.coursemodule) + .then((hasRatings) => { + result.hasOfflineRatings = hasRatings; + }); + + let fetchPromise: Promise; + if (search || advSearch) { + fetchPromise = this.dataProvider.searchEntries(data.id, groupId, search, advSearch, sort, order, page, perPage, + site.id).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) => { + result.entries = fetchResult.entries; + result.totalcount = fetchResult.totalcount; + }); + } + + return Promise.all([offlinePromise, ratingsPromise, fetchPromise]).then(() => { + // Apply offline actions to online and offline entries. + const promises = []; + result.entries.forEach((entry) => { + promises.push(this.applyOfflineActions(entry, offlineActions[entry.id] || [], fields)); + }); + result.offlineEntries.forEach((entry) => { + promises.push(this.applyOfflineActions(entry, offlineActions[entry.id] || [], fields)); + }); + + return Promise.all(promises); + }).then(() => { + return result; + }); + }); + } + + /** + * Fetch an online or offline entry. + * + * @param {any} data Database. + * @param {any[]} fields List of database fields. + * @param {number} entryId Entry ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise<{entry: AddonModDataEntry, ratinginfo?: CoreRatingInfo}>} Promise resolved with the entry. + */ + fetchEntry(data: any, fields: any[], entryId: number, siteId?: string): + Promise<{entry: AddonModDataEntry, ratinginfo?: CoreRatingInfo}> { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.dataOffline.getEntryActions(data.id, entryId, site.id).then((offlineActions) => { + let promise: Promise<{entry: AddonModDataEntry, ratinginfo?: CoreRatingInfo}>; + + if (entryId > 0) { + // Online entry. + promise = this.dataProvider.getEntry(data.id, entryId, false, site.id); + } else { + // Offline entry or new entry. + promise = Promise.resolve({ + entry: { + id: entryId, + userid: site.getUserId(), + groupid: 0, + dataid: data.id, + timecreated: -entryId, + timemodified: -entryId, + approved: !data.approval || data.manageapproved, + canmanageentry: true, + fullname: site.getInfo().fullname, + contents: [], + } + }); + } + + return promise.then((response) => { + return this.applyOfflineActions(response.entry, offlineActions, fields).then(() => { + return response; + }); + }); + }); + }); + } + /** * Returns an object with all the actions that the user can do over the record. * @@ -352,45 +501,6 @@ export class AddonModDataHelperProvider { }); } - /** - * Get an online or offline entry. - * - * @param {any} data Database. - * @param {number} entryId Entry ID. - * @param {any} [offlineActions] Offline data with the actions done. Required for offline entries. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with the entry. - */ - getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise { - if (entryId > 0) { - // It's an online entry, get it from WS. - return this.dataProvider.getEntry(data.id, entryId, false, siteId); - } - - // It's an offline entry, search it in the offline actions. - return this.sitesProvider.getSite(siteId).then((site) => { - const offlineEntry = offlineActions.find((offlineAction) => offlineAction.action == 'add'); - - if (offlineEntry) { - const siteInfo = site.getInfo(); - - return {entry: { - id: offlineEntry.entryid, - canmanageentry: true, - approved: !data.approval || data.manageapproved, - dataid: offlineEntry.dataid, - groupid: offlineEntry.groupid, - timecreated: -offlineEntry.entryid, - timemodified: -offlineEntry.entryid, - userid: siteInfo.userid, - fullname: siteInfo.fullname, - contents: {} - } - }; - } - }); - } - /** * Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles. * diff --git a/src/addon/mod/data/providers/offline.ts b/src/addon/mod/data/providers/offline.ts index 92e38cb25..2d6d6ca4a 100644 --- a/src/addon/mod/data/providers/offline.ts +++ b/src/addon/mod/data/providers/offline.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFileProvider } from '@providers/file'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; import { SQLiteDB } from '@classes/sqlitedb'; @@ -101,7 +102,8 @@ export class AddonModDataOfflineProvider { }; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, - private fileProvider: CoreFileProvider, private fileUploaderProvider: CoreFileUploaderProvider) { + private fileProvider: CoreFileProvider, private fileUploaderProvider: CoreFileUploaderProvider, + private utils: CoreUtilsProvider) { this.logger = logger.getInstance('AddonModDataOfflineProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -201,7 +203,7 @@ export class AddonModDataOfflineProvider { } /** - * Get all the stored entry data from a certain database. + * Get all the stored entry actions from a certain database, sorted by modification time. * * @param {number} dataId Database ID. * @param {string} [siteId] Site ID. If not defined, current site. @@ -209,7 +211,7 @@ export class AddonModDataOfflineProvider { */ getDatabaseEntries(dataId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}); + return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}, 'timemodified'); }).then((entries) => { return entries.map(this.parseRecord.bind(this)); }); @@ -257,11 +259,10 @@ export class AddonModDataOfflineProvider { * @return {Promise} Promise resolved with boolean: true if has offline answers, false otherwise. */ hasOfflineData(dataId: number, siteId?: string): Promise { - return this.getDatabaseEntries(dataId, siteId).then((entries) => { - return !!entries.length; - }).catch(() => { - // No offline data found, return false. - return false; + return this.sitesProvider.getSite(siteId).then((site) => { + return this.utils.promiseWorks( + site.getDb().recordExists(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}) + ); }); } From fccf79bbd26c668dbea1be90a5df4280446a41aa Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 10 Apr 2019 18:06:27 +0200 Subject: [PATCH 4/7] MOBILE-2856 data: Fix syncing entries with multiple actions --- src/addon/mod/data/providers/sync.ts | 161 ++++++++++++++++----------- 1 file changed, 94 insertions(+), 67 deletions(-) diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts index 03590485a..0553511c4 100644 --- a/src/addon/mod/data/providers/sync.ts +++ b/src/addon/mod/data/providers/sync.ts @@ -235,26 +235,32 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { protected syncEntry(data: any, entryActions: AddonModDataOfflineAction[], result: any, siteId?: string): Promise { let discardError, timePromise, - entryId = 0, + entryId = entryActions[0].entryid, offlineId, deleted = false; - const promises = []; - - // Sort entries by timemodified. - entryActions = entryActions.sort((a: any, b: any) => a.timemodified - b.timemodified); - - entryId = entryActions[0].entryid; + 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'); if (entryId > 0) { - timePromise = this.dataProvider.getEntry(data.id, entryId, false, siteId).then((entry) => { + timePromise = this.dataProvider.getEntry(data.id, entryId, true, siteId).then((entry) => { return entry.entry.timemodified; - }).catch(() => { - return -1; + }).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means the entry has been deleted. + return Promise.resolve(-1); + } + + return Promise.reject(error); }); - } else { + } else if (editAction) { + // New entry. offlineId = entryId; timePromise = Promise.resolve(0); + } else { + // New entry but the add action is missing, discard. + timePromise = Promise.resolve(-1); } return timePromise.then((timemodified) => { @@ -266,58 +272,11 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { return this.dataOffline.deleteAllEntryActions(data.id, entryId, siteId); } - entryActions.forEach((action) => { - let actionPromise; - const proms = []; - - entryId = action.entryid > 0 ? action.entryid : entryId; - - if (action.fields) { - action.fields.forEach((field) => { - // Upload Files if asked. - const value = this.textUtils.parseJSON(field.value); - if (value.online || value.offline) { - let files = value.online || []; - const fileProm = value.offline ? this.dataHelper.getStoredFiles(action.dataid, entryId, field.fieldid) : - Promise.resolve([]); - - proms.push(fileProm.then((offlineFiles) => { - files = files.concat(offlineFiles); - - return this.dataHelper.uploadOrStoreFiles(action.dataid, 0, entryId, field.fieldid, files, false, - siteId).then((filesResult) => { - field.value = JSON.stringify(filesResult); - }); - })); - } - }); - } - - actionPromise = Promise.all(proms).then(() => { - // Perform the action. - switch (action.action) { - case 'add': - return this.dataProvider.addEntryOnline(action.dataid, action.fields, data.groupid, siteId) - .then((result) => { - entryId = result.newentryid; - }); - case 'edit': - return this.dataProvider.editEntryOnline(entryId, action.fields, siteId); - case 'approve': - return this.dataProvider.approveEntryOnline(entryId, true, siteId); - case 'disapprove': - return this.dataProvider.approveEntryOnline(entryId, false, siteId); - case 'delete': - return this.dataProvider.deleteEntryOnline(entryId, siteId).then(() => { - deleted = true; - }); - default: - break; - } - }); - - promises.push(actionPromise.catch((error) => { - if (error && error.wserror) { + if (deleteAction) { + return this.dataProvider.deleteEntryOnline(entryId, siteId).then(() => { + deleted = true; + }).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { // The WebService has thrown an error, this means it cannot be performed. Discard. discardError = this.textUtils.getErrorMessageFromError(error); } else { @@ -328,11 +287,79 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { // Delete the offline data. result.updated = true; - return this.dataOffline.deleteEntry(action.dataid, action.entryid, action.action, siteId); - })); - }); + return this.dataOffline.deleteAllEntryActions(deleteAction.dataid, deleteAction.entryid, siteId); + }); + } + + let editPromise; + + if (editAction) { + editPromise = Promise.all(editAction.fields.map((field) => { + // Upload Files if asked. + const value = this.textUtils.parseJSON(field.value); + if (value.online || value.offline) { + let files = value.online || []; + const fileProm = value.offline ? + this.dataHelper.getStoredFiles(editAction.dataid, entryId, field.fieldid) : + Promise.resolve([]); + + return fileProm.then((offlineFiles) => { + files = files.concat(offlineFiles); + + return this.dataHelper.uploadOrStoreFiles(editAction.dataid, 0, entryId, field.fieldid, files, + false, siteId).then((filesResult) => { + field.value = JSON.stringify(filesResult); + }); + }); + } + })).then(() => { + if (editAction.action == 'add') { + return this.dataProvider.addEntryOnline(editAction.dataid, editAction.fields, editAction.groupid, siteId) + .then((result) => { + entryId = result.newentryid; + }); + } else { + return this.dataProvider.editEntryOnline(entryId, editAction.fields, siteId); + } + }).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = this.textUtils.getErrorMessageFromError(error); + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.dataOffline.deleteEntry(editAction.dataid, editAction.entryid, editAction.action, siteId); + }); + } else { + editPromise = Promise.resolve(); + } + + if (approveAction) { + editPromise = editPromise.then(() => { + return this.dataProvider.approveEntryOnline(entryId, approveAction.action == 'approve', siteId); + }).catch((error) => { + if (error && this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = this.textUtils.getErrorMessageFromError(error); + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.dataOffline.deleteEntry(approveAction.dataid, approveAction.entryid, approveAction.action, siteId); + }); + } + + return editPromise; - return Promise.all(promises); }).then(() => { if (discardError) { // Submission was discarded, add a warning. From cd77fb2f4e294e0cf06b5d79bc13fdb00cd814d1 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 10 Apr 2019 18:24:28 +0200 Subject: [PATCH 5/7] MOBILE-2856 data: Fix single entry view for offline entries --- src/addon/mod/data/pages/entry/entry.ts | 95 ++++++++++++++++--------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 07b3ee48f..cb17d7dfa 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -135,8 +135,13 @@ export class AddonModDataEntryPage implements OnDestroy { this.title = data.name || this.title; this.data = data; - return this.setEntryIdFromOffset(data.id, this.offset, this.selectedGroup).then(() => { - return this.dataProvider.getDatabaseAccessInformation(data.id); + return this.dataProvider.getFields(this.data.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); }); }).then((accessData) => { this.access = accessData; @@ -153,20 +158,11 @@ export class AddonModDataEntryPage implements OnDestroy { } }); }).then(() => { - return this.dataProvider.getFields(this.data.id).then((fieldsData) => { - this.fields = this.utils.arrayToObject(fieldsData, 'id'); - this.fieldsArray = fieldsData; - - return this.dataHelper.fetchEntry(this.data, fieldsData, this.entryId); - }); - }).then((entry) => { - this.entry = entry.entry; - this.ratingInfo = entry.ratinginfo; - const actions = this.dataHelper.getActions(this.data, this.access, this.entry); - const templte = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', this.fieldsArray); - this.entryHtml = this.dataHelper.displayShowFields(templte, this.fieldsArray, this.entry, this.offset, 'show', actions); + const template = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', this.fieldsArray); + this.entryHtml = this.dataHelper.displayShowFields(template, this.fieldsArray, this.entry, this.offset, 'show', + actions); this.showComments = actions.comments; const entries = {}; @@ -176,7 +172,9 @@ export class AddonModDataEntryPage implements OnDestroy { this.jsData = { fields: this.fields, entries: entries, - data: this.data + data: this.data, + module: this.module, + group: this.selectedGroup }; }).catch((message) => { if (!refresh) { @@ -249,7 +247,7 @@ export class AddonModDataEntryPage implements OnDestroy { */ setGroup(groupId: number): Promise { this.selectedGroup = groupId; - this.offset = 0; + this.offset = null; this.entry = null; this.entryId = null; this.entryLoaded = false; @@ -258,46 +256,73 @@ export class AddonModDataEntryPage implements OnDestroy { } /** - * Convenience function to translate offset to entry identifier and set next/previous entries. + * Convenience function to fetch the entry and set next/previous entries. * - * @param {number} dataId Data Id. - * @param {number} [offset] Offset of the entry. - * @param {number} [groupId] Group Id to get the entry. * @return {Promise} Resolved when done. */ - protected setEntryIdFromOffset(dataId: number, offset?: number, groupId?: number): Promise { - if (typeof offset != 'number') { + protected setEntryFromOffset(): Promise { + const emptyOffset = typeof this.offset != 'number'; + + if (emptyOffset && typeof this.entryId == 'number') { // Entry id passed as navigation parameter instead of the offset. // We don't display next/previous buttons in this case. this.nextOffset = null; this.previousOffset = null; - return Promise.resolve(); + return this.dataHelper.fetchEntry(this.data, this.fieldsArray, this.entryId).then((entry) => { + this.entry = entry.entry; + this.ratingInfo = entry.ratinginfo; + }); } const perPage = AddonModDataProvider.PER_PAGE; - const page = Math.floor(offset / perPage); - const pageOffset = offset % perPage; + const page = !emptyOffset && this.offset >= 0 ? Math.floor(this.offset / perPage) : 0; - return this.dataProvider.getEntries(dataId, groupId, undefined, undefined, page, perPage).then((entries) => { - if (!entries || !entries.entries || !entries.entries.length || pageOffset >= entries.entries.length) { - return Promise.reject(null); + return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, undefined, undefined, '0', '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. + if (emptyOffset) { + // No offset passed, display the first entry. + pageIndex = 0; + } else if (this.offset > 0) { + // Online entry. + pageIndex = this.offset % perPage + entries.offlineEntries.length; + } else { + // Offline entry. + pageIndex = this.offset + entries.offlineEntries.length; } - this.entryId = entries.entries[pageOffset].id; - this.previousOffset = offset > 0 ? offset - 1 : null; - if (pageOffset + 1 < entries.entries.length) { + this.entry = pageEntries[pageIndex]; + this.entryId = this.entry.id; + + this.previousOffset = page > 0 || pageIndex > 0 ? this.offset - 1 : null; + + let promise; + + if (pageIndex + 1 < pageEntries.length) { // Not the last entry on the page; - this.nextOffset = offset + 1; - } else if (entries.entries.length < perPage) { + this.nextOffset = this.offset + 1; + } else if (pageEntries.length < perPage) { // Last entry of the last page. this.nextOffset = null; } else { // Last entry of the page, check if there are more pages. - return this.dataProvider.getEntries(dataId, groupId, undefined, undefined, page + 1, perPage).then((entries) => { - this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? offset + 1 : null; + promise = this.dataProvider.getEntries(this.data.id, this.selectedGroup, '0', 'DESC', page + 1, perPage) + .then((entries) => { + this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? this.offset + 1 : null; }); } + + 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) => { + this.ratingInfo = entry.ratinginfo; + }); + } + }); }); } From b810862fcaa5fb35fff72c3100988fd3c1761d90 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 10 Apr 2019 18:27:01 +0200 Subject: [PATCH 6/7] MOBILE-2856 data: Go to first page when changing group --- src/addon/mod/data/components/index/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index 3236fbd37..dcae5876c 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -395,6 +395,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp */ setGroup(groupId: number): Promise { this.selectedGroup = groupId; + this.search.page = 0; return this.fetchEntriesData().catch((message) => { this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); From e6f9ecebcca5886486d26963b5103f5c773e5449 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Wed, 10 Apr 2019 18:19:52 +0200 Subject: [PATCH 7/7] MOBILE-2856 data: Fix actions for offline entries --- .../mod/data/components/action/action.ts | 101 ++++++++++------ .../action/addon-mod-data-action.html | 10 +- .../data/providers/approve-link-handler.ts | 52 +-------- .../mod/data/providers/delete-link-handler.ts | 61 +--------- src/addon/mod/data/providers/helper.ts | 109 +++++++++++++++++- 5 files changed, 187 insertions(+), 146 deletions(-) diff --git a/src/addon/mod/data/components/action/action.ts b/src/addon/mod/data/components/action/action.ts index 32be60ec3..9e96c3d1a 100644 --- a/src/addon/mod/data/components/action/action.ts +++ b/src/addon/mod/data/components/action/action.ts @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. import { Component, Input, OnInit, Injector } from '@angular/core'; +import { NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { AddonModDataProvider } from '../../providers/data'; +import { AddonModDataHelperProvider } from '../../providers/helper'; import { AddonModDataOfflineProvider } from '../../providers/offline'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { CoreUserProvider } from '@core/user/providers/user'; /** @@ -30,6 +33,8 @@ export class AddonModDataActionComponent implements OnInit { @Input() action: string; // The field to render. @Input() entry?: any; // The value of the field. @Input() database: any; // Database object. + @Input() module: any; // Module object. + @Input() group: number; // Module object. @Input() offset?: number; // Offset of the entry. siteId: string; @@ -39,11 +44,72 @@ export class AddonModDataActionComponent implements OnInit { constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider, protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider, - sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider) { + sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider, private navCtrl: NavController, + protected linkHelper: CoreContentLinksHelperProvider, private dataHelper: AddonModDataHelperProvider) { this.rootUrl = sitesProvider.getCurrentSite().getURL(); this.siteId = sitesProvider.getCurrentSiteId(); } + /** + * Component being initialized. + */ + ngOnInit(): void { + if (this.action == 'userpicture') { + this.userProvider.getProfile(this.entry.userid, this.database.courseid).then((profile) => { + this.userPicture = profile.profileimageurl; + }); + } + } + + /** + * Approve the entry. + */ + approveEntry(): void { + this.dataHelper.approveOrDisapproveEntry(this.database.id, this.entry.id, true, this.database.courseid); + } + + /** + * Show confirmation modal for deleting the entry. + */ + deleteEntry(): void { + this.dataHelper.showDeleteEntryModal(this.database.id, this.entry.id, this.database.courseid); + } + + /** + * Disapprove the entry. + */ + disapproveEntry(): void { + this.dataHelper.approveOrDisapproveEntry(this.database.id, this.entry.id, false, this.database.courseid); + } + + /** + * Go to the edit page of the entry. + */ + editEntry(): void { + const pageParams = { + courseId: this.database.course, + module: this.module, + entryId: this.entry.id + }; + + this.linkHelper.goInSite(this.navCtrl, 'AddonModDataEditPage', pageParams); + } + + /** + * Go to the view page of the entry. + */ + viewEntry(): void { + const pageParams: any = { + courseId: this.database.course, + module: this.module, + entryId: this.entry.id, + group: this.group, + offset: this.offset + }; + + this.linkHelper.goInSite(this.navCtrl, 'AddonModDataEntryPage', pageParams); + } + /** * Undo delete action. * @@ -60,37 +126,4 @@ export class AddonModDataActionComponent implements OnInit { this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, this.siteId); }); } - - /** - * Component being initialized. - */ - ngOnInit(): void { - switch (this.action) { - case 'more': - this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id; - if (typeof this.offset == 'number') { - this.url += '&mode=single&page=' + this.offset; - } - break; - case 'edit': - this.url = this.rootUrl + '/mod/data/edit.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id; - break; - case 'delete': - this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&delete=' + this.entry.id; - break; - case 'approve': - this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&approve=' + this.entry.id; - break; - case 'disapprove': - this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&disapprove=' + this.entry.id; - break; - case 'userpicture': - this.userProvider.getProfile(this.entry.userid, this.database.courseid).then((profile) => { - this.userPicture = profile.profileimageurl; - }); - break; - default: - break; - } - } } diff --git a/src/addon/mod/data/components/action/addon-mod-data-action.html b/src/addon/mod/data/components/action/addon-mod-data-action.html index 2faa09162..41a44e5fa 100644 --- a/src/addon/mod/data/components/action/addon-mod-data-action.html +++ b/src/addon/mod/data/components/action/addon-mod-data-action.html @@ -1,12 +1,12 @@ - + - + - + @@ -14,11 +14,11 @@ - + - + diff --git a/src/addon/mod/data/providers/approve-link-handler.ts b/src/addon/mod/data/providers/approve-link-handler.ts index 5e7a88a7d..5084b8fb3 100644 --- a/src/addon/mod/data/providers/approve-link-handler.ts +++ b/src/addon/mod/data/providers/approve-link-handler.ts @@ -16,9 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; import { AddonModDataProvider } from './data'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreEventsProvider } from '@providers/events'; +import { AddonModDataHelperProvider } from './helper'; /** * Content links handler for database approve/disapprove entry. @@ -30,29 +28,10 @@ export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase featureName = 'CoreCourseModuleDelegate_AddonModData'; pattern = /\/mod\/data\/view\.php.*([\?\&](d|approve|disapprove)=\d+)/; - constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, - private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider) { + constructor(private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider) { super(); } - /** - * Convenience function to help get courseId. - * - * @param {number} dataId Database Id. - * @param {string} siteId Site Id, if not set, current site will be used. - * @param {number} courseId Course Id if already set. - * @return {Promise} Resolved with course Id when done. - */ - protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise { - if (courseId) { - return Promise.resolve(courseId); - } - - return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { - return module.course; - }); - } - /** * Get the list of actions for a link (url). * @@ -66,34 +45,11 @@ export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase CoreContentLinksAction[] | Promise { return [{ action: (siteId, navCtrl?): void => { - const modal = this.domUtils.showModalLoading(), - dataId = parseInt(params.d, 10), + const dataId = parseInt(params.d, 10), entryId = parseInt(params.approve, 10) || parseInt(params.disapprove, 10), approve = parseInt(params.approve, 10) ? true : false; - this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => { - courseId = cId; - - // Approve/disapprove entry. - return this.dataProvider.approveEntry(dataId, entryId, approve, courseId, siteId).catch((message) => { - this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errorapproving', true); - - return Promise.reject(null); - }); - }).then(() => { - const promises = []; - promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId)); - promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId)); - - return Promise.all(promises); - }).then(() => { - this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, siteId); - - this.domUtils.showToast(approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', true, - 3000); - }).finally(() => { - modal.dismiss(); - }); + this.dataHelper.approveOrDisapproveEntry(dataId, entryId, approve, courseId, siteId); } }]; } diff --git a/src/addon/mod/data/providers/delete-link-handler.ts b/src/addon/mod/data/providers/delete-link-handler.ts index 5ba8f1b31..8da37ba6b 100644 --- a/src/addon/mod/data/providers/delete-link-handler.ts +++ b/src/addon/mod/data/providers/delete-link-handler.ts @@ -13,13 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; import { AddonModDataProvider } from './data'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreEventsProvider } from '@providers/events'; +import { AddonModDataHelperProvider } from './helper'; /** * Content links handler for database delete entry. @@ -31,30 +28,10 @@ export class AddonModDataDeleteLinkHandler extends CoreContentLinksHandlerBase { featureName = 'CoreCourseModuleDelegate_AddonModData'; pattern = /\/mod\/data\/view\.php.*([\?\&](d|delete)=\d+)/; - constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, - private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider, - private translate: TranslateService) { + constructor(private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider) { super(); } - /** - * Convenience function to help get courseId. - * - * @param {number} dataId Database Id. - * @param {string} siteId Site Id, if not set, current site will be used. - * @param {number} courseId Course Id if already set. - * @return {Promise} Resolved with course Id when done. - */ - protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise { - if (courseId) { - return Promise.resolve(courseId); - } - - return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { - return module.course; - }); - } - /** * Get the list of actions for a link (url). * @@ -68,38 +45,10 @@ export class AddonModDataDeleteLinkHandler extends CoreContentLinksHandlerBase { CoreContentLinksAction[] | Promise { return [{ action: (siteId, navCtrl?): void => { + const dataId = parseInt(params.d, 10); + const entryId = parseInt(params.delete, 10); - this.domUtils.showConfirm(this.translate.instant('addon.mod_data.confirmdeleterecord')).then(() => { - const modal = this.domUtils.showModalLoading(), - dataId = parseInt(params.d, 10), - entryId = parseInt(params.delete, 10); - - return this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => { - courseId = cId; - - // Delete entry. - return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId).catch((message) => { - this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true); - - return Promise.reject(null); - }); - }).then(() => { - const promises = []; - promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId)); - promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId)); - - return Promise.all(promises); - }).then(() => { - this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId, - deleted: true}, siteId); - - this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); - }).finally(() => { - modal.dismiss(); - }); - }).catch(() => { - // Nothing to do. - }); + this.dataHelper.showDeleteEntryModal(dataId, entryId, courseId); } }]; } diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index c2428fabb..b5aaadcec 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -14,8 +14,12 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +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'; import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; @@ -32,7 +36,9 @@ 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 ratingOffline: CoreRatingOfflineProvider) { } + private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private utils: CoreUtilsProvider, + private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, + private ratingOffline: CoreRatingOfflineProvider) {} /** * Returns the record with the offline actions applied. @@ -104,6 +110,46 @@ export class AddonModDataHelperProvider { }); } + /** + * Approve or disapprove a database entry. + * + * @param {number} dataId Database ID. + * @param {number} entryId Entry ID. + * @param {boolaen} approve True to approve, false to disapprove. + * @param {number} [courseId] Course ID. It not defined, it will be fetched. + * @param {string} [siteId] Site ID. If not defined, current site. + */ + approveOrDisapproveEntry(dataId: number, entryId: number, approve: boolean, courseId?: number, siteId?: string): void { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const modal = this.domUtils.showModalLoading('core.sending', true); + + this.getActivityCourseIdIfNotSet(dataId, courseId, siteId).then((courseId) => { + // Approve/disapprove entry. + return this.dataProvider.approveEntry(dataId, entryId, approve, courseId, siteId).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errorapproving', true); + + return Promise.reject(null); + }); + }).then(() => { + const promises = []; + promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId)); + promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId)); + + return Promise.all(promises).catch(() => { + // Ignore errors. + }); + }).then(() => { + this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, siteId); + + this.domUtils.showToast(approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', true, 3000); + }).catch(() => { + // Ignore error, it was already displayed. + }).finally(() => { + modal.dismiss(); + }); + } + /** * Displays fields for being shown. * @@ -146,8 +192,8 @@ export class AddonModDataHelperProvider { } else if (action == 'approvalstatus') { render = this.translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved')); } else { - render = ''; + render = ''; } template = template.replace(replace, render); } else { @@ -337,6 +383,24 @@ export class AddonModDataHelperProvider { }; } + /** + * Convenience function to get the course id of the database. + * + * @param {number} dataId Database id. + * @param {number} [courseId] Course id, if known. + * @param {string} [siteId] Site id, if not set, current site will be used. + * @return {Promise} Resolved with course Id when done. + */ + protected getActivityCourseIdIfNotSet(dataId: number, courseId?: number, siteId?: string): Promise { + if (courseId) { + return Promise.resolve(courseId); + } + + return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { + return module.course; + }); + } + /** * Returns the default template of a certain type. * @@ -543,6 +607,45 @@ export class AddonModDataHelperProvider { }); } + /** + * Displays a confirmation modal for deleting an entry. + * + * @param {number} dataId Database ID. + * @param {number} entryId Entry ID. + * @param {number} [courseId] Course ID. It not defined, it will be fetched. + * @param {string} [siteId] Site ID. If not defined, current site. + */ + showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): void { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + this.domUtils.showConfirm(this.translate.instant('addon.mod_data.confirmdeleterecord')).then(() => { + const modal = this.domUtils.showModalLoading(); + + return this.getActivityCourseIdIfNotSet(dataId, courseId, siteId).then((courseId) => { + return 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); + + this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); + }).finally(() => { + modal.dismiss(); + }); + }).catch(() => { + // Ignore error, it was already displayed. + }); + } + /** * Given a list of files (either online files or local files), store the local files in a local folder * to be submitted later.