Merge pull request #1793 from albertgasset/MOBILE-2485

MOBILE-2485 data: Ratings
main
Juan Leyva 2019-03-06 16:35:05 +01:00 committed by GitHub
commit 62e392eab1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 137 additions and 12 deletions

View File

@ -22,7 +22,7 @@
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description> <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
<!-- Data done in offline but not synchronized --> <!-- 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> <ion-icon name="warning"></ion-icon>
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }} {{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
</div> </div>

View File

@ -19,6 +19,9 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { CoreCommentsProvider } from '@core/comments/providers/comments'; 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 { AddonModDataProvider } from '../../providers/data';
import { AddonModDataHelperProvider } from '../../providers/helper'; import { AddonModDataHelperProvider } from '../../providers/helper';
import { AddonModDataOfflineProvider } from '../../providers/offline'; import { AddonModDataOfflineProvider } from '../../providers/offline';
@ -74,11 +77,16 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
protected hasComments = false; protected hasComments = false;
protected fieldsArray: any; protected fieldsArray: any;
hasOfflineRatings: boolean;
protected ratingOfflineObserver: any;
protected ratingSyncObserver: any;
constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider, constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider,
private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content, private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content,
private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider, private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider,
private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, 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); super(injector, content);
// Refresh entries on change. // Refresh entries on change.
@ -89,7 +97,22 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
return this.loadContent(true); return this.loadContent(true);
} }
}, this.siteId); }, 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. * 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. * @return {Promise<any>} Promise resolved when done.
*/ */
protected sync(): Promise<any> { 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 { ngOnDestroy(): void {
super.ngOnDestroy(); super.ngOnDestroy();
this.entryChangedObserver && this.entryChangedObserver.off(); this.entryChangedObserver && this.entryChangedObserver.off();
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
this.ratingSyncObserver && this.ratingSyncObserver.off();
} }
} }

View File

@ -30,6 +30,9 @@
<core-compile-html [text]="entryRendered" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html> <core-compile-html [text]="entryRendered" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</div> </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"> <ion-item *ngIf="data && entry">
<core-comments contextLevel="module" [instanceId]="data.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry"></core-comments> <core-comments contextLevel="module" [instanceId]="data.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry"></core-comments>
</ion-item> </ion-item>

View File

@ -19,6 +19,7 @@ import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.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 { AddonModDataComponentsModule } from '../../components/components.module';
import { AddonModDataEntryPage } from './entry'; import { AddonModDataEntryPage } from './entry';
@ -33,7 +34,8 @@ import { AddonModDataEntryPage } from './entry';
CoreCompileHtmlComponentModule, CoreCompileHtmlComponentModule,
CoreCommentsComponentsModule, CoreCommentsComponentsModule,
IonicPageModule.forChild(AddonModDataEntryPage), IonicPageModule.forChild(AddonModDataEntryPage),
TranslateModule.forChild() TranslateModule.forChild(),
CoreRatingComponentsModule
], ],
}) })
export class AddonModDataEntryPageModule {} export class AddonModDataEntryPageModule {}

View File

@ -20,6 +20,7 @@ import { CoreSitesProvider } from '@providers/sites';
import { CoreGroupsProvider } from '@providers/groups'; import { CoreGroupsProvider } from '@providers/groups';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreRatingInfo } from '@core/rating/providers/rating';
import { AddonModDataProvider } from '../../providers/data'; import { AddonModDataProvider } from '../../providers/data';
import { AddonModDataHelperProvider } from '../../providers/helper'; import { AddonModDataHelperProvider } from '../../providers/helper';
import { AddonModDataOfflineProvider } from '../../providers/offline'; import { AddonModDataOfflineProvider } from '../../providers/offline';
@ -66,6 +67,7 @@ export class AddonModDataEntryPage implements OnDestroy {
cssClass = ''; cssClass = '';
extraImports = [AddonModDataComponentsModule]; extraImports = [AddonModDataComponentsModule];
jsData; jsData;
ratingInfo: CoreRatingInfo;
constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, 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); return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions);
}); });
}).then((entry) => { }).then((entry) => {
this.ratingInfo = entry.ratinginfo;
entry = entry.entry; entry = entry.entry;
this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass); this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass);
// Index contents by fieldid. // 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. * Component being destroyed.
*/ */

