From 9c1be97635113615da3b124bb9b65e1507974019 Mon Sep 17 00:00:00 2001
From: Albert Gasset <albertgasset@fsfe.org>
Date: Tue, 5 Mar 2019 13:31:11 +0100
Subject: [PATCH] MOBILE-2485 data: Ratings

---
 .../index/addon-mod-data-index.html           |  2 +-
 src/addon/mod/data/components/index/index.ts  | 43 +++++++++++-
 src/addon/mod/data/pages/entry/entry.html     |  3 +
 .../mod/data/pages/entry/entry.module.ts      |  4 +-
 src/addon/mod/data/pages/entry/entry.ts       | 11 ++++
 src/addon/mod/data/providers/data.ts          |  8 ++-
 src/addon/mod/data/providers/helper.ts        |  2 +-
 .../mod/data/providers/prefetch-handler.ts    | 10 ++-
 src/addon/mod/data/providers/sync.ts          | 66 +++++++++++++++++--
 9 files changed, 137 insertions(+), 12 deletions(-)

diff --git a/src/addon/mod/data/components/index/addon-mod-data-index.html b/src/addon/mod/data/components/index/addon-mod-data-index.html
index 62d8c38a0..acc54a271 100644
--- a/src/addon/mod/data/components/index/addon-mod-data-index.html
+++ b/src/addon/mod/data/components/index/addon-mod-data-index.html
@@ -22,7 +22,7 @@
     <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
 
     <!-- Data done in offline but not synchronized -->
-    <div class="core-warning-card" icon-start *ngIf="hasOffline">
+    <div class="core-warning-card" icon-start *ngIf="hasOffline || hasOfflineRatings">
         <ion-icon name="warning"></ion-icon>
         {{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
     </div>
diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts
index 9da9180fe..68bdccef6 100644
--- a/src/addon/mod/data/components/index/index.ts
+++ b/src/addon/mod/data/components/index/index.ts
@@ -19,6 +19,9 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
 import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
 import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
 import { CoreCommentsProvider } from '@core/comments/providers/comments';
+import { CoreRatingProvider } from '@core/rating/providers/rating';
+import { CoreRatingOfflineProvider } from '@core/rating/providers/offline';
+import { CoreRatingSyncProvider } from '@core/rating/providers/sync';
 import { AddonModDataProvider } from '../../providers/data';
 import { AddonModDataHelperProvider } from '../../providers/helper';
 import { AddonModDataOfflineProvider } from '../../providers/offline';
@@ -74,11 +77,16 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
     protected hasComments = false;
     protected fieldsArray: any;
 
+    hasOfflineRatings: boolean;
+    protected ratingOfflineObserver: any;
+    protected ratingSyncObserver: any;
+
     constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider,
             private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content,
             private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider,
             private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider,
-            private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController) {
+            private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController,
+            private ratingOffline: CoreRatingOfflineProvider) {
         super(injector, content);
 
         // Refresh entries on change.
@@ -89,7 +97,22 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
                 return this.loadContent(true);
             }
         }, this.siteId);
+
+        // Listen for offline ratings saved and synced.
+        this.ratingOfflineObserver = this.eventsProvider.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
+            if (this.data && data.component == 'mod_data' && data.ratingArea == 'entry' && data.contextLevel == 'module'
+                    && data.instanceId == this.data.coursemodule) {
+                this.hasOfflineRatings = true;
+            }
+        });
+        this.ratingSyncObserver = this.eventsProvider.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
+            if (this.data && data.component == 'mod_data' && data.ratingArea == 'entry' && data.contextLevel == 'module'
+                    && data.instanceId == this.data.coursemodule) {
+                this.hasOfflineRatings = false;
+            }
+        });
     }
+
     /**
      * Component being initialized.
      */
@@ -487,6 +510,10 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
                     }
                 });
             }
+        }).then(() => {
+            return this.ratingOffline.hasRatings('mod_data', 'entry', 'module', this.data.coursemodule).then((hasRatings) => {
+                this.hasOfflineRatings = hasRatings;
+            });
         });
     }
 
