forked from EVOgeek/Vmeda.Online
		
	
						commit
						b52cc6477b
					
				| @ -1,8 +1,5 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ 'addon.messages.groupinfo' | translate }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||||
|  | |||||||
| @ -1,8 +1,5 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ plugin.name }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||||
|  | |||||||
| @ -1,8 +1,5 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ 'addon.mod_book.toc' | translate }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <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 { CoreEditorComponentsModule } from '@features/editor/components/components.module'; | ||||||
| import { CoreSharedModule } from '@/core/shared.module'; | import { CoreSharedModule } from '@/core/shared.module'; | ||||||
| import { CoreTagComponentsModule } from '@features/tag/components/components.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 { AddonModForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu'; | ||||||
| import { AddonModForumEditPostComponent } from './edit-post/edit-post'; | import { AddonModForumEditPostComponent } from './edit-post/edit-post'; | ||||||
| @ -40,6 +41,7 @@ import { AddonModForumSortOrderSelectorComponent } from './sort-order-selector/s | |||||||
|         CoreCourseComponentsModule, |         CoreCourseComponentsModule, | ||||||
|         CoreTagComponentsModule, |         CoreTagComponentsModule, | ||||||
|         CoreEditorComponentsModule, |         CoreEditorComponentsModule, | ||||||
|  |         CoreRatingComponentsModule, | ||||||
|     ], |     ], | ||||||
|     exports: [ |     exports: [ | ||||||
|         AddonModForumIndexComponent, |         AddonModForumIndexComponent, | ||||||
|  | |||||||
| @ -1,8 +1,5 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ 'addon.mod_forum.yourreply' | translate }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <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 { CoreArray } from '@singletons/array'; | ||||||
| import { AddonModForumPrefetchHandler } from '../../services/handlers/prefetch'; | import { AddonModForumPrefetchHandler } from '../../services/handlers/prefetch'; | ||||||
| import { AddonModForumModuleHandlerService } from '../../services/handlers/module'; | 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. |  * Component that displays a forum entry page. | ||||||
| @ -88,9 +92,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|     protected viewDiscObserver?: CoreEventObserver; |     protected viewDiscObserver?: CoreEventObserver; | ||||||
|     protected changeDiscObserver?: CoreEventObserver; |     protected changeDiscObserver?: CoreEventObserver; | ||||||
| 
 | 
 | ||||||
|     hasOfflineRatings?: boolean; |     hasOfflineRatings = false; | ||||||
|     protected ratingOfflineObserver: any; |     protected ratingOfflineObserver?: CoreEventObserver; | ||||||
|     protected ratingSyncObserver: any; |     protected ratingSyncObserver?: CoreEventObserver; | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         route: ActivatedRoute, |         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> { |     async ngAfterViewInit(): Promise<void> { | ||||||
| @ -224,7 +242,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|             await Promise.all([ |             await Promise.all([ | ||||||
|                 this.fetchOfflineDiscussions(), |                 this.fetchOfflineDiscussions(), | ||||||
|                 this.fetchDiscussions(refresh), |                 this.fetchDiscussions(refresh), | ||||||
|                 // @todo fetch hasOfflineRatings.
 |                 CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum!.cmid).then((hasRatings) => { | ||||||
|  |                     this.hasOfflineRatings = hasRatings; | ||||||
|  | 
 | ||||||
|  |                     return; | ||||||
|  |                 }), | ||||||
|             ]); |             ]); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             if (refresh) { |             if (refresh) { | ||||||
|  | |||||||
| @ -75,8 +75,7 @@ | |||||||
|                 <core-tag-list [tags]="post.tags"></core-tag-list> |                 <core-tag-list [tags]="post.tags"></core-tag-list> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|         <!-- @todo --> |         <core-rating-rate *ngIf="forum && ratingInfo" | ||||||
|         <!-- <core-rating-rate *ngIf="forum && ratingInfo" |  | ||||||
|             [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" |             [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" | ||||||
|             [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" |             [itemSetId]="discussionId" [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale" | ||||||
|             [userId]="post.author.id" (onUpdate)="ratingUpdated()"> |             [userId]="post.author.id" (onUpdate)="ratingUpdated()"> | ||||||
| @ -84,7 +83,7 @@ | |||||||
|         <core-rating-aggregate *ngIf="forum && ratingInfo" |         <core-rating-aggregate *ngIf="forum && ratingInfo" | ||||||
|             [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" |             [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="componentId" [itemId]="post.id" | ||||||
|             [courseId]="courseId" [aggregateMethod]="forum.assessed" [scaleId]="forum.scale"> |             [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" |         <ion-item *ngIf="post.id > 0 && post.capabilities.reply && !post.isprivatereply" | ||||||
|             class="ion-no-padding ion-text-end addon-forum-reply-button"> |             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 { CoreUtils } from '@services/utils/utils'; | ||||||
| import { AddonModForumPostOptionsMenuComponent } from '../post-options-menu/post-options-menu'; | import { AddonModForumPostOptionsMenuComponent } from '../post-options-menu/post-options-menu'; | ||||||
| import { AddonModForumEditPostComponent } from '../edit-post/edit-post'; | 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.). |  * 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() forum!: AddonModForumData; // The forum the post belongs to. Required for attachments and offline posts.
 | ||||||
|     @Input() accessInfo!: AddonModForumAccessInformation; // Forum access information.
 |     @Input() accessInfo!: AddonModForumAccessInformation; // Forum access information.
 | ||||||
|     @Input() parentSubject?: string; // Subject of parent post.
 |     @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() leavingPage?: boolean; // Whether the page that contains this post is being left and will be destroyed.
 | ||||||
|     @Input() highlight = false; |     @Input() highlight = false; | ||||||
|     @Output() onPostChange: EventEmitter<void> = new EventEmitter<void>(); // Event emitted when a reply is posted or modified.
 |     @Output() onPostChange: EventEmitter<void> = new EventEmitter<void>(); // Event emitted when a reply is posted or modified.
 | ||||||
|  | |||||||
| @ -1,8 +1,5 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ 'core.sort' | translate }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||||
|  | |||||||
| @ -12,9 +12,13 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
| import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core'; | import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core'; | ||||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||||
| import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; | 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 { CoreUser } from '@features/user/services/user'; | ||||||
| import { CanLeave } from '@guards/can-leave'; | import { CanLeave } from '@guards/can-leave'; | ||||||
| import { IonContent } from '@ionic/angular'; | import { IonContent } from '@ionic/angular'; | ||||||
| @ -34,7 +38,6 @@ import { | |||||||
|     AddonModForumDiscussion, |     AddonModForumDiscussion, | ||||||
|     AddonModForumPost, |     AddonModForumPost, | ||||||
|     AddonModForumProvider, |     AddonModForumProvider, | ||||||
|     AddonModForumRatingInfo, |  | ||||||
| } from '../../services/forum'; | } from '../../services/forum'; | ||||||
| import { AddonModForumHelper } from '../../services/helper'; | import { AddonModForumHelper } from '../../services/helper'; | ||||||
| import { AddonModForumOffline } from '../../services/offline'; | import { AddonModForumOffline } from '../../services/offline'; | ||||||
| @ -101,8 +104,8 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes | |||||||
|     protected syncObserver?: CoreEventObserver; |     protected syncObserver?: CoreEventObserver; | ||||||
|     protected syncManualObserver?: CoreEventObserver; |     protected syncManualObserver?: CoreEventObserver; | ||||||
| 
 | 
 | ||||||
|     ratingInfo?: AddonModForumRatingInfo; |     ratingInfo?: CoreRatingInfo; | ||||||
|     hasOfflineRatings!: boolean; |     hasOfflineRatings = false; | ||||||
|     protected ratingOfflineObserver?: CoreEventObserver; |     protected ratingOfflineObserver?: CoreEventObserver; | ||||||
|     protected ratingSyncObserver?: CoreEventObserver; |     protected ratingSyncObserver?: CoreEventObserver; | ||||||
|     protected changeDiscObserver?: CoreEventObserver; |     protected changeDiscObserver?: CoreEventObserver; | ||||||
| @ -203,7 +206,20 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes | |||||||
|             AddonModForum.invalidateDiscussionsList(this.forumId); |             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 => { |         this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => { | ||||||
|             if ((this.forumId && this.forumId === data.forumId) || data.cmId === this.cmId) { |             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 response = await AddonModForum.getDiscussionPosts(this.discussionId, { cmId: this.cmId }); | ||||||
|             const replies = await AddonModForumOffline.getDiscussionReplies(this.discussionId); |             const replies = await AddonModForumOffline.getDiscussionReplies(this.discussionId); | ||||||
|             const ratingInfo = response.ratinginfo; |             this.ratingInfo = response.ratinginfo; | ||||||
|  | 
 | ||||||
|             onlinePosts = response.posts; |             onlinePosts = response.posts; | ||||||
|             this.courseId = response.courseid || this.courseId; |             this.courseId = response.courseid || this.courseId; | ||||||
|             this.forumId = response.forumid || this.forumId; |             this.forumId = response.forumid || this.forumId; | ||||||
| @ -462,7 +479,6 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             this.posts = posts; |             this.posts = posts; | ||||||
|             this.ratingInfo = ratingInfo; |  | ||||||
|             this.postSubjects = this.getAllPosts().reduce( |             this.postSubjects = this.getAllPosts().reduce( | ||||||
|                 (postSubjects, post) => { |                 (postSubjects, post) => { | ||||||
|                     postSubjects[post.id] = post.subject; |                     postSubjects[post.id] = post.subject; | ||||||
| @ -487,7 +503,8 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes | |||||||
|                 this.canPin = false; |                 this.canPin = false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // @todo fetch hasOfflineRatings.
 |             this.hasOfflineRatings = | ||||||
|  |                 await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModal(error); |             CoreDomUtils.showErrorModal(error); | ||||||
|         } finally { |         } finally { | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | |||||||
| import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; | import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; | ||||||
| import { CoreCourseLogHelper } from '@features/course/services/log-helper'; | import { CoreCourseLogHelper } from '@features/course/services/log-helper'; | ||||||
| import { CoreFileEntry } from '@features/fileuploader/services/fileuploader'; | import { CoreFileEntry } from '@features/fileuploader/services/fileuploader'; | ||||||
|  | import { CoreRatingInfo } from '@features/rating/services/rating'; | ||||||
| import { CoreUser } from '@features/user/services/user'; | import { CoreUser } from '@features/user/services/user'; | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { CoreFilepool } from '@services/filepool'; | import { CoreFilepool } from '@services/filepool'; | ||||||
| @ -553,7 +554,7 @@ export class AddonModForumProvider { | |||||||
|         posts: AddonModForumPost[]; |         posts: AddonModForumPost[]; | ||||||
|         courseid?: number; |         courseid?: number; | ||||||
|         forumid?: number; |         forumid?: number; | ||||||
|         ratinginfo?: AddonModForumRatingInfo; |         ratinginfo?: CoreRatingInfo; | ||||||
|     }> { |     }> { | ||||||
|         // Convenience function to translate legacy data to new format.
 |         // Convenience function to translate legacy data to new format.
 | ||||||
|         const translateLegacyPostsFormat = (posts: AddonModForumLegacyPost[]): AddonModForumPost[] => posts.map((post) => { |         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. |  * Options to pass to get discussions. | ||||||
|  */ |  */ | ||||||
| @ -1994,7 +1961,7 @@ export type AddonModForumGetDiscussionPostsWSResponse = { | |||||||
|     posts: AddonModForumWSPost[]; |     posts: AddonModForumWSPost[]; | ||||||
|     forumid: number; // The forum id.
 |     forumid: number; // The forum id.
 | ||||||
|     courseid: number; // The forum course id.
 |     courseid: number; // The forum course id.
 | ||||||
|     ratinginfo?: AddonModForumRatingInfo; // Rating information.
 |     ratinginfo?: CoreRatingInfo; // Rating information.
 | ||||||
|     warnings?: CoreWSExternalWarning[]; |     warnings?: CoreWSExternalWarning[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -2012,7 +1979,7 @@ export type AddonModForumGetForumDiscussionPostsWSParams = { | |||||||
|  */ |  */ | ||||||
| export type AddonModForumGetForumDiscussionPostsWSResponse = { | export type AddonModForumGetForumDiscussionPostsWSResponse = { | ||||||
|     posts: AddonModForumLegacyPost[]; |     posts: AddonModForumLegacyPost[]; | ||||||
|     ratinginfo?: AddonModForumRatingInfo; // Rating information.
 |     ratinginfo?: CoreRatingInfo; // Rating information.
 | ||||||
|     warnings?: CoreWSExternalWarning[]; |     warnings?: CoreWSExternalWarning[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -12,11 +12,13 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { CoreSyncBaseProvider } from '@classes/base-sync'; | import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||||
| import { CoreCourse } from '@features/course/services/course'; | import { CoreCourse } from '@features/course/services/course'; | ||||||
| import { CoreCourseLogHelper } from '@features/course/services/log-helper'; | import { CoreCourseLogHelper } from '@features/course/services/log-helper'; | ||||||
| import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; | import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; | ||||||
|  | import { CoreRatingSync } from '@features/rating/services/rating-sync'; | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { CoreGroups } from '@services/groups'; | import { CoreGroups } from '@services/groups'; | ||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| @ -327,14 +329,44 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider<AddonModForu | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved if sync is successful, rejected otherwise. |      * @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<AddonModForumSyncResult> { | ||||||
|     async syncRatings(cmId?: number, discussionId?: number, force?: boolean, siteId?: string): Promise<{ |         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||||
|         updated: boolean; |  | ||||||
|         warnings: string[]; |  | ||||||
|     }> { |  | ||||||
|         // @todo
 |  | ||||||
| 
 | 
 | ||||||
|         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-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ 'addon.mod_imscp.toc' | translate }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||||
|  | |||||||
| @ -1,8 +1,5 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ 'core.comments.addcomment' | translate }}</ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <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_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,
 | ||||||
|  | |||||||
| @ -1,8 +1,5 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ 'core.courses.selfenrolment' | translate }}</ion-title> | ||||||
| 
 | 
 | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|  | |||||||
							
								
								
									
										144
									
								
								src/core/features/rating/components/aggregate/aggregate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/core/features/rating/components/aggregate/aggregate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
|  | import { Component, Input, OnChanges, OnDestroy } from '@angular/core'; | ||||||
|  | import { | ||||||
|  |     CoreRating, | ||||||
|  |     CoreRatingInfo, | ||||||
|  |     CoreRatingInfoItem, | ||||||
|  |     CoreRatingProvider, | ||||||
|  | } from '@features/rating/services/rating'; | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
|  | import { ModalController } from '@singletons'; | ||||||
|  | import { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@singletons/events'; | ||||||
|  | import { CoreRatingRatingsComponent } from '../ratings/ratings'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component that displays the aggregation of a rating item. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-rating-aggregate', | ||||||
|  |     templateUrl: 'core-rating-aggregate.html', | ||||||
|  | }) | ||||||
|  | export class CoreRatingAggregateComponent implements OnChanges, OnDestroy { | ||||||
|  | 
 | ||||||
|  |     @Input() ratingInfo!: CoreRatingInfo; | ||||||
|  |     @Input() contextLevel!: ContextLevel; | ||||||
|  |     @Input() instanceId!: number; | ||||||
|  |     @Input() itemId!: number; | ||||||
|  |     @Input() aggregateMethod!: number; | ||||||
|  |     @Input() scaleId!: number; | ||||||
|  |     @Input() courseId?: number; | ||||||
|  | 
 | ||||||
|  |     item?: CoreRatingInfoItem; | ||||||
|  |     showCount = false; | ||||||
|  |     disabled = false; | ||||||
|  |     labelKey = ''; | ||||||
|  | 
 | ||||||
|  |     protected aggregateObserver: CoreEventObserver; | ||||||
|  |     protected updateSiteObserver: CoreEventObserver; | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         this.disabled = CoreRating.isRatingDisabledInSite(); | ||||||
|  | 
 | ||||||
|  |         // Update visibility if current site info is updated.
 | ||||||
|  |         this.updateSiteObserver = CoreEvents.on<CoreEventSiteUpdatedData>(CoreEvents.SITE_UPDATED, () => { | ||||||
|  |             this.disabled = CoreRating.isRatingDisabledInSite(); | ||||||
|  |         }, CoreSites.getCurrentSiteId()); | ||||||
|  | 
 | ||||||
|  |         // Update aggrgate when the user adds or edits a rating.
 | ||||||
|  |         this.aggregateObserver = | ||||||
|  |             CoreEvents.on(CoreRatingProvider.AGGREGATE_CHANGED_EVENT, (data) => { | ||||||
|  |                 if (this.item && | ||||||
|  |                     data.contextLevel == this.contextLevel && | ||||||
|  |                     data.instanceId == this.instanceId && | ||||||
|  |                     data.component == this.ratingInfo.component && | ||||||
|  |                     data.ratingArea == this.ratingInfo.ratingarea && | ||||||
|  |                     data.itemId == this.itemId) { | ||||||
|  |                     this.item.aggregatestr = data.aggregate; | ||||||
|  |                     this.item.count = data.count; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Detect changes on input properties. | ||||||
|  |      */ | ||||||
|  |     ngOnChanges(): void { | ||||||
|  |         this.aggregateObserver && this.aggregateObserver.off(); | ||||||
|  | 
 | ||||||
|  |         this.item = (this.ratingInfo.ratings || []).find((rating) => rating.itemid == this.itemId); | ||||||
|  |         if (!this.item) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         switch (this.aggregateMethod) { | ||||||
|  |             case CoreRatingProvider.AGGREGATE_AVERAGE: | ||||||
|  |                 this.labelKey = 'core.rating.aggregateavg'; | ||||||
|  |                 break; | ||||||
|  |             case CoreRatingProvider.AGGREGATE_COUNT: | ||||||
|  |                 this.labelKey = 'core.rating.aggregatecount'; | ||||||
|  |                 break; | ||||||
|  |             case CoreRatingProvider.AGGREGATE_MAXIMUM: | ||||||
|  |                 this.labelKey = 'core.rating.aggregatemax'; | ||||||
|  |                 break; | ||||||
|  |             case CoreRatingProvider.AGGREGATE_MINIMUM: | ||||||
|  |                 this.labelKey = 'core.rating.aggregatemin'; | ||||||
|  |                 break; | ||||||
|  |             case CoreRatingProvider.AGGREGATE_SUM: | ||||||
|  |                 this.labelKey = 'core.rating.aggregatesum'; | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 this.labelKey = ''; | ||||||
|  | 
 | ||||||
|  |                 return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.showCount = (this.aggregateMethod != CoreRatingProvider.AGGREGATE_COUNT); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Open the individual ratings page. | ||||||
|  |      */ | ||||||
|  |     async openRatings(): Promise<void> { | ||||||
|  |         if (!this.ratingInfo.canviewall || !this.item!.count || this.disabled) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const modal = await ModalController.create({ | ||||||
|  |             component: CoreRatingRatingsComponent, | ||||||
|  |             componentProps: { | ||||||
|  |                 contextLevel: this.contextLevel, | ||||||
|  |                 instanceId: this.instanceId, | ||||||
|  |                 ratingComponent: this.ratingInfo.component, | ||||||
|  |                 ratingArea: this.ratingInfo.ratingarea, | ||||||
|  |                 itemId: this.itemId, | ||||||
|  |                 scaleId: this.scaleId, | ||||||
|  |                 courseId: this.courseId, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         await modal.present(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being destroyed. | ||||||
|  |      */ | ||||||
|  |     ngOnDestroy(): void { | ||||||
|  |         this.aggregateObserver.off(); | ||||||
|  |         this.updateSiteObserver.off(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -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> | ||||||
							
								
								
									
										39
									
								
								src/core/features/rating/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/core/features/rating/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { NgModule } from '@angular/core'; | ||||||
|  | import { CoreRatingAggregateComponent } from './aggregate/aggregate'; | ||||||
|  | import { CoreRatingRateComponent } from './rate/rate'; | ||||||
|  | import { CoreSharedModule } from '@/core/shared.module'; | ||||||
|  | import { CoreRatingRatingsComponent } from './ratings/ratings'; | ||||||
|  | 
 | ||||||
|  | @NgModule({ | ||||||
|  |     declarations: [ | ||||||
|  |         CoreRatingAggregateComponent, | ||||||
|  |         CoreRatingRateComponent, | ||||||
|  |         CoreRatingRatingsComponent, | ||||||
|  |     ], | ||||||
|  |     imports: [ | ||||||
|  |         CoreSharedModule, | ||||||
|  |     ], | ||||||
|  |     exports: [ | ||||||
|  |         CoreRatingAggregateComponent, | ||||||
|  |         CoreRatingRateComponent, | ||||||
|  |         CoreRatingRatingsComponent, | ||||||
|  |     ], | ||||||
|  |     entryComponents: [ | ||||||
|  |         CoreRatingRatingsComponent, | ||||||
|  |     ], | ||||||
|  | }) | ||||||
|  | export class CoreRatingComponentsModule {} | ||||||
| @ -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> | ||||||
							
								
								
									
										164
									
								
								src/core/features/rating/components/rate/rate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/core/features/rating/components/rate/rate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,164 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
|  | import { Component, EventEmitter, Input, OnChanges, Output, OnDestroy } from '@angular/core'; | ||||||
|  | import { | ||||||
|  |     CoreRatingProvider, | ||||||
|  |     CoreRatingInfo, | ||||||
|  |     CoreRatingInfoItem, | ||||||
|  |     CoreRatingScale, | ||||||
|  |     CoreRating, | ||||||
|  | } from '@features/rating/services/rating'; | ||||||
|  | import { CoreRatingOffline } from '@features/rating/services/rating-offline'; | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { Translate } from '@singletons'; | ||||||
|  | import { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@singletons/events'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component that displays the user rating select. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-rating-rate', | ||||||
|  |     templateUrl: 'core-rating-rate.html', | ||||||
|  | }) | ||||||
|  | export class CoreRatingRateComponent implements OnChanges, OnDestroy { | ||||||
|  | 
 | ||||||
|  |     @Input() protected ratingInfo!: CoreRatingInfo; | ||||||
|  |     @Input() protected contextLevel!: ContextLevel; // Context level: course, module, user, etc.
 | ||||||
|  |     @Input() protected instanceId!: number; // Context instance id.
 | ||||||
|  |     @Input() protected itemId!: number; // Item id. Example: forum post id.
 | ||||||
|  |     @Input() protected itemSetId!: number; // Item set id. Example: forum discussion id.
 | ||||||
|  |     @Input() protected courseId!: number; | ||||||
|  |     @Input() protected aggregateMethod!: number; | ||||||
|  |     @Input() protected scaleId!: number; | ||||||
|  |     @Input() protected userId!: number; | ||||||
|  |     @Output() protected onLoading: EventEmitter<boolean>; // Eevent that indicates whether the component is loading data.
 | ||||||
|  |     @Output() protected onUpdate: EventEmitter<void>; // Event emitted when the rating is updated online.
 | ||||||
|  | 
 | ||||||
|  |     item?: CoreRatingInfoItem; | ||||||
|  |     scale?: CoreRatingScale; | ||||||
|  |     rating?: number; | ||||||
|  |     disabled = false; | ||||||
|  | 
 | ||||||
|  |     protected updateSiteObserver: CoreEventObserver; | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  | 
 | ||||||
|  |         this.onLoading = new EventEmitter<boolean>(); | ||||||
|  |         this.onUpdate = new EventEmitter<void>(); | ||||||
|  | 
 | ||||||
|  |         this.disabled = CoreRating.isRatingDisabledInSite(); | ||||||
|  | 
 | ||||||
|  |         // Update visibility if current site info is updated.
 | ||||||
|  |         this.updateSiteObserver = CoreEvents.on<CoreEventSiteUpdatedData>(CoreEvents.SITE_UPDATED, () => { | ||||||
|  |             this.disabled = CoreRating.isRatingDisabledInSite(); | ||||||
|  |         }, CoreSites.getCurrentSiteId()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Detect changes on input properties. | ||||||
|  |      */ | ||||||
|  |     async ngOnChanges(): Promise<void> { | ||||||
|  |         this.item = (this.ratingInfo.ratings || []).find((rating) => rating.itemid == this.itemId); | ||||||
|  |         this.scale = (this.ratingInfo.scales || []).find((scale) => scale.id == this.scaleId); | ||||||
|  | 
 | ||||||
|  |         if (!this.item || !this.scale || !CoreRating.isAddRatingWSAvailable()) { | ||||||
|  |             this.item = undefined; | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Set numeric scale items.
 | ||||||
|  |         if (!this.scale.items) { | ||||||
|  |             this.scale.items = []; | ||||||
|  |             if (this.scale.isnumeric) { | ||||||
|  |                 for (let n = 0; n <= this.scale.max; n++) { | ||||||
|  |                     this.scale.items.push({ name: String(n), value: n }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Add "No rating" item to the scale.
 | ||||||
|  |         if (!this.scale.items[0] || this.scale.items[0].value != CoreRatingProvider.UNSET_RATING) { | ||||||
|  |             this.scale.items.unshift({ | ||||||
|  |                 name: Translate.instant('core.none'), | ||||||
|  |                 value: CoreRatingProvider.UNSET_RATING, | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.onLoading.emit(true); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const rating = await CoreRatingOffline.getRating( | ||||||
|  |                 this.contextLevel, | ||||||
|  |                 this.instanceId, | ||||||
|  |                 this.ratingInfo.component, | ||||||
|  |                 this.ratingInfo.ratingarea, | ||||||
|  |                 this.itemId, | ||||||
|  |             ); | ||||||
|  |             this.rating = rating.rating; | ||||||
|  |         } catch { | ||||||
|  |             if (this.item && this.item.rating != null) { | ||||||
|  |                 this.rating = this.item.rating; | ||||||
|  |             } else { | ||||||
|  |                 this.rating = CoreRatingProvider.UNSET_RATING; | ||||||
|  |             } | ||||||
|  |         } finally { | ||||||
|  |             this.onLoading.emit(false); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Send or save the user rating when changed. | ||||||
|  |      */ | ||||||
|  |     async userRatingChanged(): Promise<void> { | ||||||
|  |         const modal = await CoreDomUtils.showModalLoading('core.sending', true); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const response = await CoreRating.addRating( | ||||||
|  |                 this.ratingInfo.component, | ||||||
|  |                 this.ratingInfo.ratingarea, | ||||||
|  |                 this.contextLevel, | ||||||
|  |                 this.instanceId, | ||||||
|  |                 this.itemId, | ||||||
|  |                 this.itemSetId, | ||||||
|  |                 this.courseId, | ||||||
|  |                 this.scaleId, | ||||||
|  |                 this.rating!, | ||||||
|  |                 this.userId, | ||||||
|  |                 this.aggregateMethod, | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             if (typeof response == 'undefined') { | ||||||
|  |                 CoreDomUtils.showToast('core.datastoredoffline', true, 3000); | ||||||
|  |             } else { | ||||||
|  |                 this.onUpdate.emit(); | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             CoreDomUtils.showErrorModal(error); | ||||||
|  |         } finally { | ||||||
|  |             modal.dismiss(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being destroyed. | ||||||
|  |      */ | ||||||
|  |     ngOnDestroy(): void { | ||||||
|  |         this.updateSiteObserver.off(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -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> | ||||||
							
								
								
									
										70
									
								
								src/core/features/rating/components/ratings/ratings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/core/features/rating/components/ratings/ratings.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
|  | import { Component, Input, OnInit } from '@angular/core'; | ||||||
|  | import { CoreRating, CoreRatingItemRating } from '@features/rating/services/rating'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { ModalController } from '@singletons'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Modal that displays individual ratings | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     templateUrl: 'ratings-modal.html', | ||||||
|  | }) | ||||||
|  | export class CoreRatingRatingsComponent implements OnInit { | ||||||
|  | 
 | ||||||
|  |     @Input() protected contextLevel!: ContextLevel; | ||||||
|  |     @Input() protected instanceId!: number; | ||||||
|  |     @Input() protected ratingComponent!: string; | ||||||
|  |     @Input() protected ratingArea!: string; | ||||||
|  |     @Input() protected aggregateMethod!: number; | ||||||
|  |     @Input() protected itemId!: number; | ||||||
|  |     @Input() protected scaleId!: number; | ||||||
|  |     @Input() courseId!: number; | ||||||
|  | 
 | ||||||
|  |     loaded = false; | ||||||
|  |     ratings: CoreRatingItemRating[] = []; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Modal loaded. | ||||||
|  |      */ | ||||||
|  |     async ngOnInit(): Promise<void> { | ||||||
|  |         try { | ||||||
|  |             this.ratings = await CoreRating.getItemRatings( | ||||||
|  |                 this.contextLevel, | ||||||
|  |                 this.instanceId, | ||||||
|  |                 this.ratingComponent, | ||||||
|  |                 this.ratingArea, | ||||||
|  |                 this.itemId, | ||||||
|  |                 this.scaleId, | ||||||
|  |                 undefined, | ||||||
|  |                 this.courseId, | ||||||
|  |             ); | ||||||
|  |         } catch (error) { | ||||||
|  |             CoreDomUtils.showErrorModal(error); | ||||||
|  |         } finally { | ||||||
|  |             this.loaded = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Close modal. | ||||||
|  |      */ | ||||||
|  |     closeModal(): void { | ||||||
|  |         ModalController.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								src/core/features/rating/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/core/features/rating/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | { | ||||||
|  |     "aggregateavg": "Average of ratings", | ||||||
|  |     "aggregatecount": "Count of ratings", | ||||||
|  |     "aggregatemax": "Maximum rating", | ||||||
|  |     "aggregatemin": "Minimum rating", | ||||||
|  |     "aggregatesum": "Sum of ratings", | ||||||
|  |     "noratings": "No ratings submitted", | ||||||
|  |     "rating": "Rating", | ||||||
|  |     "ratings": "Ratings" | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								src/core/features/rating/rating.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/core/features/rating/rating.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { NgModule, Type } from '@angular/core'; | ||||||
|  | import { CORE_SITE_SCHEMAS } from '@services/sites'; | ||||||
|  | import { RATINGS_SITE_SCHEMA } from './services/database/rating'; | ||||||
|  | import { CoreRatingProvider } from './services/rating'; | ||||||
|  | import { CoreRatingOfflineProvider } from './services/rating-offline'; | ||||||
|  | import { CoreRatingSyncProvider } from './services/rating-sync'; | ||||||
|  | 
 | ||||||
|  | export const CORE_RATING_SERVICES: Type<unknown>[] = [ | ||||||
|  |     CoreRatingProvider, | ||||||
|  |     CoreRatingSyncProvider, | ||||||
|  |     CoreRatingOfflineProvider, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | @NgModule({ | ||||||
|  |     providers: [ | ||||||
|  |         { | ||||||
|  |             provide: CORE_SITE_SCHEMAS, | ||||||
|  |             useValue: [RATINGS_SITE_SCHEMA], | ||||||
|  |             multi: true, | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  | }) | ||||||
|  | export class CoreRatingModule {} | ||||||
							
								
								
									
										101
									
								
								src/core/features/rating/services/database/rating.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/core/features/rating/services/database/rating.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
|  | import { CoreSiteSchema } from '@services/sites'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Database variables for CoreRatingOffline service. | ||||||
|  |  */ | ||||||
|  | export const RATINGS_TABLE = 'rating_ratings'; | ||||||
|  | export const RATINGS_SITE_SCHEMA: CoreSiteSchema = { | ||||||
|  |     name: 'CoreRatingOfflineProvider', | ||||||
|  |     version: 1, | ||||||
|  |     tables: [ | ||||||
|  |         { | ||||||
|  |             name: RATINGS_TABLE, | ||||||
|  |             columns: [ | ||||||
|  |                 { | ||||||
|  |                     name: 'component', | ||||||
|  |                     type: 'TEXT', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'ratingarea', | ||||||
|  |                     type: 'TEXT', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'contextlevel', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'instanceid', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'itemid', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'itemsetid', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'courseid', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'scaleid', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'rating', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'rateduserid', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: 'aggregation', | ||||||
|  |                     type: 'INTEGER', | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |             primaryKeys: ['component', 'ratingarea', 'contextlevel', 'instanceid', 'itemid'], | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Primary data to identify a stored rating. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingDBPrimaryData = { | ||||||
|  |     component: string; | ||||||
|  |     ratingarea: string; | ||||||
|  |     contextlevel: ContextLevel; | ||||||
|  |     instanceid: number; | ||||||
|  |     itemid: number; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Rating stored. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingDBRecord = CoreRatingDBPrimaryData & { | ||||||
|  |     itemsetid: number; | ||||||
|  |     courseid?: number; | ||||||
|  |     scaleid: number; | ||||||
|  |     rating: number; | ||||||
|  |     rateduserid: number; | ||||||
|  |     aggregation: number; | ||||||
|  | }; | ||||||
							
								
								
									
										271
									
								
								src/core/features/rating/services/rating-offline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								src/core/features/rating/services/rating-offline.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,271 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
|  | import { Injectable } from '@angular/core'; | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { makeSingleton } from '@singletons'; | ||||||
|  | import { CoreRatingDBPrimaryData, CoreRatingDBRecord, RATINGS_TABLE } from './database/rating'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Structure of item sets. | ||||||
|  |  */ | ||||||
|  | export interface CoreRatingItemSet { | ||||||
|  |     component: string; | ||||||
|  |     ratingArea: string; | ||||||
|  |     contextLevel: ContextLevel; | ||||||
|  |     instanceId: number; | ||||||
|  |     itemSetId: number; | ||||||
|  |     courseId?: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Service to handle offline data for rating. | ||||||
|  |  */ | ||||||
|  | @Injectable( { providedIn: 'root' }) | ||||||
|  | export class CoreRatingOfflineProvider { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get an offline rating. | ||||||
|  |      * | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating area. Example: "post". | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved with the saved rating, rejected if not found. | ||||||
|  |      */ | ||||||
|  |     async getRating( | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         itemId: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<CoreRatingDBRecord> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const conditions: CoreRatingDBPrimaryData = { | ||||||
|  |             contextlevel: contextLevel, | ||||||
|  |             instanceid: instanceId, | ||||||
|  |             component: component, | ||||||
|  |             ratingarea: ratingArea, | ||||||
|  |             itemid: itemId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         return site.getDb().getRecord(RATINGS_TABLE, conditions); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add an offline rating. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param itemSetId Item set id. Example: forum discussion id. | ||||||
|  |      * @param courseId Course id. | ||||||
|  |      * @param scaleId Scale id. | ||||||
|  |      * @param rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating. | ||||||
|  |      * @param ratedUserId Rated user id. | ||||||
|  |      * @param aggregateMethod Aggregate method. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved when the rating is saved. | ||||||
|  |      */ | ||||||
|  |     async addRating( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemId: number, | ||||||
|  |         itemSetId: number, | ||||||
|  |         courseId: number, | ||||||
|  |         scaleId: number, | ||||||
|  |         rating: number, | ||||||
|  |         ratedUserId: number, | ||||||
|  |         aggregateMethod: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<void> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const data: CoreRatingDBRecord = { | ||||||
|  |             component: component, | ||||||
|  |             ratingarea: ratingArea, | ||||||
|  |             contextlevel: contextLevel, | ||||||
|  |             instanceid: instanceId, | ||||||
|  |             itemid: itemId, | ||||||
|  |             itemsetid: itemSetId, | ||||||
|  |             courseid: courseId, | ||||||
|  |             scaleid: scaleId, | ||||||
|  |             rating: rating, | ||||||
|  |             rateduserid: ratedUserId, | ||||||
|  |             aggregation: aggregateMethod, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await site.getDb().insertRecord(RATINGS_TABLE, data); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Delete offline rating. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved when the rating is saved. | ||||||
|  |      */ | ||||||
|  |     async deleteRating( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemId: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<void> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const conditions: CoreRatingDBPrimaryData = { | ||||||
|  |             component: component, | ||||||
|  |             ratingarea: ratingArea, | ||||||
|  |             contextlevel: contextLevel, | ||||||
|  |             instanceid: instanceId, | ||||||
|  |             itemid: itemId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         await site.getDb().deleteRecords(RATINGS_TABLE, conditions); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the list of item sets in a component or instance. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating Area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemSetId Item set id. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved with the list of item set ids. | ||||||
|  |      */ | ||||||
|  |     async getItemSets( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel?: ContextLevel, | ||||||
|  |         instanceId?: number, | ||||||
|  |         itemSetId?: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<CoreRatingItemSet[]> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const fields = 'DISTINCT contextlevel, instanceid, itemsetid, courseid'; | ||||||
|  | 
 | ||||||
|  |         const conditions: Partial<CoreRatingDBRecord> = { | ||||||
|  |             component, | ||||||
|  |             ratingarea: ratingArea, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         if (contextLevel && instanceId) { | ||||||
|  |             conditions.contextlevel = contextLevel; | ||||||
|  |             conditions.instanceid = instanceId; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (itemSetId) { | ||||||
|  |             conditions.itemsetid = itemSetId; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const records = await site.getDb().getRecords<CoreRatingDBRecord>(RATINGS_TABLE, conditions, undefined, fields); | ||||||
|  | 
 | ||||||
|  |         return records.map((record) => ({ | ||||||
|  |             component, | ||||||
|  |             ratingArea, | ||||||
|  |             contextLevel: record.contextlevel, | ||||||
|  |             instanceId: record.instanceid, | ||||||
|  |             itemSetId: record.itemsetid, | ||||||
|  |             courseId: record.courseid, | ||||||
|  |         })); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get offline ratings of an item set. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating Area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param itemSetId Item set id. Example: forum discussion id. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved with the list of ratings. | ||||||
|  |      */ | ||||||
|  |     async getRatings( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemSetId: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<CoreRatingDBRecord[]> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const conditions: Partial<CoreRatingDBRecord> = { | ||||||
|  |             component, | ||||||
|  |             ratingarea: ratingArea, | ||||||
|  |             contextlevel: contextLevel, | ||||||
|  |             instanceid: instanceId, | ||||||
|  |             itemsetid: itemSetId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         return site.getDb().getRecords(RATINGS_TABLE, conditions); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return whether a component, instance or item set has offline ratings. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating Area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemSetId Item set id. Example: forum discussion id. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved with a boolean. | ||||||
|  |      */ | ||||||
|  |     async hasRatings( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel?: ContextLevel, | ||||||
|  |         instanceId?: number, | ||||||
|  |         itemSetId?: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<boolean> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const conditions: Partial<CoreRatingDBRecord> = { | ||||||
|  |             component, | ||||||
|  |             ratingarea: ratingArea, | ||||||
|  |         }; | ||||||
|  |         if (contextLevel && instanceId) { | ||||||
|  |             conditions.contextlevel = contextLevel; | ||||||
|  |             conditions.instanceid = instanceId; | ||||||
|  |         } | ||||||
|  |         if (itemSetId) { | ||||||
|  |             conditions.itemsetid = itemSetId; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return CoreUtils.promiseWorks(site.getDb().recordExists(RATINGS_TABLE, conditions)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | export const CoreRatingOffline = makeSingleton(CoreRatingOfflineProvider); | ||||||
							
								
								
									
										315
									
								
								src/core/features/rating/services/rating-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								src/core/features/rating/services/rating-sync.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,315 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
|  | import { Injectable } from '@angular/core'; | ||||||
|  | import { CoreSyncBaseProvider } from '@classes/base-sync'; | ||||||
|  | import { CoreNetworkError } from '@classes/errors/network-error'; | ||||||
|  | import { CoreApp } from '@services/app'; | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
|  | import { CoreTextUtils } from '@services/utils/text'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { makeSingleton } from '@singletons'; | ||||||
|  | import { CoreEvents } from '@singletons/events'; | ||||||
|  | import { CoreRating } from './rating'; | ||||||
|  | import { CoreRatingItemSet, CoreRatingOffline } from './rating-offline'; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Service to sync ratings. | ||||||
|  |  */ | ||||||
|  | @Injectable( { providedIn: 'root' }) | ||||||
|  | export class CoreRatingSyncProvider extends CoreSyncBaseProvider<CoreRatingSyncItem> { | ||||||
|  | 
 | ||||||
|  |     static readonly SYNCED_EVENT = 'core_rating_synced'; | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         super('CoreRatingSyncProvider'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Try to synchronize all the ratings of a certain component, instance or item set. | ||||||
|  |      * | ||||||
|  |      * This function should be called from the sync provider of activities with ratings. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating Area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemSetId Item set id. | ||||||
|  |      * @param force Wether to force sync not depending on last execution. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved if sync is successful, rejected if sync fails. | ||||||
|  |      */ | ||||||
|  |     async syncRatings( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel?: ContextLevel, | ||||||
|  |         instanceId?: number, | ||||||
|  |         itemSetId?: number, | ||||||
|  |         force?: boolean, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<CoreRatingSyncItem[]> { | ||||||
|  |         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         const itemSets = await CoreRatingOffline.getItemSets(component, ratingArea, contextLevel, instanceId, itemSetId, siteId); | ||||||
|  | 
 | ||||||
|  |         const results: CoreRatingSyncItem[] = []; | ||||||
|  |         await Promise.all(itemSets.map(async (itemSet) => { | ||||||
|  |             const result = force | ||||||
|  |                 ? await this.syncItemSet( | ||||||
|  |                     component, | ||||||
|  |                     ratingArea, | ||||||
|  |                     itemSet.contextLevel, | ||||||
|  |                     itemSet.instanceId, | ||||||
|  |                     itemSet.itemSetId, | ||||||
|  |                     siteId, | ||||||
|  |                 ) | ||||||
|  |                 : await this.syncItemSetIfNeeded( | ||||||
|  |                     component, | ||||||
|  |                     ratingArea, | ||||||
|  |                     itemSet.contextLevel, | ||||||
|  |                     itemSet.instanceId, | ||||||
|  |                     itemSet.itemSetId, | ||||||
|  |                     siteId, | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |             if (result) { | ||||||
|  |                 if (result.updated) { | ||||||
|  |                     // Sync successful, send event.
 | ||||||
|  |                     CoreEvents.trigger(CoreRatingSyncProvider.SYNCED_EVENT, { | ||||||
|  |                         ...itemSet, | ||||||
|  |                         warnings: result.warnings, | ||||||
|  |                     }, siteId); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 results.push( | ||||||
|  |                     { | ||||||
|  |                         itemSet, | ||||||
|  |                         ...result, | ||||||
|  |                     }, | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         return results; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sync ratings of an item set only if a certain time has passed since the last time. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating Area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemSetId Item set id. Example: forum discussion id. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved when ratings are synced or if it doesn't need to be synced. | ||||||
|  |      */ | ||||||
|  |     protected async syncItemSetIfNeeded( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemSetId: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<CoreRatingSyncItem | undefined> { | ||||||
|  |         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         const syncId = this.getItemSetSyncId(component, ratingArea, contextLevel, instanceId, itemSetId); | ||||||
|  | 
 | ||||||
|  |         const needed = await this.isSyncNeeded(syncId, siteId); | ||||||
|  | 
 | ||||||
|  |         if (needed) { | ||||||
|  |             return this.syncItemSet(component, ratingArea, contextLevel, instanceId, itemSetId, siteId); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Synchronize all offline ratings of an item set. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating Area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemSetId Item set id. Example: forum discussion id. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved if sync is successful, rejected otherwise. | ||||||
|  |      */ | ||||||
|  |     protected syncItemSet( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemSetId: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<CoreRatingSyncItem> { | ||||||
|  |         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         const syncId = this.getItemSetSyncId(component, ratingArea, contextLevel, instanceId, itemSetId); | ||||||
|  |         if (this.isSyncing(syncId, siteId)) { | ||||||
|  |             // There's already a sync ongoing for this item set, return the promise.
 | ||||||
|  |             return this.getOngoingSync(syncId, siteId)!; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.logger.debug(`Try to sync ratings of component '${component}' rating area '${ratingArea}'` + | ||||||
|  |             ` context level '${contextLevel}' instance ${instanceId} item set ${itemSetId}`); | ||||||
|  | 
 | ||||||
|  |         // Get offline events.
 | ||||||
|  |         const syncPromise = this.performSyncItemSet(component, ratingArea, contextLevel, instanceId, itemSetId, siteId); | ||||||
|  | 
 | ||||||
|  |         return this.addOngoingSync(syncId, syncPromise, siteId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Synchronize all offline ratings of an item set. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating Area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemSetId Item set id. Example: forum discussion id. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved if sync is successful, rejected otherwise. | ||||||
|  |      */ | ||||||
|  |     protected async performSyncItemSet( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemSetId: number, | ||||||
|  |         siteId: string, | ||||||
|  |     ): Promise<CoreRatingSyncItem> { | ||||||
|  |         const result: CoreRatingSyncItem = { | ||||||
|  |             updated: [], | ||||||
|  |             warnings: [], | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const ratings = await CoreRatingOffline.getRatings(component, ratingArea, contextLevel, instanceId, itemSetId, siteId); | ||||||
|  | 
 | ||||||
|  |         if (!ratings.length) { | ||||||
|  |             // Nothing to sync.
 | ||||||
|  |             return result; | ||||||
|  |         } | ||||||
|  |         if (!CoreApp.isOnline()) { | ||||||
|  |             // Cannot sync in offline.
 | ||||||
|  |             throw new CoreNetworkError(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const promises = ratings.map(async (rating) => { | ||||||
|  |             try { | ||||||
|  |                 await CoreRating.addRatingOnline( | ||||||
|  |                     component, | ||||||
|  |                     ratingArea, | ||||||
|  |                     rating.contextlevel, | ||||||
|  |                     rating.instanceid, | ||||||
|  |                     rating.itemid, | ||||||
|  |                     rating.scaleid, | ||||||
|  |                     rating.rating, | ||||||
|  |                     rating.rateduserid, | ||||||
|  |                     rating.aggregation, | ||||||
|  |                     siteId, | ||||||
|  |                 ); | ||||||
|  |             } catch (error) { | ||||||
|  |                 if (!CoreUtils.isWebServiceError(error)) { | ||||||
|  |                     // Couldn't connect to server, reject.
 | ||||||
|  |                     throw error; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 const warning = CoreTextUtils.getErrorMessageFromError(error); | ||||||
|  | 
 | ||||||
|  |                 if (warning) { | ||||||
|  |                     result.warnings.push(warning); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             result.updated.push(rating.itemid); | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 return CoreRatingOffline.deleteRating( | ||||||
|  |                     component, | ||||||
|  |                     ratingArea, | ||||||
|  |                     rating.contextlevel, | ||||||
|  |                     rating.instanceid, | ||||||
|  |                     rating.itemid, | ||||||
|  |                     siteId, | ||||||
|  |                 ); | ||||||
|  |             } finally { | ||||||
|  |                 await CoreRating.invalidateRatingItems( | ||||||
|  |                     rating.contextlevel, | ||||||
|  |                     rating.instanceid, | ||||||
|  |                     component, | ||||||
|  |                     ratingArea, | ||||||
|  |                     rating.itemid, | ||||||
|  |                     rating.scaleid, | ||||||
|  |                     undefined, | ||||||
|  |                     siteId, | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         await Promise.all(promises); | ||||||
|  | 
 | ||||||
|  |         // All done, return the result.
 | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the sync id of an item set. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating Area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemSetId Item set id. Example: forum discussion id. | ||||||
|  |      * @return Sync id. | ||||||
|  |      */ | ||||||
|  |     protected getItemSetSyncId( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemSetId: number, | ||||||
|  |     ): string { | ||||||
|  |         return `itemSet#${component}#${ratingArea}#${contextLevel}#${instanceId}#${itemSetId}`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | export const CoreRatingSync = makeSingleton(CoreRatingSyncProvider); | ||||||
|  | 
 | ||||||
|  | declare module '@singletons/events' { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Augment CoreEventsData interface with events specific to this service. | ||||||
|  |      * | ||||||
|  |      * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
 | ||||||
|  |      */ | ||||||
|  |     export interface CoreEventsData { | ||||||
|  |         [CoreRatingSyncProvider.SYNCED_EVENT]: CoreRatingSyncEventData; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type CoreRatingSyncItem = { | ||||||
|  |     itemSet?: CoreRatingItemSet; | ||||||
|  |     warnings: string[]; | ||||||
|  |     updated: number[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data passed to SYNCED_EVENT event. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingSyncEventData = CoreRatingItemSet & { | ||||||
|  |     warnings: string[]; | ||||||
|  | }; | ||||||
							
								
								
									
										588
									
								
								src/core/features/rating/services/rating.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										588
									
								
								src/core/features/rating/services/rating.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,588 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ContextLevel } from '@/core/constants'; | ||||||
|  | import { Injectable } from '@angular/core'; | ||||||
|  | import { CoreSiteWSPreSets, CoreSite } from '@classes/site'; | ||||||
|  | import { CoreUser } from '@features/user/services/user'; | ||||||
|  | import { CoreApp } from '@services/app'; | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { CoreWSExternalWarning } from '@services/ws'; | ||||||
|  | import { makeSingleton } from '@singletons'; | ||||||
|  | import { CoreEvents } from '@singletons/events'; | ||||||
|  | import { CoreRatingOffline } from './rating-offline'; | ||||||
|  | 
 | ||||||
|  | const ROOT_CACHE_KEY = 'CoreRating:'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Service to handle ratings. | ||||||
|  |  */ | ||||||
|  | @Injectable( { providedIn: 'root' }) | ||||||
|  | export class CoreRatingProvider { | ||||||
|  | 
 | ||||||
|  |     static readonly AGGREGATE_NONE = 0; // No ratings.
 | ||||||
|  |     static readonly AGGREGATE_AVERAGE = 1; | ||||||
|  |     static readonly AGGREGATE_COUNT = 2; | ||||||
|  |     static readonly AGGREGATE_MAXIMUM = 3; | ||||||
|  |     static readonly AGGREGATE_MINIMUM = 4; | ||||||
|  |     static readonly AGGREGATE_SUM = 5; | ||||||
|  | 
 | ||||||
|  |     static readonly UNSET_RATING = -999; | ||||||
|  | 
 | ||||||
|  |     static readonly AGGREGATE_CHANGED_EVENT = 'core_rating_aggregate_changed'; | ||||||
|  |     static readonly RATING_SAVED_EVENT = 'core_rating_rating_saved'; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns whether the web serivce to add ratings is available. | ||||||
|  |      * | ||||||
|  |      * @return If WS is abalaible. | ||||||
|  |      * @since 3.2 | ||||||
|  |      */ | ||||||
|  |     isAddRatingWSAvailable(): boolean { | ||||||
|  |         return CoreSites.wsAvailableInCurrentSite('core_rating_add_rating'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add a rating to an item. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param itemSetId Item set id. Example: forum discussion id. | ||||||
|  |      * @param courseId Course id. | ||||||
|  |      * @param scaleId Scale id. | ||||||
|  |      * @param rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating. | ||||||
|  |      * @param ratedUserId Rated user id. | ||||||
|  |      * @param aggregateMethod Aggregate method. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved with the aggregated rating or void if stored offline. | ||||||
|  |      * @since 3.2 | ||||||
|  |      */ | ||||||
|  |     async addRating( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemId: number, | ||||||
|  |         itemSetId: number, | ||||||
|  |         courseId: number, | ||||||
|  |         scaleId: number, | ||||||
|  |         rating: number, | ||||||
|  |         ratedUserId: number, | ||||||
|  |         aggregateMethod: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<CoreRatingAddRatingWSResponse | void> { | ||||||
|  |         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         // Convenience function to store a rating to be synchronized later.
 | ||||||
|  |         const storeOffline = async (): Promise<void> => { | ||||||
|  |             await CoreRatingOffline.addRating( | ||||||
|  |                 component, | ||||||
|  |                 ratingArea, | ||||||
|  |                 contextLevel, | ||||||
|  |                 instanceId, | ||||||
|  |                 itemId, | ||||||
|  |                 itemSetId, | ||||||
|  |                 courseId, | ||||||
|  |                 scaleId, | ||||||
|  |                 rating, | ||||||
|  |                 ratedUserId, | ||||||
|  |                 aggregateMethod, | ||||||
|  |                 siteId, | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             CoreEvents.trigger(CoreRatingProvider.RATING_SAVED_EVENT, { | ||||||
|  |                 component, | ||||||
|  |                 ratingArea, | ||||||
|  |                 contextLevel, | ||||||
|  |                 instanceId, | ||||||
|  |                 itemSetId, | ||||||
|  |                 itemId, | ||||||
|  |             }, siteId); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         if (!CoreApp.isOnline()) { | ||||||
|  |             // App is offline, store the action.
 | ||||||
|  |             return storeOffline(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             await CoreRatingOffline.deleteRating(component, ratingArea, contextLevel, instanceId, itemId, siteId); | ||||||
|  |             this.addRatingOnline( | ||||||
|  |                 component, | ||||||
|  |                 ratingArea, | ||||||
|  |                 contextLevel, | ||||||
|  |                 instanceId, | ||||||
|  |                 itemId, | ||||||
|  |                 scaleId, | ||||||
|  |                 rating, | ||||||
|  |                 ratedUserId, | ||||||
|  |                 aggregateMethod, | ||||||
|  |                 siteId, | ||||||
|  |             ); | ||||||
|  |         } catch (error) { | ||||||
|  |             if (CoreUtils.isWebServiceError(error)) { | ||||||
|  |                 // The WebService has thrown an error or offline not supported, reject.
 | ||||||
|  |                 return Promise.reject(error); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Couldn't connect to server, store offline.
 | ||||||
|  |             return storeOffline(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add a rating to an item. It will fail if offline or cannot connect. | ||||||
|  |      * | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating area. Example: "post". | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param scaleId Scale id. | ||||||
|  |      * @param rating Rating value. Use CoreRatingProvider.UNSET_RATING to delete rating. | ||||||
|  |      * @param ratedUserId Rated user id. | ||||||
|  |      * @param aggregateMethod Aggregate method. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved with the aggregated rating. | ||||||
|  |      * @since 3.2 | ||||||
|  |      */ | ||||||
|  |     async addRatingOnline( | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         itemId: number, | ||||||
|  |         scaleId: number, | ||||||
|  |         rating: number, | ||||||
|  |         ratedUserId: number, | ||||||
|  |         aggregateMethod: number, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<CoreRatingAddRatingWSResponse> { | ||||||
|  | 
 | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  |         const params: CoreRatingAddRatingWSParams = { | ||||||
|  |             contextlevel: contextLevel, | ||||||
|  |             instanceid: instanceId, | ||||||
|  |             component, | ||||||
|  |             ratingarea: ratingArea, | ||||||
|  |             itemid: itemId, | ||||||
|  |             scaleid: scaleId, | ||||||
|  |             rating, | ||||||
|  |             rateduserid: ratedUserId, | ||||||
|  |             aggregation: aggregateMethod, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const response = await site.write<CoreRatingAddRatingWSResponse>('core_rating_add_rating', params); | ||||||
|  | 
 | ||||||
|  |         await this.invalidateRatingItems(contextLevel, instanceId, component, ratingArea, itemId, scaleId); | ||||||
|  | 
 | ||||||
|  |         CoreEvents.trigger(CoreRatingProvider.AGGREGATE_CHANGED_EVENT, { | ||||||
|  |             contextLevel, | ||||||
|  |             instanceId, | ||||||
|  |             component, | ||||||
|  |             ratingArea, | ||||||
|  |             itemId, | ||||||
|  |             aggregate: response.aggregate, | ||||||
|  |             count: response.count, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get item ratings. | ||||||
|  |      * | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating area. Example: "post". | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param scaleId Scale id. | ||||||
|  |      * @param sort Sort field. | ||||||
|  |      * @param courseId Course id. Used for fetching user profiles. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). | ||||||
|  |      * @return Promise resolved with the list of ratings. | ||||||
|  |      */ | ||||||
|  |     async getItemRatings( | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         itemId: number, | ||||||
|  |         scaleId: number, | ||||||
|  |         sort: string = 'timemodified', | ||||||
|  |         courseId?: number, | ||||||
|  |         siteId?: string, | ||||||
|  |         ignoreCache: boolean = false, | ||||||
|  |     ): Promise<CoreRatingItemRating[]> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const params: CoreRatingGetItemRatingsWSParams = { | ||||||
|  |             contextlevel: contextLevel, | ||||||
|  |             instanceid: instanceId, | ||||||
|  |             component, | ||||||
|  |             ratingarea: ratingArea, | ||||||
|  |             itemid: itemId, | ||||||
|  |             scaleid: scaleId, | ||||||
|  |             sort, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const preSets: CoreSiteWSPreSets = { | ||||||
|  |             cacheKey: this.getItemRatingsCacheKey(contextLevel, instanceId, component, ratingArea, itemId, scaleId, sort), | ||||||
|  |             updateFrequency: CoreSite.FREQUENCY_RARELY, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         if (ignoreCache) { | ||||||
|  |             preSets.getFromCache = false; | ||||||
|  |             preSets.emergencyCache = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const response = await site.read<CoreRatingGetItemRatingsWSResponse>('core_rating_get_item_ratings', params, preSets); | ||||||
|  | 
 | ||||||
|  |         if (!site.isVersionGreaterEqualThan([' 3.6.5', '3.7.1', '3.8'])) { | ||||||
|  |             // MDL-65042 We need to fetch profiles because the returned profile pictures are incorrect.
 | ||||||
|  |             const promises = response.ratings.map((rating: CoreRatingItemRating) => | ||||||
|  |                 CoreUser.getProfile(rating.userid, courseId, true, site.id).then((user) => { | ||||||
|  |                     rating.userpictureurl = user.profileimageurl || ''; | ||||||
|  | 
 | ||||||
|  |                     return; | ||||||
|  |                 }).catch(() => { | ||||||
|  |                     // Ignore error.
 | ||||||
|  |                     rating.userpictureurl = ''; | ||||||
|  |                 })); | ||||||
|  | 
 | ||||||
|  |             await Promise.all(promises); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return response.ratings; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Invalidate item ratings. | ||||||
|  |      * | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Context instance id. | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating area. Example: "post". | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param scaleId Scale id. | ||||||
|  |      * @param sort Sort field. | ||||||
|  |      * @param siteId Site ID. If not defined, current site. | ||||||
|  |      * @return Promise resolved when the data is invalidated. | ||||||
|  |      */ | ||||||
|  |     async invalidateRatingItems( | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         itemId: number, | ||||||
|  |         scaleId: number, | ||||||
|  |         sort: string = 'timemodified', | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<void> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const key = this.getItemRatingsCacheKey(contextLevel, instanceId, component, ratingArea, itemId, scaleId, sort); | ||||||
|  | 
 | ||||||
|  |         await site.invalidateWsCacheForKey(key); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if rating is disabled in a certain site. | ||||||
|  |      * | ||||||
|  |      * @param site Site. If not defined, use current site. | ||||||
|  |      * @return Whether it's disabled. | ||||||
|  |      */ | ||||||
|  |     isRatingDisabledInSite(site?: CoreSite): boolean { | ||||||
|  |         site = site || CoreSites.getCurrentSite(); | ||||||
|  | 
 | ||||||
|  |         return !!site?.isFeatureDisabled('NoDelegate_CoreRating'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if rating is disabled in a certain site. | ||||||
|  |      * | ||||||
|  |      * @param siteId Site Id. If not defined, use current site. | ||||||
|  |      * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. | ||||||
|  |      */ | ||||||
|  |     async isRatingDisabled(siteId?: string): Promise<boolean> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         return this.isRatingDisabledInSite(site); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Convenience function to merge two or more rating infos of the same instance. | ||||||
|  |      * | ||||||
|  |      * @param ratingInfos Array of rating infos. | ||||||
|  |      * @return Merged rating info or undefined. | ||||||
|  |      */ | ||||||
|  |     mergeRatingInfos(ratingInfos: CoreRatingInfo[]): CoreRatingInfo | undefined { | ||||||
|  |         let result: CoreRatingInfo | undefined; | ||||||
|  |         const scales: Record<number, CoreRatingScale> = {}; | ||||||
|  |         const ratings: Record<number, CoreRatingInfoItem> = {}; | ||||||
|  | 
 | ||||||
|  |         ratingInfos.forEach((ratingInfo) => { | ||||||
|  |             if (!ratingInfo) { | ||||||
|  |                 // Skip null rating infos.
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!result) { | ||||||
|  |                 result = Object.assign({}, ratingInfo); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             (ratingInfo.scales || []).forEach((scale) => { | ||||||
|  |                 scales[scale.id] = scale; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             (ratingInfo.ratings || []).forEach((rating) => { | ||||||
|  |                 ratings[rating.itemid] = rating; | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         if (result) { | ||||||
|  |             result.scales = CoreUtils.objectToArray(scales); | ||||||
|  |             result.ratings = CoreUtils.objectToArray(ratings); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Prefetch individual ratings. | ||||||
|  |      * | ||||||
|  |      * This function should be called from the prefetch handler of activities with ratings. | ||||||
|  |      * | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param instanceId Instance id. | ||||||
|  |      * @param siteId Site id. If not defined, current site. | ||||||
|  |      * @param courseId Course id. Used for prefetching user profiles. | ||||||
|  |      * @param ratingInfo Rating info returned by web services. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async prefetchRatings( | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         scaleId: number, | ||||||
|  |         courseId?: number, | ||||||
|  |         ratingInfo?: CoreRatingInfo, | ||||||
|  |         siteId?: string, | ||||||
|  |     ): Promise<void> { | ||||||
|  |         if (!ratingInfo || !ratingInfo.ratings) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  |         const promises = ratingInfo.ratings.map((item) => this.getItemRatings( | ||||||
|  |             contextLevel, | ||||||
|  |             instanceId, | ||||||
|  |             ratingInfo.component, | ||||||
|  |             ratingInfo.ratingarea, | ||||||
|  |             item.itemid, | ||||||
|  |             scaleId, | ||||||
|  |             undefined, | ||||||
|  |             courseId, | ||||||
|  |             site.id, | ||||||
|  |             true, | ||||||
|  |         )); | ||||||
|  | 
 | ||||||
|  |         if (!site.isVersionGreaterEqualThan([' 3.6.5', '3.7.1', '3.8'])) { | ||||||
|  |             promises.map((promise) => promise.then(async (ratings) => { | ||||||
|  |                 const userIds = ratings.map((rating: CoreRatingItemRating) => rating.userid); | ||||||
|  | 
 | ||||||
|  |                 await CoreUser.prefetchProfiles(userIds, courseId, site.id); | ||||||
|  | 
 | ||||||
|  |                 return; | ||||||
|  |             })); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await Promise.all(promises); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get cache key for rating items WS calls. | ||||||
|  |      * | ||||||
|  |      * @param contextLevel Context level: course, module, user, etc. | ||||||
|  |      * @param component Component. Example: "mod_forum". | ||||||
|  |      * @param ratingArea Rating area. Example: "post". | ||||||
|  |      * @param itemId Item id. Example: forum post id. | ||||||
|  |      * @param scaleId Scale id. | ||||||
|  |      * @param sort Sort field. | ||||||
|  |      * @return Cache key. | ||||||
|  |      */ | ||||||
|  |     protected getItemRatingsCacheKey( | ||||||
|  |         contextLevel: ContextLevel, | ||||||
|  |         instanceId: number, | ||||||
|  |         component: string, | ||||||
|  |         ratingArea: string, | ||||||
|  |         itemId: number, | ||||||
|  |         scaleId: number, | ||||||
|  |         sort: string, | ||||||
|  |     ): string { | ||||||
|  |         return `${ROOT_CACHE_KEY}${contextLevel}:${instanceId}:${component}:${ratingArea}:${itemId}:${scaleId}:${sort}`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | export const CoreRating = makeSingleton(CoreRatingProvider); | ||||||
|  | 
 | ||||||
|  | declare module '@singletons/events' { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Augment CoreEventsData interface with events specific to this service. | ||||||
|  |      * | ||||||
|  |      * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
 | ||||||
|  |      */ | ||||||
|  |     export interface CoreEventsData { | ||||||
|  |         [CoreRatingProvider.AGGREGATE_CHANGED_EVENT]: CoreRatingAggregateChangedEventData; | ||||||
|  |         [CoreRatingProvider.RATING_SAVED_EVENT]: CoreRatingSavedEventData; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Structure of the rating info returned by web services. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingInfo = { | ||||||
|  |     contextid: number; // Context id.
 | ||||||
|  |     component: string; // Context name.
 | ||||||
|  |     ratingarea: string; // Rating area name.
 | ||||||
|  |     canviewall?: boolean; // Whether the user can view all the individual ratings.
 | ||||||
|  |     canviewany?: boolean; // Whether the user can view aggregate of ratings of others.
 | ||||||
|  |     scales?: CoreRatingScale[]; // Different scales used information.
 | ||||||
|  |     ratings?: CoreRatingInfoItem[]; // The ratings.
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Structure of scales in the rating info. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingScale = { | ||||||
|  |     id: number; // Scale id.
 | ||||||
|  |     courseid?: number; // Course id.
 | ||||||
|  |     name?: string; // Scale name (when a real scale is used).
 | ||||||
|  |     max: number; // Max value for the scale.
 | ||||||
|  |     isnumeric: boolean; // Whether is a numeric scale.
 | ||||||
|  |     items?: { // Scale items. Only returned for not numerical scales.
 | ||||||
|  |         value: number; // Scale value/option id.
 | ||||||
|  |         name: string; // Scale name.
 | ||||||
|  |     }[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Structure of items in the rating info. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingInfoItem = { | ||||||
|  |     itemid: number; // Item id.
 | ||||||
|  |     scaleid?: number; // Scale id.
 | ||||||
|  |     scale?: CoreRatingScale; // Added for rendering purposes.
 | ||||||
|  |     userid?: number; // User who rated id.
 | ||||||
|  |     aggregate?: number; // Aggregated ratings grade.
 | ||||||
|  |     aggregatestr?: string; // Aggregated ratings as string.
 | ||||||
|  |     aggregatelabel?: string; // The aggregation label.
 | ||||||
|  |     count?: number; // Ratings count (used when aggregating).
 | ||||||
|  |     rating?: number; // The rating the user gave.
 | ||||||
|  |     canrate?: boolean; // Whether the user can rate the item.
 | ||||||
|  |     canviewaggregate?: boolean; // Whether the user can view the aggregated grade.
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Structure of a rating returned by the item ratings web service. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingItemRating = { | ||||||
|  |     id: number; // Rating id.
 | ||||||
|  |     userid: number; // User id.
 | ||||||
|  |     userpictureurl: string; // URL user picture.
 | ||||||
|  |     userfullname: string; // User fullname.
 | ||||||
|  |     rating: string; // Rating on scale.
 | ||||||
|  |     timemodified: number; // Time modified (timestamp).
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Params of core_rating_get_item_ratings WS. | ||||||
|  |  */ | ||||||
|  | type CoreRatingGetItemRatingsWSParams = { | ||||||
|  |     contextlevel: ContextLevel; // Context level: course, module, user, etc...
 | ||||||
|  |     instanceid: number; // The instance id of item associated with the context level.
 | ||||||
|  |     component: string; // Component.
 | ||||||
|  |     ratingarea: string; // Rating area.
 | ||||||
|  |     itemid: number; // Associated id.
 | ||||||
|  |     scaleid: number; // Scale id.
 | ||||||
|  |     sort: string; // Sort order (firstname, rating or timemodified).
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data returned by core_rating_get_item_ratings WS. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingGetItemRatingsWSResponse = { | ||||||
|  |     ratings: CoreRatingItemRating[]; | ||||||
|  |     warnings?: CoreWSExternalWarning[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Params of core_rating_add_rating WS. | ||||||
|  |  */ | ||||||
|  | type CoreRatingAddRatingWSParams = { | ||||||
|  |     contextlevel: ContextLevel; // Context level: course, module, user, etc...
 | ||||||
|  |     instanceid: number; // The instance id of item associated with the context level.
 | ||||||
|  |     component: string; // Component.
 | ||||||
|  |     ratingarea: string; // Rating area.
 | ||||||
|  |     itemid: number; // Associated id.
 | ||||||
|  |     scaleid: number; // Scale id.
 | ||||||
|  |     rating: number; // User rating.
 | ||||||
|  |     rateduserid: number; // Rated user id.
 | ||||||
|  |     aggregation?: number; // Agreggation method.
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data returned by core_rating_add_rating WS. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingAddRatingWSResponse = { | ||||||
|  |     success: boolean; // Whether the rate was successfully created.
 | ||||||
|  |     aggregate?: string; // New aggregate.
 | ||||||
|  |     count?: number; // Ratings count.
 | ||||||
|  |     itemid?: number; // Rating item id.
 | ||||||
|  |     warnings?: CoreWSExternalWarning[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data sent by AGGREGATE_CHANGED_EVENT event. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingAggregateChangedEventData = { | ||||||
|  |     contextLevel: ContextLevel; | ||||||
|  |     instanceId: number; | ||||||
|  |     component: string; | ||||||
|  |     ratingArea: string; | ||||||
|  |     itemId: number; | ||||||
|  |     aggregate?: string; | ||||||
|  |     count?: number; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data sent by RATING_SAVED_EVENT event. | ||||||
|  |  */ | ||||||
|  | export type CoreRatingSavedEventData = { | ||||||
|  |     component: string; | ||||||
|  |     ratingArea: string; | ||||||
|  |     contextLevel: ContextLevel; | ||||||
|  |     instanceId: number; | ||||||
|  |     itemSetId: number; | ||||||
|  |     itemId: number; | ||||||
|  | }; | ||||||
| @ -1,8 +1,5 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <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-title>{{ title }}</ion-title> | ||||||
| 
 | 
 | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user