MOBILE-3663 ratings: Implement ratings
parent
e35e849ee4
commit
44a03b61e7
|
@ -58,6 +58,7 @@ import { CORE_MAINMENU_SERVICES } from '@features/mainmenu/mainmenu.module';
|
||||||
import { CORE_PUSHNOTIFICATIONS_SERVICES } from '@features/pushnotifications/pushnotifications.module';
|
import { CORE_PUSHNOTIFICATIONS_SERVICES } from '@features/pushnotifications/pushnotifications.module';
|
||||||
import { CORE_QUESTION_SERVICES } from '@features/question/question.module';
|
import { CORE_QUESTION_SERVICES } from '@features/question/question.module';
|
||||||
// @todo import { CORE_SHAREDFILES_SERVICES } from '@features/sharedfiles/sharedfiles.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_SEARCH_SERVICES } from '@features/search/search.module';
|
||||||
import { CORE_SETTINGS_SERVICES } from '@features/settings/settings.module';
|
import { CORE_SETTINGS_SERVICES } from '@features/settings/settings.module';
|
||||||
import { CORE_SITEHOME_SERVICES } from '@features/sitehome/sitehome.module';
|
import { CORE_SITEHOME_SERVICES } from '@features/sitehome/sitehome.module';
|
||||||
|
@ -266,6 +267,7 @@ export class CoreCompileProvider {
|
||||||
...CORE_LOGIN_SERVICES,
|
...CORE_LOGIN_SERVICES,
|
||||||
...CORE_QUESTION_SERVICES,
|
...CORE_QUESTION_SERVICES,
|
||||||
...CORE_PUSHNOTIFICATIONS_SERVICES,
|
...CORE_PUSHNOTIFICATIONS_SERVICES,
|
||||||
|
...CORE_RATING_SERVICES,
|
||||||
...CORE_SEARCH_SERVICES,
|
...CORE_SEARCH_SERVICES,
|
||||||
...CORE_SETTINGS_SERVICES,
|
...CORE_SETTINGS_SERVICES,
|
||||||
// @todo ...CORE_SHAREDFILES_SERVICES,
|
// @todo ...CORE_SHAREDFILES_SERVICES,
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
|
@ -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[];
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
Loading…
Reference in New Issue