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 || '-' }}
+ 0">({{ 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 }}
+
+
+
+
+
+
+
+
+
+
+ 0">
+
+
+
+ {{ 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 {}