2021-03-31 16:50:42 +02:00

318 lines
11 KiB
TypeScript

// (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 { CoreSyncBaseProvider } from '@classes/base-sync';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { CoreRating } from './rating';
import { CoreRatingItemSet, CoreRatingOffline } from './rating-offline';
/**
* Service to sync ratings.
*/
@Injectable( { providedIn: 'root' })
export class CoreRatingSyncProvider extends CoreSyncBaseProvider<CoreRatingSyncItem> {
static readonly SYNCED_EVENT = 'core_rating_synced';
constructor() {
super('CoreRatingSyncProvider');
}
/**
* Try to synchronize all the ratings of a certain component, instance or item set.
*
* This function should be called from the sync provider of activities with ratings.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id.
* @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 if sync fails.
*/
async syncRatings(
component: string,
ratingArea: string,
contextLevel?: ContextLevel,
instanceId?: number,
itemSetId?: number,
force?: boolean,
siteId?: string,
): Promise<CoreRatingSyncItemResult[]> {
siteId = siteId || CoreSites.getCurrentSiteId();
const itemSets = await CoreRatingOffline.getItemSets(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
const results: CoreRatingSyncItemResult[] = [];
await Promise.all(itemSets.map(async (itemSet) => {
const result = force
? await this.syncItemSet(
component,
ratingArea,
itemSet.contextLevel,
itemSet.instanceId,
itemSet.itemSetId,
siteId,
)
: await this.syncItemSetIfNeeded(
component,
ratingArea,
itemSet.contextLevel,
itemSet.instanceId,
itemSet.itemSetId,
siteId,
);
if (result) {
if (result.updated) {
// Sync successful, send event.
CoreEvents.trigger(CoreRatingSyncProvider.SYNCED_EVENT, {
...itemSet,
warnings: result.warnings,
}, siteId);
}
results.push(
{
itemSet,
...result,
},
);
}
}));
return results;
}
/**
* Sync ratings of an item set only if a certain time has passed since the last time.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when ratings are synced or if it doesn't need to be synced.
*/
protected async syncItemSetIfNeeded(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
siteId?: string,
): Promise<CoreRatingSyncItem | undefined> {
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getItemSetSyncId(component, ratingArea, contextLevel, instanceId, itemSetId);
const needed = await this.isSyncNeeded(syncId, siteId);
if (needed) {
return this.syncItemSet(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
}
}
/**
* Synchronize all offline ratings of an item set.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
protected syncItemSet(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
siteId?: string,
): Promise<CoreRatingSyncItem> {
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getItemSetSyncId(component, ratingArea, contextLevel, instanceId, itemSetId);
if (this.isSyncing(syncId, siteId)) {
// There's already a sync ongoing for this item set, return the promise.
return this.getOngoingSync(syncId, siteId)!;
}
this.logger.debug(`Try to sync ratings of component '${component}' rating area '${ratingArea}'` +
` context level '${contextLevel}' instance ${instanceId} item set ${itemSetId}`);
// Get offline events.
const syncPromise = this.performSyncItemSet(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
return this.addOngoingSync(syncId, syncPromise, siteId);
}
/**
* Synchronize all offline ratings of an item set.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
protected async performSyncItemSet(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
siteId: string,
): Promise<CoreRatingSyncItem> {
const result: CoreRatingSyncItem = {
updated: [],
warnings: [],
};
const ratings = await CoreRatingOffline.getRatings(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
if (!ratings.length) {
// Nothing to sync.
return result;
}
if (!CoreApp.isOnline()) {
// Cannot sync in offline.
throw new CoreNetworkError();
}
const promises = ratings.map(async (rating) => {
try {
await CoreRating.addRatingOnline(
component,
ratingArea,
rating.contextlevel,
rating.instanceid,
rating.itemid,
rating.scaleid,
rating.rating,
rating.rateduserid,
rating.aggregation,
siteId,
);
} catch (error) {
if (!CoreUtils.isWebServiceError(error)) {
// Couldn't connect to server, reject.
throw error;
}
const warning = CoreTextUtils.getErrorMessageFromError(error);
if (warning) {
result.warnings.push(warning);
}
}
result.updated.push(rating.itemid);
try {
return CoreRatingOffline.deleteRating(
component,
ratingArea,
rating.contextlevel,
rating.instanceid,
rating.itemid,
siteId,
);
} finally {
await CoreRating.invalidateRatingItems(
rating.contextlevel,
rating.instanceid,
component,
ratingArea,
rating.itemid,
rating.scaleid,
undefined,
siteId,
);
}
});
await Promise.all(promises);
// All done, return the result.
return result;
}
/**
* Get the sync id of an item set.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @return Sync id.
*/
protected getItemSetSyncId(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
): string {
return `itemSet#${component}#${ratingArea}#${contextLevel}#${instanceId}#${itemSetId}`;
}
}
export const CoreRatingSync = makeSingleton(CoreRatingSyncProvider);
declare module '@singletons/events' {
/**
* Augment CoreEventsData interface with events specific to this service.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
[CoreRatingSyncProvider.SYNCED_EVENT]: CoreRatingSyncEventData;
}
}
export type CoreRatingSyncItem = {
warnings: string[];
updated: number[];
};
export type CoreRatingSyncItemResult = CoreRatingSyncItem & {
itemSet: CoreRatingItemSet;
};
/**
* Data passed to SYNCED_EVENT event.
*/
export type CoreRatingSyncEventData = CoreRatingItemSet & {
warnings: string[];
};