commit
b52cc6477b
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.messages.groupinfo' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ plugin.name }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.mod_book.toc' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
|
|
|
@ -18,6 +18,7 @@ import { CoreCourseComponentsModule } from '@features/course/components/componen
|
|||
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
|
||||
import { CoreRatingComponentsModule } from '@features/rating/components/components.module';
|
||||
|
||||
import { AddonModForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu';
|
||||
import { AddonModForumEditPostComponent } from './edit-post/edit-post';
|
||||
|
@ -40,6 +41,7 @@ import { AddonModForumSortOrderSelectorComponent } from './sort-order-selector/s
|
|||
CoreCourseComponentsModule,
|
||||
CoreTagComponentsModule,
|
||||
CoreEditorComponentsModule,
|
||||
CoreRatingComponentsModule,
|
||||
],
|
||||
exports: [
|
||||
AddonModForumIndexComponent,
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.mod_forum.yourreply' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
|
|
|
@ -50,6 +50,10 @@ import { CoreScreen } from '@services/screen';
|
|||
import { CoreArray } from '@singletons/array';
|
||||
import { AddonModForumPrefetchHandler } from '../../services/handlers/prefetch';
|
||||
import { AddonModForumModuleHandlerService } from '../../services/handlers/module';
|
||||
import { CoreRatingProvider } from '@features/rating/services/rating';
|
||||
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||
import { ContextLevel } from '@/core/constants';
|
||||
|
||||
/**
|
||||
* Component that displays a forum entry page.
|
||||
|
@ -88,9 +92,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
protected viewDiscObserver?: CoreEventObserver;
|
||||
protected changeDiscObserver?: CoreEventObserver;
|
||||
|
||||
hasOfflineRatings?: boolean;
|
||||
protected ratingOfflineObserver: any;
|
||||
protected ratingSyncObserver: any;
|
||||
hasOfflineRatings = false;
|
||||
protected ratingOfflineObserver?: CoreEventObserver;
|
||||
protected ratingSyncObserver?: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
route: ActivatedRoute,
|
||||
|
@ -166,7 +170,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
}
|
||||
});
|
||||
|
||||
// @todo Listen for offline ratings saved and synced.
|
||||
// Listen for offline ratings saved and synced.
|
||||
this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
|
||||
if (this.forum && data.component == 'mod_forum' && data.ratingArea == 'post' &&
|
||||
data.contextLevel == ContextLevel.MODULE && data.instanceId == this.forum.cmid) {
|
||||
this.hasOfflineRatings = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, async (data) => {
|
||||
if (this.forum && data.component == 'mod_forum' && data.ratingArea == 'post' &&
|
||||
data.contextLevel == ContextLevel.MODULE && data.instanceId == this.forum.cmid) {
|
||||
this.hasOfflineRatings =
|
||||
await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum.cmid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
|
@ -224,7 +242,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
await Promise.all([
|
||||
this.fetchOfflineDiscussions(),
|
||||
this.fetchDiscussions(refresh),
|
||||
// @todo fetch hasOfflineRatings.
|
||||
CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum!.cmid).then((hasRatings) => {
|
||||
this.hasOfflineRatings = hasRatings;
|
||||
|
||||
return;
|
||||
}),
|
||||
]);
|
||||
} catch (error) {
|
||||
if (refresh) {
|
||||
|
|
|
@ -75,8 +75,7 @@
|
|||
<core-tag-list [tags]="post.tags"></core-tag-list>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- @todo -->
|
||||
<!-- <core-rating-rate *ngIf="forum && ratingInfo"
|
||||
<core-rating-rate *ngIf="forum && ratingInfo"
|
||||
[ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id"
|
||||
[itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"
|
||||
[userId]="post.author.id" (onUpdate)="ratingUpdated()">
|
||||
|
@ -84,7 +83,7 @@
|
|||
<core-rating-aggregate *ngIf="forum && ratingInfo"
|
||||
[ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id"
|
||||
[courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale">
|
||||
</core-rating-aggregate> -->
|
||||
</core-rating-aggregate>
|
||||
|
||||
<ion-item *ngIf="post.id > 0 && post.capabilities.reply && !post.isprivatereply"
|
||||
class="ion-no-padding ion-text-end addon-forum-reply-button">
|
||||
|
|
|
@ -52,6 +52,7 @@ import { AddonModForumOffline, AddonModForumReplyOptions } from '../../services/
|
|||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { AddonModForumPostOptionsMenuComponent } from '../post-options-menu/post-options-menu';
|
||||
import { AddonModForumEditPostComponent } from '../edit-post/edit-post';
|
||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||
|
||||
/**
|
||||
* Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.).
|
||||
|
@ -75,7 +76,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
@Input() forum!: AddonModForumData; // The forum the post belongs to. Required for attachments and offline posts.
|
||||
@Input() accessInfo!: AddonModForumAccessInformation; // Forum access information.
|
||||
@Input() parentSubject?: string; // Subject of parent post.
|
||||
@Input() ratingInfo?: any; // TODO CoreRatingInfo; // Rating info item.
|
||||
@Input() ratingInfo?: CoreRatingInfo; // Rating info item.
|
||||
@Input() leavingPage?: boolean; // Whether the page that contains this post is being left and will be destroyed.
|
||||
@Input() highlight = false;
|
||||
@Output() onPostChange: EventEmitter<void> = new EventEmitter<void>(); // Event emitted when a reply is posted or modified.
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.sort' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
|
|
|
@ -12,9 +12,13 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ContextLevel } from '@/core/constants';
|
||||
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
|
@ -34,7 +38,6 @@ import {
|
|||
AddonModForumDiscussion,
|
||||
AddonModForumPost,
|
||||
AddonModForumProvider,
|
||||
AddonModForumRatingInfo,
|
||||
} from '../../services/forum';
|
||||
import { AddonModForumHelper } from '../../services/helper';
|
||||
import { AddonModForumOffline } from '../../services/offline';
|
||||
|
@ -101,8 +104,8 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
protected syncObserver?: CoreEventObserver;
|
||||
protected syncManualObserver?: CoreEventObserver;
|
||||
|
||||
ratingInfo?: AddonModForumRatingInfo;
|
||||
hasOfflineRatings!: boolean;
|
||||
ratingInfo?: CoreRatingInfo;
|
||||
hasOfflineRatings = false;
|
||||
protected ratingOfflineObserver?: CoreEventObserver;
|
||||
protected ratingSyncObserver?: CoreEventObserver;
|
||||
protected changeDiscObserver?: CoreEventObserver;
|
||||
|
@ -203,7 +206,20 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
AddonModForum.invalidateDiscussionsList(this.forumId);
|
||||
}
|
||||
|
||||
// @todo Listen for offline ratings saved and synced.
|
||||
// Listen for offline ratings saved and synced.
|
||||
this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
|
||||
if (data.component == 'mod_forum' && data.ratingArea == 'post' && data.contextLevel == ContextLevel.MODULE &&
|
||||
data.instanceId == this.cmId && data.itemSetId == this.discussionId) {
|
||||
this.hasOfflineRatings = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, async (data) => {
|
||||
if (data.component == 'mod_forum' && data.ratingArea == 'post' && data.contextLevel == ContextLevel.MODULE &&
|
||||
data.instanceId == this.cmId && data.itemSetId == this.discussionId) {
|
||||
this.hasOfflineRatings = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
|
||||
if ((this.forumId && this.forumId === data.forumId) || data.cmId === this.cmId) {
|
||||
|
@ -345,7 +361,8 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
|
||||
const response = await AddonModForum.getDiscussionPosts(this.discussionId, { cmId: this.cmId });
|
||||
const replies = await AddonModForumOffline.getDiscussionReplies(this.discussionId);
|
||||
const ratingInfo = response.ratinginfo;
|
||||
this.ratingInfo = response.ratinginfo;
|
||||
|
||||
onlinePosts = response.posts;
|
||||
this.courseId = response.courseid || this.courseId;
|
||||
this.forumId = response.forumid || this.forumId;
|
||||
|
@ -462,7 +479,6 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
}
|
||||
|
||||
this.posts = posts;
|
||||
this.ratingInfo = ratingInfo;
|
||||
this.postSubjects = this.getAllPosts().reduce(
|
||||
(postSubjects, post) => {
|
||||
postSubjects[post.id] = post.subject;
|
||||
|
@ -487,7 +503,8 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
this.canPin = false;
|
||||
}
|
||||
|
||||
// @todo fetch hasOfflineRatings.
|
||||
this.hasOfflineRatings =
|
||||
await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
} finally {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
|||
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreFileEntry } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
|
@ -553,7 +554,7 @@ export class AddonModForumProvider {
|
|||
posts: AddonModForumPost[];
|
||||
courseid?: number;
|
||||
forumid?: number;
|
||||
ratinginfo?: AddonModForumRatingInfo;
|
||||
ratinginfo?: CoreRatingInfo;
|
||||
}> {
|
||||
// Convenience function to translate legacy data to new format.
|
||||
const translateLegacyPostsFormat = (posts: AddonModForumLegacyPost[]): AddonModForumPost[] => posts.map((post) => {
|
||||
|
@ -1526,40 +1527,6 @@ export type AddonModForumLegacyPost = {
|
|||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Forum rating info.
|
||||
*/
|
||||
export type AddonModForumRatingInfo = {
|
||||
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?: { // Different scales used information.
|
||||
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.
|
||||
}[];
|
||||
}[];
|
||||
ratings?: { // The ratings.
|
||||
itemid: number; // Item id.
|
||||
scaleid?: number; // Scale id.
|
||||
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.
|
||||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to pass to get discussions.
|
||||
*/
|
||||
|
@ -1994,7 +1961,7 @@ export type AddonModForumGetDiscussionPostsWSResponse = {
|
|||
posts: AddonModForumWSPost[];
|
||||
forumid: number; // The forum id.
|
||||
courseid: number; // The forum course id.
|
||||
ratinginfo?: AddonModForumRatingInfo; // Rating information.
|
||||
ratinginfo?: CoreRatingInfo; // Rating information.
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
|
@ -2012,7 +1979,7 @@ export type AddonModForumGetForumDiscussionPostsWSParams = {
|
|||
*/
|
||||
export type AddonModForumGetForumDiscussionPostsWSResponse = {
|
||||
posts: AddonModForumLegacyPost[];
|
||||
ratinginfo?: AddonModForumRatingInfo; // Rating information.
|
||||
ratinginfo?: CoreRatingInfo; // Rating information.
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
// 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 { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreRatingSync } from '@features/rating/services/rating-sync';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreGroups } from '@services/groups';
|
||||
import { CoreSites } from '@services/sites';
|
||||
|
@ -327,14 +329,44 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async syncRatings(cmId?: number, discussionId?: number, force?: boolean, siteId?: string): Promise<{
|
||||
updated: boolean;
|
||||
warnings: string[];
|
||||
}> {
|
||||
// @todo
|
||||
async syncRatings(cmId?: number, discussionId?: number, force?: boolean, siteId?: string): Promise<AddonModForumSyncResult> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
return { updated: true, warnings: [] };
|
||||
const results =
|
||||
await CoreRatingSync.syncRatings('mod_forum', 'post', ContextLevel.MODULE, cmId, discussionId, force, siteId);
|
||||
|
||||
let updated = false;
|
||||
const warnings: string[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result.updated.length) {
|
||||
updated = true;
|
||||
|
||||
// Invalidate discussions of updated ratings.
|
||||
promises.push(AddonModForum.invalidateDiscussionPosts(result.itemSet!.itemSetId, undefined, siteId));
|
||||
}
|
||||
|
||||
if (result.warnings.length) {
|
||||
// Fetch forum to construct the warning message.
|
||||
promises.push(AddonModForum.getForum(result.itemSet!.courseId!, result.itemSet!.instanceId, { siteId })
|
||||
.then((forum) => {
|
||||
result.warnings.forEach((warning) => {
|
||||
warnings.push(Translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: forum.name,
|
||||
error: warning,
|
||||
}));
|
||||
});
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
await CoreUtils.allPromises(promises);
|
||||
|
||||
return { updated, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.mod_imscp.toc' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.comments.addcomment' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.courses.selfenrolment' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -1,8 +1,5 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ title }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
|
|
Loading…
Reference in New Issue