MOBILE-2486 glossary: Ratings

main
Albert Gasset 2019-03-05 11:50:29 +01:00
parent 8fe3e09e9b
commit af6694c915
11 changed files with 155 additions and 30 deletions

View File

@ -294,7 +294,7 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider {
const promises = [];
results.forEach((result) => {
if (result.updated) {
if (result.updated.length) {
updated = true;
// Invalidate discussions of updated ratings.

View File

@ -25,7 +25,7 @@
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
<!-- Glossary entries found to be synchronized -->
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline">
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline || hasOfflineRatings">
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
</ion-card>

View File

@ -16,6 +16,9 @@ import { Component, Injector, ViewChild } from '@angular/core';
import { Content, PopoverController } from 'ionic-angular';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { CoreRatingProvider } from '@core/rating/providers/rating';
import { CoreRatingOfflineProvider } from '@core/rating/providers/offline';
import { CoreRatingSyncProvider } from '@core/rating/providers/sync';
import { AddonModGlossaryProvider } from '../../providers/glossary';
import { AddonModGlossaryOfflineProvider } from '../../providers/offline';
import { AddonModGlossarySyncProvider } from '../../providers/sync';
@ -57,11 +60,16 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
protected getDivider: (entry: any) => string;
protected addEntryObserver: any;
hasOfflineRatings: boolean;
protected ratingOfflineObserver: any;
protected ratingSyncObserver: any;
constructor(injector: Injector,
private popoverCtrl: PopoverController,
private glossaryProvider: AddonModGlossaryProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider,
private glossarySync: AddonModGlossarySyncProvider) {
private glossarySync: AddonModGlossarySyncProvider,
private ratingOffline: CoreRatingOfflineProvider) {
super(injector);
}
@ -74,6 +82,20 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
// When an entry is added, we reload the data.
this.addEntryObserver = this.eventsProvider.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, this.eventReceived.bind(this));
// Listen for offline ratings saved and synced.
this.ratingOfflineObserver = this.eventsProvider.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
&& data.instanceId == this.glossary.coursemodule) {
this.hasOfflineRatings = true;
}
});
this.ratingSyncObserver = this.eventsProvider.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
&& data.instanceId == this.glossary.coursemodule) {
this.hasOfflineRatings = false;
}
});
this.loadContent(false, true).then(() => {
if (!this.glossary) {
return;
@ -118,15 +140,23 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
return this.syncActivity(showErrors);
}
}).then(() => {
const promises = [];
return this.fetchEntries().then(() => {
promises.push(this.fetchEntries().then(() => {
// Check if there are responses stored in offline.
return this.glossaryOffline.getGlossaryNewEntries(this.glossary.id).then((offlineEntries) => {
offlineEntries.sort((a, b) => a.concept.localeCompare(b.fullname));
this.hasOffline = !!offlineEntries.length;
this.offlineEntries = offlineEntries || [];
});
});
}));
promises.push(this.ratingOffline.hasRatings('mod_glossary', 'entry', 'module', this.glossary.coursemodule)
.then((hasRatings) => {
this.hasOfflineRatings = hasRatings;
}));
return Promise.all(promises);
}).then(() => {
// All data obtained, now fill the context menu.
this.fillContextMenu(refresh);
@ -189,7 +219,17 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
* @return {Promise<any>} Promise resolved when done.
*/
protected sync(): Promise<boolean> {
return this.glossarySync.syncGlossaryEntries(this.glossary.id);
const promises = [
this.glossarySync.syncGlossaryEntries(this.glossary.id),
this.glossarySync.syncRatings(this.glossary.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});
});
}
/**
@ -345,7 +385,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
openEntry(entryId: number): void {
const params = {
courseId: this.courseId,
entryId: entryId,
entryId: entryId
};
this.splitviewCtrl.push('AddonModGlossaryEntryPage', params);
this.selectedEntry = entryId;
@ -400,5 +440,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
super.ngOnDestroy();
this.addEntryObserver && this.addEntryObserver.off();
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
this.ratingSyncObserver && this.ratingSyncObserver.off();
}
}

View File

@ -31,6 +31,8 @@
<ion-item text-wrap *ngIf="entry.approved != 1">
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
</ion-item>
<core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.courseid" [aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()"></core-rating-rate>
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.courseid" [aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale"></core-rating-aggregate>
</ng-container>
<ion-card *ngIf="!entry">

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreRatingComponentsModule } from '@core/rating/components/components.module';
import { AddonModGlossaryEntryPage } from './entry';
@NgModule({
@ -29,7 +30,8 @@ import { AddonModGlossaryEntryPage } from './entry';
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonModGlossaryEntryPage),
TranslateModule.forChild()
TranslateModule.forChild(),
CoreRatingComponentsModule
],
})
export class AddonModForumDiscussionPageModule {}

View File

@ -15,6 +15,7 @@
import { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreRatingInfo } from '@core/rating/providers/rating';
import { AddonModGlossaryProvider } from '../../providers/glossary';
/**
@ -29,9 +30,11 @@ export class AddonModGlossaryEntryPage {
component = AddonModGlossaryProvider.COMPONENT;
componentId: number;
entry: any;
glossary: any;
loaded = false;
showAuthor = false;
showDate = false;
ratingInfo: CoreRatingInfo;
protected courseId: number;
protected entryId: number;
@ -80,11 +83,13 @@ export class AddonModGlossaryEntryPage {
*/
protected fetchEntry(refresh?: boolean): Promise<any> {
return this.glossaryProvider.getEntry(this.entryId).then((result) => {
this.entry = result;
this.entry = result.entry;
this.ratingInfo = result.ratinginfo;
if (!refresh) {
// Load the glossary.
return this.glossaryProvider.getGlossaryById(this.courseId, this.entry.glossaryid).then((glossary) => {
this.glossary = glossary;
this.componentId = glossary.coursemodule;
switch (glossary.displayformat) {
@ -109,4 +114,11 @@ export class AddonModGlossaryEntryPage {
return Promise.reject(null);
});
}
/**
* Function called when rating is updated online.
*/
ratingUpdated(): void {
this.glossaryProvider.invalidateEntry(this.entryId);
}
}

View File

@ -61,8 +61,8 @@ export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBas
this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
return Promise.reject(null);
}).then((entry) => {
return this.courseHelper.getModuleCourseIdByInstance(entry.glossaryid, 'glossary', siteId);
}).then((response) => {
return this.courseHelper.getModuleCourseIdByInstance(response.entry.glossaryid, 'glossary', siteId);
});
}

View File

@ -21,6 +21,7 @@ import { CoreSitesProvider } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { CoreRatingInfo } from '@core/rating/providers/rating';
import { AddonModGlossaryOfflineProvider } from './offline';
/**
@ -489,7 +490,7 @@ export class AddonModGlossaryProvider {
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the entry.
*/
getEntry(entryId: number, siteId?: string): Promise<any> {
getEntry(entryId: number, siteId?: string): Promise<{entry: any, ratinginfo: CoreRatingInfo}> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: entryId
@ -500,7 +501,7 @@ export class AddonModGlossaryProvider {
return site.read('mod_glossary_get_entry_by_id', params, preSets).then((response) => {
if (response && response.entry) {
return response.entry;
return response;
} else {
return Promise.reject(null);
}
@ -615,31 +616,35 @@ export class AddonModGlossaryProvider {
*
* @param {any} glossary The glossary object.
* @param {boolean} [onlyEntriesList] If true, entries won't be invalidated.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when data is invalidated.
*/
invalidateGlossaryEntries(glossary: any, onlyEntriesList?: boolean): Promise<any> {
invalidateGlossaryEntries(glossary: any, onlyEntriesList?: boolean, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const promises = [];
if (!onlyEntriesList) {
promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true).then((entries) => {
return this.invalidateEntries(entries);
promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true, siteId).then((entries) => {
return this.invalidateEntries(entries, siteId);
}));
}
glossary.browsemodes.forEach((mode) => {
switch (mode) {
case 'letter':
promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL'));
promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL', siteId));
break;
case 'cat':
promises.push(this.invalidateEntriesByCategory(glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES));
promises.push(this.invalidateEntriesByCategory(glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES,
siteId));
break;
case 'date':
promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC'));
promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC'));
promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC', siteId));
promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC', siteId));
break;
case 'author':
promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC'));
promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC', siteId));
break;
default:
}

View File

@ -23,6 +23,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonModGlossaryProvider } from './glossary';
import { CoreRatingProvider } from '@core/rating/providers/rating';
/**
* Handler to prefetch forums.
@ -42,6 +43,7 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
sitesProvider: CoreSitesProvider,
domUtils: CoreDomUtilsProvider,
private userProvider: CoreUserProvider,
private ratingProvider: CoreRatingProvider,
private glossaryProvider: AddonModGlossaryProvider) {
super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils);
@ -164,7 +166,11 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
// Fetch user avatars.
entries.forEach((entry) => {
// Fetch individual entries.
promises.push(this.glossaryProvider.getEntry(entry.id, siteId));
promises.push(this.glossaryProvider.getEntry(entry.id, siteId).then((entry) => {
// Fetch individual ratings.
return this.ratingProvider.prefetchRatings('module', module.id, glossary.scale, courseId, entry.ratinginfo,
siteId);
}));
userIds.push(entry.userid);
});
@ -177,6 +183,8 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
return Promise.all(promises);
}));
return Promise.all(promises);
});
}
}

View File

@ -29,6 +29,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { AddonModGlossaryProvider } from './glossary';
import { AddonModGlossaryHelperProvider } from './helper';
import { AddonModGlossaryOfflineProvider } from './offline';
import { CoreRatingSyncProvider } from '@core/rating/providers/sync';
/**
* Service to sync glossaries.
@ -54,7 +55,8 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider {
private glossaryProvider: AddonModGlossaryProvider,
private glossaryHelper: AddonModGlossaryHelperProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider,
private logHelper: CoreCourseLogHelperProvider) {
private logHelper: CoreCourseLogHelperProvider,
private ratingSync: CoreRatingSyncProvider) {
super('AddonModGlossarySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate,
timeUtils);
@ -79,8 +81,12 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider {
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
protected syncAllGlossariesFunc(siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const promises = [];
// Sync all new entries
return this.glossaryOffline.getAllNewEntries(siteId).then((entries) => {
promises.push(this.glossaryOffline.getAllNewEntries(siteId).then((entries) => {
const promises = {};
// Do not sync same glossary twice.
@ -106,7 +112,11 @@ export class AddonModGlossarySyncProvider 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);
}
/**
@ -241,6 +251,50 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider {
return this.addOngoingSync(syncId, syncPromise, siteId);
}
/**
* Synchronize offline ratings.
*
* @param {number} [cmId] Course module to be synced. If not defined, sync all glossaries.
* @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_glossary', 'entry', 'module', cmId, 0, siteId).then((results) => {
let updated = false;
const warnings = [];
const promises = [];
results.forEach((result) => {
if (result.updated.length) {
updated = true;
// Invalidate entry of updated ratings.
result.updated.forEach((itemId) => {
promises.push(this.glossaryProvider.invalidateEntry(itemId, siteId));
});
}
if (result.warnings.length) {
promises.push(this.glossaryProvider.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, siteId)
.then((glossary) => {
result.warnings.forEach((warning) => {
warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: glossary.name,
error: warning
}));
});
}));
}
});
return this.utils.allPromises(promises).then(() => {
return { updated, warnings };
});
});
}
/**
* Delete a new entry.
*

View File

@ -63,7 +63,7 @@ export class CoreRatingSyncProvider extends CoreSyncBaseProvider {
* @return {Promise<any>} 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?: string): Promise<{itemSet: CoreRatingItemSet, updated: number[], warnings: string[]}[]> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.ratingOffline.getItemSets(component, ratingArea, contextLevel, instanceId, itemSetId, siteId)
@ -102,7 +102,7 @@ export class CoreRatingSyncProvider extends CoreSyncBaseProvider {
* @return {Promise<any>} 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[]}> {
itemSetId: number, siteId?: string): Promise<{updated: number[], warnings: string[]}> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const syncId = this.getItemSetSyncId(component, ratingArea, contextLevel, instanceId, itemSetId);
@ -126,7 +126,7 @@ export class CoreRatingSyncProvider extends CoreSyncBaseProvider {
* @return {Promise<any>} 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?: string): Promise<{updated: number[], warnings: string[]}> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
@ -139,7 +139,7 @@ export class CoreRatingSyncProvider extends CoreSyncBaseProvider {
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 updated = [];
const warnings = [];
return this.ratingOffline.getRatings(component, ratingArea, contextLevel, instanceId, itemSetId, siteId).then((ratings) => {
@ -162,7 +162,7 @@ export class CoreRatingSyncProvider extends CoreSyncBaseProvider {
return Promise.reject(error);
}
}).then(() => {
updated = true;
updated.push(rating.itemid);
return this.ratingOffline.deleteRating(component, ratingArea, rating.contextlevel, rating.instanceid,
rating.itemid, siteId).finally(() => {