Merge pull request #3583 from NoelDeMartin/MOBILE-4188
MOBILE-4188: Implement teacher gradebookmain
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);
|
|
@ -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.
|
||||
*/
|
|
@ -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…
Reference in New Issue