Merge pull request #3583 from NoelDeMartin/MOBILE-4188
MOBILE-4188: Implement teacher gradebook
This commit is contained in:
		
						commit
						e87408418b
					
				| @ -22,7 +22,6 @@ const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: CoreGradesCoursePage, | ||||
|         data: { swipeEnabled: false }, | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,43 @@ | ||||
| // (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 { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| 
 | ||||
| import { CoreUserParticipantsPageModule } from '@features/user/pages/participants/participants.module'; | ||||
| import { CoreUserParticipantsPage } from '@features/user/pages/participants/participants.page'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: CoreUserParticipantsPage, | ||||
|         children: conditionalRoutes([ | ||||
|             { | ||||
|                 path: ':userId', | ||||
|                 loadChildren: () => import('./grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule), | ||||
|                 data: { swipeManagerSource: 'participants' }, | ||||
|             }, | ||||
|         ], () => CoreScreen.isTablet), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         CoreUserParticipantsPageModule, | ||||
|     ], | ||||
| }) | ||||
| export class CoreGradesCourseParticipantsLazyModule {} | ||||
| @ -22,12 +22,16 @@ import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-ro | ||||
| import { CoreUserDelegate } from '@features/user/services/user-delegate'; | ||||
| import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module'; | ||||
| import { CoreGradesProvider } from './services/grades'; | ||||
| import { CoreGradesHelperProvider, GRADES_PAGE_NAME } from './services/grades-helper'; | ||||
| import { CoreGradesHelperProvider, GRADES_PAGE_NAME, GRADES_PARTICIPANTS_PAGE_NAME } from './services/grades-helper'; | ||||
| import { CoreGradesCourseOptionHandler } from './services/handlers/course-option'; | ||||
| import { CoreGradesOverviewLinkHandler } from './services/handlers/overview-link'; | ||||
| import { CoreGradesUserHandler } from './services/handlers/user'; | ||||
| import { CoreGradesReportLinkHandler } from './services/handlers/report-link'; | ||||
| import { CoreGradesUserLinkHandler } from './services/handlers/user-link'; | ||||
| import { CoreGradesCourseParticipantsOptionHandler } from '@features/grades/services/handlers/course-participants-option'; | ||||
| import { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { COURSE_INDEX_PATH } from '@features/course/course-lazy.module'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| export const CORE_GRADES_SERVICES: Type<unknown>[] = [ | ||||
|     CoreGradesProvider, | ||||
| @ -38,11 +42,19 @@ const mainMenuChildrenRoutes: Routes = [ | ||||
|     { | ||||
|         path: GRADES_PAGE_NAME, | ||||
|         loadChildren: () => import('./grades-courses-lazy.module').then(m => m.CoreGradesCoursesLazyModule), | ||||
|         data: { swipeManagerSource: 'courses' }, | ||||
|     }, | ||||
|     { | ||||
|         path: `${COURSE_PAGE_NAME}/:courseId/${PARTICIPANTS_PAGE_NAME}/:userId/${GRADES_PAGE_NAME}`, | ||||
|         loadChildren: () => import('./grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule), | ||||
|     }, | ||||
|     ...conditionalRoutes([ | ||||
|         { | ||||
|             path: `${COURSE_PAGE_NAME}/${COURSE_INDEX_PATH}/${GRADES_PARTICIPANTS_PAGE_NAME}/:userId`, | ||||
|             loadChildren: () => import('./grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule), | ||||
|             data: { swipeManagerSource: 'participants' }, | ||||
|         }, | ||||
|     ], () => CoreScreen.isMobile), | ||||
| ]; | ||||
| 
 | ||||
| const courseIndexRoutes: Routes = [ | ||||
| @ -50,6 +62,10 @@ const courseIndexRoutes: Routes = [ | ||||
|         path: GRADES_PAGE_NAME, | ||||
|         loadChildren: () => import('./grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule), | ||||
|     }, | ||||
|     { | ||||
|         path: GRADES_PARTICIPANTS_PAGE_NAME, | ||||
|         loadChildren: () => import('./grades-course-participants-lazy.module').then(m => m.CoreGradesCourseParticipantsLazyModule), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
| @ -67,6 +83,7 @@ const courseIndexRoutes: Routes = [ | ||||
|                 CoreContentLinksDelegate.registerHandler(CoreGradesUserLinkHandler.instance); | ||||
|                 CoreContentLinksDelegate.registerHandler(CoreGradesOverviewLinkHandler.instance); | ||||
|                 CoreCourseOptionsDelegate.registerHandler(CoreGradesCourseOptionHandler.instance); | ||||
|                 CoreCourseOptionsDelegate.registerHandler(CoreGradesCourseParticipantsOptionHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|         </ion-title> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content [core-swipe-navigation]="courses"> | ||||
| <ion-content [core-swipe-navigation]="swipeManager"> | ||||
|     <ion-refresher slot="fixed" [disabled]="!columns || !rows" (ionRefresh)="refreshGrades($event.target)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; | ||||
| import { AfterViewInit, Component, ElementRef, OnDestroy } from '@angular/core'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| 
 | ||||
| @ -21,6 +21,7 @@ import { CoreGrades } from '@features/grades/services/grades'; | ||||
| import { | ||||
|     CoreGradesFormattedTableColumn, | ||||
|     CoreGradesFormattedTableRow, | ||||
|     CoreGradesGradeOverviewWithCourseData, | ||||
|     CoreGradesHelper, | ||||
| } from '@features/grades/services/grades-helper'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| @ -30,6 +31,8 @@ import { CoreScreen } from '@services/screen'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; | ||||
| import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; | ||||
| import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; | ||||
| import { CoreUserData, CoreUserParticipant } from '@features/user/services/user'; | ||||
| import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source'; | ||||
| import { CoreDom } from '@singletons/dom'; | ||||
| 
 | ||||
| @ -49,7 +52,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|     expandLabel!: string; | ||||
|     collapseLabel!: string; | ||||
|     title?: string; | ||||
|     courses?: CoreSwipeNavigationItemsManager; | ||||
|     swipeManager?: CoreGradesCourseSwipeManager; | ||||
|     columns: CoreGradesFormattedTableColumn[] = []; | ||||
|     rows: CoreGradesFormattedTableRow[] = []; | ||||
|     rowsOnView = 0; | ||||
| @ -72,10 +75,17 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|             this.collapseLabel = Translate.instant('core.collapse'); | ||||
|             this.useLegacyLayout = !CoreSites.getRequiredCurrentSite().isVersionGreaterEqualThan('4.1'); | ||||
| 
 | ||||
|             if (route.snapshot.data.swipeEnabled ?? true) { | ||||
|                 const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreGradesCoursesSource, []); | ||||
| 
 | ||||
|                 this.courses = new CoreSwipeNavigationItemsManager(source); | ||||
|             switch (route.snapshot.data.swipeManagerSource) { | ||||
|                 case 'courses': | ||||
|                     this.swipeManager = new CoreGradesCourseCoursesSwipeManager( | ||||
|                         CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreGradesCoursesSource, []), | ||||
|                     ); | ||||
|                     break; | ||||
|                 case 'participants': | ||||
|                     this.swipeManager = new CoreGradesCourseParticipantsSwipeManager( | ||||
|                         CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]), | ||||
|                     ); | ||||
|                     break; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
| @ -96,7 +106,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|     async ngAfterViewInit(): Promise<void> { | ||||
|         this.withinSplitView = !!this.element.nativeElement.parentElement?.closest('core-split-view'); | ||||
| 
 | ||||
|         await this.courses?.start(); | ||||
|         await this.swipeManager?.start(); | ||||
|         await this.fetchInitialGrades(); | ||||
|     } | ||||
| 
 | ||||
| @ -104,7 +114,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.courses?.destroy(); | ||||
|         this.swipeManager?.destroy(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -208,7 +218,9 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|         const table = await CoreGrades.getCourseGradesTable(this.courseId, this.userId); | ||||
|         const formattedTable = await CoreGradesHelper.formatGradesTable(table); | ||||
| 
 | ||||
|         this.title = formattedTable.rows[0]?.gradeitem ?? Translate.instant('core.grades.grades'); | ||||
|         this.title = this.swipeManager?.getPageTitle() | ||||
|             ?? formattedTable.rows[0]?.gradeitem | ||||
|             ?? Translate.instant('core.grades.grades'); | ||||
|         this.columns = formattedTable.columns; | ||||
|         this.rows = formattedTable.rows; | ||||
|         this.rowsOnView = this.getRowsOnHeight(); | ||||
| @ -240,3 +252,64 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Swipe manager helper methods. | ||||
|  */ | ||||
| interface CoreGradesCourseSwipeManager extends CoreSwipeNavigationItemsManager { | ||||
| 
 | ||||
|     /** | ||||
|      * Get title to use in the current page. | ||||
|      */ | ||||
|     getPageTitle(): string | undefined; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Swipe manager for courses grades. | ||||
|  */ | ||||
| class CoreGradesCourseCoursesSwipeManager extends CoreSwipeNavigationItemsManager<CoreGradesGradeOverviewWithCourseData> | ||||
|     implements CoreGradesCourseSwipeManager { | ||||
| 
 | ||||
|     constructor(source: CoreGradesCoursesSource) { | ||||
|         super(source); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getPageTitle(): string | undefined { | ||||
|         const selectedItem = this.getSelectedItem(); | ||||
| 
 | ||||
|         return selectedItem?.courseFullName; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Swipe manager for participants grades. | ||||
|  */ | ||||
| class CoreGradesCourseParticipantsSwipeManager extends CoreSwipeNavigationItemsManager<CoreUserParticipant | CoreUserData> | ||||
|     implements CoreGradesCourseSwipeManager { | ||||
| 
 | ||||
|     constructor(source: CoreUserParticipantsSource) { | ||||
|         super(source); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getPageTitle(): string | undefined { | ||||
|         const selectedItem = this.getSelectedItem(); | ||||
| 
 | ||||
|         return selectedItem?.fullname; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { | ||||
|         return route.params.userId; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -16,8 +16,13 @@ import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | ||||
| import { CoreCourses, CoreEnrolledCourseData, CoreCourseSearchedData } from '@features/courses/services/courses'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreCourses, | ||||
|     CoreEnrolledCourseData, | ||||
|     CoreCourseSearchedData, | ||||
|     CoreCourseUserAdminOrNavOptionIndexed, | ||||
| } from '@features/courses/services/courses'; | ||||
| import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreGrades, | ||||
|     CoreGradesGradeItem, | ||||
| @ -38,8 +43,10 @@ import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreCourseHelper } from '@features/course/services/course-helper'; | ||||
| import { CoreAppProvider } from '@services/app'; | ||||
| import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | ||||
| import { CoreCourseAccess } from '@features/course/services/course-options-delegate'; | ||||
| 
 | ||||
| export const GRADES_PAGE_NAME = 'grades'; | ||||
| export const GRADES_PARTICIPANTS_PAGE_NAME = 'participant-grades'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features regarding grades information. | ||||
| @ -787,6 +794,30 @@ export class CoreGradesHelperProvider { | ||||
|         return 'outcomeid' in item; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether to show the gradebook to this user. | ||||
|      * | ||||
|      * @param courseId The course ID. | ||||
|      * @param accessData Access type and data. Default, guest, ... | ||||
|      * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @returns Whether to show the gradebook to this user. | ||||
|      */ | ||||
|     async showGradebook( | ||||
|         courseId: number, | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { | ||||
|             return false; // Not enabled for guests.
 | ||||
|         } | ||||
| 
 | ||||
|         if (navOptions && navOptions.grades !== undefined) { | ||||
|             return navOptions.grades; | ||||
|         } | ||||
| 
 | ||||
|         return CoreGrades.isPluginEnabledForCourse(courseId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreGradesHelper = makeSingleton(CoreGradesHelperProvider); | ||||
|  | ||||
| @ -77,6 +77,16 @@ export class CoreGradesProvider { | ||||
|         return this.ROOT_CACHE_KEY + 'items:' + courseId + ':'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get prefix cache key for grade permissions WS calls. | ||||
|      * | ||||
|      * @param courseId ID of the course to check permissions. | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getCourseGradesPermissionsCacheKey(courseId: number): string { | ||||
|         return this.getCourseGradesPrefixCacheKey(courseId) + ':canviewallgrades'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for courses grade WS calls. | ||||
|      * | ||||
| @ -290,6 +300,17 @@ export class CoreGradesProvider { | ||||
|         await site.invalidateWsCacheForKey(this.getCourseGradesItemsCacheKey(courseId, userId, groupId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates course grade permissions WS calls. | ||||
|      * | ||||
|      * @param courseId ID of the course to get the permissions from. | ||||
|      */ | ||||
|     async invalidateCourseGradesPermissionsData(courseId: number): Promise<void> { | ||||
|         const site = CoreSites.getRequiredCurrentSite(); | ||||
| 
 | ||||
|         await site.invalidateWsCacheForKey(this.getCourseGradesPermissionsCacheKey(courseId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not the plugin is enabled for a certain site. | ||||
|      * | ||||
| @ -389,6 +410,30 @@ export class CoreGradesProvider { | ||||
|         await site?.write('gradereport_overview_view_grade_report', params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the current user can view all the grades in the course. | ||||
|      * | ||||
|      * @param courseId Course id. | ||||
|      * @returns Whether the current user can view all the grades. | ||||
|      */ | ||||
|     async canViewAllGrades(courseId: number): Promise<boolean> { | ||||
|         const site = CoreSites.getRequiredCurrentSite(); | ||||
| 
 | ||||
|         if (!site.wsAvailable('gradereport_user_get_access_information')) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const params: CoreGradesGetUserAccessInformationWSParams = { courseid: courseId }; | ||||
|         const preSets: CoreSiteWSPreSets = { cacheKey: this.getCourseGradesPermissionsCacheKey(courseId) }; | ||||
|         const access = await site.read<CoreGradesGetUserAccessInformationWSResponse>( | ||||
|             'gradereport_user_get_access_information', | ||||
|             params, | ||||
|             preSets, | ||||
|         ); | ||||
| 
 | ||||
|         return access.canviewallgrades; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreGrades = makeSingleton(CoreGradesProvider); | ||||
| @ -426,6 +471,13 @@ type CoreGradesGetOverviewCourseGradesWSParams = { | ||||
|     userid?: number; // Get grades for this user (optional, default current).
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Params of gradereport_user_get_access_information WS. | ||||
|  */ | ||||
| type CoreGradesGetUserAccessInformationWSParams = { | ||||
|     courseid: number; // Id of the course.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by gradereport_user_get_grade_items WS. | ||||
|  */ | ||||
| @ -457,6 +509,15 @@ export type CoreGradesGetOverviewCourseGradesWSResponse = { | ||||
|     warnings?: CoreWSExternalWarning[]; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by gradereport_user_get_access_information WS. | ||||
|  */ | ||||
| type CoreGradesGetUserAccessInformationWSResponse = { | ||||
|     canviewusergradereport: boolean; | ||||
|     canviewmygrades: boolean; | ||||
|     canviewallgrades: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Grade item data. | ||||
|  */ | ||||
|  | ||||
| @ -13,13 +13,13 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCourseProvider } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreCourseAccess, | ||||
|     CoreCourseOptionsHandler, | ||||
|     CoreCourseOptionsHandlerData, | ||||
| } from '@features/course/services/course-options-delegate'; | ||||
| import { CoreCourseAnyCourseData, CoreCourses, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; | ||||
| import { CoreGradesHelper, GRADES_PAGE_NAME } from '@features/grades/services/grades-helper'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreGrades } from '../grades'; | ||||
| 
 | ||||
| @ -35,13 +35,15 @@ export class CoreGradesCourseOptionHandlerService implements CoreCourseOptionsHa | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise<void> { | ||||
|     async invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise<void> { | ||||
|         await CoreGrades.invalidateCourseGradesPermissionsData(courseId); | ||||
| 
 | ||||
|         if (navOptions && navOptions.grades !== undefined) { | ||||
|             // No need to invalidate anything.
 | ||||
|             return Promise.resolve(); | ||||
|             // No need to invalidate user courses.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         return CoreCourses.invalidateUserCourses(); | ||||
|         await CoreCourses.invalidateUserCourses(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -54,20 +56,20 @@ export class CoreGradesCourseOptionHandlerService implements CoreCourseOptionsHa | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     isEnabledForCourse( | ||||
|     async isEnabledForCourse( | ||||
|         courseId: number, | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): boolean | Promise<boolean> { | ||||
|         if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { | ||||
|             return false; // Not enabled for guests.
 | ||||
|     ): Promise<boolean> { | ||||
|         const showGradebook = await CoreGradesHelper.showGradebook(courseId, accessData, navOptions); | ||||
| 
 | ||||
|         if (!showGradebook) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         if (navOptions && navOptions.grades !== undefined) { | ||||
|             return navOptions.grades; | ||||
|         } | ||||
|         const canViewAllGrades = await CoreGrades.canViewAllGrades(courseId); | ||||
| 
 | ||||
|         return CoreGrades.isPluginEnabledForCourse(courseId); | ||||
|         return !canViewAllGrades; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -77,7 +79,7 @@ export class CoreGradesCourseOptionHandlerService implements CoreCourseOptionsHa | ||||
|         return { | ||||
|             title: 'core.grades.grades', | ||||
|             class: 'core-grades-course-handler', | ||||
|             page: 'grades', | ||||
|             page: GRADES_PAGE_NAME, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,88 @@ | ||||
| // (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 { Injectable } from '@angular/core'; | ||||
| import { | ||||
|     CoreCourseAccess, | ||||
|     CoreCourseOptionsHandler, | ||||
|     CoreCourseOptionsHandlerData, | ||||
| } from '@features/course/services/course-options-delegate'; | ||||
| import { CoreCourses, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; | ||||
| import { CoreGrades } from '@features/grades/services/grades'; | ||||
| import { CoreGradesHelper, GRADES_PARTICIPANTS_PAGE_NAME } from '@features/grades/services/grades-helper'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Course nav handler. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreGradesCourseParticipantsOptionHandlerService implements CoreCourseOptionsHandler { | ||||
| 
 | ||||
|     name = 'CoreGradesParticipants'; | ||||
|     priority = 400; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise<void> { | ||||
|         await CoreGrades.invalidateCourseGradesPermissionsData(courseId); | ||||
| 
 | ||||
|         if (navOptions && navOptions.grades !== undefined) { | ||||
|             // No need to invalidate user courses.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await CoreCourses.invalidateUserCourses(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabledForCourse( | ||||
|         courseId: number, | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         const showGradebook = await CoreGradesHelper.showGradebook(courseId, accessData, navOptions); | ||||
| 
 | ||||
|         if (!showGradebook) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const canViewAllGrades = await CoreGrades.canViewAllGrades(courseId); | ||||
| 
 | ||||
|         return canViewAllGrades; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getDisplayData(): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> { | ||||
|         return { | ||||
|             title: 'core.grades.grades', | ||||
|             class: 'core-grades-course-participants-handler', | ||||
|             page: GRADES_PARTICIPANTS_PAGE_NAME, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreGradesCourseParticipantsOptionHandler = makeSingleton(CoreGradesCourseParticipantsOptionHandlerService); | ||||
							
								
								
									
										100
									
								
								src/core/features/grades/tests/behat/navigation-401.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/core/features/grades/tests/behat/navigation-401.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| @app @javascript @lms_upto4.1 | ||||
| Feature: Grades navigation | ||||
| 
 | ||||
|   Background: | ||||
|     Given the following "users" exist: | ||||
|       | username  | firstname | lastname | | ||||
|       | student1  | Student   | first    | | ||||
|       | student2  | Student   | second    | | ||||
|       | teacher1  | Teacher   | first   | | ||||
|     And the following "courses" exist: | ||||
|       | fullname | shortname | | ||||
|       | Course 2 | C2        | | ||||
|       | Course 1 | C1        | | ||||
|     And the following "course enrolments" exist: | ||||
|       | user     | course | role    | | ||||
|       | student1 | C1     | student | | ||||
|       | student1 | C2     | student | | ||||
|       | student2 | C2     | student | | ||||
|       | teacher1 | C2     | editingteacher | | ||||
|     And the following "grade categories" exist: | ||||
|       | fullname | course | | ||||
|       | GC C1    | C1     | | ||||
|       | GC C2.1  | C2     | | ||||
|       | GC C2.2  | C2     | | ||||
|     And the following "grade items" exist: | ||||
|       | gradecategory | itemname  | grademin | grademax | course | | ||||
|       | GC C1         | GI C1     | 20       | 40       | C1     | | ||||
|       | GC C2.1       | GI C2.1.1 | 60       | 80       | C2     | | ||||
|       | GC C2.1       | GI C2.1.2 | 10       | 90       | C2     | | ||||
|       | GC C2.2       | GI C2.2.1 | 0        | 100      | C2     | | ||||
|     And the following "grade grades" exist: | ||||
|       | gradeitem | user     | grade | | ||||
|       | GI C1     | student1 | 30    | | ||||
|       | GI C2.1.1 | student1 | 70    | | ||||
|       | GI C2.1.2 | student1 | 20    | | ||||
|       | GI C2.2.1 | student1 | 40    | | ||||
| 
 | ||||
|   Scenario: Mobile navigation (teacher) | ||||
|     Given I entered the course "Course 2" as "teacher1" in the app | ||||
| 
 | ||||
|     # Course grades | ||||
|     When I press "Participants" in the app | ||||
|     And I press "Student first" in the app | ||||
|     And I press "Grades" in the app | ||||
|     Then I should find "GC C2.1" in the app | ||||
|     And I should find "70" within "GI C2.1.1" "tr" in the app | ||||
|     And I should find "20" within "GI C2.1.2" "tr" in the app | ||||
|     And I should find "90" within "GC C2.1 total" "tr" in the app | ||||
|     And I should find "GC C2.2" in the app | ||||
|     And I should find "40" within "GI C2.2.1" "tr" in the app | ||||
|     And I should find "40" within "GC C2.2 total" "tr" in the app | ||||
|     And I should find "130" within "Course total" "tr" in the app | ||||
|     But I should not find "GC C1" in the app | ||||
|     And I should not find "GI C1" in the app | ||||
| 
 | ||||
|     # Course grades details | ||||
|     When I press "GI C2.1.1" in the app | ||||
|     Then I should find "Weight" in the app | ||||
|     And I should find "70.00" within "Grade" "ion-item" in the app | ||||
|     And I should find "60–80" within "Range" "ion-item" in the app | ||||
|     And I should find "50.00 %" within "Percentage" "ion-item" in the app | ||||
|     And I should find "Contribution to course total" in the app | ||||
|     And I should find "GI C2.1.2" in the app | ||||
| 
 | ||||
|     When I press "GI C2.1.1" in the app | ||||
|     Then I should not find "Weight" in the app | ||||
|     And I should not find "Range" in the app | ||||
|     And I should not find "Percentage" in the app | ||||
|     And I should not find "Contribution to course total" in the app | ||||
|     But I should find "GI C2.1.1" in the app | ||||
|     And I should find "GI C2.1.2" in the app | ||||
| 
 | ||||
|     When I press "Course total" in the app | ||||
|     Then I should find "130" within "Grade" "ion-item" in the app | ||||
|     And I should find "0–270" within "Range" "ion-item" in the app | ||||
| 
 | ||||
|     When I press "Course total" in the app | ||||
|     Then I should not find "Weight" in the app | ||||
|     And I should not find "Percentage" in the app | ||||
| 
 | ||||
|   Scenario: Tablet navigation (teacher) | ||||
|     Given I entered the course "Course 2" as "teacher1" in the app | ||||
|     And I change viewport size to "1200x640" | ||||
| 
 | ||||
|     # Course grades | ||||
|     When I press "Participants" in the app | ||||
|     And I press "Student first" in the app | ||||
|     And I press "Grades" in the app | ||||
|     Then I should find "GC C2.1" in the app | ||||
|     And I should find "Weight" in the app | ||||
|     And I should find "Contribution to course total" in the app | ||||
|     And I should find "70.00" within "GI C2.1.1" "tr" in the app | ||||
|     And I should find "60–80" within "GI C2.1.1" "tr" in the app | ||||
|     And I should find "50.00 %" within "GI C2.1.1" "tr" in the app | ||||
|     And I should find "20" within "GI C2.1.2" "tr" in the app | ||||
|     And I should find "90" within "GC C2.1 total" "tr" in the app | ||||
|     And I should find "GC C2.2" in the app | ||||
|     And I should find "40" within "GI C2.2.1" "tr" in the app | ||||
|     And I should find "40" within "GC C2.2 total" "tr" in the app | ||||
|     And I should find "130" within "Course total" "tr" in the app | ||||
| @ -151,13 +151,13 @@ Feature: Grades navigation | ||||
|     Then I should find "Course 1" in the app | ||||
|     And I should find "Course 2" in the app | ||||
| 
 | ||||
|   @lms_from4.2 | ||||
|   Scenario: Mobile navigation (teacher) | ||||
|     Given I entered the course "Course 2" as "teacher1" in the app | ||||
| 
 | ||||
|     # Course grades | ||||
|     When I press "Participants" in the app | ||||
|     When I press "Grades" in the app | ||||
|     And I press "Student first" in the app | ||||
|     And I press "Grades" in the app | ||||
|     Then I should find "GC C2.1" in the app | ||||
|     And I should find "70" within "GI C2.1.1" "tr" in the app | ||||
|     And I should find "20" within "GI C2.1.2" "tr" in the app | ||||
| @ -194,6 +194,13 @@ Feature: Grades navigation | ||||
|     Then I should not find "Weight" in the app | ||||
|     And I should not find "Percentage" in the app | ||||
| 
 | ||||
|     # Profile grades | ||||
|     When I press the back button in the app | ||||
|     And I press "Participants" in the app | ||||
|     And I press "Student first" in the app | ||||
|     And I press "Grades" in the app | ||||
|     Then I should find "GC C2.1" in the app | ||||
| 
 | ||||
|   Scenario: Tablet navigation (student) | ||||
|     Given I entered the course "Course 2" as "student1" in the app | ||||
|     And I change viewport size to "1200x640" | ||||
| @ -274,23 +281,53 @@ Feature: Grades navigation | ||||
|     Then I should not find "Weight" inside the split-view content in the app | ||||
|     And I should not find "Percentage" inside the split-view content in the app | ||||
| 
 | ||||
|   @lms_from4.2 | ||||
|   Scenario: Tablet navigation (teacher) | ||||
|     Given I entered the course "Course 2" as "teacher1" in the app | ||||
|     And I change viewport size to "1200x640" | ||||
| 
 | ||||
|     # Course grades | ||||
|     # User grades | ||||
|     When I press "Grades" in the app | ||||
|     And I press "Student first" in the app | ||||
|     Then "Student first" should be selected in the app | ||||
|     And I should find "GC C2.1" inside the split-view content in the app | ||||
|     And I should find "70" within "GI C2.1.1" "tr" inside the split-view content in the app | ||||
|     And I should find "20" within "GI C2.1.2" "tr" inside the split-view content in the app | ||||
|     And I should find "90" within "GC C2.1 total" "tr" inside the split-view content in the app | ||||
|     And I should find "GC C2.2" inside the split-view content in the app | ||||
|     And I should find "40" within "GI C2.2.1" "tr" inside the split-view content in the app | ||||
|     And I should find "40" within "GC C2.2 total" "tr" inside the split-view content in the app | ||||
|     And I should find "130" within "Course total" "tr" inside the split-view content in the app | ||||
|     But I should not find "GC C1" inside the split-view content in the app | ||||
|     And I should not find "GI C1" inside the split-view content in the app | ||||
| 
 | ||||
|     # User grades details | ||||
|     When I press "GI C2.1.1" in the app | ||||
|     Then I should find "Weight" inside the split-view content in the app | ||||
|     And I should find "70.00" within "Grade" "ion-item" inside the split-view content in the app | ||||
|     And I should find "60–80" within "Range" "ion-item" inside the split-view content in the app | ||||
|     And I should find "50.00 %" within "Percentage" "ion-item" inside the split-view content in the app | ||||
|     And I should find "Contribution to course total" inside the split-view content in the app | ||||
|     And I should find "GI C2.1.2" inside the split-view content in the app | ||||
| 
 | ||||
|     When I press "GI C2.1.1" in the app | ||||
|     Then I should not find "Weight" inside the split-view content in the app | ||||
|     And I should not find "Range" inside the split-view content in the app | ||||
|     And I should not find "Percentage" inside the split-view content in the app | ||||
|     And I should not find "Contribution to course total" inside the split-view content in the app | ||||
|     But I should find "GI C2.1.1" inside the split-view content in the app | ||||
|     And I should find "GI C2.1.2" inside the split-view content in the app | ||||
| 
 | ||||
|     When I press "Course total" in the app | ||||
|     Then I should find "130" within "Grade" "ion-item" inside the split-view content in the app | ||||
|     And I should find "0–270" within "Range" "ion-item" inside the split-view content in the app | ||||
| 
 | ||||
|     When I press "Course total" in the app | ||||
|     Then I should not find "Weight" inside the split-view content in the app | ||||
|     And I should not find "Percentage" inside the split-view content in the app | ||||
| 
 | ||||
|     # Profile grades | ||||
|     When I press "Participants" in the app | ||||
|     And I press "Student first" in the app | ||||
|     And I press "Grades" in the app | ||||
|     Then I should find "GC C2.1" in the app | ||||
|     And I should find "Weight" in the app | ||||
|     And I should find "Contribution to course total" in the app | ||||
|     And I should find "70.00" within "GI C2.1.1" "tr" in the app | ||||
|     And I should find "60–80" within "GI C2.1.1" "tr" in the app | ||||
|     And I should find "50.00 %" within "GI C2.1.1" "tr" in the app | ||||
|     And I should find "20" within "GI C2.1.2" "tr" in the app | ||||
|     And I should find "90" within "GC C2.1 total" "tr" in the app | ||||
|     And I should find "GC C2.2" in the app | ||||
|     And I should find "40" within "GI C2.2.1" "tr" in the app | ||||
|     And I should find "40" within "GC C2.2 total" "tr" in the app | ||||
|     And I should find "130" within "Course total" "tr" in the app | ||||
|  | ||||
| @ -1,17 +1,11 @@ | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <ion-button [hidden]="!searchEnabled" (click)="toggleSearch()" [attr.aria-label]="'core.search' | translate"> | ||||
|         <ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <ion-content> | ||||
|     <core-split-view> | ||||
|         <ion-refresher slot="fixed" [disabled]="!participants.loaded || searchInProgress" (ionRefresh)="refreshParticipants($event.target)"> | ||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|         </ion-refresher> | ||||
| 
 | ||||
|         <core-search-box *ngIf="showSearchBox" [disabled]="searchInProgress" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1" | ||||
|             autocorrect="off" searchArea="CoreUserParticipants" (onSubmit)="search($event)" (onClear)="clearSearch()"> | ||||
|         <core-search-box [disabled]="searchInProgress" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1" autocorrect="off" | ||||
|             searchArea="CoreUserParticipants" (onSubmit)="search($event)" (onClear)="clearSearch()"> | ||||
|         </core-search-box> | ||||
| 
 | ||||
|         <core-loading [hideUntil]="participants.loaded"> | ||||
|  | ||||
| @ -0,0 +1,31 @@ | ||||
| // (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 { CoreSharedModule } from '@/core/shared.module'; | ||||
| 
 | ||||
| import { CoreUserParticipantsPage } from './participants.page'; | ||||
| import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|         CoreSearchComponentsModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         CoreUserParticipantsPage, | ||||
|     ], | ||||
| }) | ||||
| export class CoreUserParticipantsPageModule {} | ||||
| @ -31,6 +31,7 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ | ||||
| @Component({ | ||||
|     selector: 'page-core-user-participants', | ||||
|     templateUrl: 'participants.html', | ||||
|     styleUrls: ['participants.scss'], | ||||
| }) | ||||
| export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestroy { | ||||
| 
 | ||||
| @ -39,7 +40,6 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|     searchQuery: string | null = null; | ||||
|     searchInProgress = false; | ||||
|     searchEnabled = false; | ||||
|     showSearchBox = false; | ||||
|     fetchMoreParticipantsFailed = false; | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| @ -84,20 +84,6 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|         this.participants.destroy(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show or hide search box. | ||||
|      */ | ||||
|     toggleSearch(): void { | ||||
|         this.showSearchBox = !this.showSearchBox; | ||||
| 
 | ||||
|         if (this.showSearchBox) { | ||||
|             // Make search bar visible.
 | ||||
|             this.splitView.menuContent.scrollToTop(); | ||||
|         } else { | ||||
|             this.clearSearch(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clear search. | ||||
|      */ | ||||
							
								
								
									
										13
									
								
								src/core/features/user/pages/participants/participants.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/core/features/user/pages/participants/participants.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| :host { | ||||
| 
 | ||||
|     core-split-view { | ||||
|         isolation: isolate; | ||||
|     } | ||||
| 
 | ||||
|     core-search-box { | ||||
|         position: sticky; | ||||
|         top: 8px; | ||||
|         z-index: 1; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -15,12 +15,10 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | ||||
| 
 | ||||
| import { CoreUserParticipantsPage } from './pages/participants/participants'; | ||||
| import { CoreUserParticipantsPage } from './pages/participants/participants.page'; | ||||
| import { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreUserParticipantsPageModule } from '@features/user/pages/participants/participants.module'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
| @ -38,11 +36,7 @@ const routes: Routes = [ | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         CoreSharedModule, | ||||
|         CoreSearchComponentsModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         CoreUserParticipantsPage, | ||||
|         CoreUserParticipantsPageModule, | ||||
|     ], | ||||
| }) | ||||
| export class CoreUserCourseLazyModule {} | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user