MOBILE-3663 ratings: Implement ratings

main
Pau Ferrer Ocaña 2021-03-04 16:58:31 +01:00
parent e35e849ee4
commit 44a03b61e7
14 changed files with 1782 additions and 0 deletions

View File

@ -58,6 +58,7 @@ import { CORE_MAINMENU_SERVICES } from '@features/mainmenu/mainmenu.module';
import { CORE_PUSHNOTIFICATIONS_SERVICES } from '@features/pushnotifications/pushnotifications.module';
import { CORE_QUESTION_SERVICES } from '@features/question/question.module';
// @todo import { CORE_SHAREDFILES_SERVICES } from '@features/sharedfiles/sharedfiles.module';
import { CORE_RATING_SERVICES } from '@features/rating/rating.module';
import { CORE_SEARCH_SERVICES } from '@features/search/search.module';
import { CORE_SETTINGS_SERVICES } from '@features/settings/settings.module';
import { CORE_SITEHOME_SERVICES } from '@features/sitehome/sitehome.module';
@ -266,6 +267,7 @@ export class CoreCompileProvider {
...CORE_LOGIN_SERVICES,
...CORE_QUESTION_SERVICES,
...CORE_PUSHNOTIFICATIONS_SERVICES,
...CORE_RATING_SERVICES,
...CORE_SEARCH_SERVICES,
...CORE_SETTINGS_SERVICES,
// @todo ...CORE_SHAREDFILES_SERVICES,

View File

@ -0,0 +1,144 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { ContextLevel } from '@/core/constants';
import { Component, Input, OnChanges, OnDestroy } from '@angular/core';
import {
CoreRating,
CoreRatingInfo,
CoreRatingInfoItem,
CoreRatingProvider,
} from '@features/rating/services/rating';
import { CoreSites } from '@services/sites';
import { ModalController } from '@singletons';
import { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@singletons/events';
import { CoreRatingRatingsComponent } from '../ratings/ratings';
/**
* Component that displays the aggregation of a rating item.
*/
@Component({
selector: 'core-rating-aggregate',
templateUrl: 'core-rating-aggregate.html',
})
export class CoreRatingAggregateComponent implements OnChanges, OnDestroy {
@Input() ratingInfo!: CoreRatingInfo;
@Input() contextLevel!: ContextLevel;
@Input() instanceId!: number;
@Input() itemId!: number;
@Input() aggregateMethod!: number;
@Input() scaleId!: number;
@Input() courseId?: number;
item?: CoreRatingInfoItem;
showCount = false;
disabled = false;
labelKey = '';
protected aggregateObserver: CoreEventObserver;
protected updateSiteObserver: CoreEventObserver;
constructor() {
this.disabled = CoreRating.isRatingDisabledInSite();
// Update visibility if current site info is updated.
this.updateSiteObserver = CoreEvents.on<CoreEventSiteUpdatedData>(CoreEvents.SITE_UPDATED, () => {
this.disabled = CoreRating.isRatingDisabledInSite();
}, CoreSites.getCurrentSiteId());
// Update aggrgate when the user adds or edits a rating.
this.aggregateObserver =
CoreEvents.on(CoreRatingProvider.AGGREGATE_CHANGED_EVENT, (data) => {
if (this.item &&
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;
}
});
}
/**
* Detect changes on input properties.
*/
ngOnChanges(): void {
this.aggregateObserver && this.aggregateObserver.off();
this.item = (this.ratingInfo.ratings || []).find((rating) => rating.itemid == this.itemId);
if (!this.item) {
return;
}
switch (this.aggregateMethod) {
case CoreRatingProvider.AGGREGATE_AVERAGE:
this.labelKey = 'core.rating.aggregateavg';
break;
case CoreRatingProvider.AGGREGATE_COUNT:
this.labelKey = 'core.rating.aggregatecount';
break;
case CoreRatingProvider.AGGREGATE_MAXIMUM:
this.labelKey = 'core.rating.aggregatemax';
break;
case CoreRatingProvider.AGGREGATE_MINIMUM:
this.labelKey = 'core.rating.aggregatemin';
break;
case CoreRatingProvider.AGGREGATE_SUM:
this.labelKey = 'core.rating.aggregatesum';
break;
default:
this.labelKey = '';
return;
}
this.showCount = (this.aggregateMethod != CoreRatingProvider.AGGREGATE_COUNT);
}
/**
* Open the individual ratings page.
*/
async openRatings(): Promise<void> {
if (!this.ratingInfo.canviewall || !this.item!.count || this.disabled) {
return;
}
const modal = await ModalController.create({
component: CoreRatingRatingsComponent,
componentProps: {
contextLevel: this.contextLevel,
instanceId: this.instanceId,
ratingComponent: this.ratingInfo.component,
ratingArea: this.ratingInfo.ratingarea,
itemId: this.itemId,
scaleId: this.scaleId,
courseId: this.courseId,
},
});
await modal.present();
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.aggregateObserver.off();
this.updateSiteObserver.off();
}
}

View File

@ -0,0 +1,7 @@
<ion-item *ngIf="item && item!.canviewaggregate && labelKey && !disabled" class="ion-text-wrap"
[detail]="ratingInfo.canviewall && item!.count" (click)="openRatings()">
<ion-label>
{{ labelKey | translate }}{{ 'core.labelsep' | translate }} {{ item!.aggregatestr || '-' }}
<span *ngIf="showCount && item!.count && item!.count! > 0">({{ item!.count! }})</span>
</ion-label>
</ion-item>

View File

@ -0,0 +1,39 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreRatingAggregateComponent } from './aggregate/aggregate';
import { CoreRatingRateComponent } from './rate/rate';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreRatingRatingsComponent } from './ratings/ratings';
@NgModule({
declarations: [
CoreRatingAggregateComponent,
CoreRatingRateComponent,
CoreRatingRatingsComponent,
],
imports: [
CoreSharedModule,
],
exports: [
CoreRatingAggregateComponent,
CoreRatingRateComponent,
CoreRatingRatingsComponent,
],
entryComponents: [
CoreRatingRatingsComponent,
],
})
export class CoreRatingComponentsModule {}