@@ -496,7 +523,17 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
      * @return {Promise<any>} Promise resolved when done.
      */
     protected sync(): Promise<any> {
-        return this.dataSync.syncDatabase(this.data.id);
+        const promises = [
+            this.dataSync.syncDatabase(this.data.id),
+            this.dataSync.syncRatings(this.data.coursemodule)
+        ];
+
+        return Promise.all(promises).then((results) => {
+            return results.reduce((a, b) => ({
+                updated: a.updated || b.updated,
+                warnings: (a.warnings || []).concat(b.warnings || []),
+            }), {updated: false});
+        });
     }
 
     /**
@@ -515,5 +552,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
     ngOnDestroy(): void {
         super.ngOnDestroy();
         this.entryChangedObserver && this.entryChangedObserver.off();
+        this.ratingOfflineObserver && this.ratingOfflineObserver.off();
+        this.ratingSyncObserver && this.ratingSyncObserver.off();
     }
 }
diff --git a/src/addon/mod/data/pages/entry/entry.html b/src/addon/mod/data/pages/entry/entry.html
index a49acdf57..3cc182e48 100644
--- a/src/addon/mod/data/pages/entry/entry.html
+++ b/src/addon/mod/data/pages/entry/entry.html
@@ -30,6 +30,9 @@
             <core-compile-html [text]="entryRendered" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
         </div>
 
+        <core-rating-rate *ngIf="data && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="data.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="courseId" [aggregateMethod]="data.assessed" [scaleId]="data.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()"></core-rating-rate>
+        <core-rating-aggregate *ngIf="data && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="data.coursemodule" [itemId]="entry.id" [courseId]="courseId" [aggregateMethod]="data.assessed" [scaleId]="data.scale"></core-rating-aggregate>
+
         <ion-item *ngIf="data && entry">
             <core-comments contextLevel="module" [instanceId]="data.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry"></core-comments>
         </ion-item>
diff --git a/src/addon/mod/data/pages/entry/entry.module.ts b/src/addon/mod/data/pages/entry/entry.module.ts
index cebf202e2..eb77745c6 100644
--- a/src/addon/mod/data/pages/entry/entry.module.ts
+++ b/src/addon/mod/data/pages/entry/entry.module.ts
@@ -19,6 +19,7 @@ import { CoreDirectivesModule } from '@directives/directives.module';
 import { CoreComponentsModule } from '@components/components.module';
 import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
 import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
+import { CoreRatingComponentsModule } from '@core/rating/components/components.module';
 import { AddonModDataComponentsModule } from '../../components/components.module';
 import { AddonModDataEntryPage } from './entry';
 
@@ -33,7 +34,8 @@ import { AddonModDataEntryPage } from './entry';
         CoreCompileHtmlComponentModule,
         CoreCommentsComponentsModule,
         IonicPageModule.forChild(AddonModDataEntryPage),
-        TranslateModule.forChild()
+        TranslateModule.forChild(),
+        CoreRatingComponentsModule
     ],
 })
 export class AddonModDataEntryPageModule {}
diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts
index 16a35f39e..d1b26a8f1 100644
--- a/src/addon/mod/data/pages/entry/entry.ts
+++ b/src/addon/mod/data/pages/entry/entry.ts
@@ -20,6 +20,7 @@ import { CoreSitesProvider } from '@providers/sites';
 import { CoreGroupsProvider } from '@providers/groups';
 import { CoreEventsProvider } from '@providers/events';
 import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreRatingInfo } from '@core/rating/providers/rating';
 import { AddonModDataProvider } from '../../providers/data';
 import { AddonModDataHelperProvider } from '../../providers/helper';
 import { AddonModDataOfflineProvider } from '../../providers/offline';
@@ -66,6 +67,7 @@ export class AddonModDataEntryPage implements OnDestroy {
     cssClass = '';
     extraImports = [AddonModDataComponentsModule];
     jsData;
+    ratingInfo: CoreRatingInfo;
 
     constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider,
             protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
@@ -162,7 +164,9 @@ export class AddonModDataEntryPage implements OnDestroy {
                 return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions);
             });
         }).then((entry) => {
+            this.ratingInfo = entry.ratinginfo;
             entry = entry.entry;
+
             this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass);
 
             // Index contents by fieldid.
@@ -311,6 +315,13 @@ export class AddonModDataEntryPage implements OnDestroy {
         });
     }
 
+    /**
+     * Function called when rating is updated online.
+     */
+    ratingUpdated(): void {
+        this.dataProvider.invalidateEntryData(this.data.id, this.entryId);
+    }
+
     /**
      * Component being destroyed.
      */
diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts
index 8ab7f43e2..a50cf5156 100644
--- a/src/addon/mod/data/providers/data.ts
+++ b/src/addon/mod/data/providers/data.ts
@@ -652,10 +652,11 @@ export class AddonModDataProvider {
      *
      * @param   {number}    dataId    Data ID for caching purposes.
      * @param   {number}    entryId   Entry ID.
+     * @param   {boolean}   [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down).
      * @param   {string}    [siteId]  Site ID. If not defined, current site.
      * @return  {Promise<any>}        Promise resolved when the database entry is retrieved.
      */
-    getEntry(dataId: number, entryId: number, siteId?: string): Promise<any> {
+    getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string): Promise<any> {
         return this.sitesProvider.getSite(siteId).then((site) => {
             const params = {
                     entryid: entryId,
@@ -665,6 +666,11 @@ export class AddonModDataProvider {
                     cacheKey: this.getEntryCacheKey(dataId, entryId)
                 };
 
+            if (ignoreCache) {
+                preSets['getFromCache'] = false;
+                preSets['emergencyCache'] = false;
+            }
+
             return site.read('mod_data_get_entry', params, preSets);
         });
     }
diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts
index a94879196..29d6435c4 100644
--- a/src/addon/mod/data/providers/helper.ts
+++ b/src/addon/mod/data/providers/helper.ts
@@ -354,7 +354,7 @@ export class AddonModDataHelperProvider {
     getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise<any> {
         if (entryId > 0) {
             // It's an online entry, get it from WS.
-            return this.dataProvider.getEntry(data.id, entryId, siteId);
+            return this.dataProvider.getEntry(data.id, entryId, false, siteId);
         }
 
         // It's an offline entry, search it in the offline actions.
diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts
index b39b6b204..85c54d10e 100644
--- a/src/addon/mod/data/providers/prefetch-handler.ts
+++ b/src/addon/mod/data/providers/prefetch-handler.ts
@@ -24,6 +24,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
 import { CoreCommentsProvider } from '@core/comments/providers/comments';
 import { CoreCourseProvider } from '@core/course/providers/course';
 import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
+import { CoreRatingProvider } from '@core/rating/providers/rating';
 import { AddonModDataProvider } from './data';
 import { AddonModDataHelperProvider } from './helper';
 
@@ -41,7 +42,8 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
             courseProvider: CoreCourseProvider, filepoolProvider: CoreFilepoolProvider, sitesProvider: CoreSitesProvider,
             domUtils: CoreDomUtilsProvider, protected dataProvider: AddonModDataProvider,
             protected timeUtils: CoreTimeUtilsProvider, protected dataHelper: AddonModDataHelperProvider,
-            protected groupsProvider: CoreGroupsProvider, protected commentsProvider: CoreCommentsProvider) {
+            protected groupsProvider: CoreGroupsProvider, protected commentsProvider: CoreCommentsProvider,
+            private ratingProvider: CoreRatingProvider) {
 
         super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils);
     }
@@ -282,7 +284,11 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
             });
 
             info.entries.forEach((entry) => {
-                promises.push(this.dataProvider.getEntry(database.id, entry.id, siteId));
+                promises.push(this.dataProvider.getEntry(database.id, entry.id, true, siteId).then((entry) => {
+                    return this.ratingProvider.prefetchRatings('module', module.id, database.scale, courseId, entry.ratinginfo,
+                        siteId);
+                }));
+
                 if (database.comments) {
                     promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id,
                         'database_entry', 0, siteId));
diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts
index 7cf764845..aeb16b190 100644
--- a/src/addon/mod/data/providers/sync.ts
+++ b/src/addon/mod/data/providers/sync.ts
@@ -28,6 +28,7 @@ import { TranslateService } from '@ngx-translate/core';
 import { CoreCourseProvider } from '@core/course/providers/course';
 import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
 import { CoreSyncProvider } from '@providers/sync';
+import { CoreRatingSyncProvider } from '@core/rating/providers/sync';
 
 /**
  * Service to sync databases.
@@ -43,7 +44,8 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
             private eventsProvider: CoreEventsProvider,  private dataProvider: AddonModDataProvider,
             protected translate: TranslateService, private utils: CoreUtilsProvider, courseProvider: CoreCourseProvider,
             syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, timeUtils: CoreTimeUtilsProvider,
-            private dataHelper: AddonModDataHelperProvider, private logHelper: CoreCourseLogHelperProvider) {
+            private dataHelper: AddonModDataHelperProvider, private logHelper: CoreCourseLogHelperProvider,
+            private ratingSync: CoreRatingSyncProvider) {
         super('AddonModDataSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate,
                 timeUtils);
 
@@ -77,8 +79,12 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
      * @param {Promise<any>}     Promise resolved if sync is successful, rejected if sync fails.
      */
     protected syncAllDatabasesFunc(siteId?: string): Promise<any> {
+        siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+        const promises = [];
+
         // Get all data answers pending to be sent in the site.
-        return this.dataOffline.getAllEntries(siteId).then((offlineActions) => {
+        promises.push(this.dataOffline.getAllEntries(siteId).then((offlineActions) => {
             const promises = {};
 
             // Do not sync same database twice.
@@ -101,7 +107,11 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
 
             // Promises will be an object so, convert to an array first;
             return Promise.all(this.utils.objectToArray(promises));
-        });
+        }));
+
+        promises.push(this.syncRatings(undefined, siteId));
+
+        return Promise.all(promises);
     }
 
     /**
@@ -232,7 +242,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
         entryId = entryActions[0].entryid;
 
         if (entryId > 0) {
-            timePromise = this.dataProvider.getEntry(data.id, entryId, siteId).then((entry) => {
+            timePromise = this.dataProvider.getEntry(data.id, entryId, false, siteId).then((entry) => {
                 return entry.entry.timemodified;
             }).catch(() => {
                 return -1;
@@ -343,4 +353,52 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
         });
     }
 
+    /**
+     * Synchronize offline ratings.
+     *
+     * @param {number} [cmId] Course module to be synced. If not defined, sync all databases.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
+     */
+    syncRatings(cmId?: number, siteId?: string): Promise<any> {
+        siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+         return this.ratingSync.syncRatings('mod_data', 'entry', 'module', cmId, 0, siteId).then((results) => {
+            let updated = false;
+            const warnings = [];
+            const promises = [];
+
+            results.forEach((result) => {
+                promises.push(this.dataProvider.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, siteId)
+                        .then((data) => {
+                    const promises = [];
+
+                    if (result.updated.length) {
+                        updated = true;
+
+                        // Invalidate entry of updated ratings.
+                        result.updated.forEach((itemId) => {
+                            promises.push(this.dataProvider.invalidateEntryData(data.id, itemId, siteId));
+                        });
+                    }
+
+                    if (result.warnings.length) {
+                        result.warnings.forEach((warning) => {
+                            warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
+                                component: this.componentTranslate,
+                                name: data.name,
+                                error: warning
+                            }));
+                        });
+                    }
+
+                    return this.utils.allPromises(promises);
+                }));
+            });
+
+            return Promise.all(promises).then(() => {
+                return { updated, warnings };
+            });
+        });
+    }
 }