diff --git a/src/core/features/grades/grades-course-lazy.module.ts b/src/core/features/grades/grades-course-lazy.module.ts index 974a15c9d..877a0fec9 100644 --- a/src/core/features/grades/grades-course-lazy.module.ts +++ b/src/core/features/grades/grades-course-lazy.module.ts @@ -22,7 +22,6 @@ const routes: Routes = [ { path: '', component: CoreGradesCoursePage, - data: { swipeEnabled: false }, }, ]; diff --git a/src/core/features/grades/grades-course-participants-lazy.module.ts b/src/core/features/grades/grades-course-participants-lazy.module.ts new file mode 100644 index 000000000..68400a443 --- /dev/null +++ b/src/core/features/grades/grades-course-participants-lazy.module.ts @@ -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 {} diff --git a/src/core/features/grades/grades.module.ts b/src/core/features/grades/grades.module.ts index 24796d3de..13858a21b 100644 --- a/src/core/features/grades/grades.module.ts +++ b/src/core/features/grades/grades.module.ts @@ -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[] = [ 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); }, }, ], diff --git a/src/core/features/grades/pages/course/course.html b/src/core/features/grades/pages/course/course.html index 70cc02b61..908fc1671 100644 --- a/src/core/features/grades/pages/course/course.html +++ b/src/core/features/grades/pages/course/course.html @@ -8,7 +8,7 @@ - + diff --git a/src/core/features/grades/pages/course/course.page.ts b/src/core/features/grades/pages/course/course.page.ts index c70adc1a8..a26e5a86e 100644 --- a/src/core/features/grades/pages/course/course.page.ts +++ b/src/core/features/grades/pages/course/course.page.ts @@ -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 { 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 + 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 + 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; + } + +} diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index 05f78ddf2..0dcb0b9ea 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -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 { + 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); diff --git a/src/core/features/grades/services/grades.ts b/src/core/features/grades/services/grades.ts index cb08c31c5..be80ee574 100644 --- a/src/core/features/grades/services/grades.ts +++ b/src/core/features/grades/services/grades.ts @@ -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 { + 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 { + 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( + '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. */ diff --git a/src/core/features/grades/services/handlers/course-option.ts b/src/core/features/grades/services/handlers/course-option.ts index f701608de..b6f7f1448 100644 --- a/src/core/features/grades/services/handlers/course-option.ts +++ b/src/core/features/grades/services/handlers/course-option.ts @@ -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 { + async invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise { + 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 { - if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { - return false; // Not enabled for guests. + ): Promise { + 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, }; } diff --git a/src/core/features/grades/services/handlers/course-participants-option.ts b/src/core/features/grades/services/handlers/course-participants-option.ts new file mode 100644 index 000000000..5fc6bfa69 --- /dev/null +++ b/src/core/features/grades/services/handlers/course-participants-option.ts @@ -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 { + await CoreGrades.invalidateCourseGradesPermissionsData(courseId); + + if (navOptions && navOptions.grades !== undefined) { + // No need to invalidate user courses. + return; + } + + await CoreCourses.invalidateUserCourses(); + } + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + async isEnabledForCourse( + courseId: number, + accessData: CoreCourseAccess, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + const showGradebook = await CoreGradesHelper.showGradebook(courseId, accessData, navOptions); + + if (!showGradebook) { + return false; + } + + const canViewAllGrades = await CoreGrades.canViewAllGrades(courseId); + + return canViewAllGrades; + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreCourseOptionsHandlerData | Promise { + return { + title: 'core.grades.grades', + class: 'core-grades-course-participants-handler', + page: GRADES_PARTICIPANTS_PAGE_NAME, + }; + } + +} + +export const CoreGradesCourseParticipantsOptionHandler = makeSingleton(CoreGradesCourseParticipantsOptionHandlerService); diff --git a/src/core/features/grades/tests/behat/navigation-401.feature b/src/core/features/grades/tests/behat/navigation-401.feature new file mode 100644 index 000000000..483e1adb1 --- /dev/null +++ b/src/core/features/grades/tests/behat/navigation-401.feature @@ -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 diff --git a/src/core/features/grades/tests/behat/navigation.feature b/src/core/features/grades/tests/behat/navigation.feature index 9f3d69054..1ee0a463a 100644 --- a/src/core/features/grades/tests/behat/navigation.feature +++ b/src/core/features/grades/tests/behat/navigation.feature @@ -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 diff --git a/src/core/features/user/pages/participants/participants.module.ts b/src/core/features/user/pages/participants/participants.module.ts new file mode 100644 index 000000000..b3e3eb4b5 --- /dev/null +++ b/src/core/features/user/pages/participants/participants.module.ts @@ -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 {} diff --git a/src/core/features/user/pages/participants/participants.ts b/src/core/features/user/pages/participants/participants.page.ts similarity index 100% rename from src/core/features/user/pages/participants/participants.ts rename to src/core/features/user/pages/participants/participants.page.ts diff --git a/src/core/features/user/user-course-lazy.module.ts b/src/core/features/user/user-course-lazy.module.ts index e7fe47eb7..8653a7b50 100644 --- a/src/core/features/user/user-course-lazy.module.ts +++ b/src/core/features/user/user-course-lazy.module.ts @@ -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 {}