View File

@ -0,0 +1,7 @@
<ion-item class="ion-text-wrap" *ngIf="item && (item!.canrate || item!.rating != null) && !disabled">
<ion-label>{{ 'core.rating.rating' | translate }}</ion-label>
<ion-select class="ion-text-start" [(ngModel)]="rating" (ngModelChange)="userRatingChanged()" interface="action-sheet"
[disabled]="!item!.canrate">
<ion-select-option *ngFor="let scaleItem of scale!.items" [value]="scaleItem.value">{{ scaleItem.name }}</ion-select-option>
</ion-select>
</ion-item>

View File

@ -0,0 +1,164 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { ContextLevel } from '@/core/constants';
import { Component, EventEmitter, Input, OnChanges, Output, OnDestroy } from '@angular/core';
import {
CoreRatingProvider,
CoreRatingInfo,
CoreRatingInfoItem,
CoreRatingScale,
CoreRating,
} from '@features/rating/services/rating';
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@singletons/events';
/**
* Component that displays the user rating select.
*/
@Component({
selector: 'core-rating-rate',
templateUrl: 'core-rating-rate.html',
})
export class CoreRatingRateComponent implements OnChanges, OnDestroy {
@Input() protected ratingInfo!: CoreRatingInfo;
@Input() protected contextLevel!: ContextLevel; // Context level: course, module, user, etc.
@Input() protected instanceId!: number; // Context instance id.
@Input() protected itemId!: number; // Item id. Example: forum post id.
@Input() protected itemSetId!: number; // Item set id. Example: forum discussion id.
@Input() protected courseId!: number;
@Input() protected aggregateMethod!: number;
@Input() protected scaleId!: number;
@Input() protected userId!: number;
@Output() protected onLoading: EventEmitter<boolean>; // Eevent that indicates whether the component is loading data.
@Output() protected onUpdate: EventEmitter<void>; // Event emitted when the rating is updated online.
item?: CoreRatingInfoItem;
scale?: CoreRatingScale;
rating?: number;
disabled = false;
protected updateSiteObserver: CoreEventObserver;
constructor() {
this.onLoading = new EventEmitter<boolean>();
this.onUpdate = new EventEmitter<void>();
this.disabled = CoreRating.isRatingDisabledInSite();
// Update visibility if current site info is updated.
this.updateSiteObserver = CoreEvents.on<CoreEventSiteUpdatedData>(CoreEvents.SITE_UPDATED, () => {
this.disabled = CoreRating.isRatingDisabledInSite();
}, CoreSites.getCurrentSiteId());
}
/**
* Detect changes on input properties.
*/
async ngOnChanges(): Promise<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 || !CoreRating.isAddRatingWSAvailable()) {
this.item = undefined;
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: Translate.instant('core.none'),
value: CoreRatingProvider.UNSET_RATING,
});
}
this.onLoading.emit(true);
try {
const rating = await CoreRatingOffline.getRating(
this.contextLevel,
this.instanceId,
this.ratingInfo.component,
this.ratingInfo.ratingarea,
this.itemId,
);
this.rating = rating.rating;
} catch {
if (this.item && this.item.rating != null) {
this.rating = this.item.rating;
} else {
this.rating = CoreRatingProvider.UNSET_RATING;
}
} finally {
this.onLoading.emit(false);
}
}
/**
* Send or save the user rating when changed.
*/
async userRatingChanged(): Promise<void> {
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
try {
const response = await CoreRating.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,
);
if (typeof response == 'undefined') {
CoreDomUtils.showToast('core.datastoredoffline', true, 3000);
} else {
this.onUpdate.emit();
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
} finally {
modal.dismiss();
}
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.updateSiteObserver.off();
}
}