View File

@ -652,10 +652,11 @@ export class AddonModDataProvider {
* *
* @param {number} dataId Data ID for caching purposes. * @param {number} dataId Data ID for caching purposes.
* @param {number} entryId Entry ID. * @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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the database entry is retrieved. * @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) => { return this.sitesProvider.getSite(siteId).then((site) => {
const params = { const params = {
entryid: entryId, entryid: entryId,
@ -665,6 +666,11 @@ export class AddonModDataProvider {
cacheKey: this.getEntryCacheKey(dataId, entryId) cacheKey: this.getEntryCacheKey(dataId, entryId)
}; };
if (ignoreCache) {
preSets['getFromCache'] = false;
preSets['emergencyCache'] = false;
}
return site.read('mod_data_get_entry', params, preSets); return site.read('mod_data_get_entry', params, preSets);
}); });
} }

View File

@ -354,7 +354,7 @@ export class AddonModDataHelperProvider {
getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise<any> { getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise<any> {
if (entryId > 0) { if (entryId > 0) {
// It's an online entry, get it from WS. // 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. // It's an offline entry, search it in the offline actions.

View File

@ -24,6 +24,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreCommentsProvider } from '@core/comments/providers/comments'; import { CoreCommentsProvider } from '@core/comments/providers/comments';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
import { CoreRatingProvider } from '@core/rating/providers/rating';
import { AddonModDataProvider } from './data'; import { AddonModDataProvider } from './data';
import { AddonModDataHelperProvider } from './helper'; import { AddonModDataHelperProvider } from './helper';
@ -41,7 +42,8 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
courseProvider: CoreCourseProvider, filepoolProvider: CoreFilepoolProvider, sitesProvider: CoreSitesProvider, courseProvider: CoreCourseProvider, filepoolProvider: CoreFilepoolProvider, sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider, protected dataProvider: AddonModDataProvider, domUtils: CoreDomUtilsProvider, protected dataProvider: AddonModDataProvider,
protected timeUtils: CoreTimeUtilsProvider, protected dataHelper: AddonModDataHelperProvider, 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); super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils);
} }
@ -282,7 +284,11 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl
}); });
info.entries.forEach((entry) => { 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) { if (database.comments) {
promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id, promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id,
'database_entry', 0, siteId)); 'database_entry', 0, siteId));

View File

@ -28,6 +28,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { CoreSyncProvider } from '@providers/sync'; import { CoreSyncProvider } from '@providers/sync';
import { CoreRatingSyncProvider } from '@core/rating/providers/sync';
/** /**
* Service to sync databases. * Service to sync databases.
@ -43,7 +44,8 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
private eventsProvider: CoreEventsProvider, private dataProvider: AddonModDataProvider, private eventsProvider: CoreEventsProvider, private dataProvider: AddonModDataProvider,
protected translate: TranslateService, private utils: CoreUtilsProvider, courseProvider: CoreCourseProvider, protected translate: TranslateService, private utils: CoreUtilsProvider, courseProvider: CoreCourseProvider,
syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, timeUtils: CoreTimeUtilsProvider, 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, super('AddonModDataSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate,
timeUtils); timeUtils);
@ -77,8 +79,12 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider {
* @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails. * @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/ */
protected syncAllDatabasesFunc(siteId?: string): Promise<any> { protected syncAllDatabasesFunc(siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const promises = [];
// Get all data answers pending to be sent in the site. // 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 = {}; const promises = {};
// Do not sync same database twice. // 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; // Promises will be an object so, convert to an array first;
return Promise.all(this.utils.objectToArray(promises)); 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; entryId = entryActions[0].entryid;
if (entryId > 0) { 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; return entry.entry.timemodified;
}).catch(() => { }).catch(() => {
return -1; 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 };
});
});
}
} }