2021-03-18 17:17:47 +01:00
|
|
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
import { ContextLevel } from '@/core/constants';
|
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
import { CoreCourse } from '@features/course/services/course';
|
|
|
|
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
|
|
|
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
2021-04-16 15:15:21 +02:00
|
|
|
import { FileEntry } from '@ionic-native/file/ngx';
|
2021-12-17 00:27:04 +01:00
|
|
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
2021-03-18 17:17:47 +01:00
|
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
|
|
|
import { CoreFormFields } from '@singletons/form';
|
|
|
|
import { CoreTextUtils } from '@services/utils/text';
|
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
|
|
import { makeSingleton, Translate } from '@singletons';
|
|
|
|
import { CoreEvents } from '@singletons/events';
|
|
|
|
import {
|
|
|
|
AddonModDataEntry,
|
|
|
|
AddonModData,
|
|
|
|
AddonModDataProvider,
|
|
|
|
AddonModDataSearchEntriesOptions,
|
|
|
|
AddonModDataEntries,
|
|
|
|
AddonModDataEntryFields,
|
|
|
|
AddonModDataAction,
|
|
|
|
AddonModDataGetEntryFormatted,
|
|
|
|
AddonModDataData,
|
|
|
|
AddonModDataTemplateType,
|
|
|
|
AddonModDataGetDataAccessInformationWSResponse,
|
|
|
|
AddonModDataTemplateMode,
|
|
|
|
AddonModDataField,
|
|
|
|
AddonModDataEntryWSField,
|
|
|
|
} from './data';
|
|
|
|
import { AddonModDataFieldsDelegate } from './data-fields-delegate';
|
|
|
|
import { AddonModDataOffline, AddonModDataOfflineAction } from './data-offline';
|
2021-04-16 15:15:21 +02:00
|
|
|
import { CoreFileEntry } from '@services/file-helper';
|
2021-03-18 17:17:47 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Service that provides helper functions for datas.
|
|
|
|
*/
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
|
|
export class AddonModDataHelperProvider {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the record with the offline actions applied.
|
|
|
|
*
|
|
|
|
* @param record Entry to modify.
|
|
|
|
* @param offlineActions Offline data with the actions done.
|
|
|
|
* @param fields Entry defined fields indexed by fieldid.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
|
|
|
protected async applyOfflineActions(
|
|
|
|
record: AddonModDataEntry,
|
|
|
|
offlineActions: AddonModDataOfflineAction[],
|
|
|
|
fields: AddonModDataField[],
|
|
|
|
): Promise<AddonModDataEntry> {
|
|
|
|
const promises: Promise<void>[] = [];
|
|
|
|
|
|
|
|
offlineActions.forEach((action) => {
|
|
|
|
record.timemodified = action.timemodified;
|
|
|
|
record.hasOffline = true;
|
|
|
|
const offlineContents: Record<number, CoreFormFields> = {};
|
|
|
|
|
|
|
|
switch (action.action) {
|
|
|
|
case AddonModDataAction.APPROVE:
|
|
|
|
record.approved = true;
|
|
|
|
break;
|
|
|
|
case AddonModDataAction.DISAPPROVE:
|
|
|
|
record.approved = false;
|
|
|
|
break;
|
|
|
|
case AddonModDataAction.DELETE:
|
|
|
|
record.deleted = true;
|
|
|
|
break;
|
|
|
|
case AddonModDataAction.ADD:
|
|
|
|
case AddonModDataAction.EDIT:
|
|
|
|
record.groupid = action.groupid;
|
|
|
|
|
|
|
|
action.fields.forEach((offlineContent) => {
|
2021-12-16 10:46:40 +01:00
|
|
|
if (offlineContents[offlineContent.fieldid] === undefined) {
|
2021-03-18 17:17:47 +01:00
|
|
|
offlineContents[offlineContent.fieldid] = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (offlineContent.subfield) {
|
|
|
|
offlineContents[offlineContent.fieldid][offlineContent.subfield] =
|
2021-06-21 09:11:51 +02:00
|
|
|
CoreTextUtils.parseJSON(offlineContent.value, '');
|
2021-03-18 17:17:47 +01:00
|
|
|
} else {
|
2021-06-21 09:11:51 +02:00
|
|
|
offlineContents[offlineContent.fieldid][''] = CoreTextUtils.parseJSON(offlineContent.value, '');
|
2021-03-18 17:17:47 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Override field contents.
|
|
|
|
fields.forEach((field) => {
|
|
|
|
if (AddonModDataFieldsDelegate.hasFiles(field)) {
|
|
|
|
promises.push(this.getStoredFiles(record.dataid, record.id, field.id).then((offlineFiles) => {
|
|
|
|
record.contents[field.id] = AddonModDataFieldsDelegate.overrideData(
|
|
|
|
field,
|
|
|
|
record.contents[field.id],
|
|
|
|
offlineContents[field.id],
|
|
|
|
offlineFiles,
|
|
|
|
);
|
|
|
|
record.contents[field.id].fieldid = field.id;
|
|
|
|
|
|
|
|
return;
|
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
record.contents[field.id] = AddonModDataFieldsDelegate.overrideData(
|
|
|
|
field,
|
|
|
|
record.contents[field.id],
|
|
|
|
offlineContents[field.id],
|
|
|
|
);
|
|
|
|
record.contents[field.id].fieldid = field.id;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
|
|
return record;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Approve or disapprove a database entry.
|
|
|
|
*
|
|
|
|
* @param dataId Database ID.
|
|
|
|
* @param entryId Entry ID.
|
|
|
|
* @param approve True to approve, false to disapprove.
|
|
|
|
* @param courseId Course ID. It not defined, it will be fetched.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
*/
|
|
|
|
async approveOrDisapproveEntry(
|
|
|
|
dataId: number,
|
|
|
|
entryId: number,
|
|
|
|
approve: boolean,
|
|
|
|
courseId?: number,
|
|
|
|
siteId?: string,
|
|
|
|
): Promise<void> {
|
|
|
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
|
|
|
|
|
|
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
|
|
|
|
|
|
|
try {
|
|
|
|
courseId = await this.getActivityCourseIdIfNotSet(dataId, courseId, siteId);
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Approve/disapprove entry.
|
|
|
|
await AddonModData.approveEntry(dataId, entryId, approve, courseId, siteId);
|
|
|
|
} catch (error) {
|
|
|
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_data.errorapproving', true);
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
|
|
|
const promises: Promise<void>[] = [];
|
|
|
|
|
|
|
|
promises.push(AddonModData.invalidateEntryData(dataId, entryId, siteId));
|
|
|
|
promises.push(AddonModData.invalidateEntriesData(dataId, siteId));
|
|
|
|
|
|
|
|
await CoreUtils.ignoreErrors(Promise.all(promises));
|
|
|
|
|
|
|
|
CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId: dataId, entryId: entryId }, siteId);
|
|
|
|
|
|
|
|
CoreDomUtils.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 template Template HMTL.
|
|
|
|
* @param fields Fields that defines every content in the entry.
|
|
|
|
* @param entry Entry.
|
|
|
|
* @param offset Entry offset.
|
|
|
|
* @param mode Mode list or show.
|
|
|
|
* @param actions Actions that can be performed to the record.
|
|
|
|
* @return Generated HTML.
|
|
|
|
*/
|
|
|
|
displayShowFields(
|
|
|
|
template: string,
|
|
|
|
fields: AddonModDataField[],
|
|
|
|
entry: AddonModDataEntry,
|
|
|
|
offset = 0,
|
|
|
|
mode: AddonModDataTemplateMode,
|
|
|
|
actions: Record<AddonModDataAction, boolean>,
|
|
|
|
): string {
|
|
|
|
|
|
|
|
if (!template) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Replace the fields found on template.
|
|
|
|
fields.forEach((field) => {
|
|
|
|
let replace = '[[' + field.name + ']]';
|
|
|
|
replace = replace.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
|
|
|
|
const replaceRegex = new RegExp(replace, 'gi');
|
|
|
|
|
|
|
|
// Replace field by a generic directive.
|
|
|
|
const render = '<addon-mod-data-field-plugin [field]="fields[' + field.id + ']" [value]="entries[' + entry.id +
|
|
|
|
'].contents[' + field.id + ']" mode="' + mode + '" [database]="database" (gotoEntry)="gotoEntry(' + entry.id +
|
|
|
|
')"></addon-mod-data-field-plugin>';
|
|
|
|
template = template.replace(replaceRegex, render);
|
|
|
|
});
|
|
|
|
|
|
|
|
for (const action in actions) {
|
|
|
|
const replaceRegex = new RegExp('##' + action + '##', 'gi');
|
|
|
|
// Is enabled?
|
|
|
|
if (actions[action]) {
|
|
|
|
let render = '';
|
|
|
|
if (action == AddonModDataAction.MOREURL) {
|
|
|
|
// Render more url directly because it can be part of an HTML attribute.
|
2021-09-09 13:37:51 +02:00
|
|
|
render = CoreSites.getRequiredCurrentSite().getURL() + '/mod/data/view.php?d={{database.id}}&rid=' + entry.id;
|
2021-03-18 17:17:47 +01:00
|
|
|
} else if (action == 'approvalstatus') {
|
|
|
|
render = Translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved'));
|
|
|
|
} else {
|
|
|
|
render = '<addon-mod-data-action action="' + action + '" [entry]="entries[' + entry.id + ']" mode="' + mode +
|
2021-12-17 00:27:04 +01:00
|
|
|
'" [database]="database" [title]="title" [offset]="' + offset + '" [group]="group" ></addon-mod-data-action>';
|
2021-03-18 17:17:47 +01:00
|
|
|
}
|
|
|
|
template = template.replace(replaceRegex, render);
|
|
|
|
} else {
|
|
|
|
template = template.replace(replaceRegex, '');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return template;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get online and offline entries, or search entries.
|
|
|
|
*
|
|
|
|
* @param database Database object.
|
|
|
|
* @param fields The fields that define the contents.
|
|
|
|
* @param options Other options.
|
|
|
|
* @return Promise resolved when the database is retrieved.
|
|
|
|
*/
|
|
|
|
async fetchEntries(
|
|
|
|
database: AddonModDataData,
|
|
|
|
fields: AddonModDataField[],
|
|
|
|
options: AddonModDataSearchEntriesOptions = {},
|
|
|
|
): Promise<AddonModDataEntries> {
|
|
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
options.groupId = options.groupId || 0;
|
|
|
|
options.page = options.page || 0;
|
|
|
|
|
|
|
|
const offlineActions: Record<number, AddonModDataOfflineAction[]> = {};
|
|
|
|
const result: AddonModDataEntries = {
|
|
|
|
entries: [],
|
|
|
|
totalcount: 0,
|
|
|
|
offlineEntries: [],
|
|
|
|
};
|
|
|
|
options.siteId = site.id;
|
|
|
|
|
|
|
|
const offlinePromise = AddonModDataOffline.getDatabaseEntries(database.id, site.id).then((actions) => {
|
|
|
|
result.hasOfflineActions = !!actions.length;
|
|
|
|
|
|
|
|
actions.forEach((action) => {
|
2021-12-16 10:46:40 +01:00
|
|
|
if (offlineActions[action.entryid] === undefined) {
|
2021-03-18 17:17:47 +01:00
|
|
|
offlineActions[action.entryid] = [];
|
|
|
|
}
|
|
|
|
offlineActions[action.entryid].push(action);
|
|
|
|
|
|
|
|
// We only display new entries in the first page when not searching.
|
|
|
|
if (action.action == AddonModDataAction.ADD && options.page == 0 && !options.search && !options.advSearch &&
|
|
|
|
(!action.groupid || !options.groupId || action.groupid == options.groupId)) {
|
|
|
|
result.offlineEntries!.push({
|
|
|
|
id: action.entryid,
|
|
|
|
canmanageentry: true,
|
|
|
|
approved: !database.approval || database.manageapproved,
|
|
|
|
dataid: database.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((a, b) => b.timecreated - a.timecreated);
|
|
|
|
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
|
|
|
|
const ratingsPromise = CoreRatingOffline.hasRatings('mod_data', 'entry', ContextLevel.MODULE, database.coursemodule)
|
|
|
|
.then((hasRatings) => {
|
|
|
|
result.hasOfflineRatings = hasRatings;
|
|
|
|
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
|
|
|
|
let fetchPromise: Promise<void>;
|
|
|
|
if (options.search || options.advSearch) {
|
|
|
|
fetchPromise = AddonModData.searchEntries(database.id, options).then((searchResult) => {
|
|
|
|
result.entries = searchResult.entries;
|
|
|
|
result.totalcount = searchResult.totalcount;
|
|
|
|
result.maxcount = searchResult.maxcount;
|
|
|
|
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
fetchPromise = AddonModData.getEntries(database.id, options).then((entriesResult) => {
|
|
|
|
result.entries = entriesResult.entries;
|
|
|
|
result.totalcount = entriesResult.totalcount;
|
|
|
|
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
await Promise.all([offlinePromise, ratingsPromise, fetchPromise]);
|
|
|
|
|
|
|
|
// Apply offline actions to online and offline entries.
|
|
|
|
const promises: Promise<AddonModDataEntry>[] = [];
|
|
|
|
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));
|
|
|
|
});
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch an online or offline entry.
|
|
|
|
*
|
|
|
|
* @param database Database.
|
|
|
|
* @param fields List of database fields.
|
|
|
|
* @param entryId Entry ID.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Promise resolved with the entry.
|
|
|
|
*/
|
|
|
|
async fetchEntry(
|
|
|
|
database: AddonModDataData,
|
|
|
|
fields: AddonModDataField[],
|
|
|
|
entryId: number,
|
|
|
|
siteId?: string,
|
|
|
|
): Promise<AddonModDataGetEntryFormatted> {
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
|
|
|
|
const offlineActions = await AddonModDataOffline.getEntryActions(database.id, entryId, site.id);
|
|
|
|
|
|
|
|
let response: AddonModDataGetEntryFormatted;
|
|
|
|
if (entryId > 0) {
|
|
|
|
// Online entry.
|
|
|
|
response = await AddonModData.getEntry(database.id, entryId, { cmId: database.coursemodule, siteId: site.id });
|
|
|
|
} else {
|
|
|
|
// Offline entry or new entry.
|
|
|
|
response = {
|
|
|
|
entry: {
|
|
|
|
id: entryId,
|
|
|
|
userid: site.getUserId(),
|
|
|
|
groupid: 0,
|
|
|
|
dataid: database.id,
|
|
|
|
timecreated: -entryId,
|
|
|
|
timemodified: -entryId,
|
|
|
|
approved: !database.approval || database.manageapproved,
|
|
|
|
canmanageentry: true,
|
|
|
|
fullname: site.getInfo()?.fullname,
|
|
|
|
contents: {},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.applyOfflineActions(response.entry, offlineActions, fields);
|
|
|
|
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an object with all the actions that the user can do over the record.
|
|
|
|
*
|
|
|
|
* @param database Database activity.
|
|
|
|
* @param accessInfo Access info to the activity.
|
|
|
|
* @param entry Entry or record where the actions will be performed.
|
|
|
|
* @return Keyed with the action names and boolean to evalute if it can or cannot be done.
|
|
|
|
*/
|
|
|
|
getActions(
|
|
|
|
database: AddonModDataData,
|
|
|
|
accessInfo: AddonModDataGetDataAccessInformationWSResponse,
|
|
|
|
entry: AddonModDataEntry,
|
2021-05-13 14:12:42 +02:00
|
|
|
): Record<AddonModDataAction, boolean> {
|
2021-03-18 17:17:47 +01:00
|
|
|
return {
|
|
|
|
add: false, // Not directly used on entries.
|
|
|
|
more: true,
|
|
|
|
moreurl: true,
|
|
|
|
user: true,
|
|
|
|
userpicture: true,
|
|
|
|
timeadded: true,
|
|
|
|
timemodified: true,
|
|
|
|
tags: true,
|
|
|
|
|
|
|
|
edit: entry.canmanageentry && !entry.deleted, // This already checks capabilities and readonly period.
|
|
|
|
delete: entry.canmanageentry,
|
|
|
|
approve: database.approval && accessInfo.canapprove && !entry.approved && !entry.deleted,
|
|
|
|
disapprove: database.approval && accessInfo.canapprove && entry.approved && !entry.deleted,
|
|
|
|
|
|
|
|
approvalstatus: database.approval,
|
|
|
|
comments: database.comments,
|
|
|
|
|
|
|
|
// Unsupported actions.
|
|
|
|
delcheck: false,
|
|
|
|
export: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convenience function to get the course id of the database.
|
|
|
|
*
|
|
|
|
* @param dataId Database id.
|
|
|
|
* @param courseId Course id, if known.
|
|
|
|
* @param siteId Site id, if not set, current site will be used.
|
|
|
|
* @return Resolved with course Id when done.
|
|
|
|
*/
|
|
|
|
protected async getActivityCourseIdIfNotSet(dataId: number, courseId?: number, siteId?: string): Promise<number> {
|
|
|
|
if (courseId) {
|
|
|
|
return courseId;
|
|
|
|
}
|
|
|
|
|
2021-12-17 00:27:04 +01:00
|
|
|
const module = await CoreCourse.getModuleBasicInfoByInstance(
|
|
|
|
dataId,
|
|
|
|
'data',
|
|
|
|
{ siteId, readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE },
|
|
|
|
);
|
2021-03-18 17:17:47 +01:00
|
|
|
|
|
|
|
return module.course;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the default template of a certain type.
|
|
|
|
*
|
|
|
|
* Based on Moodle function data_generate_default_template.
|
|
|
|
*
|
|
|
|
* @param type Type of template.
|
|
|
|
* @param fields List of database fields.
|
|
|
|
* @return Template HTML.
|
|
|
|
*/
|
|
|
|
getDefaultTemplate(type: AddonModDataTemplateType, fields: AddonModDataField[]): string {
|
|
|
|
if (type == AddonModDataTemplateType.LIST_HEADER || type == AddonModDataTemplateType.LIST_FOOTER) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
const html: string[] = [];
|
|
|
|
|
|
|
|
if (type == AddonModDataTemplateType.LIST) {
|
|
|
|
html.push('##delcheck##<br />');
|
|
|
|
}
|
|
|
|
|
|
|
|
html.push(
|
|
|
|
'<div class="defaulttemplate">',
|
|
|
|
'<table class="mod-data-default-template ##approvalstatus##">',
|
|
|
|
'<tbody>',
|
|
|
|
);
|
|
|
|
|
|
|
|
fields.forEach((field) => {
|
|
|
|
html.push(
|
|
|
|
'<tr class="">',
|
|
|
|
'<td class="template-field cell c0" style="">',
|
|
|
|
field.name,
|
|
|
|
': </td>',
|
|
|
|
'<td class="template-token cell c1 lastcol" style="">[[',
|
|
|
|
field.name,
|
|
|
|
']]</td>',
|
|
|
|
'</tr>',
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (type == AddonModDataTemplateType.LIST) {
|
|
|
|
html.push(
|
|
|
|
'<tr class="lastrow">',
|
|
|
|
'<td class="controls template-field cell c0 lastcol" style="" colspan="2">',
|
|
|
|
'##edit## ##more## ##delete## ##approve## ##disapprove## ##export##',
|
|
|
|
'</td>',
|
|
|
|
'</tr>',
|
|
|
|
);
|
|
|
|
} else if (type == AddonModDataTemplateType.SINGLE) {
|
|
|
|
html.push(
|
|
|
|
'<tr class="lastrow">',
|
|
|
|
'<td class="controls template-field cell c0 lastcol" style="" colspan="2">',
|
|
|
|
'##edit## ##delete## ##approve## ##disapprove## ##export##',
|
|
|
|
'</td>',
|
|
|
|
'</tr>',
|
|
|
|
);
|
|
|
|
} else if (type == AddonModDataTemplateType.SEARCH) {
|
|
|
|
html.push(
|
|
|
|
'<tr class="searchcontrols">',
|
|
|
|
'<td class="template-field cell c0" style="">Author first name: </td>',
|
|
|
|
'<td class="template-token cell c1 lastcol" style="">##firstname##</td>',
|
|
|
|
'</tr>',
|
|
|
|
'<tr class="searchcontrols lastrow">',
|
|
|
|
'<td class="template-field cell c0" style="">Author surname: </td>',
|
|
|
|
'<td class="template-token cell c1 lastcol" style="">##lastname##</td>',
|
|
|
|
'</tr>',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
html.push(
|
|
|
|
'</tbody>',
|
|
|
|
'</table>',
|
|
|
|
'</div>',
|
|
|
|
);
|
|
|
|
|
|
|
|
if (type == AddonModDataTemplateType.LIST) {
|
|
|
|
html.push('<hr />');
|
|
|
|
}
|
|
|
|
|
|
|
|
return html.join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve the entered data in the edit form.
|
|
|
|
* We don't use ng-model because it doesn't detect changes done by JavaScript.
|
|
|
|
*
|
|
|
|
* @param inputData Array with the entered form values.
|
|
|
|
* @param fields Fields that defines every content in the entry.
|
|
|
|
* @param dataId Database Id. If set, files will be uploaded and itemId set.
|
|
|
|
* @param entryId Entry Id.
|
|
|
|
* @param entryContents Original entry contents.
|
|
|
|
* @param offline True to prepare the data for an offline uploading, false otherwise.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return That contains object with the answers.
|
|
|
|
*/
|
|
|
|
async getEditDataFromForm(
|
|
|
|
inputData: CoreFormFields,
|
|
|
|
fields: AddonModDataField[],
|
|
|
|
dataId: number,
|
|
|
|
entryId: number,
|
|
|
|
entryContents: AddonModDataEntryFields,
|
2021-04-16 10:12:47 +02:00
|
|
|
offline = false,
|
2021-03-18 17:17:47 +01:00
|
|
|
siteId?: string,
|
|
|
|
): Promise<AddonModDataEntryWSField[]> {
|
|
|
|
if (!inputData) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
|
|
|
|
|
|
|
// Filter and translate fields to each field plugin.
|
|
|
|
const entryFieldDataToSend: AddonModDataEntryWSField[] = [];
|
|
|
|
|
|
|
|
const promises = fields.map(async (field) => {
|
|
|
|
const fieldData = AddonModDataFieldsDelegate.getFieldEditData(field, inputData, entryContents[field.id]);
|
|
|
|
if (!fieldData) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const proms = fieldData.map(async (fieldSubdata) => {
|
|
|
|
let value = fieldSubdata.value;
|
|
|
|
|
|
|
|
// Upload Files if asked.
|
|
|
|
if (dataId && fieldSubdata.files) {
|
|
|
|
value = await this.uploadOrStoreFiles(
|
|
|
|
dataId,
|
|
|
|
0,
|
|
|
|
entryId,
|
|
|
|
fieldSubdata.fieldid,
|
|
|
|
fieldSubdata.files,
|
|
|
|
offline,
|
|
|
|
siteId,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// WS wants values in JSON format.
|
|
|
|
entryFieldDataToSend.push({
|
|
|
|
fieldid: fieldSubdata.fieldid,
|
|
|
|
subfield: fieldSubdata.subfield || '',
|
2021-03-31 09:25:57 +02:00
|
|
|
value: value ? JSON.stringify(value) : '',
|
2021-03-18 17:17:47 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
|
|
|
|
await Promise.all(proms);
|
|
|
|
});
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
|
|
return entryFieldDataToSend;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve the temp files to be updated.
|
|
|
|
*
|
|
|
|
* @param inputData Array with the entered form values.
|
|
|
|
* @param fields Fields that defines every content in the entry.
|
|
|
|
* @param entryContents Original entry contents indexed by field id.
|
|
|
|
* @return That contains object with the files.
|
|
|
|
*/
|
|
|
|
async getEditTmpFiles(
|
|
|
|
inputData: CoreFormFields,
|
|
|
|
fields: AddonModDataField[],
|
|
|
|
entryContents: AddonModDataEntryFields,
|
2021-04-16 15:15:21 +02:00
|
|
|
): Promise<CoreFileEntry[]> {
|
2021-03-18 17:17:47 +01:00
|
|
|
if (!inputData) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Filter and translate fields to each field plugin.
|
|
|
|
const promises = fields.map((field) =>
|
|
|
|
AddonModDataFieldsDelegate.getFieldEditFiles(field, inputData, entryContents[field.id]));
|
|
|
|
|
|
|
|
const fieldsFiles = await Promise.all(promises);
|
|
|
|
|
|
|
|
return fieldsFiles.reduce((files, fieldFiles) => files.concat(fieldFiles), []);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles.
|
|
|
|
*
|
|
|
|
* @param dataId Database ID.
|
|
|
|
* @param entryId Entry ID or, if creating, timemodified.
|
|
|
|
* @param fieldId Field ID.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Promise resolved with the files.
|
|
|
|
*/
|
|
|
|
async getStoredFiles(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise<FileEntry[]> {
|
|
|
|
const folderPath = await AddonModDataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId);
|
|
|
|
|
|
|
|
try {
|
2021-06-21 09:11:51 +02:00
|
|
|
return await CoreFileUploader.getStoredFiles(folderPath);
|
2021-03-18 17:17:47 +01:00
|
|
|
} catch {
|
|
|
|
// Ignore not found files.
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the template of a certain type.
|
|
|
|
*
|
|
|
|
* @param data Database object.
|
|
|
|
* @param type Type of template.
|
|
|
|
* @param fields List of database fields.
|
|
|
|
* @return Template HTML.
|
|
|
|
*/
|
|
|
|
getTemplate(data: AddonModDataData, type: AddonModDataTemplateType, fields: AddonModDataField[]): string {
|
|
|
|
let template = data[type] || this.getDefaultTemplate(type, fields);
|
|
|
|
|
|
|
|
if (type != AddonModDataTemplateType.LIST_HEADER && type != AddonModDataTemplateType.LIST_FOOTER) {
|
|
|
|
// Try to fix syntax errors so the template can be parsed by Angular.
|
|
|
|
template = CoreDomUtils.fixHtml(template);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add core-link directive to links.
|
|
|
|
template = template.replace(
|
|
|
|
/<a ([^>]*href="[^>]*)>/ig,
|
2021-06-23 17:45:22 +02:00
|
|
|
(match, attributes) => '<a core-link capture="true" ' + attributes + '>',
|
2021-03-18 17:17:47 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
return template;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if data has been changed by the user.
|
|
|
|
*
|
|
|
|
* @param inputData Object with the entered form values.
|
|
|
|
* @param fields Fields that defines every content in the entry.
|
|
|
|
* @param dataId Database Id. If set, fils will be uploaded and itemId set.
|
|
|
|
* @param entryContents Original entry contents indexed by field id.
|
|
|
|
* @return True if changed, false if not.
|
|
|
|
*/
|
|
|
|
hasEditDataChanged(
|
|
|
|
inputData: CoreFormFields,
|
|
|
|
fields: AddonModDataField[],
|
|
|
|
entryContents: AddonModDataEntryFields,
|
|
|
|
): boolean {
|
|
|
|
return fields.some((field) =>
|
|
|
|
AddonModDataFieldsDelegate.hasFieldDataChanged(field, inputData, entryContents[field.id]));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Displays a confirmation modal for deleting an entry.
|
|
|
|
*
|
|
|
|
* @param dataId Database ID.
|
|
|
|
* @param entryId Entry ID.
|
|
|
|
* @param courseId Course ID. It not defined, it will be fetched.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
*/
|
|
|
|
async showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): Promise<void> {
|
|
|
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
|
|
|
|
|
|
|
try {
|
|
|
|
await CoreDomUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord');
|
|
|
|
|
|
|
|
const modal = await CoreDomUtils.showModalLoading();
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (entryId > 0) {
|
|
|
|
courseId = await this.getActivityCourseIdIfNotSet(dataId, courseId, siteId);
|
|
|
|
}
|
|
|
|
|
|
|
|
AddonModData.deleteEntry(dataId, entryId, courseId!, siteId);
|
|
|
|
} catch (message) {
|
|
|
|
CoreDomUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true);
|
|
|
|
|
|
|
|
modal.dismiss();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await AddonModData.invalidateEntryData(dataId, entryId, siteId);
|
|
|
|
await AddonModData.invalidateEntriesData(dataId, siteId);
|
|
|
|
} catch (error) {
|
|
|
|
// Ignore errors.
|
|
|
|
}
|
|
|
|
|
2021-05-13 14:12:42 +02:00
|
|
|
CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId, entryId, deleted: true }, siteId);
|
2021-03-18 17:17:47 +01:00
|
|
|
|
|
|
|
CoreDomUtils.showToast('addon.mod_data.recorddeleted', true, 3000);
|
|
|
|
|
|
|
|
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.
|
|
|
|
*
|
|
|
|
* @param dataId Database ID.
|
|
|
|
* @param entryId Entry ID or, if creating, timemodified.
|
|
|
|
* @param fieldId Field ID.
|
|
|
|
* @param files List of files.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Promise resolved if success, rejected otherwise.
|
|
|
|
*/
|
|
|
|
async storeFiles(
|
|
|
|
dataId: number,
|
|
|
|
entryId: number,
|
|
|
|
fieldId: number,
|
2021-04-16 15:15:21 +02:00
|
|
|
files: CoreFileEntry[],
|
2021-03-18 17:17:47 +01:00
|
|
|
siteId?: string,
|
|
|
|
): Promise<CoreFileUploaderStoreFilesResult> {
|
|
|
|
// Get the folder where to store the files.
|
|
|
|
const folderPath = await AddonModDataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId);
|
|
|
|
|
|
|
|
return CoreFileUploader.storeFilesToUpload(folderPath, files);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Upload or store some files, depending if the user is offline or not.
|
|
|
|
*
|
|
|
|
* @param dataId Database ID.
|
|
|
|
* @param itemId Draft ID to use. Undefined or 0 to create a new draft ID.
|
|
|
|
* @param entryId Entry ID or, if creating, timemodified.
|
|
|
|
* @param fieldId Field ID.
|
|
|
|
* @param files List of files.
|
|
|
|
* @param offline True if files sould be stored for offline, false to upload them.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Promise resolved with the itemId for the uploaded file/s.
|
|
|
|
*/
|
2021-04-16 10:12:47 +02:00
|
|
|
async uploadOrStoreFiles(
|
|
|
|
dataId: number,
|
|
|
|
itemId: number,
|
|
|
|
entryId: number,
|
|
|
|
fieldId: number,
|
|
|
|
files: CoreFileEntry[],
|
|
|
|
offline: true,
|
|
|
|
siteId?: string,
|
|
|
|
): Promise<CoreFileUploaderStoreFilesResult>;
|
|
|
|
async uploadOrStoreFiles(
|
|
|
|
dataId: number,
|
|
|
|
itemId: number,
|
|
|
|
entryId: number,
|
|
|
|
fieldId: number,
|
|
|
|
files: CoreFileEntry[],
|
|
|
|
offline: false,
|
|
|
|
siteId?: string,
|
|
|
|
): Promise<number>;
|
|
|
|
async uploadOrStoreFiles(
|
|
|
|
dataId: number,
|
|
|
|
itemId: number,
|
|
|
|
entryId: number,
|
|
|
|
fieldId: number,
|
|
|
|
files: CoreFileEntry[],
|
|
|
|
offline: boolean,
|
|
|
|
siteId?: string,
|
|
|
|
): Promise<number | CoreFileUploaderStoreFilesResult>;
|
2021-03-18 17:17:47 +01:00
|
|
|
async uploadOrStoreFiles(
|
|
|
|
dataId: number,
|
|
|
|
itemId: number = 0,
|
|
|
|
entryId: number,
|
|
|
|
fieldId: number,
|
2021-04-16 15:15:21 +02:00
|
|
|
files: CoreFileEntry[],
|
2021-03-18 17:17:47 +01:00
|
|
|
offline: boolean,
|
|
|
|
siteId?: string,
|
|
|
|
): Promise<number | CoreFileUploaderStoreFilesResult> {
|
|
|
|
if (offline) {
|
|
|
|
return this.storeFiles(dataId, entryId, fieldId, files, siteId);
|
|
|
|
}
|
|
|
|
|
2021-06-11 16:23:40 +02:00
|
|
|
if (!files.length) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2021-03-18 17:17:47 +01:00
|
|
|
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModDataProvider.COMPONENT, itemId, siteId);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
export const AddonModDataHelper = makeSingleton(AddonModDataHelperProvider);
|