From beaab9d2f6ff8c3473dc7c42c1bec62a230d5c72 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 28 Feb 2019 17:33:36 +0100 Subject: [PATCH] MOBILE-1633 core: Rating components and providers --- scripts/langindex.json | 9 + src/app/app.module.ts | 2 + src/assets/lang/en.json | 9 + .../rating/components/aggregate/aggregate.ts | 112 ++++++ .../aggregate/core-rating-aggregate.html | 4 + .../rating/components/components.module.ts | 39 ++ .../components/rate/core-rating-rate.html | 6 + src/core/rating/components/rate/rate.ts | 106 ++++++ src/core/rating/lang/en.json | 11 + src/core/rating/pages/ratings/ratings.html | 28 ++ .../rating/pages/ratings/ratings.module.ts | 35 ++ src/core/rating/pages/ratings/ratings.ts | 95 +++++ src/core/rating/providers/offline.ts | 305 +++++++++++++++ src/core/rating/providers/rating.ts | 352 ++++++++++++++++++ src/core/rating/providers/sync.ts | 196 ++++++++++ src/core/rating/rating.module.ts | 31 ++ 16 files changed, 1340 insertions(+) create mode 100644 src/core/rating/components/aggregate/aggregate.ts create mode 100644 src/core/rating/components/aggregate/core-rating-aggregate.html create mode 100644 src/core/rating/components/components.module.ts create mode 100644 src/core/rating/components/rate/core-rating-rate.html create mode 100644 src/core/rating/components/rate/rate.ts create mode 100644 src/core/rating/lang/en.json create mode 100644 src/core/rating/pages/ratings/ratings.html create mode 100644 src/core/rating/pages/ratings/ratings.module.ts create mode 100644 src/core/rating/pages/ratings/ratings.ts create mode 100644 src/core/rating/providers/offline.ts create mode 100644 src/core/rating/providers/rating.ts create mode 100644 src/core/rating/providers/sync.ts create mode 100644 src/core/rating/rating.module.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 92ad0f979..1d6bf8eae 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1590,6 +1590,15 @@ "core.question.questionno": "question", "core.question.requiresgrading": "question", "core.quotausage": "moodle", + "core.rating.aggregateavg": "moodle", + "core.rating.aggregatecount": "moodle", + "core.rating.aggregatemax": "moodle", + "core.rating.aggregatemin": "moodle", + "core.rating.aggregatesum": "moodle", + "core.rating.norating": "local_moodlemobileapp", + "core.rating.noratings": "moodle", + "core.rating.rating": "moodle", + "core.rating.ratings": "moodle", "core.redirectingtosite": "local_moodlemobileapp", "core.refresh": "moodle", "core.remove": "moodle", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 551e7a880..95751524c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -78,6 +78,7 @@ import { CoreCompileModule } from '@core/compile/compile.module'; import { CoreQuestionModule } from '@core/question/question.module'; import { CoreCommentsModule } from '@core/comments/comments.module'; import { CoreBlockModule } from '@core/block/block.module'; +import { CoreRatingModule } from '@core/rating/rating.module'; // Addon modules. import { AddonBadgesModule } from '@addon/badges/badges.module'; @@ -198,6 +199,7 @@ export const CORE_PROVIDERS: any[] = [ CoreQuestionModule, CoreCommentsModule, CoreBlockModule, + CoreRatingModule, AddonBadgesModule, AddonBlogModule, AddonCalendarModule, diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 0731749b0..c883b3e34 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1590,6 +1590,15 @@ "core.question.questionno": "Question {{$a}}", "core.question.requiresgrading": "Requires grading", "core.quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.", + "core.rating.aggregateavg": "Average of ratings", + "core.rating.aggregatecount": "Count of ratings", + "core.rating.aggregatemax": "Maximum rating", + "core.rating.aggregatemin": "Minimum rating", + "core.rating.aggregatesum": "Sum of ratings", + "core.rating.norating": "No rating", + "core.rating.noratings": "No ratings submitted", + "core.rating.rating": "Rating", + "core.rating.ratings": "Ratings", "core.redirectingtosite": "You will be redirected to the site.", "core.refresh": "Refresh", "core.remove": "Remove", diff --git a/src/core/rating/components/aggregate/aggregate.ts b/src/core/rating/components/aggregate/aggregate.ts new file mode 100644 index 000000000..75148751a --- /dev/null +++ b/src/core/rating/components/aggregate/aggregate.ts @@ -0,0 +1,112 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, Input, OnChanges, SimpleChange } from '@angular/core'; +import { ModalController } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreRatingProvider, CoreRatingInfo, CoreRatingInfoItem } from '@core/rating/providers/rating'; + +/** + * Component that displays the aggregation of a rating item. + */ +@Component({ + selector: 'core-rating-aggregate', + templateUrl: 'core-rating-aggregate.html' +}) +export class CoreRatingAggregateComponent implements OnChanges { + @Input() ratingInfo: CoreRatingInfo; + @Input() contextLevel: string; + @Input() instanceId: number; + @Input() itemId: number; + @Input() aggregateMethod: number; + @Input() scaleId: number; + @Input() courseId?: number; + + protected labelKey: string; + protected showCount: boolean; + protected item: CoreRatingInfoItem; + protected aggregateObserver; + + constructor(private eventsProvider: CoreEventsProvider, private modalCtrl: ModalController) {} + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + this.aggregateObserver && this.aggregateObserver.off(); + + this.item = (this.ratingInfo.ratings || []).find((rating) => rating.itemid == this.itemId); + if (!this.item) { + return; + } + + if (this.aggregateMethod == CoreRatingProvider.AGGREGATE_AVERAGE) { + this.labelKey = 'core.rating.aggregateavg'; + } else if (this.aggregateMethod == CoreRatingProvider.AGGREGATE_COUNT) { + this.labelKey = 'core.rating.aggregatecount'; + } else if (this.aggregateMethod == CoreRatingProvider.AGGREGATE_MAXIMUM) { + this.labelKey = 'core.rating.aggregatemax'; + } else if (this.aggregateMethod == CoreRatingProvider.AGGREGATE_MINIMUM) { + this.labelKey = 'core.rating.aggregatemin'; + } else if (this.aggregateMethod == CoreRatingProvider.AGGREGATE_SUM) { + this.labelKey = 'core.rating.aggregatesum'; + } else { + this.labelKey = ''; + + return; + } + + this.showCount = (this.aggregateMethod != CoreRatingProvider.AGGREGATE_COUNT); + + // Update aggrgate when the user adds or edits a rating. + this.aggregateObserver = this.eventsProvider.on(CoreRatingProvider.AGGREGATE_CHANGED_EVENT, (data) => { + if (data.contextLevel == this.contextLevel && + data.instanceId == this.instanceId && + data.component == this.ratingInfo.component && + data.ratingArea == this.ratingInfo.ratingarea && + data.itemId == this.itemId) { + this.item.aggregatestr = data.aggregate; + this.item.count = data.count; + } + }); + } + + /** + * Open the individual ratings page. + */ + openRatings(): void { + if (!this.ratingInfo.canviewall || !this.item.count) { + return; + } + + const params = { + contextLevel: this.contextLevel, + instanceId: this.instanceId, + ratingComponent: this.ratingInfo.component, + ratingArea: this.ratingInfo.ratingarea, + itemId: this.itemId, + scaleId: this.scaleId, + courseId: this.courseId + }; + const modal = this.modalCtrl.create('CoreRatingRatingsPage', params); + modal.present(); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.aggregateObserver && this.aggregateObserver.off(); + } +} diff --git a/src/core/rating/components/aggregate/core-rating-aggregate.html b/src/core/rating/components/aggregate/core-rating-aggregate.html new file mode 100644 index 000000000..2f3b3b063 --- /dev/null +++ b/src/core/rating/components/aggregate/core-rating-aggregate.html @@ -0,0 +1,4 @@ + + {{ labelKey | translate }}{{ 'core.labelsep' | translate }} {{ item.aggregatestr || '-' }} + ({{ item.count }}) + diff --git a/src/core/rating/components/components.module.ts b/src/core/rating/components/components.module.ts new file mode 100644 index 000000000..d9563e376 --- /dev/null +++ b/src/core/rating/components/components.module.ts @@ -0,0 +1,39 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreRatingAggregateComponent } from './aggregate/aggregate'; +import { CoreRatingRateComponent } from './rate/rate'; + +@NgModule({ + declarations: [ + CoreRatingAggregateComponent, + CoreRatingRateComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild() + ], + providers: [ + ], + exports: [ + CoreRatingAggregateComponent, + CoreRatingRateComponent + ] +}) +export class CoreRatingComponentsModule {} diff --git a/src/core/rating/components/rate/core-rating-rate.html b/src/core/rating/components/rate/core-rating-rate.html new file mode 100644 index 000000000..cbf5357b3 --- /dev/null +++ b/src/core/rating/components/rate/core-rating-rate.html @@ -0,0 +1,6 @@ + + {{ 'core.rating.rating' | translate }} + + {{ scaleItem.name }} + + diff --git a/src/core/rating/components/rate/rate.ts b/src/core/rating/components/rate/rate.ts new file mode 100644 index 000000000..d2407543f --- /dev/null +++ b/src/core/rating/components/rate/rate.ts @@ -0,0 +1,106 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, Input, OnChanges, SimpleChange } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreRatingProvider, CoreRatingInfo, CoreRatingInfoItem, CoreRatingScale } from '@core/rating/providers/rating'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; + +/** + * Component that displays the user rating select. + */ +@Component({ + selector: 'core-rating-rate', + templateUrl: 'core-rating-rate.html' +}) +export class CoreRatingRateComponent implements OnChanges { + @Input() ratingInfo: CoreRatingInfo; + @Input() contextLevel: string; // Context level: course, module, user, etc. + @Input() instanceId: number; // Context instance id. + @Input() itemId: number; // Item id. Example: forum post id. + @Input() itemSetId: number; // Item set id. Example: forum discussion id. + @Input() courseId: number; + @Input() aggregateMethod: number; + @Input() scaleId: number; + @Input() userId: number; + + item: CoreRatingInfoItem; + scale: CoreRatingScale; + rating: number; + + constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService, + private ratingProvider: CoreRatingProvider, private ratingOffline: CoreRatingOfflineProvider) {} + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + this.item = (this.ratingInfo.ratings || []).find((rating) => rating.itemid == this.itemId); + this.scale = (this.ratingInfo.scales || []).find((scale) => scale.id == this.scaleId); + + if (!this.item || !this.scale || !this.ratingProvider.isAddRatingWSAvailable()) { + this.item = null; + + return; + } + + // Set numeric scale items. + if (!this.scale.items) { + this.scale.items = []; + if (this.scale.isnumeric) { + for (let n = 0; n <= this.scale.max; n++) { + this.scale.items.push({name: String(n), value: n}); + } + } + } + + // Add "No rating" item to the scale. + if (!this.scale.items[0] || this.scale.items[0].value != CoreRatingProvider.UNSET_RATING) { + this.scale.items.unshift({ + name: this.translate.instant('core.rating.norating'), + value: CoreRatingProvider.UNSET_RATING + }); + } + + this.ratingOffline.getRating(this.contextLevel, this.instanceId, this.ratingInfo.component, this.ratingInfo.ratingarea, + this.itemId).then((rating) => { + this.rating = rating.rating; + }).catch(() => { + if (this.item && this.item.rating != null) { + this.rating = this.item.rating; + } else { + this.rating = CoreRatingProvider.UNSET_RATING; + } + }); + } + + /** + * Send or save the user rating when changed. + */ + protected userRatingChanged(): void { + const modal = this.domUtils.showModalLoading('core.sending', true); + this.ratingProvider.addRating(this.ratingInfo.component, this.ratingInfo.ratingarea, this.contextLevel, this.instanceId, + this.itemId, this.itemSetId, this.courseId, this.scaleId, this.rating, this.userId, this.aggregateMethod) + .then((response) => { + if (response == null) { + this.domUtils.showToast('core.datastoredoffline', true, 3000); + } + }).catch((error) => { + this.domUtils.showErrorModal(error); + }).finally(() => { + modal.dismiss(); + }); + } +} diff --git a/src/core/rating/lang/en.json b/src/core/rating/lang/en.json new file mode 100644 index 000000000..d8a28f089 --- /dev/null +++ b/src/core/rating/lang/en.json @@ -0,0 +1,11 @@ +{ + "aggregateavg": "Average of ratings", + "aggregatecount": "Count of ratings", + "aggregatemax": "Maximum rating", + "aggregatemin": "Minimum rating", + "aggregatesum": "Sum of ratings", + "norating": "No rating", + "noratings": "No ratings submitted", + "rating": "Rating", + "ratings": "Ratings" +} diff --git a/src/core/rating/pages/ratings/ratings.html b/src/core/rating/pages/ratings/ratings.html new file mode 100644 index 000000000..6123f3730 --- /dev/null +++ b/src/core/rating/pages/ratings/ratings.html @@ -0,0 +1,28 @@ + + + {{ 'core.rating.ratings' | translate }} + + + + + + + + + + + + + + + {{ rating.timemodified | coreDateDayOrTime }} + +

