// (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 { 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 { 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 { 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 { 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 { 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[]; };