View File

@ -0,0 +1,27 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ 'core.rating.ratings' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon slot="icon-only" name="fas-times"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="ratings.length > 0">
<ion-item class="ion-text-wrap" *ngFor="let rating of ratings">
<core-user-avatar [user]="rating" [courseId]="courseId" slot="start"></core-user-avatar>
<ion-label>
<h2>{{ rating.userfullname }}</h2>
<p>{{ rating.rating }}</p>
</ion-label>
<ion-note slot="end" class="ion-padding-left" *ngIf="rating.timemodified">
{{ rating.timemodified | coreDateDayOrTime }}
</ion-note>
</ion-item>
</ion-list>
<core-empty-box *ngIf="ratings.length == 0" icon="fas-star-half-alt" [message]="'core.rating.noratings' | translate"></core-empty-box>
</core-loading>
</ion-content>

View File

@ -0,0 +1,70 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { ContextLevel } from '@/core/constants';
import { Component, Input, OnInit } from '@angular/core';
import { CoreRating, CoreRatingItemRating } from '@features/rating/services/rating';
import { CoreDomUtils } from '@services/utils/dom';
import { ModalController } from '@singletons';
/**
* Modal that displays individual ratings
*/
@Component({
templateUrl: 'ratings-modal.html',
})
export class CoreRatingRatingsComponent implements OnInit {
@Input() protected contextLevel!: ContextLevel;
@Input() protected instanceId!: number;
@Input() protected ratingComponent!: string;
@Input() protected ratingArea!: string;
@Input() protected aggregateMethod!: number;
@Input() protected itemId!: number;
@Input() protected scaleId!: number;
@Input() courseId!: number;
loaded = false;
ratings: CoreRatingItemRating[] = [];
/**
* Modal loaded.
*/
async ngOnInit(): Promise<void> {
try {
this.ratings = await CoreRating.getItemRatings(
this.contextLevel,
this.instanceId,
this.ratingComponent,
this.ratingArea,
this.itemId,
this.scaleId,
undefined,
this.courseId,
);
} catch (error) {
CoreDomUtils.showErrorModal(error);
} finally {
this.loaded = true;
}
}
/**
* Close modal.
*/
closeModal(): void {
ModalController.dismiss();
}
}

View File

@ -0,0 +1,10 @@
{
"aggregateavg": "Average of ratings",
"aggregatecount": "Count of ratings",
"aggregatemax": "Maximum rating",
"aggregatemin": "Minimum rating",
"aggregatesum": "Sum of ratings",
"noratings": "No ratings submitted",
"rating": "Rating",
"ratings": "Ratings"
}

View File

@ -0,0 +1,37 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, Type } from '@angular/core';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { RATINGS_SITE_SCHEMA } from './services/database/rating';
import { CoreRatingProvider } from './services/rating';
import { CoreRatingOfflineProvider } from './services/rating-offline';
import { CoreRatingSyncProvider } from './services/rating-sync';
export const CORE_RATING_SERVICES: Type<unknown>[] = [
CoreRatingProvider,
CoreRatingSyncProvider,
CoreRatingOfflineProvider,
];
@NgModule({
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [RATINGS_SITE_SCHEMA],
multi: true,
},
],
})
export class CoreRatingModule {}

View File

@ -0,0 +1,101 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { ContextLevel } from '@/core/constants';
import { CoreSiteSchema } from '@services/sites';
/**
* Database variables for CoreRatingOffline service.
*/
export const RATINGS_TABLE = 'rating_ratings';
export const RATINGS_SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreRatingOfflineProvider',
version: 1,
tables: [
{
name: 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'],
},
],
};
/**
* Primary data to identify a stored rating.
*/
export type CoreRatingDBPrimaryData = {
component: string;
ratingarea: string;
contextlevel: ContextLevel;
instanceid: number;
itemid: number;
};
/**
* Rating stored.
*/
export type CoreRatingDBRecord = CoreRatingDBPrimaryData & {
itemsetid: number;
courseid?: number;
scaleid: number;
rating: number;
rateduserid: number;
aggregation: number;
};

View File