+

{{ rating.rating }}

+
+
+ +
+
diff --git a/src/core/rating/pages/ratings/ratings.module.ts b/src/core/rating/pages/ratings/ratings.module.ts new file mode 100644 index 000000000..62bf4821b --- /dev/null +++ b/src/core/rating/pages/ratings/ratings.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreRatingRatingsPage } from './ratings'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; + +@NgModule({ + declarations: [ + CoreRatingRatingsPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(CoreRatingRatingsPage), + TranslateModule.forChild() + ], +}) +export class CoreRatingRatingsPageModule {} diff --git a/src/core/rating/pages/ratings/ratings.ts b/src/core/rating/pages/ratings/ratings.ts new file mode 100644 index 000000000..1114562ca --- /dev/null +++ b/src/core/rating/pages/ratings/ratings.ts @@ -0,0 +1,95 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component } from '@angular/core'; +import { IonicPage, NavParams, ViewController } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreRatingProvider, CoreRatingItemRating } from '@core/rating/providers/rating'; + +/** + * Page that displays individual ratings + */ +@IonicPage({ segment: 'core-rating-ratings' }) +@Component({ + selector: 'page-core-rating-ratings', + templateUrl: 'ratings.html', +}) +export class CoreRatingRatingsPage { + contextLevel: string; + instanceId: number; + component: string; + ratingArea: string; + aggregateMethod: number; + itemId: number; + scaleId: number; + courseId: number; + loaded = false; + ratings: CoreRatingItemRating[] = []; + + constructor(navParams: NavParams, private viewCtrl: ViewController, private domUtils: CoreDomUtilsProvider, + private ratingProvider: CoreRatingProvider) { + this.contextLevel = navParams.get('contextLevel'); + this.instanceId = navParams.get('instanceId'); + this.component = navParams.get('ratingComponent'); + this.ratingArea = navParams.get('ratingArea'); + this.aggregateMethod = navParams.get('aggregateMethod'); + this.itemId = navParams.get('itemId'); + this.scaleId = navParams.get('scaleId'); + this.courseId = navParams.get('courseId'); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchData().finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch all the data required for the view. + * + * @return {Promise} Resolved when done. + */ + fetchData(): Promise { + return this.ratingProvider.getItemRatings(this.contextLevel, this.instanceId, this.component, this.ratingArea, this.itemId, + this.scaleId, undefined, this.courseId).then((ratings) => { + this.ratings = ratings; + }).catch((error) => { + this.domUtils.showErrorModal(error); + }); + } + + /** + * Refresh data. + * + * @param {any} refresher Refresher. + */ + refreshRatings(refresher: any): void { + this.ratingProvider.invalidateRatingItems(this.contextLevel, this.instanceId, this.component, this.ratingArea, this.itemId, + this.scaleId).finally(() => { + return this.fetchData().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } +} diff --git a/src/core/rating/providers/offline.ts b/src/core/rating/providers/offline.ts new file mode 100644 index 000000000..6f033da1c --- /dev/null +++ b/src/core/rating/providers/offline.ts @@ -0,0 +1,305 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; + +/** + * Structure of offline ratings. + */ +export interface CoreRatingOfflineRating { + component: string; + ratingarea: string; + contextlevel: string; + instanceid: number; + itemid: number; + itemsetid: number; + courseid: number; + scaleid: number; + rating: number; + rateduserid: number; + aggregation: number; +} + +/** + * Structure of item sets. + */ +export interface CoreRatingItemSet { + component: string; + ratingArea: string; + contextLevel: string; + instanceId: number; + itemSetId: number; + courseId: number; +} + +/** + * Service to handle offline data for rating. + */ +@Injectable() +export class CoreRatingOfflineProvider { + + // Variables for database. + static RATINGS_TABLE = 'rating_ratings'; + protected siteSchema: CoreSiteSchema = { + name: 'CoreCourseOfflineProvider', + version: 1, + tables: [ + { + name: CoreRatingOfflineProvider.RATINGS_TABLE, + columns: [ + { + name: 'component', + type: 'TEXT' + }, + { + name: 'ratingarea', + type: 'TEXT' + }, + { + name: 'contextlevel', + type: 'INTEGER', + }, + { + name: 'instanceid', + type: 'INTEGER' + }, + { + name: 'itemid', + type: 'INTEGER' + }, + { + name: 'itemsetid', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'scaleid', + type: 'INTEGER' + }, + { + name: 'rating', + type: 'INTEGER' + }, + { + name: 'rateduserid', + type: 'INTEGER' + }, + { + name: 'aggregation', + type: 'INTEGER' + } + ], + primaryKeys: ['component', 'ratingarea', 'contextlevel', 'instanceid', 'itemid'] + } + ] + }; + + constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) { + this.sitesProvider.registerSiteSchema(this.siteSchema); + } + + /** + * Get an offline rating. + * + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {numnber} instanceId Context instance id. + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating area. Example: "post". + * @param {number} itemId Item id. Example: forum post id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the saved rating, rejected if not found. + */ + getRating(contextLevel: string, instanceId: number, component: string, ratingArea: string, itemId: number, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + ratingarea: ratingArea, + itemid: itemId + }; + + return site.getDb().getRecord(CoreRatingOfflineProvider.RATINGS_TABLE, conditions); + }); + } + + /** + * Add an offline rating. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating area. Example: "post". + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {numnber} instanceId Context instance id. + * @param {number} itemId Item id. Example: forum post id. + * @param {number} itemSetId Item set id. Example: forum discussion id. + * @param {number} courseId Course id. + * @param {number} scaleId Scale id. + * @param {number} rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating. + * @param {number} ratedUserId Rated user id. + * @param {number} aggregateMethod Aggregate method. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the rating is saved. + */ + addRating(component: string, ratingArea: string, contextLevel: string, instanceId: number, itemId: number, itemSetId: number, + courseId: number, scaleId: number, rating: number, ratedUserId: number, aggregateMethod: number, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const data: CoreRatingOfflineRating = { + component: component, + ratingarea: ratingArea, + contextlevel: contextLevel, + instanceid: instanceId, + itemid: itemId, + itemsetid: itemSetId, + courseid: courseId, + scaleid: scaleId, + rating: rating, + rateduserid: ratedUserId, + aggregation: aggregateMethod + }; + + return site.getDb().insertRecord(CoreRatingOfflineProvider.RATINGS_TABLE, data); + }); + } + + /** + * Delete offline rating. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating area. Example: "post". + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} itemId Item id. Example: forum post id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the rating is saved. + */ + deleteRating(component: string, ratingArea: string, contextLevel: string, instanceId: number, itemId: number, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + component: component, + ratingarea: ratingArea, + contextlevel: contextLevel, + instanceid: instanceId, + itemid: itemId + }; + + return site.getDb().deleteRecords(CoreRatingOfflineProvider.RATINGS_TABLE, conditions); + }); + } + + /** + * Get the list of item sets in a component or instance. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating Area. Example: "post". + * @param {string} [contextLevel] Context level: course, module, user, etc. + * @param {numnber} [instanceId] Context instance id. + * @param {number} [itemSetId] Item set id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the list of item set ids. + */ + getItemSets(component: string, ratingArea: string, contextLevel?: string, instanceId?: number, itemSetId?: number, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const fields = 'DISTINCT contextlevel, instanceid, itemsetid, courseid'; + const conditions: any = { + component, + ratingarea: ratingArea + }; + if (contextLevel != null && instanceId != null) { + conditions.contextlevel = contextLevel; + conditions.instanceId = instanceId; + } + if (itemSetId != null) { + conditions.itemSetId = itemSetId; + } + + return site.getDb().getRecords(CoreRatingOfflineProvider.RATINGS_TABLE, conditions, undefined, fields) + .then((records: any[]) => { + return records.map((record) => { + return { + component, + ratingArea, + contextLevel: record.contextlevel, + instanceId: record.instanceid, + itemSetId: record.itemsetid, + courseId: record.courseid + }; + }); + }); + }); + } + + /** + * Get offline ratings of an item set. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating Area. Example: "post". + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} itemId Item id. Example: forum post id. + * @param {number} itemSetId Item set id. Example: forum discussion id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the list of ratings. + */ + getRatings(component: string, ratingArea: string, contextLevel: string, instanceId: number, itemSetId: number, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + component, + ratingarea: ratingArea, + contextlevel: contextLevel, + instanceid: instanceId, + itemsetid: itemSetId + }; + + return site.getDb().getRecords(CoreRatingOfflineProvider.RATINGS_TABLE, conditions); + }); + } + + /** + * Return whether a component, instance or item set has offline ratings. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating Area. Example: "post". + * @param {string} [contextLevel] Context level: course, module, user, etc. + * @param {number} [instanceId] Context instance id. + * @param {number} [itemSetId] Item set id. Example: forum discussion id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with a boolean. + */ + hasRatings(component: string, ratingArea: string, contextLevel?: string, instanceId?: number, itemSetId?: number, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions: any = { + component, + ratingarea: ratingArea + }; + if (contextLevel != null && instanceId != null) { + conditions.contextlevel = contextLevel; + conditions.instanceId = instanceId; + } + if (itemSetId != null) { + conditions.itemsetid = itemSetId; + } + + return this.utils.promiseWorks(site.getDb().recordExists(CoreRatingOfflineProvider.RATINGS_TABLE, conditions)); + }); + } +} diff --git a/src/core/rating/providers/rating.ts b/src/core/rating/providers/rating.ts new file mode 100644 index 000000000..8756daec7 --- /dev/null +++ b/src/core/rating/providers/rating.ts @@ -0,0 +1,352 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSiteWSPreSets } from '@classes/site'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreRatingOfflineProvider } from './offline'; + +/** + * Structure of the rating info returned by web services. + */ +export interface CoreRatingInfo { + contextid: number; + component: string; + ratingarea: string; + canviewall: boolean; + canviewany: boolean; + scales?: CoreRatingScale[]; + ratings?: CoreRatingInfoItem[]; +} + +/** + * Structure of scales in the rating info. + */ +export interface CoreRatingScale { + id: number; + courseid?: number; + name?: string; + max: number; + isnumeric: boolean; + items?: {value: number, name: string}[]; +} + +/** + * Structure of items in the rating info. + */ +export interface CoreRatingInfoItem { + itemid: number; + scaleid?: number; + scale?: CoreRatingScale; + userid?: number; + aggregate?: number; + aggregatestr?: string; + count?: number; + rating?: number; + canrate?: boolean; + canviewaggregate?: boolean; +} + +/** + * Structure of a rating returned by the item ratings web service. + */ +export interface CoreRatingItemRating { + id: number; + userid: number; + userpictureurl: string; + userfullname: string; + rating: string; + timemodified: number; +} + +/** + * Service to handle ratings. + */ +@Injectable() +export class CoreRatingProvider { + + static AGGREGATE_NONE = 0; // No ratings. + static AGGREGATE_AVERAGE = 1; + static AGGREGATE_COUNT = 2; + static AGGREGATE_MAXIMUM = 3; + static AGGREGATE_MINIMUM = 4; + static AGGREGATE_SUM = 5; + + static UNSET_RATING = -999; + + static AGGREGATE_CHANGED_EVENT = 'core_rating_aggregate_changed'; + static RATING_SAVED_EVENT = 'core_rating_rating_saved'; + + protected ROOT_CACHE_KEY = 'CoreRating:'; + + constructor(private appProvider: CoreAppProvider, + private eventsProvider: CoreEventsProvider, + private sitesProvider: CoreSitesProvider, + private userProvider: CoreUserProvider, + private utils: CoreUtilsProvider, + private ratingOffline: CoreRatingOfflineProvider) {} + + /** + * Returns whether the web serivce to add ratings is available. + * + * @return {boolean} If WS is abalaible. + * @since 3.2 + */ + isAddRatingWSAvailable(): boolean { + return this.sitesProvider.wsAvailableInCurrentSite('core_rating_add_rating'); + } + + /** + * Add a rating to an item. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating area. Example: "post". + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} instanceId Context instance id. + * @param {number} itemId Item id. Example: forum post id. + * @param {number} itemSetId Item set id. Example: forum discussion id. + * @param {number} courseId Course id. + * @param {number} scaleId Scale id. + * @param {number} rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating. + * @param {number} ratedUserId Rated user id. + * @param {number} aggregateMethod Aggregate method. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the aggregated rating or null if stored offline. + * @since 3.2 + */ + addRating(component: string, ratingArea: string, contextLevel: string, instanceId: number, itemId: number, itemSetId: number, + courseId: number, scaleId: number, rating: number, ratedUserId: number, aggregateMethod: number, siteId?: string): + Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a rating to be synchronized later. + const storeOffline = (): Promise => { + return this.ratingOffline.addRating(component, ratingArea, contextLevel, instanceId, itemId, itemSetId, courseId, + scaleId, rating, ratedUserId, aggregateMethod, siteId).then(() => { + this.eventsProvider.trigger(CoreRatingProvider.RATING_SAVED_EVENT, { + component, + ratingArea, + contextLevel, + instanceId, + itemSetId, + itemId + }, siteId); + + return null; + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + return this.ratingOffline.deleteRating(component, ratingArea, contextLevel, instanceId, itemId, siteId).then(() => { + return this.addRatingOnline(component, ratingArea, contextLevel, instanceId, itemId, scaleId, rating, ratedUserId, + aggregateMethod, siteId).catch((error) => { + + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error or offline not supported, reject. + return Promise.reject(error); + } + + // Couldn't connect to server, store offline. + return storeOffline(); + }); + }); + } + + /** + * Add a rating to an item. It will fail if offline or cannot connect. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating area. Example: "post". + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} instanceId Context instance id. + * @param {number} itemId Item id. Example: forum post id. + * @param {number} scaleId Scale id. + * @param {number} rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating. + * @param {number} ratedUserId Rated user id. + * @param {number} aggregateMethod Aggregate method. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the aggregated rating. + * @since 3.2 + */ + addRatingOnline(component: string, ratingArea: string, contextLevel: string, instanceId: number, itemId: number, + scaleId: number, rating: number, ratedUserId: number, aggregateMethod: number, siteId?: string): + Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + ratingarea: ratingArea, + itemid: itemId, + scaleid: scaleId, + rating: rating, + rateduserid: ratedUserId, + aggregation: aggregateMethod + }; + + return site.write('core_rating_add_rating', params).then((response) => { + return this.invalidateRatingItems(contextLevel, instanceId, component, ratingArea, itemId, scaleId).then(() => { + this.eventsProvider.trigger(CoreRatingProvider.AGGREGATE_CHANGED_EVENT, { + contextLevel, + instanceId, + component, + ratingArea, + itemId, + aggregate: response.aggregate, + count: response.count + }); + + return response; + }); + }); + }); + } + + /** + * Get item ratings. + * + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} instanceId Context instance id. + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating area. Example: "post". + * @param {number} itemId Item id. Example: forum post id. + * @param {number} scaleId Scale id. + * @param {string} [sort="timemodified"] Sort field. + * @param {number} [courseId] Course id. Used for fetching user profiles. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @return {Promise} Promise resolved with the list of ratings. + */ + getItemRatings(contextLevel: string, instanceId: number, component: string, ratingArea: string, itemId: number, + scaleId: number, sort: string = 'timemodified', courseId?: number, siteId?: string, ignoreCache: boolean = false): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + contextlevel: contextLevel, + instanceid: instanceId, + component: component, + ratingarea: ratingArea, + itemid: itemId, + scaleid: scaleId, + sort: sort + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getItemRatingsCacheKey(contextLevel, instanceId, component, ratingArea, itemId, scaleId, sort) + }; + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('core_rating_get_item_ratings', params, preSets).then((response) => { + if (!response || !response.ratings) { + return Promise.reject(null); + } + + // We need to fetch profiles because the returned profile pictures are incorrect. + const promises = response.ratings.map((rating: CoreRatingItemRating) => { + return this.userProvider.getProfile(rating.userid, courseId, true, site.id).then((user) => { + rating.userpictureurl = user.profileimageurl; + }).catch(() => { + // Ignore error. + rating.userpictureurl = null; + }); + }); + + return Promise.all(promises).then(() => { + return response.ratings; + }); + }); + }); + } + + /** + * Invalidate item ratings. + * + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} instanceId Context instance id. + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating area. Example: "post". + * @param {number} itemId Item id. Example: forum post id. + * @param {number} scaleId Scale id. + * @param {string} [sort="timemodified"] Sort field. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateRatingItems(contextLevel: string, instanceId: number, component: string, ratingArea: string, + itemId: number, scaleId: number, sort: string = 'timemodified', siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getItemRatingsCacheKey(contextLevel, instanceId, component, ratingArea, itemId, scaleId, sort); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Prefetch individual ratings. + * + * This function should be called from the prefetch handler of activities with ratings. + * + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} instanceId Instance id. + * @param {string} [siteId] Site id. If not defined, current site. + * @param {number} [courseId] Course id. Used for prefetching user profiles. + * @param {CoreRatingInfo} [ratingInfo] Rating info returned by web services. + * @return {Promise} Promise resolved when done. + */ + prefetchRatings(contextLevel: string, instanceId: number, scaleId: number, courseId?: number, ratingInfo?: CoreRatingInfo, + siteId?: string): Promise { + if (!ratingInfo || !ratingInfo.ratings) { + return Promise.resolve(); + } + + return this.sitesProvider.getSite(siteId).then((site) => { + const promises = ratingInfo.ratings.map((item) => { + return this.getItemRatings(contextLevel, instanceId, ratingInfo.component, ratingInfo.ratingarea, item.itemid, + scaleId, undefined, courseId, site.id, true).then((ratings) => { + const userIds = ratings.map((rating: CoreRatingItemRating) => rating.userid); + + return this.userProvider.prefetchProfiles(userIds, courseId, site.id); + }); + }); + + return Promise.all(promises); + }); + } + + /** + * Get cache key for rating items WS calls. + * + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating area. Example: "post". + * @param {number} itemId Item id. Example: forum post id. + * @param {number} scaleId Scale id. + * @param {string} sort Sort field. + * @return {string} Cache key. + */ + protected getItemRatingsCacheKey(contextLevel: string, instanceId: number, component: string, ratingArea: string, + itemId: number, scaleId: number, sort: string): string { + return `${this.ROOT_CACHE_KEY}${contextLevel}:${instanceId}:${component}:${ratingArea}:${itemId}:${scaleId}:${sort}`; + } +} diff --git a/src/core/rating/providers/sync.ts b/src/core/rating/providers/sync.ts new file mode 100644 index 000000000..cffa42d42 --- /dev/null +++ b/src/core/rating/providers/sync.ts @@ -0,0 +1,196 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { TranslateService } from '@ngx-translate/core'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreRatingProvider } from './rating'; +import { CoreRatingOfflineProvider, CoreRatingItemSet } from './offline'; +import { CoreEventsProvider } from '@providers/events'; + +/** + * Service to sync ratings. + */ +@Injectable() +export class CoreRatingSyncProvider extends CoreSyncBaseProvider { + + static SYNCED_EVENT = 'core_rating_synced'; + + constructor(translate: TranslateService, + appProvider: CoreAppProvider, + private eventsProvider: CoreEventsProvider, + loggerProvider: CoreLoggerProvider, + sitesProvider: CoreSitesProvider, + syncProvider: CoreSyncProvider, + textUtils: CoreTextUtilsProvider, + timeUtils: CoreTimeUtilsProvider, + private utils: CoreUtilsProvider, + private ratingProvider: CoreRatingProvider, + private ratingOffline: CoreRatingOfflineProvider) { + + super('CoreRatingSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils); + } + + /** + * 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 {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating Area. Example: "post". + * @param {string} [contextLevel] Context level: course, module, user, etc. + * @param {numnber} [instanceId] Context instance id. + * @param {number} [itemSetId] Item set id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncRatings(component: string, ratingArea: string, contextLevel?: string, instanceId?: number, itemSetId?: number, + siteId?: string): Promise<{itemSet: CoreRatingItemSet, updated: boolean, warnings: string[]}[]> { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.ratingOffline.getItemSets(component, ratingArea, contextLevel, instanceId, itemSetId, siteId) + .then((itemSets) => { + const results = []; + const promises = itemSets.map((itemSet) => { + return this.syncItemSetIfNeeded(component, ratingArea, itemSet.contextLevel, itemSet.instanceId, + itemSet.itemSetId, siteId).then((result) => { + if (result.updated) { + // Sync successful, send event. + this.eventsProvider.trigger(CoreRatingSyncProvider.SYNCED_EVENT, { + ...itemSet, + warnings: result.warnings + }, siteId); + } + + results.push({itemSet, ...result}); + }); + }); + + return Promise.all(promises).then(() => { + return results; + }); + }); + } + + /** + * Sync ratings of an item set only if a certain time has passed since the last time. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating Area. Example: "post". + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} instanceId Context instance id. + * @param {number} itemSetId Item set id. Example: forum discussion id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when ratings are synced or if it doesn't need to be synced. + */ + protected syncItemSetIfNeeded(component: string, ratingArea: string, contextLevel: string, instanceId: number, + itemSetId: number, siteId?: string): Promise<{updated: boolean, warnings: string[]}> { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const syncId = this.getItemSetSyncId(component, ratingArea, contextLevel, instanceId, itemSetId); + + return this.isSyncNeeded(syncId, siteId).then((needed) => { + if (needed) { + return this.syncItemSet(component, ratingArea, contextLevel, instanceId, itemSetId, siteId); + } + }); + } + + /** + * Synchronize all offline ratings of an item set. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating Area. Example: "post". + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} instanceId Context instance id. + * @param {number} itemSetId Item set id. Example: forum discussion id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + protected syncItemSet(component: string, ratingArea: string, contextLevel: string, instanceId: number, itemSetId: number, + siteId?: string): Promise<{updated: boolean, warnings: string[]}> { + + siteId = siteId || this.sitesProvider.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}`); + + let updated = false; + const warnings = []; + + return this.ratingOffline.getRatings(component, ratingArea, contextLevel, instanceId, itemSetId, siteId).then((ratings) => { + if (!ratings.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + const promises = ratings.map((rating) => { + return this.ratingProvider.addRatingOnline(component, ratingArea, rating.contextlevel, rating.instanceid, + rating.itemid, rating.scaleid, rating.rating, rating.rateduserid, rating.aggregation, siteId) + .catch((error) => { + if (this.utils.isWebServiceError(error)) { + warnings.push(this.textUtils.getErrorMessageFromError(error)); + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + }).then(() => { + updated = true; + + return this.ratingOffline.deleteRating(component, ratingArea, rating.contextlevel, rating.instanceid, + rating.itemid, siteId).finally(() => { + return this.ratingProvider.invalidateRatingItems(rating.contextlevel, rating.instanceid, component, + ratingArea, rating.itemid, rating.scaleid, undefined, siteId); + }); + }); + }); + + return Promise.all(promises).then(() => { + // All done, return the warnings. + return { updated, warnings }; + }); + }); + } + + /** + * Get the sync id of an item set. + * + * @param {string} component Component. Example: "mod_forum". + * @param {string} ratingArea Rating Area. Example: "post". + * @param {string} contextLevel Context level: course, module, user, etc. + * @param {number} instanceId Context instance id. + * @param {number} itemSetId Item set id. Example: forum discussion id. + * @return {string} Sync id. + */ + protected getItemSetSyncId(component: string, ratingArea: string, contextLevel: string, instanceId: number, itemSetId: number): + string { + return `itemSet#${component}#${ratingArea}#${contextLevel}#${instanceId}#${itemSetId}`; + } +} diff --git a/src/core/rating/rating.module.ts b/src/core/rating/rating.module.ts new file mode 100644 index 000000000..e7c512467 --- /dev/null +++ b/src/core/rating/rating.module.ts @@ -0,0 +1,31 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CoreRatingProvider } from './providers/rating'; +import { CoreRatingOfflineProvider } from './providers/offline'; +import { CoreRatingSyncProvider } from './providers/sync'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + CoreRatingProvider, + CoreRatingOfflineProvider, + CoreRatingSyncProvider + ] +}) +export class CoreRatingModule {}