MOBILE-3640 database: Add database activity services
parent
992f1ca1ad
commit
2991873dfe
|
@ -186,7 +186,7 @@ export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvid
|
|||
* Perform the assign submission.
|
||||
*
|
||||
* @param assignId Assign ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved in success.
|
||||
*/
|
||||
protected async performSyncAssign(assignId: number, siteId: string): Promise<AddonModAssignSyncResult> {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"addentries": "Add entries",
|
||||
"advancedsearch": "Advanced search",
|
||||
"alttext": "Alternative text",
|
||||
"approve": "Approve",
|
||||
"approved": "Approved",
|
||||
"ascending": "Ascending",
|
||||
"authorfirstname": "Author first name",
|
||||
"authorlastname": "Author surname",
|
||||
"confirmdeleterecord": "Are you sure you want to delete this entry?",
|
||||
"descending": "Descending",
|
||||
"disapprove": "Undo approval",
|
||||
"edittagsnotsupported": "Sorry, editing tags is not supported by the app.",
|
||||
"emptyaddform": "You did not fill out any fields!",
|
||||
"entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity",
|
||||
"entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.",
|
||||
"errorapproving": "Error approving or unapproving entry.",
|
||||
"errordeleting": "Error deleting entry.",
|
||||
"errormustsupplyvalue": "You must supply a value here.",
|
||||
"expired": "Sorry, this activity closed on {{$a}} and is no longer available",
|
||||
"fields": "Fields",
|
||||
"foundrecords": "Found records: {{$a.num}}/{{$a.max}} (<a href=\"{{$a.reseturl}}\">Reset filters</a>)",
|
||||
"gettinglocation": "Getting location",
|
||||
"latlongboth": "Both latitude and longitude are required.",
|
||||
"locationpermissiondenied": "Permission to access your location has been denied.",
|
||||
"locationnotenabled": "Location is not enabled",
|
||||
"menuchoose": "Choose...",
|
||||
"modulenameplural": "Databases",
|
||||
"more": "More",
|
||||
"mylocation": "My location",
|
||||
"noaccess": "You do not have access to this page",
|
||||
"nomatch": "No matching entries found!",
|
||||
"norecords": "No entries in database",
|
||||
"notapproved": "Entry is not approved yet.",
|
||||
"notopenyet": "Sorry, this activity is not available until {{$a}}",
|
||||
"numrecords": "{{$a}} entries",
|
||||
"other": "Other",
|
||||
"recordapproved": "Entry approved",
|
||||
"recorddeleted": "Entry deleted",
|
||||
"recorddisapproved": "Entry unapproved",
|
||||
"resetsettings": "Reset filters",
|
||||
"search": "Search",
|
||||
"searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.",
|
||||
"selectedrequired": "All selected required",
|
||||
"single": "View single",
|
||||
"tagarea_data_records": "Data records",
|
||||
"timeadded": "Time added",
|
||||
"timemodified": "Time modified",
|
||||
"usedate": "Include in search."
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { AddonModDataDefaultFieldHandler } from './handlers/default-field';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModDataEntryField,
|
||||
AddonModDataField,
|
||||
AddonModDataSearchEntriesAdvancedFieldFormatted,
|
||||
AddonModDataSubfieldData,
|
||||
} from './data';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { FileEntry } from '@ionic-native/file';
|
||||
|
||||
/**
|
||||
* Interface that all fields handlers must implement.
|
||||
*/
|
||||
export interface AddonModDataFieldHandler extends CoreDelegateHandler {
|
||||
|
||||
/**
|
||||
* Name of the type of data field the handler supports. E.g. 'checkbox'.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the plugin data.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param field The field object.
|
||||
* @return The component to use, undefined if not found.
|
||||
*/
|
||||
getComponent?(plugin: AddonModDataField): Type<unknown> | undefined;
|
||||
|
||||
/**
|
||||
* Get field search data in the input data.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the search form.
|
||||
* @return With name and value of the data to be sent.
|
||||
*/
|
||||
getFieldSearchData?(
|
||||
field: AddonModDataField,
|
||||
inputData: CoreFormFields,
|
||||
): AddonModDataSearchEntriesAdvancedFieldFormatted[];
|
||||
|
||||
/**
|
||||
* Get field edit data in the input data.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the edit form.
|
||||
* @return With name and value of the data to be sent.
|
||||
*/
|
||||
getFieldEditData?(
|
||||
field: AddonModDataField,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
inputData: CoreFormFields<any>,
|
||||
originalFieldData: AddonModDataEntryField,
|
||||
): AddonModDataSubfieldData[];
|
||||
|
||||
/**
|
||||
* Get field data in changed.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the edit form.
|
||||
* @param originalFieldData Original field entered data.
|
||||
* @return If the field has changes.
|
||||
*/
|
||||
hasFieldDataChanged?(
|
||||
field: AddonModDataField,
|
||||
inputData: CoreFormFields,
|
||||
originalFieldData: AddonModDataEntryField,
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* Get field edit files in the input data.
|
||||
*
|
||||
* @param field Defines the field..
|
||||
* @return With name and value of the data to be sent.
|
||||
*/
|
||||
getFieldEditFiles?(
|
||||
field: AddonModDataField,
|
||||
inputData: CoreFormFields,
|
||||
originalFieldData: AddonModDataEntryField,
|
||||
): (CoreWSExternalFile | FileEntry)[];
|
||||
|
||||
/**
|
||||
* Check and get field requeriments.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the edit form.
|
||||
* @return String with the notification or false.
|
||||
*/
|
||||
getFieldsNotifications?(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined;
|
||||
|
||||
/**
|
||||
* Override field content data with offline submission.
|
||||
*
|
||||
* @param originalContent Original data to be overriden.
|
||||
* @param offlineContent Array with all the offline data to override.
|
||||
* @param offlineFiles Array with all the offline files in the field.
|
||||
* @return Data overriden
|
||||
*/
|
||||
overrideData?(
|
||||
originalContent: AddonModDataEntryField,
|
||||
offlineContent: CoreFormFields,
|
||||
offlineFiles?: FileEntry[],
|
||||
): AddonModDataEntryField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate to register database fields handlers.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataFieldsDelegateService extends CoreDelegate<AddonModDataFieldHandler> {
|
||||
|
||||
protected handlerNameProperty = 'type';
|
||||
|
||||
constructor(
|
||||
protected defaultHandler: AddonModDataDefaultFieldHandler,
|
||||
) {
|
||||
super('AddonModDataFieldsDelegate', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use for a certain field field.
|
||||
*
|
||||
* @param field The field object.
|
||||
* @return Promise resolved with the component to use, undefined if not found.
|
||||
*/
|
||||
getComponentForField(field: AddonModDataField): Promise<Type<unknown> | undefined> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(field.type, 'getComponent', [field]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database data in the input data to search.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the search form.
|
||||
* @return Name and data field.
|
||||
*/
|
||||
getFieldSearchData(field: AddonModDataField, inputData: CoreFormFields): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
|
||||
return this.executeFunctionOnEnabled(field.type, 'getFieldSearchData', [field, inputData]) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database data in the input data to add or update entry.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the search form.
|
||||
* @param originalFieldData Original field entered data.
|
||||
* @return Name and data field.
|
||||
*/
|
||||
getFieldEditData(
|
||||
field: AddonModDataField,
|
||||
inputData: CoreFormFields,
|
||||
originalFieldData: AddonModDataEntryField,
|
||||
): AddonModDataSubfieldData[] {
|
||||
return this.executeFunctionOnEnabled(field.type, 'getFieldEditData', [field, inputData, originalFieldData]) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database data in the input files to add or update entry.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the search form.
|
||||
* @param originalFieldData Original field entered data.
|
||||
* @return Name and data field.
|
||||
*/
|
||||
getFieldEditFiles(
|
||||
field: AddonModDataField,
|
||||
inputData: CoreFormFields,
|
||||
originalFieldData: CoreFormFields,
|
||||
): (CoreWSExternalFile | FileEntry)[] {
|
||||
return this.executeFunctionOnEnabled(field.type, 'getFieldEditFiles', [field, inputData, originalFieldData]) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and get field requeriments.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the edit form.
|
||||
* @return String with the notification or false.
|
||||
*/
|
||||
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
|
||||
return this.executeFunctionOnEnabled(field.type, 'getFieldsNotifications', [field, inputData]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if field type manage files or not.
|
||||
*
|
||||
* @param field Defines the field to be checked.
|
||||
* @return If the field type manages files.
|
||||
*/
|
||||
hasFiles(field: AddonModDataField): boolean {
|
||||
return this.hasFunction(field.type, 'getFieldEditFiles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the data has changed for a certain field.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param inputData Data entered in the search form.
|
||||
* @param originalFieldData Original field entered data.
|
||||
* @return If the field has changes.
|
||||
*/
|
||||
hasFieldDataChanged(
|
||||
field: AddonModDataField,
|
||||
inputData: CoreFormFields,
|
||||
originalFieldData: CoreFormFields,
|
||||
): boolean {
|
||||
return !!this.executeFunctionOnEnabled(
|
||||
field.type,
|
||||
'hasFieldDataChanged',
|
||||
[field, inputData, originalFieldData],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field plugin is supported.
|
||||
*
|
||||
* @param pluginType Type of the plugin.
|
||||
* @return True if supported, false otherwise.
|
||||
*/
|
||||
isPluginSupported(pluginType: string): boolean {
|
||||
return this.hasHandler(pluginType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override field content data with offline submission.
|
||||
*
|
||||
* @param field Defines the field to be rendered.
|
||||
* @param originalContent Original data to be overriden.
|
||||
* @param offlineContent Array with all the offline data to override.
|
||||
* @param offlineFiles Array with all the offline files in the field.
|
||||
* @return Data overriden
|
||||
*/
|
||||
overrideData(
|
||||
field: AddonModDataField,
|
||||
originalContent: AddonModDataEntryField,
|
||||
offlineContent: CoreFormFields,
|
||||
offlineFiles?: FileEntry[],
|
||||
): AddonModDataEntryField {
|
||||
originalContent = originalContent || {};
|
||||
|
||||
if (!offlineContent) {
|
||||
return originalContent;
|
||||
}
|
||||
|
||||
return this.executeFunctionOnEnabled(field.type, 'overrideData', [originalContent, offlineContent, offlineFiles]) ||
|
||||
originalContent;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataFieldsDelegate = makeSingleton(AddonModDataFieldsDelegateService);
|
|
@ -0,0 +1,790 @@
|
|||
// (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';
|
||||
import { FileEntry } from '@ionic-native/file';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
if (typeof offlineContents[offlineContent.fieldid] == 'undefined') {
|
||||
offlineContents[offlineContent.fieldid] = {};
|
||||
}
|
||||
|
||||
if (offlineContent.subfield) {
|
||||
offlineContents[offlineContent.fieldid][offlineContent.subfield] =
|
||||
CoreTextUtils.parseJSON(offlineContent.value);
|
||||
} else {
|
||||
offlineContents[offlineContent.fieldid][''] = CoreTextUtils.parseJSON(offlineContent.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 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.
|
||||
render = CoreSites.getCurrentSite()!.getURL() + '/mod/data/view.php?d={{database.id}}&rid=' + entry.id;
|
||||
} 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 +
|
||||
'" [database]="database" [module]="module" [offset]="' + offset + '" [group]="group" ></addon-mod-data-action>';
|
||||
}
|
||||
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) => {
|
||||
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 == 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,
|
||||
): Record<AddonModDataAction, boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
const module = await CoreCourse.getModuleBasicInfoByInstance(dataId, 'data', siteId);
|
||||
|
||||
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,
|
||||
offline: boolean = false,
|
||||
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 || '',
|
||||
value: fieldSubdata.value ? JSON.stringify(value) : '',
|
||||
});
|
||||
|
||||
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,
|
||||
): Promise<(CoreWSExternalFile | FileEntry)[]> {
|
||||
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 {
|
||||
return CoreFileUploader.getStoredFiles(folderPath);
|
||||
} 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,
|
||||
(match, attributes) => '<a core-link capture="true" ' + attributes + '>',
|
||||
);
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId, entryId, deleted: true }, siteId);
|
||||
|
||||
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,
|
||||
files: (CoreWSExternalFile | FileEntry)[],
|
||||
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.
|
||||
*/
|
||||
async uploadOrStoreFiles(
|
||||
dataId: number,
|
||||
itemId: number = 0,
|
||||
entryId: number,
|
||||
fieldId: number,
|
||||
files: (CoreWSExternalFile | FileEntry)[],
|
||||
offline: boolean,
|
||||
siteId?: string,
|
||||
): Promise<number | CoreFileUploaderStoreFilesResult> {
|
||||
if (!files.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (offline) {
|
||||
return this.storeFiles(dataId, entryId, fieldId, files, siteId);
|
||||
}
|
||||
|
||||
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModDataProvider.COMPONENT, itemId, siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataHelper = makeSingleton(AddonModDataHelperProvider);
|
|
@ -0,0 +1,290 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModDataAction, AddonModDataEntryWSField } from './data';
|
||||
import { AddonModDataEntryDBRecord, DATA_ENTRY_TABLE } from './database/data';
|
||||
|
||||
/**
|
||||
* Service to handle Offline data.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataOfflineProvider {
|
||||
|
||||
/**
|
||||
* Delete all the actions of an entry.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param entryId Database entry ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if deleted, rejected if failure.
|
||||
*/
|
||||
async deleteAllEntryActions(dataId: number, entryId: number, siteId?: string): Promise<void> {
|
||||
const actions = await this.getEntryActions(dataId, entryId, siteId);
|
||||
|
||||
const promises = actions.map((action) => {
|
||||
this.deleteEntry(dataId, entryId, action.action, siteId);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an stored entry.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param entryId Database entry Id.
|
||||
* @param action Action to be done
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if deleted, rejected if failure.
|
||||
*/
|
||||
async deleteEntry(dataId: number, entryId: number, action: AddonModDataAction, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
await this.deleteEntryFiles(dataId, entryId, action, site.id);
|
||||
|
||||
await site.getDb().deleteRecords(DATA_ENTRY_TABLE, {
|
||||
dataid: dataId,
|
||||
entryid: entryId,
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete entry offline files.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param entryId Database entry ID.
|
||||
* @param action Action to be done.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if deleted, rejected if failure.
|
||||
*/
|
||||
protected async deleteEntryFiles(dataId: number, entryId: number, action: AddonModDataAction, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const entry = await CoreUtils.ignoreErrors(this.getEntry(dataId, entryId, action, site.id));
|
||||
|
||||
if (!entry || !entry.fields) {
|
||||
// Entry not found or no fields, ignore.
|
||||
return;
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
entry.fields.forEach((field) => {
|
||||
const value = CoreTextUtils.parseJSON<CoreFileUploaderStoreFilesResult>(field.value);
|
||||
|
||||
if (!value.offline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = this.getEntryFieldFolder(dataId, entryId, field.fieldid, site.id).then((folderPath) =>
|
||||
CoreFileUploader.getStoredFiles(folderPath)).then((files) =>
|
||||
CoreFileUploader.clearTmpFiles(files)).catch(() => { // Files not found, ignore.
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the stored entry data from all the databases.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with entries.
|
||||
*/
|
||||
async getAllEntries(siteId?: string): Promise<AddonModDataOfflineAction[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const entries = await site.getDb().getAllRecords<AddonModDataEntryDBRecord>(DATA_ENTRY_TABLE);
|
||||
|
||||
return entries.map(this.parseRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the stored entry actions from a certain database, sorted by modification time.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with entries.
|
||||
*/
|
||||
async getDatabaseEntries(dataId: number, siteId?: string): Promise<AddonModDataOfflineAction[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const entries = await site.getDb().getRecords<AddonModDataEntryDBRecord>(
|
||||
DATA_ENTRY_TABLE,
|
||||
{ dataid: dataId },
|
||||
'timemodified',
|
||||
);
|
||||
|
||||
return entries.map(this.parseRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an stored entry data.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param entryId Database entry Id.
|
||||
* @param action Action to be done
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with entry.
|
||||
*/
|
||||
async getEntry(
|
||||
dataId: number,
|
||||
entryId: number,
|
||||
action: AddonModDataAction,
|
||||
siteId?: string,
|
||||
): Promise<AddonModDataOfflineAction> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const entry = await site.getDb().getRecord<AddonModDataEntryDBRecord>(DATA_ENTRY_TABLE, {
|
||||
dataid: dataId, entryid: entryId,
|
||||
action,
|
||||
});
|
||||
|
||||
return this.parseRecord(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an all stored entry actions data.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param entryId Database entry Id.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with entry actions.
|
||||
*/
|
||||
async getEntryActions(dataId: number, entryId: number, siteId?: string): Promise<AddonModDataOfflineAction[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const entries = await site.getDb().getRecords<AddonModDataEntryDBRecord>(
|
||||
DATA_ENTRY_TABLE,
|
||||
{ dataid: dataId, entryid: entryId },
|
||||
);
|
||||
|
||||
return entries.map(this.parseRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are offline entries to send.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with boolean: true if has offline answers, false otherwise.
|
||||
*/
|
||||
async hasOfflineData(dataId: number, siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
return CoreUtils.promiseWorks(
|
||||
site.getDb().recordExists(DATA_ENTRY_TABLE, { dataid: dataId }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for offline files in a database.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the path.
|
||||
*/
|
||||
protected async getDatabaseFolder(dataId: number, siteId?: string): Promise<string> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const siteFolderPath = CoreFile.getSiteFolder(site.getId());
|
||||
const folderPath = 'offlinedatabase/' + dataId;
|
||||
|
||||
return CoreTextUtils.concatenatePaths(siteFolderPath, folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for a new offline entry.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param entryId The ID of the entry.
|
||||
* @param fieldId Field ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the path.
|
||||
*/
|
||||
async getEntryFieldFolder(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise<string> {
|
||||
const folderPath = await this.getDatabaseFolder(dataId, siteId);
|
||||
|
||||
return CoreTextUtils.concatenatePaths(folderPath, entryId + '_' + fieldId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "fields" of an offline record.
|
||||
*
|
||||
* @param record Record object
|
||||
* @return Record object with columns parsed.
|
||||
*/
|
||||
protected parseRecord(record: AddonModDataEntryDBRecord): AddonModDataOfflineAction {
|
||||
return Object.assign(record, {
|
||||
fields: CoreTextUtils.parseJSON<AddonModDataEntryWSField[]>(record.fields),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an entry data to be sent later.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param entryId Database entry Id. If action is add entryId should be 0 and -timemodified will be used.
|
||||
* @param action Action to be done to the entry: [add, edit, delete, approve, disapprove]
|
||||
* @param courseId Course ID of the database.
|
||||
* @param groupId Group ID. Only provided when adding.
|
||||
* @param fields Array of field data of the entry if needed.
|
||||
* @param timemodified The time the entry was modified. If not defined, current time.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async saveEntry(
|
||||
dataId: number,
|
||||
entryId: number,
|
||||
action: AddonModDataAction,
|
||||
courseId: number,
|
||||
groupId = 0,
|
||||
fields?: AddonModDataEntryWSField[],
|
||||
timemodified?: number,
|
||||
siteId?: string,
|
||||
): Promise<AddonModDataEntryDBRecord> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
timemodified = timemodified || new Date().getTime();
|
||||
entryId = typeof entryId == 'undefined' || entryId === null ? -timemodified : entryId;
|
||||
|
||||
const entry: AddonModDataEntryDBRecord = {
|
||||
dataid: dataId,
|
||||
courseid: courseId,
|
||||
groupid: groupId,
|
||||
action,
|
||||
entryid: entryId,
|
||||
fields: JSON.stringify(fields || []),
|
||||
timemodified,
|
||||
};
|
||||
|
||||
await site.getDb().insertRecord(DATA_ENTRY_TABLE, entry);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataOffline = makeSingleton(AddonModDataOfflineProvider);
|
||||
|
||||
/**
|
||||
* Entry action stored offline.
|
||||
*/
|
||||
export type AddonModDataOfflineAction = Omit<AddonModDataEntryDBRecord, 'fields'> & {
|
||||
fields: AddonModDataEntryWSField[];
|
||||
};
|
|
@ -0,0 +1,499 @@
|
|||
// (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 { CoreSyncBlockedError } from '@classes/base-sync';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
||||
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreRatingSync } from '@features/rating/services/rating-sync';
|
||||
import { FileEntry } from '@ionic-native/file';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { Translate, makeSingleton } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { AddonModDataProvider, AddonModData, AddonModDataData, AddonModDataAction } from './data';
|
||||
import { AddonModDataHelper } from './data-helper';
|
||||
import { AddonModDataOffline, AddonModDataOfflineAction } from './data-offline';
|
||||
|
||||
/**
|
||||
* Service to sync databases.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModDataSyncResult> {
|
||||
|
||||
static readonly AUTO_SYNCED = 'addon_mod_data_autom_synced';
|
||||
|
||||
protected componentTranslatableString = 'data';
|
||||
|
||||
constructor() {
|
||||
super('AddonModDataSyncProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a database has data to synchronize.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with boolean: true if has data to sync, false otherwise.
|
||||
*/
|
||||
hasDataToSync(dataId: number, siteId?: string): Promise<boolean> {
|
||||
return AddonModDataOffline.hasOfflineData(dataId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize all the databases in a certain site or in all sites.
|
||||
*
|
||||
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
syncAllDatabases(siteId?: string, force?: boolean): Promise<void> {
|
||||
return this.syncOnSites('all databases', this.syncAllDatabasesFunc.bind(this, !!force), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all pending databases on a site.
|
||||
*
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||
* @param Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected async syncAllDatabasesFunc(force: boolean, siteId: string): Promise<void> {
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
// Get all data answers pending to be sent in the site.
|
||||
promises.push(AddonModDataOffline.getAllEntries(siteId).then(async (offlineActions) => {
|
||||
// Get data id.
|
||||
let dataIds: number[] = offlineActions.map((action) => action.dataid);
|
||||
// Get unique values.
|
||||
dataIds = dataIds.filter((id, pos) => dataIds.indexOf(id) == pos);
|
||||
|
||||
const entriesPromises = dataIds.map(async (dataId) => {
|
||||
const result = force
|
||||
? await this.syncDatabase(dataId, siteId)
|
||||
: await this.syncDatabaseIfNeeded(dataId, siteId);
|
||||
|
||||
if (result && result.updated) {
|
||||
// Sync done. Send event.
|
||||
CoreEvents.trigger(AddonModDataSyncProvider.AUTO_SYNCED, {
|
||||
dataId: dataId,
|
||||
warnings: result.warnings,
|
||||
}, siteId);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(entriesPromises);
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
promises.push(this.syncRatings(undefined, force, siteId));
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a database only if a certain time has passed since the last time.
|
||||
*
|
||||
* @param dataId Database ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the data is synced or if it doesn't need to be synced.
|
||||
*/
|
||||
async syncDatabaseIfNeeded(dataId: number, siteId?: string): Promise<AddonModDataSyncResult | undefined> {
|
||||
const needed = await this.isSyncNeeded(dataId, siteId);
|
||||
|
||||
if (needed) {
|
||||
return this.syncDatabase(dataId, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a data.
|
||||
*
|
||||
* @param dataId Data ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
syncDatabase(dataId: number, siteId?: string): Promise<AddonModDataSyncResult> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
if (this.isSyncing(dataId, siteId)) {
|
||||
// There's already a sync ongoing for this database, return the promise.
|
||||
return this.getOngoingSync(dataId, siteId)!;
|
||||
}
|
||||
|
||||
// Verify that database isn't blocked.
|
||||
if (CoreSync.isBlocked(AddonModDataProvider.COMPONENT, dataId, siteId)) {
|
||||
this.logger.debug(`Cannot sync database '${dataId}' because it is blocked.`);
|
||||
|
||||
throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
||||
}
|
||||
|
||||
this.logger.debug(`Try to sync data '${dataId}' in site ${siteId}'`);
|
||||
|
||||
const syncPromise = this.performSyncDatabase(dataId, siteId);
|
||||
|
||||
return this.addOngoingSync(dataId, syncPromise, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the database syncronization.
|
||||
*
|
||||
* @param dataId Data ID.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
protected async performSyncDatabase(dataId: number, siteId: string): Promise<AddonModDataSyncResult> {
|
||||
// Sync offline logs.
|
||||
await CoreUtils.ignoreErrors(
|
||||
CoreCourseLogHelper.syncActivity(AddonModDataProvider.COMPONENT, dataId, siteId),
|
||||
);
|
||||
|
||||
const result: AddonModDataSyncResult = {
|
||||
warnings: [],
|
||||
updated: false,
|
||||
};
|
||||
|
||||
// Get answers to be sent.
|
||||
const offlineActions: AddonModDataOfflineAction[] =
|
||||
await CoreUtils.ignoreErrors(AddonModDataOffline.getDatabaseEntries(dataId, siteId), []);
|
||||
|
||||
if (!offlineActions.length) {
|
||||
// Nothing to sync.
|
||||
await CoreUtils.ignoreErrors(this.setSyncTime(dataId, siteId));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!CoreApp.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
throw new CoreNetworkError();
|
||||
}
|
||||
|
||||
const courseId = offlineActions[0].courseid;
|
||||
|
||||
// Send the answers.
|
||||
const database = await AddonModData.getDatabaseById(courseId, dataId, { siteId });
|
||||
|
||||
const offlineEntries: Record<number, AddonModDataOfflineAction[]> = {};
|
||||
|
||||
offlineActions.forEach((entry) => {
|
||||
if (typeof offlineEntries[entry.entryid] == 'undefined') {
|
||||
offlineEntries[entry.entryid] = [];
|
||||
}
|
||||
|
||||
offlineEntries[entry.entryid].push(entry);
|
||||
});
|
||||
|
||||
const promises = CoreUtils.objectToArray(offlineEntries).map((entryActions) =>
|
||||
this.syncEntry(database, entryActions, result, siteId));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (result.updated) {
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
await CoreUtils.ignoreErrors(AddonModData.invalidateContent(database.coursemodule, courseId, siteId));
|
||||
}
|
||||
|
||||
// Sync finished, set sync time.
|
||||
await CoreUtils.ignoreErrors(this.setSyncTime(dataId, siteId));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize an entry.
|
||||
*
|
||||
* @param database Database.
|
||||
* @param entryActions Entry actions.
|
||||
* @param result Object with the result of the sync.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected async syncEntry(
|
||||
database: AddonModDataData,
|
||||
entryActions: AddonModDataOfflineAction[],
|
||||
result: AddonModDataSyncResult,
|
||||
siteId: string,
|
||||
): Promise<void> {
|
||||
const synEntryResult = await this.performSyncEntry(database, entryActions, result, siteId);
|
||||
|
||||
if (synEntryResult.discardError) {
|
||||
// Submission was discarded, add a warning.
|
||||
const message = Translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: database.name,
|
||||
error: synEntryResult.discardError,
|
||||
});
|
||||
|
||||
if (result.warnings.indexOf(message) == -1) {
|
||||
result.warnings.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync done. Send event.
|
||||
CoreEvents.trigger(AddonModDataSyncProvider.AUTO_SYNCED, {
|
||||
dataId: database.id,
|
||||
entryId: synEntryResult.entryId,
|
||||
offlineEntryId: synEntryResult.offlineId,
|
||||
warnings: result.warnings,
|
||||
deleted: synEntryResult.deleted,
|
||||
}, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the synchronization of an entry.
|
||||
*
|
||||
* @param database Database.
|
||||
* @param entryActions Entry actions.
|
||||
* @param result Object with the result of the sync.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected async performSyncEntry(
|
||||
database: AddonModDataData,
|
||||
entryActions: AddonModDataOfflineAction[],
|
||||
result: AddonModDataSyncResult,
|
||||
siteId: string,
|
||||
): Promise<AddonModDataSyncEntryResult> {
|
||||
let entryId = entryActions[0].entryid;
|
||||
|
||||
const entryResult: AddonModDataSyncEntryResult = {
|
||||
deleted: false,
|
||||
entryId: entryId,
|
||||
};
|
||||
|
||||
const editAction = entryActions.find((action) =>
|
||||
action.action == AddonModDataAction.ADD || action.action == AddonModDataAction.EDIT);
|
||||
const approveAction = entryActions.find((action) =>
|
||||
action.action == AddonModDataAction.APPROVE || action.action == AddonModDataAction.DISAPPROVE);
|
||||
const deleteAction = entryActions.find((action) => action.action == AddonModDataAction.DELETE);
|
||||
|
||||
const options: CoreCourseCommonModWSOptions = {
|
||||
cmId: database.coursemodule,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
let timemodified = 0;
|
||||
if (entryId > 0) {
|
||||
try {
|
||||
const entry = await AddonModData.getEntry(database.id, entryId, options);
|
||||
|
||||
timemodified = entry.entry.timemodified;
|
||||
} catch (error) {
|
||||
if (error && CoreUtils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means the entry has been deleted.
|
||||
timemodified = -1;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (editAction) {
|
||||
// New entry.
|
||||
entryResult.offlineId = entryId;
|
||||
timemodified = 0;
|
||||
} else {
|
||||
// New entry but the add action is missing, discard.
|
||||
timemodified = -1;
|
||||
}
|
||||
|
||||
if (timemodified < 0 || timemodified >= entryActions[0].timemodified) {
|
||||
// The entry was not found in Moodle or the entry has been modified, discard the action.
|
||||
result.updated = true;
|
||||
entryResult.discardError = Translate.instant('addon.mod_data.warningsubmissionmodified');
|
||||
|
||||
await AddonModDataOffline.deleteAllEntryActions(database.id, entryId, siteId);
|
||||
|
||||
return entryResult;
|
||||
}
|
||||
|
||||
if (deleteAction) {
|
||||
try {
|
||||
await AddonModData.deleteEntryOnline(entryId, siteId);
|
||||
entryResult.deleted = true;
|
||||
} catch (error) {
|
||||
if (error && CoreUtils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||
entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error);
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the offline data.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModDataOffline.deleteAllEntryActions(deleteAction.dataid, deleteAction.entryid, siteId);
|
||||
|
||||
return entryResult;
|
||||
}
|
||||
|
||||
if (editAction) {
|
||||
try {
|
||||
await Promise.all(editAction.fields.map(async (field) => {
|
||||
// Upload Files if asked.
|
||||
const value = CoreTextUtils.parseJSON<CoreFileUploaderStoreFilesResult>(field.value || '');
|
||||
if (value.online || value.offline) {
|
||||
let files: (CoreWSExternalFile | FileEntry)[] = value.online || [];
|
||||
|
||||
const offlineFiles = value.offline
|
||||
? await AddonModDataHelper.getStoredFiles(editAction.dataid, entryId, field.fieldid)
|
||||
: [];
|
||||
|
||||
files = files.concat(offlineFiles);
|
||||
|
||||
const filesResult = await AddonModDataHelper.uploadOrStoreFiles(
|
||||
editAction.dataid,
|
||||
0,
|
||||
entryId,
|
||||
field.fieldid,
|
||||
files,
|
||||
false,
|
||||
siteId,
|
||||
);
|
||||
|
||||
field.value = JSON.stringify(filesResult);
|
||||
}
|
||||
}));
|
||||
|
||||
if (editAction.action == AddonModDataAction.ADD) {
|
||||
const result = await AddonModData.addEntryOnline(
|
||||
editAction.dataid,
|
||||
editAction.fields,
|
||||
editAction.groupid,
|
||||
siteId,
|
||||
);
|
||||
entryId = result.newentryid;
|
||||
entryResult.entryId = entryId;
|
||||
} else {
|
||||
await AddonModData.editEntryOnline(entryId, editAction.fields, siteId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && CoreUtils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||
entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error);
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Delete the offline data.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModDataOffline.deleteEntry(editAction.dataid, editAction.entryid, editAction.action, siteId);
|
||||
}
|
||||
|
||||
if (approveAction) {
|
||||
try {
|
||||
await AddonModData.approveEntryOnline(entryId, approveAction.action == AddonModDataAction.APPROVE, siteId);
|
||||
} catch (error) {
|
||||
if (error && CoreUtils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||
entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error);
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Delete the offline data.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModDataOffline.deleteEntry(approveAction.dataid, approveAction.entryid, approveAction.action, siteId);
|
||||
}
|
||||
|
||||
return entryResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize offline ratings.
|
||||
*
|
||||
* @param cmId Course module to be synced. If not defined, sync all databases.
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
async syncRatings(cmId?: number, force?: boolean, siteId?: string): Promise<AddonModDataSyncResult> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
const results = await CoreRatingSync.syncRatings('mod_data', 'entry', ContextLevel.MODULE, cmId, 0, force, siteId);
|
||||
let updated = false;
|
||||
const warnings = [];
|
||||
|
||||
const promises = results.map((result) =>
|
||||
AddonModData.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, { siteId })
|
||||
.then((database) => {
|
||||
const subPromises: Promise<void>[] = [];
|
||||
|
||||
if (result.updated.length) {
|
||||
updated = true;
|
||||
|
||||
// Invalidate entry of updated ratings.
|
||||
result.updated.forEach((itemId) => {
|
||||
subPromises.push(AddonModData.invalidateEntryData(database.id, itemId, siteId));
|
||||
});
|
||||
}
|
||||
|
||||
if (result.warnings.length) {
|
||||
result.warnings.forEach((warning) => {
|
||||
this.addOfflineDataDeletedWarning(warnings, database.name, warning);
|
||||
});
|
||||
}
|
||||
|
||||
return CoreUtils.allPromises(subPromises);
|
||||
}));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return ({ updated, warnings });
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataSync = makeSingleton(AddonModDataSyncProvider);
|
||||
|
||||
/**
|
||||
* Data returned by a database sync.
|
||||
*/
|
||||
export type AddonModDataSyncEntryResult = {
|
||||
discardError?: string;
|
||||
offlineId?: number;
|
||||
entryId: number;
|
||||
deleted: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by a database sync.
|
||||
*/
|
||||
export type AddonModDataSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
};
|
||||
|
||||
export type AddonModDataAutoSyncData = {
|
||||
dataId: number;
|
||||
warnings: string[];
|
||||
entryId?: number;
|
||||
offlineEntryId?: number;
|
||||
deleted?: boolean;
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,83 @@
|
|||
// (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 { SQLiteDB } from '@classes/sqlitedb';
|
||||
import { CoreSiteSchema } from '@services/sites';
|
||||
import { AddonModDataAction } from '../data';
|
||||
|
||||
/**
|
||||
* Database variables for AddonModDataOfflineProvider.
|
||||
*/
|
||||
export const DATA_ENTRY_TABLE = 'addon_mod_data_entry_1';
|
||||
export const ADDON_MOD_DATA_OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
||||
name: 'AddonModDataOfflineProvider',
|
||||
version: 1,
|
||||
tables: [
|
||||
{
|
||||
name: DATA_ENTRY_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'dataid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'groupid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'action',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'entryid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'fields',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'timemodified',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['dataid', 'entryid', 'action'],
|
||||
},
|
||||
],
|
||||
async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
|
||||
if (oldVersion > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the records from the old table.
|
||||
await db.migrateTable('addon_mod_data_entry', DATA_ENTRY_TABLE);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Data about data entries to sync.
|
||||
*/
|
||||
export type AddonModDataEntryDBRecord = {
|
||||
dataid: number; // Primary key.
|
||||
entryid: number; // Primary key. Negative for offline entries.
|
||||
action: AddonModDataAction; // Primary key.
|
||||
courseid: number;
|
||||
groupid: number;
|
||||
fields: string;
|
||||
timemodified: number;
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModData } from '../data';
|
||||
import { AddonModDataHelper } from '../data-helper';
|
||||
|
||||
/**
|
||||
* Content links handler for database approve/disapprove entry.
|
||||
* Match mod/data/view.php?d=6&approve=5 with a valid data id and entryid.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataApproveLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||
|
||||
name = 'AddonModDataApproveLinkHandler';
|
||||
featureName = 'CoreCourseModuleDelegate_AddonModData';
|
||||
pattern = /\/mod\/data\/view\.php.*([?&](d|approve|disapprove)=\d+)/;
|
||||
priority = 50; // Higher priority than the default link handler for view.php.
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getActions(siteIds: string[], url: string, params: Params, courseId?: number): CoreContentLinksAction[] {
|
||||
return [{
|
||||
action: (siteId): void => {
|
||||
const dataId = parseInt(params.d, 10);
|
||||
const entryId = parseInt(params.approve, 10) || parseInt(params.disapprove, 10);
|
||||
const approve = parseInt(params.approve, 10) ? true : false;
|
||||
|
||||
AddonModDataHelper.approveOrDisapproveEntry(dataId, entryId, approve, courseId, siteId);
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(siteId: string, url: string, params: Params): Promise<boolean> {
|
||||
if (typeof params.d == 'undefined' || (typeof params.approve == 'undefined' && typeof params.disapprove == 'undefined')) {
|
||||
// Required fields not defined. Cannot treat the URL.
|
||||
return false;
|
||||
}
|
||||
|
||||
return AddonModData.isPluginEnabled(siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataApproveLinkHandler = makeSingleton(AddonModDataApproveLinkHandlerService);
|
|
@ -0,0 +1,78 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FileEntry } from '@ionic-native/file';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { AddonModDataEntryField, AddonModDataSearchEntriesAdvancedFieldFormatted, AddonModDataSubfieldData } from '../data';
|
||||
import { AddonModDataFieldHandler } from '../data-fields-delegate';
|
||||
|
||||
/**
|
||||
* Default handler used when a field plugin doesn't have a specific implementation.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataDefaultFieldHandler implements AddonModDataFieldHandler {
|
||||
|
||||
name = 'AddonModDataDefaultFieldHandler';
|
||||
type = 'default';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getFieldSearchData(): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getFieldEditData(): AddonModDataSubfieldData[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
hasFieldDataChanged(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getFieldEditFiles(): (CoreWSExternalFile | FileEntry)[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getFieldsNotifications(): undefined {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
overrideData(originalContent: AddonModDataEntryField): AddonModDataEntryField {
|
||||
return originalContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModData } from '../data';
|
||||
import { AddonModDataHelper } from '../data-helper';
|
||||
|
||||
/**
|
||||
* Content links handler for database delete entry.
|
||||
* Match mod/data/view.php?d=6&delete=5 with a valid data id and entryid.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataDeleteLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||
|
||||
name = 'AddonModDataDeleteLinkHandler';
|
||||
featureName = 'CoreCourseModuleDelegate_AddonModData';
|
||||
pattern = /\/mod\/data\/view\.php.*([?&](d|delete)=\d+)/;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getActions(siteIds: string[], url: string, params: Params, courseId?: number): CoreContentLinksAction[] {
|
||||
return [{
|
||||
action: (siteId): void => {
|
||||
const dataId = parseInt(params.d, 10);
|
||||
const entryId = parseInt(params.delete, 10);
|
||||
|
||||
AddonModDataHelper.showDeleteEntryModal(dataId, entryId, courseId, siteId);
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(siteId: string, url: string, params: Params): Promise<boolean> {
|
||||
if (typeof params.d == 'undefined' || typeof params.delete == 'undefined') {
|
||||
// Required fields not defined. Cannot treat the URL.
|
||||
return false;
|
||||
}
|
||||
|
||||
return AddonModData.isPluginEnabled(siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataDeleteLinkHandler = makeSingleton(AddonModDataDeleteLinkHandlerService);
|
|
@ -0,0 +1,79 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModData } from '../data';
|
||||
import { AddonModDataModuleHandlerService } from './module';
|
||||
|
||||
/**
|
||||
* Content links handler for database add or edit entry.
|
||||
* Match mod/data/edit.php?d=6&rid=6 with a valid data and optional record id.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataEditLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||
|
||||
name = 'AddonModDataEditLinkHandler';
|
||||
featureName = 'CoreCourseModuleDelegate_AddonModData';
|
||||
pattern = /\/mod\/data\/edit\.php.*([?&](d|rid)=\d+)/;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] {
|
||||
return [{
|
||||
action: async (siteId): Promise<void> => {
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
const dataId = parseInt(params.d, 10);
|
||||
const rId = params.rid || '';
|
||||
|
||||
try {
|
||||
const module = await CoreCourse.getModuleBasicInfoByInstance(dataId, 'data', siteId);
|
||||
const pageParams: Params = {
|
||||
module,
|
||||
courseId: module.course,
|
||||
};
|
||||
|
||||
CoreNavigator.navigateToSitePath(
|
||||
`${AddonModDataModuleHandlerService.PAGE_NAME}/${module.course}/${module.id}/edit/${rId}`,
|
||||
{ siteId, params: pageParams },
|
||||
);
|
||||
} finally {
|
||||
// Just in case. In fact we need to dismiss the modal before showing a toast or error message.
|
||||
modal.dismiss();
|
||||
}
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(siteId: string, url: string, params: Params): Promise<boolean> {
|
||||
if (typeof params.d == 'undefined') {
|
||||
// Id not defined. Cannot treat the URL.
|
||||
return false;
|
||||
}
|
||||
|
||||
return AddonModData.isPluginEnabled(siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataEditLinkHandler = makeSingleton(AddonModDataEditLinkHandlerService);
|
|
@ -0,0 +1,40 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModData } from '../data';
|
||||
|
||||
/**
|
||||
* Handler to treat links to data.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
|
||||
|
||||
name = 'AddonModDataLinkHandler';
|
||||
|
||||
constructor() {
|
||||
super('AddonModData', 'data', 'd');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
isEnabled(siteId: string): Promise<boolean> {
|
||||
return AddonModData.isPluginEnabled(siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataIndexLinkHandler = makeSingleton(AddonModDataIndexLinkHandlerService);
|
|
@ -0,0 +1,40 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModData } from '../data';
|
||||
|
||||
/**
|
||||
* Handler to treat links to data list page.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataListLinkHandlerService extends CoreContentLinksModuleListHandler {
|
||||
|
||||
name = 'AddonModDataListLinkHandler';
|
||||
|
||||
constructor() {
|
||||
super('AddonModData', 'data');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
isEnabled(siteId?: string): Promise<boolean> {
|
||||
return AddonModData.isPluginEnabled(siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataListLinkHandler = makeSingleton(AddonModDataListLinkHandlerService);
|
|
@ -0,0 +1,85 @@
|
|||
// (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 { CoreConstants } from '@/core/constants';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModDataIndexComponent } from '../../components/index';
|
||||
import { AddonModData } from '../data';
|
||||
|
||||
/**
|
||||
* Handler to support data modules.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataModuleHandlerService implements CoreCourseModuleHandler {
|
||||
|
||||
static readonly PAGE_NAME = 'mod_data';
|
||||
|
||||
name = 'AddonModData';
|
||||
modName = 'data';
|
||||
|
||||
supportedFeatures = {
|
||||
[CoreConstants.FEATURE_GROUPS]: true,
|
||||
[CoreConstants.FEATURE_GROUPINGS]: true,
|
||||
[CoreConstants.FEATURE_MOD_INTRO]: true,
|
||||
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
|
||||
[CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true,
|
||||
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
|
||||
[CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
|
||||
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
|
||||
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
|
||||
[CoreConstants.FEATURE_RATE]: true,
|
||||
[CoreConstants.FEATURE_COMMENT]: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return AddonModData.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
|
||||
return {
|
||||
icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
|
||||
title: module.name,
|
||||
class: 'addon-mod_data-handler',
|
||||
showDownloadButton: true,
|
||||
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
|
||||
options = options || {};
|
||||
options.params = options.params || {};
|
||||
Object.assign(options.params, { module });
|
||||
const routeParams = '/' + courseId + '/' + module.id;
|
||||
|
||||
CoreNavigator.navigateToSitePath(AddonModDataModuleHandlerService.PAGE_NAME + routeParams, options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getMainComponent(): Promise<Type<unknown>> {
|
||||
return AddonModDataIndexComponent;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataModuleHandler = makeSingleton(AddonModDataModuleHandlerService);
|
|
@ -0,0 +1,300 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreComments } from '@features/comments/services/comments';
|
||||
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
|
||||
import { CoreCourseCommonModWSOptions, CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreGroup, CoreGroups } from '@services/groups';
|
||||
import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModDataProvider, AddonModDataEntry, AddonModData, AddonModDataData } from '../data';
|
||||
import { AddonModDataSync, AddonModDataSyncResult } from '../data-sync';
|
||||
|
||||
/**
|
||||
* Handler to prefetch databases.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
|
||||
|
||||
name = 'AddonModData';
|
||||
modName = 'data';
|
||||
component = AddonModDataProvider.COMPONENT;
|
||||
updatesNames = /^configuration$|^.*files$|^entries$|^gradeitems$|^outcomes$|^comments$|^ratings/;
|
||||
|
||||
/**
|
||||
* Retrieves all the entries for all the groups and then returns only unique entries.
|
||||
*
|
||||
* @param dataId Database Id.
|
||||
* @param groups Array of groups in the activity.
|
||||
* @param options Other options.
|
||||
* @return All unique entries.
|
||||
*/
|
||||
protected async getAllUniqueEntries(
|
||||
dataId: number,
|
||||
groups: CoreGroup[],
|
||||
options: CoreSitesCommonWSOptions = {},
|
||||
): Promise<AddonModDataEntry[]> {
|
||||
|
||||
const promises = groups.map((group) => AddonModData.fetchAllEntries(dataId, {
|
||||
groupId: group.id,
|
||||
...options, // Include all options.
|
||||
}));
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
const uniqueEntries: Record<number, AddonModDataEntry> = {};
|
||||
responses.forEach((groupEntries) => {
|
||||
groupEntries.forEach((entry) => {
|
||||
uniqueEntries[entry.id] = entry;
|
||||
});
|
||||
});
|
||||
|
||||
return CoreUtils.objectToArray(uniqueEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get all database info just once.
|
||||
*
|
||||
* @param module Module to get the files.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param omitFail True to always return even if fails. Default false.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the info fetched.
|
||||
*/
|
||||
protected async getDatabaseInfoHelper(
|
||||
module: CoreCourseAnyModuleData,
|
||||
courseId: number,
|
||||
omitFail: boolean,
|
||||
options: CoreCourseCommonModWSOptions = {},
|
||||
): Promise<{ database: AddonModDataData; groups: CoreGroup[]; entries: AddonModDataEntry[]; files: CoreWSExternalFile[]}> {
|
||||
let groups: CoreGroup[] = [];
|
||||
let entries: AddonModDataEntry[] = [];
|
||||
let files: CoreWSExternalFile[] = [];
|
||||
|
||||
options.cmId = options.cmId || module.id;
|
||||
options.siteId = options.siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
const database = await AddonModData.getDatabase(courseId, module.id, options);
|
||||
|
||||
try {
|
||||
files = this.getIntroFilesFromInstance(module, database);
|
||||
|
||||
const groupInfo = await CoreGroups.getActivityGroupInfo(module.id, false, undefined, options.siteId);
|
||||
if (!groupInfo.groups || groupInfo.groups.length == 0) {
|
||||
groupInfo.groups = [{ id: 0, name: '' }];
|
||||
}
|
||||
groups = groupInfo.groups || [];
|
||||
|
||||
entries = await this.getAllUniqueEntries(database.id, groups, options);
|
||||
files = files.concat(this.getEntriesFiles(entries));
|
||||
|
||||
return {
|
||||
database,
|
||||
groups,
|
||||
entries,
|
||||
files,
|
||||
};
|
||||
} catch (error) {
|
||||
if (omitFail) {
|
||||
// Any error, return the info we have.
|
||||
return {
|
||||
database,
|
||||
groups,
|
||||
entries,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file contained in the entries.
|
||||
*
|
||||
* @param entries List of entries to get files from.
|
||||
* @return List of files.
|
||||
*/
|
||||
protected getEntriesFiles(entries: AddonModDataEntry[]): CoreWSExternalFile[] {
|
||||
let files: CoreWSExternalFile[] = [];
|
||||
|
||||
entries.forEach((entry) => {
|
||||
CoreUtils.objectToArray(entry.contents).forEach((content) => {
|
||||
files = files.concat(<CoreWSExternalFile[]>content.files);
|
||||
});
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
|
||||
return this.getDatabaseInfoHelper(module, courseId, true).then((info) => info.files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
|
||||
const data = await CoreUtils.ignoreErrors(AddonModData.getDatabase(courseId, module.id));
|
||||
|
||||
return this.getIntroFilesFromInstance(module, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||
await AddonModData.invalidateContent(moduleId, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
promises.push(AddonModData.invalidateDatabaseData(courseId));
|
||||
promises.push(AddonModData.invalidateDatabaseAccessInformationData(module.instance!));
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
|
||||
const database = await AddonModData.getDatabase(courseId, module.id, {
|
||||
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||
});
|
||||
|
||||
const accessData = await AddonModData.getDatabaseAccessInformation(database.id, { cmId: module.id });
|
||||
// Check if database is restricted by time.
|
||||
if (!accessData.timeavailable) {
|
||||
const time = CoreTimeUtils.timestamp();
|
||||
|
||||
// It is restricted, checking times.
|
||||
if (database.timeavailablefrom && time < database.timeavailablefrom) {
|
||||
return false;
|
||||
}
|
||||
if (database.timeavailableto && time > database.timeavailableto) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return AddonModData.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
|
||||
return this.prefetchPackage(module, courseId, this.prefetchDatabase.bind(this, module, courseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a database.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchDatabase(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
|
||||
const siteId = CoreSites.getCurrentSiteId();
|
||||
courseId = courseId || module.course || CoreSites.getCurrentSiteHomeId();
|
||||
|
||||
const options = {
|
||||
cmId: module.id,
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
|
||||
const info = await this.getDatabaseInfoHelper(module, courseId, false, options);
|
||||
|
||||
// Prefetch the database data.
|
||||
const database = info.database;
|
||||
|
||||
const commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
||||
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
promises.push(AddonModData.getFields(database.id, options));
|
||||
promises.push(CoreFilepool.addFilesToQueue(siteId, info.files, this.component, module.id));
|
||||
|
||||
info.groups.forEach((group) => {
|
||||
promises.push(AddonModData.getDatabaseAccessInformation(database.id, {
|
||||
groupId: group.id,
|
||||
...options, // Include all options.
|
||||
}));
|
||||
});
|
||||
|
||||
info.entries.forEach((entry) => {
|
||||
promises.push(AddonModData.getEntry(database.id, entry.id, options));
|
||||
|
||||
if (commentsEnabled && database.comments) {
|
||||
promises.push(CoreComments.getComments(
|
||||
'module',
|
||||
database.coursemodule,
|
||||
'mod_data',
|
||||
entry.id,
|
||||
'database_entry',
|
||||
0,
|
||||
siteId,
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
// Add Basic Info to manage links.
|
||||
promises.push(CoreCourse.getModuleBasicInfoByInstance(database.id, 'data', siteId));
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a module.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModDataSyncResult> {
|
||||
const promises = [
|
||||
AddonModDataSync.syncDatabase(module.instance!, siteId),
|
||||
AddonModDataSync.syncRatings(module.id, true, siteId),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
return results.reduce((a, b) => ({
|
||||
updated: a.updated || b.updated,
|
||||
warnings: (a.warnings || []).concat(b.warnings || []),
|
||||
}), { updated: false , warnings: [] });
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataPrefetchHandler = makeSingleton(AddonModDataPrefetchHandlerService);
|
|
@ -0,0 +1,94 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModData } from '../data';
|
||||
import { AddonModDataModuleHandlerService } from './module';
|
||||
|
||||
/**
|
||||
* Content links handler for database show entry.
|
||||
* Match mod/data/view.php?d=6&rid=5 with a valid data id and entryid.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataShowLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||
|
||||
name = 'AddonModDataShowLinkHandler';
|
||||
featureName = 'CoreCourseModuleDelegate_AddonModData';
|
||||
pattern = /\/mod\/data\/view\.php.*([?&](d|rid|page|group|mode)=\d+)/;
|
||||
priority = 50; // Higher priority than the default link handler for view.php.
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] {
|
||||
return [{
|
||||
action: async (siteId): Promise<void> => {
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
const dataId = parseInt(params.d, 10);
|
||||
const rId = params.rid || '';
|
||||
const group = parseInt(params.group, 10) || false;
|
||||
const page = parseInt(params.page, 10) || false;
|
||||
|
||||
try {
|
||||
const module = await CoreCourse.getModuleBasicInfoByInstance(dataId, 'data', siteId);
|
||||
const pageParams: Params = {
|
||||
module: module,
|
||||
courseId: module.course,
|
||||
};
|
||||
|
||||
if (group) {
|
||||
pageParams.group = group;
|
||||
}
|
||||
|
||||
if (params.mode && params.mode == 'single') {
|
||||
pageParams.offset = page || 0;
|
||||
}
|
||||
|
||||
CoreNavigator.navigateToSitePath(
|
||||
`${AddonModDataModuleHandlerService.PAGE_NAME}/${module.course}/${module.id}/${rId}`,
|
||||
{ siteId, params: pageParams },
|
||||
);
|
||||
} finally {
|
||||
// Just in case. In fact we need to dismiss the modal before showing a toast or error message.
|
||||
modal.dismiss();
|
||||
}
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(siteId: string, url: string, params: Params): Promise<boolean> {
|
||||
if (typeof params.d == 'undefined') {
|
||||
// Id not defined. Cannot treat the URL.
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((!params.mode || params.mode != 'single') && typeof params.rid == 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return AddonModData.isPluginEnabled(siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataShowLinkHandler = makeSingleton(AddonModDataShowLinkHandlerService);
|
|
@ -0,0 +1,43 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreCronHandler } from '@services/cron';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModDataSync } from '../data-sync';
|
||||
|
||||
/**
|
||||
* Synchronization cron handler.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataSyncCronHandlerService implements CoreCronHandler {
|
||||
|
||||
name = 'AddonModDataSyncCronHandler';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
execute(siteId?: string, force?: boolean): Promise<void> {
|
||||
return AddonModDataSync.syncAllDatabases(siteId, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getInterval(): number {
|
||||
return AddonModDataSync.syncInterval;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataSyncCronHandler = makeSingleton(AddonModDataSyncCronHandlerService);
|
|
@ -0,0 +1,53 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreTagFeedComponent } from '@features/tag/components/feed/feed';
|
||||
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
|
||||
import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModData } from '../data';
|
||||
|
||||
/**
|
||||
* Handler to support tags.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModDataTagAreaHandlerService implements CoreTagAreaHandler {
|
||||
|
||||
name = 'AddonModDataTagAreaHandler';
|
||||
type = 'mod_data/data_records';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return AddonModData.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
parseContent(content: string): CoreTagFeedElement[] {
|
||||
return CoreTagHelper.parseFeedContent(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getComponent(): Type<unknown> {
|
||||
return CoreTagFeedComponent;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModDataTagAreaHandler = makeSingleton(AddonModDataTagAreaHandlerService);
|
|
@ -330,12 +330,12 @@ export class AddonModForumSyncProvider extends CoreCourseActivitySyncBaseProvide
|
|||
updated = true;
|
||||
|
||||
// Invalidate discussions of updated ratings.
|
||||
promises.push(AddonModForum.invalidateDiscussionPosts(result.itemSet!.itemSetId, undefined, siteId));
|
||||
promises.push(AddonModForum.invalidateDiscussionPosts(result.itemSet.itemSetId, undefined, siteId));
|
||||
}
|
||||
|
||||
if (result.warnings.length) {
|
||||
// Fetch forum to construct the warning message.
|
||||
promises.push(AddonModForum.getForum(result.itemSet!.courseId!, result.itemSet!.instanceId, { siteId })
|
||||
promises.push(AddonModForum.getForum(result.itemSet.courseId, result.itemSet.instanceId, { siteId })
|
||||
.then((forum) => {
|
||||
result.warnings.forEach((warning) => {
|
||||
this.addOfflineDataDeletedWarning(warnings, forum.name, warning);
|
||||
|
|
|
@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
|
|||
|
||||
import { AddonModAssignModule } from './assign/assign.module';
|
||||
import { AddonModBookModule } from './book/book.module';
|
||||
import { AddonModDataModule } from './data/data.module';
|
||||
import { AddonModFolderModule } from './folder/folder.module';
|
||||
import { AddonModForumModule } from './forum/forum.module';
|
||||
import { AddonModLabelModule } from './label/label.module';
|
||||
|
@ -32,10 +33,10 @@ import { AddonModScormModule } from './scorm/scorm.module';
|
|||
import { AddonModChoiceModule } from './choice/choice.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonModAssignModule,
|
||||
AddonModBookModule,
|
||||
AddonModDataModule,
|
||||
AddonModForumModule,
|
||||
AddonModLessonModule,
|
||||
AddonModPageModule,
|
||||
|
@ -51,7 +52,5 @@ import { AddonModChoiceModule } from './choice/choice.module';
|
|||
AddonModScormModule,
|
||||
AddonModChoiceModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class AddonModModule { }
|
||||
|
|
|
@ -126,6 +126,7 @@ import { ADDON_MOD_ASSIGN_SERVICES } from '@addons/mod/assign/assign.module';
|
|||
import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module';
|
||||
// @todo import { ADDON_MOD_CHAT_SERVICES } from '@addons/mod/chat/chat.module';
|
||||
import { ADDON_MOD_CHOICE_SERVICES } from '@addons/mod/choice/choice.module';
|
||||
import { ADDON_MOD_DATA_SERVICES } from '@addons/mod/data/data.module';
|
||||
// @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module';
|
||||
import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module';
|
||||
import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module';
|
||||
|
@ -291,6 +292,7 @@ export class CoreCompileProvider {
|
|||
...ADDON_MOD_BOOK_SERVICES,
|
||||
// @todo ...ADDON_MOD_CHAT_SERVICES,
|
||||
...ADDON_MOD_CHOICE_SERVICES,
|
||||
...ADDON_MOD_DATA_SERVICES,
|
||||
// @todo ...ADDON_MOD_FEEDBACK_SERVICES,
|
||||
...ADDON_MOD_FOLDER_SERVICES,
|
||||
...ADDON_MOD_FORUM_SERVICES,
|
||||
|
|
|
@ -92,7 +92,7 @@ export type CoreRatingDBPrimaryData = {
|
|||
*/
|
||||
export type CoreRatingDBRecord = CoreRatingDBPrimaryData & {
|
||||
itemsetid: number;
|
||||
courseid?: number;
|
||||
courseid: number;
|
||||
scaleid: number;
|
||||
rating: number;
|
||||
rateduserid: number;
|
||||
|
|
|
@ -28,7 +28,7 @@ export interface CoreRatingItemSet {
|
|||
contextLevel: ContextLevel;
|
||||
instanceId: number;
|
||||
itemSetId: number;
|
||||
courseId?: number;
|
||||
courseId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -59,12 +59,12 @@ export class CoreRatingSyncProvider extends CoreSyncBaseProvider<CoreRatingSyncI
|
|||
itemSetId?: number,
|
||||
force?: boolean,
|
||||
siteId?: string,
|
||||
): Promise<CoreRatingSyncItem[]> {
|
||||
): Promise<CoreRatingSyncItemResult[]> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
const itemSets = await CoreRatingOffline.getItemSets(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
|
||||
|
||||
const results: CoreRatingSyncItem[] = [];
|
||||
const results: CoreRatingSyncItemResult[] = [];
|
||||
await Promise.all(itemSets.map(async (itemSet) => {
|
||||
const result = force
|
||||
? await this.syncItemSet(
|
||||
|
@ -301,11 +301,14 @@ declare module '@singletons/events' {
|
|||
}
|
||||
|
||||
export type CoreRatingSyncItem = {
|
||||
itemSet?: CoreRatingItemSet;
|
||||
warnings: string[];
|
||||
updated: number[];
|
||||
};
|
||||
|
||||
export type CoreRatingSyncItemResult = CoreRatingSyncItem & {
|
||||
itemSet: CoreRatingItemSet;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to SYNCED_EVENT event.
|
||||
*/
|
||||
|
|
|
@ -2181,7 +2181,7 @@ export class CoreFilepoolProvider {
|
|||
* @return Resolved when done.
|
||||
*/
|
||||
async invalidateFilesByComponent(
|
||||
siteId: string,
|
||||
siteId: string | undefined,
|
||||
component: string,
|
||||
componentId?: string | number,
|
||||
onlyUnknown: boolean = true,
|
||||
|
|
|
@ -1036,8 +1036,10 @@ export class CoreSitesProvider {
|
|||
* @param siteId The site ID. If not defined, current site (if available).
|
||||
* @return Promise resolved with the database.
|
||||
*/
|
||||
getSiteDb(siteId?: string): Promise<SQLiteDB> {
|
||||
return this.getSite(siteId).then((site) => site.getDb());
|
||||
async getSiteDb(siteId?: string): Promise<SQLiteDB> {
|
||||
const site = await this.getSite(siteId);
|
||||
|
||||
return site.getDb();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -117,7 +117,7 @@ export class CoreUtilsProvider {
|
|||
* @return The object.
|
||||
*/
|
||||
arrayToObject<T>(
|
||||
array: T[],
|
||||
array: T[] = [],
|
||||
propertyName?: string,
|
||||
result: Record<string, T> = {},
|
||||
): Record<string, T> {
|
||||
|
|
Loading…
Reference in New Issue