@ -0,0 +1,271 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { ContextLevel } from '@/core/constants';
import { Injectable } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { CoreRatingDBPrimaryData, CoreRatingDBRecord, RATINGS_TABLE } from './database/rating';
/**
* Structure of item sets.
*/
export interface CoreRatingItemSet {
component: string;
ratingArea: string;
contextLevel: ContextLevel;
instanceId: number;
itemSetId: number;
courseId?: number;
}
/**
* Service to handle offline data for rating.
*/
@Injectable( { providedIn: 'root' })
export class CoreRatingOfflineProvider {
/**
* Get an offline rating.
*
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating area. Example: "post".
* @param itemId Item id. Example: forum post id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the saved rating, rejected if not found.
*/
async getRating(
contextLevel: ContextLevel,
instanceId: number,
component: string,
ratingArea: string,
itemId: number,
siteId?: string,
): Promise<CoreRatingDBRecord> {
const site = await CoreSites.getSite(siteId);
const conditions: CoreRatingDBPrimaryData = {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
ratingarea: ratingArea,
itemid: itemId,
};
return site.getDb().getRecord(RATINGS_TABLE, conditions);
}
/**
* Add an offline rating.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemId Item id. Example: forum post id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param courseId Course id.
* @param scaleId Scale id.
* @param rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating.
* @param ratedUserId Rated user id.
* @param aggregateMethod Aggregate method.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the rating is saved.
*/
async addRating(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemId: number,
itemSetId: number,
courseId: number,
scaleId: number,
rating: number,
ratedUserId: number,
aggregateMethod: number,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
const data: CoreRatingDBRecord = {
component: component,
ratingarea: ratingArea,
contextlevel: contextLevel,
instanceid: instanceId,
itemid: itemId,
itemsetid: itemSetId,
courseid: courseId,
scaleid: scaleId,
rating: rating,
rateduserid: ratedUserId,
aggregation: aggregateMethod,
};
await site.getDb().insertRecord(RATINGS_TABLE, data);
}
/**
* Delete offline rating.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param itemId Item id. Example: forum post id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the rating is saved.
*/
async deleteRating(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemId: number,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
const conditions: CoreRatingDBPrimaryData = {
component: component,
ratingarea: ratingArea,
contextlevel: contextLevel,
instanceid: instanceId,
itemid: itemId,
};
await site.getDb().deleteRecords(RATINGS_TABLE, conditions);
}
/**
* Get the list of item sets in a component or instance.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the list of item set ids.
*/
async getItemSets(
component: string,
ratingArea: string,
contextLevel?: ContextLevel,
instanceId?: number,
itemSetId?: number,
siteId?: string,
): Promise<CoreRatingItemSet[]> {
const site = await CoreSites.getSite(siteId);
const fields = 'DISTINCT contextlevel, instanceid, itemsetid, courseid';
const conditions: Partial<CoreRatingDBRecord> = {
component,
ratingarea: ratingArea,
};
if (contextLevel && instanceId) {
conditions.contextlevel = contextLevel;
conditions.instanceid = instanceId;
}
if (itemSetId) {
conditions.itemsetid = itemSetId;
}
const records = await site.getDb().getRecords<CoreRatingDBRecord>(RATINGS_TABLE, conditions, undefined, fields);
return records.map((record) => ({
component,
ratingArea,
contextLevel: record.contextlevel,
instanceId: record.instanceid,
itemSetId: record.itemsetid,
courseId: record.courseid,
}));
}
/**
* Get offline ratings of an item set.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param itemId Item id. Example: forum post id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the list of ratings.
*/
async getRatings(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
siteId?: string,
): Promise<CoreRatingDBRecord[]> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<CoreRatingDBRecord> = {
component,
ratingarea: ratingArea,
contextlevel: contextLevel,
instanceid: instanceId,
itemsetid: itemSetId,
};
return site.getDb().getRecords(RATINGS_TABLE, conditions);
}
/**
* Return whether a component, instance or item set has offline ratings.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with a boolean.
*/
async hasRatings(
component: string,
ratingArea: string,
contextLevel?: ContextLevel,
instanceId?: number,
itemSetId?: number,
siteId?: string,
): Promise<boolean> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<CoreRatingDBRecord> = {
component,
ratingarea: ratingArea,
};
if (contextLevel && instanceId) {
conditions.contextlevel = contextLevel;
conditions.instanceid = instanceId;
}
if (itemSetId) {
conditions.itemsetid = itemSetId;
}
return CoreUtils.promiseWorks(site.getDb().recordExists(RATINGS_TABLE, conditions));
}
}
export const CoreRatingOffline = makeSingleton(CoreRatingOfflineProvider);

View File

