+
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
@@ -31,7 +31,7 @@
-
+ 0">
diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts
index 2c568ab8d..cb17d7dfa 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,16 +129,19 @@ 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) => {
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;
@@ -155,35 +156,13 @@ 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;
-
- return this.dataProvider.getFields(this.data.id).then((fieldsData) => {
- this.fields = this.utils.arrayToObject(fieldsData, 'id');
-
- return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions);
- });
- }).then((entry) => {
- 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);
- }).then((entryData) => {
- this.entry = entryData;
-
+ }).then(() => {
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 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 = {};
@@ -193,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) {
@@ -266,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;
@@ -275,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;
+ });
+ }
+ });
});
}
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/data.ts b/src/addon/mod/data/providers/data.ts
index a50cf5156..f79ab69a2 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.
@@ -76,6 +137,8 @@ export class AddonModDataProvider {
fieldnotifications: notifications
});
}
+
+ return storeOffline();
}
return this.addEntryOnline(dataId, contents, groupId, siteId).catch((error) => {
@@ -93,12 +156,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 +247,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 +352,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 +433,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 +460,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 +483,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 +658,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 +685,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 +723,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 +741,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 +945,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 +985,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/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 c713f3674..b5aaadcec 100644
--- a/src/addon/mod/data/providers/helper.ts
+++ b/src/addon/mod/data/providers/helper.ts
@@ -14,12 +14,18 @@
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 } from './offline';
-import { AddonModDataProvider } from './data';
+import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline';
+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,20 +36,26 @@ 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 eventsProvider: CoreEventsProvider, private utils: CoreUtilsProvider,
+ private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider,
+ private ratingOffline: CoreRatingOfflineProvider) {}
/**
* 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 +68,8 @@ export class AddonModDataHelperProvider {
break;
case 'add':
case 'edit':
+ record.groupid = action.groupid;
+
const offlineContents = {};
action.fields.forEach((offlineContent) => {
@@ -77,10 +91,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;
@@ -94,18 +110,59 @@ 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.
*
- * @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 '';
}
@@ -135,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 {
@@ -147,6 +204,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.
*
@@ -179,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.
*
@@ -256,17 +478,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 +544,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([]);
}
@@ -343,45 +565,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.
*
@@ -403,13 +586,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]);
});
@@ -424,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.
diff --git a/src/addon/mod/data/providers/offline.ts b/src/addon/mod/data/providers/offline.ts
index 0c773f978..2d6d6ca4a 100644
--- a/src/addon/mod/data/providers/offline.ts
+++ b/src/addon/mod/data/providers/offline.ts
@@ -16,9 +16,24 @@ 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';
+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.
@@ -87,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);
}
@@ -175,10 +191,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) => {
@@ -187,15 +203,15 @@ 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.
- * @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});
+ return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}, 'timemodified');
}).then((entries) => {
return entries.map(this.parseRecord.bind(this));
});
@@ -208,9 +224,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 +241,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) => {
@@ -243,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})
+ );
});
}
@@ -286,10 +301,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 +323,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..0553511c4 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,35 +226,41 @@ 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,
+ 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.