@ -0,0 +1,315 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { ContextLevel } from '@/core/constants';
import { Injectable } from '@angular/core';
import { CoreSyncBaseProvider } from '@classes/base-sync';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { CoreRating } from './rating';
import { CoreRatingItemSet, CoreRatingOffline } from './rating-offline';
/**
* Service to sync ratings.
*/
@Injectable( { providedIn: 'root' })
export class CoreRatingSyncProvider extends CoreSyncBaseProvider<CoreRatingSyncItem> {
static readonly SYNCED_EVENT = 'core_rating_synced';
constructor() {
super('CoreRatingSyncProvider');
}
/**
* 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 component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id.
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
async syncRatings(
component: string,
ratingArea: string,
contextLevel?: ContextLevel,
instanceId?: number,
itemSetId?: number,
force?: boolean,
siteId?: string,
): Promise<CoreRatingSyncItem[]> {
siteId = siteId || CoreSites.getCurrentSiteId();
const itemSets = await CoreRatingOffline.getItemSets(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
const results: CoreRatingSyncItem[] = [];
await Promise.all(itemSets.map(async (itemSet) => {
const result = force
? await this.syncItemSet(
component,
ratingArea,
itemSet.contextLevel,
itemSet.instanceId,
itemSet.itemSetId,
siteId,
)
: await this.syncItemSetIfNeeded(
component,
ratingArea,
itemSet.contextLevel,
itemSet.instanceId,
itemSet.itemSetId,
siteId,
);
if (result) {
if (result.updated) {
// Sync successful, send event.
CoreEvents.trigger(CoreRatingSyncProvider.SYNCED_EVENT, {
...itemSet,
warnings: result.warnings,
}, siteId);
}
results.push(
{
itemSet,
...result,
},
);
}
}));
return results;
}
/**
* Sync ratings of an item set only if a certain time has passed since the last time.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when ratings are synced or if it doesn't need to be synced.
*/
protected async syncItemSetIfNeeded(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
siteId?: string,
): Promise<CoreRatingSyncItem | undefined> {
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getItemSetSyncId(component, ratingArea, contextLevel, instanceId, itemSetId);
const needed = await this.isSyncNeeded(syncId, siteId);
if (needed) {
return this.syncItemSet(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
}
}
/**
* Synchronize all offline ratings of an item set.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
protected syncItemSet(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
siteId?: string,
): Promise<CoreRatingSyncItem> {
siteId = siteId || CoreSites.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}`);
// Get offline events.
const syncPromise = this.performSyncItemSet(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
return this.addOngoingSync(syncId, syncPromise, siteId);
}
/**
* Synchronize all offline ratings of an item set.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
protected async performSyncItemSet(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
siteId: string,
): Promise<CoreRatingSyncItem> {
const result: CoreRatingSyncItem = {
updated: [],
warnings: [],
};
const ratings = await CoreRatingOffline.getRatings(component, ratingArea, contextLevel, instanceId, itemSetId, siteId);
if (!ratings.length) {
// Nothing to sync.
return result;
}
if (!CoreApp.isOnline()) {
// Cannot sync in offline.
throw new CoreNetworkError();
}
const promises = ratings.map(async (rating) => {
try {
await CoreRating.addRatingOnline(
component,
ratingArea,
rating.contextlevel,
rating.instanceid,
rating.itemid,
rating.scaleid,
rating.rating,
rating.rateduserid,
rating.aggregation,
siteId,
);
} catch (error) {
if (!CoreUtils.isWebServiceError(error)) {
// Couldn't connect to server, reject.
throw error;
}
const warning = CoreTextUtils.getErrorMessageFromError(error);
if (warning) {
result.warnings.push(warning);
}
}
result.updated.push(rating.itemid);
try {
return CoreRatingOffline.deleteRating(
component,
ratingArea,
rating.contextlevel,
rating.instanceid,
rating.itemid,
siteId,
);
} finally {
await CoreRating.invalidateRatingItems(
rating.contextlevel,
rating.instanceid,
component,
ratingArea,
rating.itemid,
rating.scaleid,
undefined,
siteId,
);
}
});
await Promise.all(promises);
// All done, return the result.
return result;
}
/**
* Get the sync id of an item set.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating Area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemSetId Item set id. Example: forum discussion id.
* @return Sync id.
*/
protected getItemSetSyncId(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemSetId: number,
): string {
return `itemSet#${component}#${ratingArea}#${contextLevel}#${instanceId}#${itemSetId}`;
}
}
export const CoreRatingSync = makeSingleton(CoreRatingSyncProvider);
declare module '@singletons/events' {
/**
* Augment CoreEventsData interface with events specific to this service.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
[CoreRatingSyncProvider.SYNCED_EVENT]: CoreRatingSyncEventData;
}
}
export type CoreRatingSyncItem = {
itemSet?: CoreRatingItemSet;
warnings: string[];
updated: number[];
};
/**
* Data passed to SYNCED_EVENT event.
*/
export type CoreRatingSyncEventData = CoreRatingItemSet & {
warnings: string[];
};

View File

@ -0,0 +1,588 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { ContextLevel } from '@/core/constants';
import { Injectable } from '@angular/core';
import { CoreSiteWSPreSets, CoreSite } from '@classes/site';
import { CoreUser } from '@features/user/services/user';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalWarning } from '@services/ws';
import { makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { CoreRatingOffline } from './rating-offline';
const ROOT_CACHE_KEY = 'CoreRating:';
/**
* Service to handle ratings.
*/
@Injectable( { providedIn: 'root' })
export class CoreRatingProvider {
static readonly AGGREGATE_NONE = 0; // No ratings.
static readonly AGGREGATE_AVERAGE = 1;
static readonly AGGREGATE_COUNT = 2;
static readonly AGGREGATE_MAXIMUM = 3;
static readonly AGGREGATE_MINIMUM = 4;
static readonly AGGREGATE_SUM = 5;
static readonly UNSET_RATING = -999;
static readonly AGGREGATE_CHANGED_EVENT = 'core_rating_aggregate_changed';
static readonly RATING_SAVED_EVENT = 'core_rating_rating_saved';
/**
* Returns whether the web serivce to add ratings is available.
*
* @return If WS is abalaible.
* @since 3.2
*/
isAddRatingWSAvailable(): boolean {
return CoreSites.wsAvailableInCurrentSite('core_rating_add_rating');
}
/**
* Add a rating to an item.
*
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemId Item id. Example: forum post id.
* @param itemSetId Item set id. Example: forum discussion id.
* @param courseId Course id.
* @param scaleId Scale id.
* @param rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating.
* @param ratedUserId Rated user id.
* @param aggregateMethod Aggregate method.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the aggregated rating or void if stored offline.
* @since 3.2
*/
async addRating(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemId: number,
itemSetId: number,
courseId: number,
scaleId: number,
rating: number,
ratedUserId: number,
aggregateMethod: number,
siteId?: string,
): Promise<CoreRatingAddRatingWSResponse | void> {
siteId = siteId || CoreSites.getCurrentSiteId();
// Convenience function to store a rating to be synchronized later.
const storeOffline = async (): Promise<void> => {
await CoreRatingOffline.addRating(
component,
ratingArea,
contextLevel,
instanceId,
itemId,
itemSetId,
courseId,
scaleId,
rating,
ratedUserId,
aggregateMethod,
siteId,
);
CoreEvents.trigger(CoreRatingProvider.RATING_SAVED_EVENT, {
component,
ratingArea,
contextLevel,
instanceId,
itemSetId,
itemId,
}, siteId);
};
if (!CoreApp.isOnline()) {
// App is offline, store the action.
return storeOffline();
}
try {
await CoreRatingOffline.deleteRating(component, ratingArea, contextLevel, instanceId, itemId, siteId);
this.addRatingOnline(
component,
ratingArea,
contextLevel,
instanceId,
itemId,
scaleId,
rating,
ratedUserId,
aggregateMethod,
siteId,
);
} catch (error) {
if (CoreUtils.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 component Component. Example: "mod_forum".
* @param ratingArea Rating area. Example: "post".
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param itemId Item id. Example: forum post id.
* @param scaleId Scale id.
* @param rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating.
* @param ratedUserId Rated user id.
* @param aggregateMethod Aggregate method.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the aggregated rating.
* @since 3.2
*/
async addRatingOnline(
component: string,
ratingArea: string,
contextLevel: ContextLevel,
instanceId: number,
itemId: number,
scaleId: number,
rating: number,
ratedUserId: number,
aggregateMethod: number,
siteId?: string,
): Promise<CoreRatingAddRatingWSResponse> {
const site = await CoreSites.getSite(siteId);
const params: CoreRatingAddRatingWSParams = {
contextlevel: contextLevel,
instanceid: instanceId,
component,
ratingarea: ratingArea,
itemid: itemId,
scaleid: scaleId,
rating,
rateduserid: ratedUserId,
aggregation: aggregateMethod,
};
const response = await site.write<CoreRatingAddRatingWSResponse>('core_rating_add_rating', params);
await this.invalidateRatingItems(contextLevel, instanceId, component, ratingArea, itemId, scaleId);
CoreEvents.trigger(CoreRatingProvider.AGGREGATE_CHANGED_EVENT, {
contextLevel,
instanceId,
component,
ratingArea,
itemId,
aggregate: response.aggregate,
count: response.count,
});
return response;
}
/**
* Get item ratings.
*
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating area. Example: "post".
* @param itemId Item id. Example: forum post id.
* @param scaleId Scale id.
* @param sort Sort field.
* @param courseId Course id. Used for fetching user profiles.
* @param siteId Site ID. If not defined, current site.
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
* @return Promise resolved with the list of ratings.
*/
async getItemRatings(
contextLevel: ContextLevel,
instanceId: number,
component: string,
ratingArea: string,
itemId: number,
scaleId: number,
sort: string = 'timemodified',
courseId?: number,
siteId?: string,
ignoreCache: boolean = false,
): Promise<CoreRatingItemRating[]> {
const site = await CoreSites.getSite(siteId);
const params: CoreRatingGetItemRatingsWSParams = {
contextlevel: contextLevel,
instanceid: instanceId,
component,
ratingarea: ratingArea,
itemid: itemId,
scaleid: scaleId,
sort,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getItemRatingsCacheKey(contextLevel, instanceId, component, ratingArea, itemId, scaleId, sort),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
const response = await site.read<CoreRatingGetItemRatingsWSResponse>('core_rating_get_item_ratings', params, preSets);
if (!site.isVersionGreaterEqualThan([' 3.6.5', '3.7.1', '3.8'])) {
// MDL-65042 We need to fetch profiles because the returned profile pictures are incorrect.
const promises = response.ratings.map((rating: CoreRatingItemRating) =>
CoreUser.getProfile(rating.userid, courseId, true, site.id).then((user) => {
rating.userpictureurl = user.profileimageurl || '';
return;
}).catch(() => {
// Ignore error.
rating.userpictureurl = '';
}));
await Promise.all(promises);
}
return response.ratings;
}
/**
* Invalidate item ratings.
*
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Context instance id.
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating area. Example: "post".
* @param itemId Item id. Example: forum post id.
* @param scaleId Scale id.
* @param sort Sort field.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateRatingItems(
contextLevel: ContextLevel,
instanceId: number,
component: string,
ratingArea: string,
itemId: number,
scaleId: number,
sort: string = 'timemodified',
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
const key = this.getItemRatingsCacheKey(contextLevel, instanceId, component, ratingArea, itemId, scaleId, sort);
await site.invalidateWsCacheForKey(key);
}
/**
* Check if rating is disabled in a certain site.
*
* @param site Site. If not defined, use current site.
* @return Whether it's disabled.
*/
isRatingDisabledInSite(site?: CoreSite): boolean {
site = site || CoreSites.getCurrentSite();
return !!site?.isFeatureDisabled('NoDelegate_CoreRating');
}
/**
* Check if rating is disabled in a certain site.
*
* @param siteId Site Id. If not defined, use current site.
* @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
async isRatingDisabled(siteId?: string): Promise<boolean> {
const site = await CoreSites.getSite(siteId);
return this.isRatingDisabledInSite(site);
}
/**
* Convenience function to merge two or more rating infos of the same instance.
*
* @param ratingInfos Array of rating infos.
* @return Merged rating info or undefined.
*/
mergeRatingInfos(ratingInfos: CoreRatingInfo[]): CoreRatingInfo | undefined {
let result: CoreRatingInfo | undefined;
const scales: Record<number, CoreRatingScale> = {};
const ratings: Record<number, CoreRatingInfoItem> = {};
ratingInfos.forEach((ratingInfo) => {
if (!ratingInfo) {
// Skip null rating infos.
return;
}
if (!result) {
result = Object.assign({}, ratingInfo);
}
(ratingInfo.scales || []).forEach((scale) => {
scales[scale.id] = scale;
});
(ratingInfo.ratings || []).forEach((rating) => {
ratings[rating.itemid] = rating;
});
});
if (result) {
result.scales = CoreUtils.objectToArray(scales);
result.ratings = CoreUtils.objectToArray(ratings);
}
return result;
}
/**
* Prefetch individual ratings.
*
* This function should be called from the prefetch handler of activities with ratings.
*
* @param contextLevel Context level: course, module, user, etc.
* @param instanceId Instance id.
* @param siteId Site id. If not defined, current site.
* @param courseId Course id. Used for prefetching user profiles.
* @param ratingInfo Rating info returned by web services.
* @return Promise resolved when done.
*/
async prefetchRatings(
contextLevel: ContextLevel,
instanceId: number,
scaleId: number,
courseId?: number,
ratingInfo?: CoreRatingInfo,
siteId?: string,
): Promise<void> {
if (!ratingInfo || !ratingInfo.ratings) {
return;
}
const site = await CoreSites.getSite(siteId);
const promises = ratingInfo.ratings.map((item) => this.getItemRatings(
contextLevel,
instanceId,
ratingInfo.component,
ratingInfo.ratingarea,
item.itemid,
scaleId,
undefined,
courseId,
site.id,
true,
));
if (!site.isVersionGreaterEqualThan([' 3.6.5', '3.7.1', '3.8'])) {
promises.map((promise) => promise.then(async (ratings) => {
const userIds = ratings.map((rating: CoreRatingItemRating) => rating.userid);
await CoreUser.prefetchProfiles(userIds, courseId, site.id);
return;
}));
}
await Promise.all(promises);
}
/**
* Get cache key for rating items WS calls.
*
* @param contextLevel Context level: course, module, user, etc.
* @param component Component. Example: "mod_forum".
* @param ratingArea Rating area. Example: "post".
* @param itemId Item id. Example: forum post id.
* @param scaleId Scale id.
* @param sort Sort field.
* @return Cache key.
*/
protected getItemRatingsCacheKey(
contextLevel: ContextLevel,
instanceId: number,
component: string,
ratingArea: string,
itemId: number,
scaleId: number,
sort: string,
): string {
return `${ROOT_CACHE_KEY}${contextLevel}:${instanceId}:${component}:${ratingArea}:${itemId}:${scaleId}:${sort}`;
}
}
export const CoreRating = makeSingleton(CoreRatingProvider);
declare module '@singletons/events' {
/**
* Augment CoreEventsData interface with events specific to this service.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
[CoreRatingProvider.AGGREGATE_CHANGED_EVENT]: CoreRatingAggregateChangedEventData;
[CoreRatingProvider.RATING_SAVED_EVENT]: CoreRatingSavedEventData;
}
}
/**
* Structure of the rating info returned by web services.
*/
export type CoreRatingInfo = {
contextid: number; // Context id.
component: string; // Context name.
ratingarea: string; // Rating area name.
canviewall?: boolean; // Whether the user can view all the individual ratings.
canviewany?: boolean; // Whether the user can view aggregate of ratings of others.
scales?: CoreRatingScale[]; // Different scales used information.
ratings?: CoreRatingInfoItem[]; // The ratings.
};
/**
* Structure of scales in the rating info.
*/
export type CoreRatingScale = {
id: number; // Scale id.
courseid?: number; // Course id.
name?: string; // Scale name (when a real scale is used).
max: number; // Max value for the scale.
isnumeric: boolean; // Whether is a numeric scale.
items?: { // Scale items. Only returned for not numerical scales.
value: number; // Scale value/option id.
name: string; // Scale name.
}[];
};
/**
* Structure of items in the rating info.
*/
export type CoreRatingInfoItem = {
itemid: number; // Item id.
scaleid?: number; // Scale id.
scale?: CoreRatingScale; // Added for rendering purposes.
userid?: number; // User who rated id.
aggregate?: number; // Aggregated ratings grade.
aggregatestr?: string; // Aggregated ratings as string.
aggregatelabel?: string; // The aggregation label.
count?: number; // Ratings count (used when aggregating).
rating?: number; // The rating the user gave.
canrate?: boolean; // Whether the user can rate the item.
canviewaggregate?: boolean; // Whether the user can view the aggregated grade.
};
/**
* Structure of a rating returned by the item ratings web service.
*/
export type CoreRatingItemRating = {
id: number; // Rating id.
userid: number; // User id.
userpictureurl: string; // URL user picture.
userfullname: string; // User fullname.
rating: string; // Rating on scale.
timemodified: number; // Time modified (timestamp).
};
/**
* Params of core_rating_get_item_ratings WS.
*/
type CoreRatingGetItemRatingsWSParams = {
contextlevel: ContextLevel; // Context level: course, module, user, etc...
instanceid: number; // The instance id of item associated with the context level.
component: string; // Component.
ratingarea: string; // Rating area.
itemid: number; // Associated id.
scaleid: number; // Scale id.
sort: string; // Sort order (firstname, rating or timemodified).
};
/**
* Data returned by core_rating_get_item_ratings WS.
*/
export type CoreRatingGetItemRatingsWSResponse = {
ratings: CoreRatingItemRating[];
warnings?: CoreWSExternalWarning[];
};
/**
* Params of core_rating_add_rating WS.
*/
type CoreRatingAddRatingWSParams = {
contextlevel: ContextLevel; // Context level: course, module, user, etc...
instanceid: number; // The instance id of item associated with the context level.
component: string; // Component.
ratingarea: string; // Rating area.
itemid: number; // Associated id.
scaleid: number; // Scale id.
rating: number; // User rating.
rateduserid: number; // Rated user id.
aggregation?: number; // Agreggation method.
};
/**
* Data returned by core_rating_add_rating WS.
*/
export type CoreRatingAddRatingWSResponse = {
success: boolean; // Whether the rate was successfully created.
aggregate?: string; // New aggregate.
count?: number; // Ratings count.
itemid?: number; // Rating item id.
warnings?: CoreWSExternalWarning[];
};
/**
* Data sent by AGGREGATE_CHANGED_EVENT event.
*/
export type CoreRatingAggregateChangedEventData = {
contextLevel: ContextLevel;
instanceId: number;
component: string;
ratingArea: string;
itemId: number;
aggregate?: string;
count?: number;
};
/**
* Data sent by RATING_SAVED_EVENT event.
*/
export type CoreRatingSavedEventData = {
component: string;
ratingArea: string;
contextLevel: ContextLevel;
instanceId: number;
itemSetId: number;
itemId: number;
};