Merge pull request #2702 from crazyserver/MOBILE-3629

Mobile 3629
main
Dani Palou 2021-03-11 15:39:40 +01:00 committed by GitHub
commit 37d6bb0464
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 820 additions and 303 deletions

View File

@ -20,6 +20,7 @@ import { AddonFilterModule } from './filter/filter.module';
import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module'; import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module';
import { AddonBadgesModule } from './badges/badges.module'; import { AddonBadgesModule } from './badges/badges.module';
import { AddonCalendarModule } from './calendar/calendar.module'; import { AddonCalendarModule } from './calendar/calendar.module';
import { AddonCourseCompletionModule } from './coursecompletion/coursecompletion.module';
import { AddonNotificationsModule } from './notifications/notifications.module'; import { AddonNotificationsModule } from './notifications/notifications.module';
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module'; import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
import { AddonMessagesModule } from './messages/messages.module'; import { AddonMessagesModule } from './messages/messages.module';
@ -35,6 +36,7 @@ import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module';
AddonBadgesModule, AddonBadgesModule,
AddonBlogModule, AddonBlogModule,
AddonCalendarModule, AddonCalendarModule,
AddonCourseCompletionModule,
AddonMessagesModule, AddonMessagesModule,
AddonPrivateFilesModule, AddonPrivateFilesModule,
AddonFilterModule, AddonFilterModule,

View File

@ -19,8 +19,8 @@ import { conditionalRoutes } from '@/app/app-routing.module';
import { CoreScreen } from '@services/screen'; import { CoreScreen } from '@services/screen';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { AddonBadgesIssuedBadgePage } from './pages/issued-badge/issued-badge'; import { AddonBadgesIssuedBadgePage } from './pages/issued-badge/issued-badge.page';
import { AddonBadgesUserBadgesPage } from './pages/user-badges/user-badges'; import { AddonBadgesUserBadgesPage } from './pages/user-badges/user-badges.page';
const mobileRoutes: Routes = [ const mobileRoutes: Routes = [
{ {

View File

@ -20,8 +20,9 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CorePageItemsListManager } from '@classes/page-items-list-manager'; import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; import { ActivatedRouteSnapshot, Params } from '@angular/router';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreNavigator } from '@services/navigator';
/** /**
* Page that displays the list of calendar events. * Page that displays the list of calendar events.
@ -37,9 +38,9 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
constructor(route: ActivatedRoute) { constructor() {
const courseId = parseInt(route.snapshot.queryParams.courseId ?? 0); // Use 0 for site badges. const courseId = CoreNavigator.getRouteNumberParam('courseId') ?? 0; // Use 0 for site badges.
const userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.getCurrentSiteUserId()); const userId = CoreNavigator.getRouteNumberParam('userId') ?? CoreSites.getCurrentSiteUserId();
this.badges = new AddonBadgesUserBadgesManager(AddonBadgesUserBadgesPage, courseId, userId); this.badges = new AddonBadgesUserBadgesManager(AddonBadgesUserBadgesPage, courseId, userId);
} }

View File

@ -14,7 +14,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
import { CoreUserProfile } from '@features/user/services/user';
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
@ -42,13 +41,11 @@ export class AddonBadgesUserHandlerService implements CoreUserProfileHandler {
/** /**
* Check if handler is enabled for this user in this context. * Check if handler is enabled for this user in this context.
* *
* @param user User to check.
* @param courseId Course ID. * @param courseId Course ID.
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @return True if enabled, false otherwise. * @return True if enabled, false otherwise.
*/ */
async isEnabledForUser( async isEnabledForCourse(
user: CoreUserProfile,
courseId: number, courseId: number,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed, navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<boolean> { ): Promise<boolean> {

View File

@ -131,9 +131,8 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
* @param entry Selected entry. * @param entry Selected entry.
*/ */
gotoCoureListModType(entry: AddonBlockActivityModuleEntry): void { gotoCoureListModType(entry: AddonBlockActivityModuleEntry): void {
CoreNavigator.navigateToSitePath('course/list-mod-type', { CoreNavigator.navigateToSitePath('course/' + this.getCourseId() + '/list-mod-type', {
params: { params: {
courseId: this.getCourseId(),
modName: entry.modName, modName: entry.modName,
title: entry.name, title: entry.name,
}, },

View File

@ -29,21 +29,14 @@ export class AddonBlockCompletionStatusHandlerService extends CoreBlockBaseHandl
blockName = 'completionstatus'; blockName = 'completionstatus';
/** /**
* Returns the data needed to render the block. * @inheritdoc
*
* @param block The block to render.
* @param contextLevel The context where the block will be used.
* @param instanceId The instance ID associated with the context level.
* @return Data or promise resolved with the data.
*/ */
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
// @todo
return { return {
title: 'addon.block_completionstatus.pluginname', title: 'addon.block_completionstatus.pluginname',
class: 'addon-block-completion-status', class: 'addon-block-completion-status',
component: CoreBlockOnlyTitleComponent, component: CoreBlockOnlyTitleComponent,
link: 'AddonCourseCompletionReportPage', link: 'coursecompletion',
linkParams: { linkParams: {
courseId: instanceId, courseId: instanceId,
}, },

View File

@ -29,22 +29,17 @@ export class AddonBlockSelfCompletionHandlerService extends CoreBlockBaseHandler
blockName = 'selfcompletion'; blockName = 'selfcompletion';
/** /**
* Returns the data needed to render the block. * @inheritdoc
*
* @param block The block to render.
* @param contextLevel The context where the block will be used.
* @param instanceId The instance ID associated with the context level.
* @return Data or promise resolved with the data.
*/ */
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
// @todo
return { return {
title: 'addon.block_selfcompletion.pluginname', title: 'addon.block_selfcompletion.pluginname',
class: 'addon-block-self-completion', class: 'addon-block-self-completion',
component: CoreBlockOnlyTitleComponent, component: CoreBlockOnlyTitleComponent,
link: 'AddonCourseCompletionReportPage', link: 'coursecompletion',
linkParams: { courseId: instanceId }, linkParams: {
courseId: instanceId,
},
}; };
} }

View File

@ -35,13 +35,6 @@ export class AddonBlogUserHandlerService implements CoreUserProfileHandler {
return AddonBlog.isPluginEnabled(); return AddonBlog.isPluginEnabled();
} }
/**
* @inheritdoc
*/
async isEnabledForUser(): Promise<boolean> {
return true;
}
/** /**
* @inheritdoc * @inheritdoc
*/ */

View File

@ -0,0 +1,41 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
import { AddonCourseCompletionReportPage } from './pages/report/report';
const routes: Routes = [
{
path: '',
component: AddonCourseCompletionReportPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
CoreCommentsComponentsModule,
CoreTagComponentsModule,
],
exports: [RouterModule],
declarations: [
AddonCourseCompletionReportPage,
],
})
export class AddonCourseCompletionLazyModule {}

View File

@ -12,33 +12,42 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
// @todo import { AddonCourseCompletionCourseOptionHandler } from './services/course-option-handler'; import { Routes } from '@angular/router';
// @todo import { AddonCourseCompletionUserHandler } from './services/user-handler'; import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module';
// @todo import { AddonCourseCompletionComponentsModule } from './components/components.module'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
// @todo import { CoreCourseOptionsDelegate } from '@features/course/services/options-delegate'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
// @todo import { CoreUserDelegate } from '@features/user/services/user-delegate'; import { CoreUserDelegate } from '@features/user/services/user-delegate';
import { AddonCourseCompletionProvider } from './services/coursecompletion';
import { AddonCourseCompletionCourseOptionHandler } from './services/handlers/course-option';
import { AddonCourseCompletionUserHandler } from './services/handlers/user';
export const ADDON_COURSECOMPLETION_SERVICES: Type<unknown>[] = [
AddonCourseCompletionProvider,
];
const routes: Routes = [
{
path: 'coursecompletion',
loadChildren: () => import('./coursecompletion-lazy.module').then(m => m.AddonCourseCompletionLazyModule),
},
];
@NgModule({ @NgModule({
imports: [ imports: [
// AddonCourseCompletionComponentsModule, CoreMainMenuTabRoutingModule.forChild(routes),
CoreCourseIndexRoutingModule.forChild({ children: routes }),
], ],
providers: [ providers: [
// AddonCourseCompletionCourseOptionHandler, {
// AddonCourseCompletionUserHandler, provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => async () => {
CoreUserDelegate.registerHandler(AddonCourseCompletionUserHandler.instance);
CoreCourseOptionsDelegate.registerHandler(AddonCourseCompletionCourseOptionHandler.instance);
},
},
], ],
}) })
export class AddonCourseCompletionModule { export class AddonCourseCompletionModule {}
/* @todo constructor(
courseOptionsDelegate: CoreCourseOptionsDelegate,
courseOptionHandler: AddonCourseCompletionCourseOptionHandler,
userDelegate: CoreUserDelegate,
userHandler: AddonCourseCompletionUserHandler,
) {
// Register handlers.
courseOptionsDelegate.registerHandler(courseOptionHandler);
userDelegate.registerHandler(userHandler);
}*/
}

View File

@ -0,0 +1,92 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.coursecompletion.coursecompletion' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!completionLoaded" (ionRefresh)="refreshCompletion($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="completionLoaded">
<ion-card *ngIf="completion && tracked">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.coursecompletion.status' | translate }}</h2>
<p>{{ statusText! | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'addon.coursecompletion.required' | translate }}</h2>
<p *ngIf="completion.aggregation === 1">{{ 'addon.coursecompletion.criteriarequiredall' | translate }}</p>
<p *ngIf="completion.aggregation === 2">{{ 'addon.coursecompletion.criteriarequiredany' | translate }}</p>
</ion-label>
</ion-item>
</ion-card>
<ion-card *ngIf="completion && tracked">
<ion-item-divider>
<ion-label>{{ 'addon.coursecompletion.requiredcriteria' | translate }}</ion-label>
</ion-item-divider>
<ion-item class="ion-hide-md-up ion-text-wrap" *ngFor="let criteria of completion.completions">
<ion-label>
<h2><core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false"></core-format-text></h2>
<p><core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false"></core-format-text></p>
</ion-label>
<strong slot="end">{{ criteria.status }}</strong>
</ion-item>
<ion-item class="ion-hide-md-down ion-text-wrap">
<ion-label>
<ion-row>
<ion-col><strong>{{ 'addon.coursecompletion.criteriagroup' | translate }}</strong></ion-col>
<ion-col><strong>{{ 'addon.coursecompletion.criteria' | translate }}</strong></ion-col>
<ion-col><strong>{{ 'addon.coursecompletion.requirement' | translate }}</strong></ion-col>
<ion-col><strong>{{ 'addon.coursecompletion.status' | translate }}</strong></ion-col>
<ion-col><strong>{{ 'addon.coursecompletion.complete' | translate }}</strong></ion-col>
<ion-col><strong>{{ 'addon.coursecompletion.completiondate' | translate }}</strong></ion-col>
</ion-row>
<ion-row *ngFor="let criteria of completion.completions">
<ion-col>
<core-format-text clean="true" [text]="criteria.title" [filter]="false"></core-format-text>
</ion-col>
<ion-col>
<core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false"></core-format-text>
</ion-col>
<ion-col>
<core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false"></core-format-text>
</ion-col>
<ion-col>
<core-format-text [text]="criteria.details.status" [filter]="false"></core-format-text>
</ion-col>
<ion-col>{{ criteria.status }}</ion-col>
<ion-col *ngIf="criteria.timecompleted">
{{ criteria.timecompleted * 1000 | coreFormatDate :'strftimedatetimeshort' }}
</ion-col>
<ion-col *ngIf="!criteria.timecompleted"></ion-col>
</ion-row>
</ion-label>
</ion-item>
</ion-card>
<ion-card *ngIf="showSelfComplete && tracked">
<ion-item-divider>
<ion-label>{{ 'addon.coursecompletion.manualselfcompletion' | translate }}</ion-label>
</ion-item-divider>
<ion-item>
<ion-label>
<ion-button expand="block" (click)="completeCourse()">
{{ 'addon.coursecompletion.completecourse' | translate }}
</ion-button>
</ion-label>
</ion-item>
</ion-card>
<ion-card class="core-warning-card" *ngIf="!tracked">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>{{ 'addon.coursecompletion.nottracked' | translate }}</ion-label>
</ion-item>
</ion-card>
</core-loading>
</ion-content>

View File

@ -0,0 +1,112 @@
// (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 {
AddonCourseCompletion,
AddonCourseCompletionCourseCompletionStatus,
} from '@addons/coursecompletion/services/coursecompletion';
import { Component, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
/**
* Page that displays the course completion report.
*/
@Component({
selector: 'page-addon-course-completion-report',
templateUrl: 'report.html',
})
export class AddonCourseCompletionReportPage implements OnInit {
protected courseId!: number;
protected userId!: number;
completionLoaded = false;
completion?: AddonCourseCompletionCourseCompletionStatus;
showSelfComplete = false;
tracked = true; // Whether completion is tracked.
statusText?: string;
/**
* @inheritdoc
*/
ngOnInit(): void {
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId();
if (!this.userId) {
this.userId = CoreSites.getCurrentSiteUserId();
}
this.fetchCompletion().finally(() => {
this.completionLoaded = true;
});
}
/**
* Fetch compleiton data.
*
* @return Promise resolved when done.
*/
protected async fetchCompletion(): Promise<void> {
try {
this.completion = await AddonCourseCompletion.getCompletion(this.courseId, this.userId);
this.statusText = AddonCourseCompletion.getCompletedStatusText(this.completion);
this.showSelfComplete = AddonCourseCompletion.canMarkSelfCompleted(this.userId, this.completion);
this.tracked = true;
} catch (error) {
if (error && error.errorcode == 'notenroled') {
// Not enrolled error, probably a teacher.
this.tracked = false;
} else {
CoreDomUtils.showErrorModalDefault(error, 'addon.coursecompletion.couldnotloadreport', true);
}
}
}
/**
* Refresh completion data on PTR.
*
* @param refresher Refresher instance.
*/
async refreshCompletion(refresher?: IonRefresher): Promise<void> {
await AddonCourseCompletion.invalidateCourseCompletion(this.courseId, this.userId).finally(() => {
this.fetchCompletion().finally(() => {
refresher?.complete();
});
});
}
/**
* Mark course as completed.
*/
async completeCourse(): Promise<void> {
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
try {
await AddonCourseCompletion.markCourseAsSelfCompleted(this.courseId);
await this.refreshCompletion();
} catch (error) {
CoreDomUtils.showErrorModal(error);
} finally {
modal.dismiss();
}
}
}

View File

@ -164,9 +164,9 @@ export class AddonCourseCompletionProvider {
* @param preferCache True if shouldn't call WS if data is cached, false otherwise. * @param preferCache True if shouldn't call WS if data is cached, false otherwise.
* @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
*/ */
async isPluginViewEnabledForCourse(courseId: number, preferCache: boolean = true): Promise<boolean> { async isPluginViewEnabledForCourse(courseId?: number, preferCache: boolean = true): Promise<boolean> {
if (!courseId) { if (!courseId) {
throw new CoreError('No courseId provided'); return false;
} }
const course = await CoreCourses.getUserCourse(courseId, preferCache); const course = await CoreCourses.getUserCourse(courseId, preferCache);

View File

@ -0,0 +1,96 @@
// (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 { CoreCourseProvider } from '@features/course/services/course';
import {
CoreCourseAccess,
CoreCourseOptionsHandler,
CoreCourseOptionsHandlerData,
} from '@features/course/services/course-options-delegate';
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper';
import { makeSingleton } from '@singletons';
import { AddonCourseCompletion } from '../coursecompletion';
/**
* Handler to inject an option into the course main menu.
*/
@Injectable({ providedIn: 'root' })
export class AddonCourseCompletionCourseOptionHandlerService implements CoreCourseOptionsHandler {
name = 'AddonCourseCompletion';
priority = 200;
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return AddonCourseCompletion.isPluginViewEnabled();
}
/**
* @inheritdoc
*/
async isEnabledForCourse(courseId: number, accessData: CoreCourseAccess): Promise<boolean> {
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guests.
}
const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(courseId);
// If is not enabled in the course, is not enabled for the user.
if (!courseEnabled) {
return false;
}
return AddonCourseCompletion.isPluginViewEnabledForUser(courseId);
}
/**
* @inheritdoc
*/
getDisplayData(): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData> {
return {
title: 'addon.coursecompletion.completionmenuitem',
class: 'addon-coursecompletion-course-handler',
page: 'coursecompletion',
};
}
/**
* @inheritdoc
*/
async invalidateEnabledForCourse(courseId: number): Promise<void> {
await AddonCourseCompletion.invalidateCourseCompletion(courseId);
}
/**
* @inheritdoc
*/
async prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void> {
try {
await AddonCourseCompletion.getCompletion(course.id, undefined, {
getFromCache: false,
emergencyCache: false,
});
} catch (error) {
if (error && error.errorcode == 'notenroled') {
// Not enrolled error, probably a teacher. Ignore error.
} else {
throw error;
}
}
}
}
export const AddonCourseCompletionCourseOptionHandler = makeSingleton(AddonCourseCompletionCourseOptionHandlerService);

View File

@ -0,0 +1,73 @@
// (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 { CoreUserProfile } from '@features/user/services/user';
import { CoreUserProfileHandler, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { AddonCourseCompletion } from '../coursecompletion';
/**
* Profile course completion handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonCourseCompletionUserHandlerService implements CoreUserProfileHandler {
name = 'AddonCourseCompletion';
type = CoreUserDelegateService.TYPE_NEW_PAGE;
priority = 200;
cacheEnabled = true;
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return AddonCourseCompletion.isPluginViewEnabled();
}
/**
* @inheritdoc
*/
async isEnabledForCourse(courseId?: number): Promise<boolean> {
return AddonCourseCompletion.isPluginViewEnabledForCourse(courseId);
}
/**
* @inheritdoc
*/
async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise<boolean> {
return await AddonCourseCompletion.isPluginViewEnabledForUser(courseId!, user.id);
}
/**
* @inheritdoc
*/
getDisplayData(): CoreUserProfileHandlerData {
return {
icon: 'fas-tasks',
title: 'addon.coursecompletion.coursecompletion',
class: 'addon-coursecompletion-handler',
action: (event, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
CoreNavigator.navigateToSitePath('/coursecompletion', {
params: { courseId, userId: user.id },
});
},
};
}
}
export const AddonCourseCompletionUserHandler = makeSingleton(AddonCourseCompletionUserHandlerService);

View File

@ -164,9 +164,9 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
this.route.queryParams.subscribe(async (params) => { this.route.queryParams.subscribe(async (params) => {
this.loaded = false; this.loaded = false;
this.conversationId = CoreNavigator.getRouteNumberParam('conversationId', params) || undefined; this.conversationId = CoreNavigator.getRouteNumberParam('conversationId', { params }) || undefined;
this.userId = CoreNavigator.getRouteNumberParam('userId', params) || undefined; this.userId = CoreNavigator.getRouteNumberParam('userId', { params }) || undefined;
this.showKeyboard = CoreNavigator.getRouteBooleanParam('showKeyboard', params) || false; this.showKeyboard = CoreNavigator.getRouteBooleanParam('showKeyboard', { params }) || false;
await this.fetchData(); await this.fetchData();

View File

@ -138,8 +138,8 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.route.queryParams.subscribe(async (params) => { this.route.queryParams.subscribe(async (params) => {
const discussionUserId = CoreNavigator.getRouteNumberParam('discussionUserId', params) || const discussionUserId = CoreNavigator.getRouteNumberParam('discussionUserId', { params }) ||
CoreNavigator.getRouteNumberParam('userId', params) || undefined; CoreNavigator.getRouteNumberParam('userId', { params }) || undefined;
if (this.loaded && this.discussionUserId == discussionUserId) { if (this.loaded && this.discussionUserId == discussionUserId) {
return; return;

View File

@ -273,9 +273,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.route.queryParams.subscribe(async (params) => { this.route.queryParams.subscribe(async (params) => {
// Conversation to load. // Conversation to load.
this.conversationId = CoreNavigator.getRouteNumberParam('conversationId', params) || undefined; this.conversationId = CoreNavigator.getRouteNumberParam('conversationId', { params }) || undefined;
if (!this.conversationId) { if (!this.conversationId) {
this.discussionUserId = CoreNavigator.getRouteNumberParam('discussionUserId', params) || undefined; this.discussionUserId = CoreNavigator.getRouteNumberParam('discussionUserId', { params }) || undefined;
} }
if (this.conversationId || this.discussionUserId) { if (this.conversationId || this.discussionUserId) {

View File

@ -40,6 +40,13 @@ export class AddonMessagesSendMessageUserHandlerService implements CoreUserProfi
return AddonMessages.isPluginEnabled(); return AddonMessages.isPluginEnabled();
} }
/**
* @inheritdoc
*/
async isEnabledForCourse(): Promise<boolean> {
return !!CoreSites.getCurrentSite();
}
/** /**
* Check if handler is enabled for this user in this context. * Check if handler is enabled for this user in this context.
* *
@ -47,14 +54,10 @@ export class AddonMessagesSendMessageUserHandlerService implements CoreUserProfi
* @return Promise resolved with true if enabled, resolved with false otherwise. * @return Promise resolved with true if enabled, resolved with false otherwise.
*/ */
async isEnabledForUser(user: CoreUserProfile): Promise<boolean> { async isEnabledForUser(user: CoreUserProfile): Promise<boolean> {
const currentSite = CoreSites.getCurrentSite(); const currentSite = CoreSites.getCurrentSite()!;
if (!currentSite) {
return false;
}
// From 3.7 you can send messages to yourself. // From 3.7 you can send messages to yourself.
return user.id != currentSite.getUserId() || currentSite.isVersionGreaterEqualThan('3.7'); return user.id != CoreSites.getCurrentSiteUserId() || currentSite.isVersionGreaterEqualThan('3.7');
} }
/** /**

View File

@ -58,7 +58,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
this.moduleId = CoreNavigator.getRouteNumberParam('cmId')!; this.moduleId = CoreNavigator.getRouteNumberParam('cmId')!;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.submitId = CoreNavigator.getRouteNumberParam('submitId') || 0; this.submitId = CoreNavigator.getRouteNumberParam('submitId') || 0;
this.blindId = CoreNavigator.getRouteNumberParam('blindId', params); this.blindId = CoreNavigator.getRouteNumberParam('blindId', { params });
this.fetchSubmission().finally(() => { this.fetchSubmission().finally(() => {
this.loaded = true; this.loaded = true;

View File

@ -117,6 +117,7 @@ import { CoreSitePluginsAssignSubmissionComponent } from '@features/siteplugins/
// Import addon providers. Do not import database module because it causes circular dependencies. // Import addon providers. Do not import database module because it causes circular dependencies.
import { ADDON_BADGES_SERVICES } from '@addons/badges/badges.module'; import { ADDON_BADGES_SERVICES } from '@addons/badges/badges.module';
import { ADDON_CALENDAR_SERVICES } from '@addons/calendar/calendar.module'; import { ADDON_CALENDAR_SERVICES } from '@addons/calendar/calendar.module';
import { ADDON_COURSECOMPLETION_SERVICES } from '@addons/coursecompletion/coursecompletion.module';
// @todo import { ADDON_COMPETENCY_SERVICES } from '@addons/competency/competency.module'; // @todo import { ADDON_COMPETENCY_SERVICES } from '@addons/competency/competency.module';
import { ADDON_MESSAGEOUTPUT_SERVICES } from '@addons/messageoutput/messageoutput.module'; import { ADDON_MESSAGEOUTPUT_SERVICES } from '@addons/messageoutput/messageoutput.module';
import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module'; import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module';
@ -281,6 +282,7 @@ export class CoreCompileProvider {
...extraProviders, ...extraProviders,
...ADDON_BADGES_SERVICES, ...ADDON_BADGES_SERVICES,
...ADDON_CALENDAR_SERVICES, ...ADDON_CALENDAR_SERVICES,
...ADDON_COURSECOMPLETION_SERVICES,
// @todo ...ADDON_COMPETENCY_SERVICES, // @todo ...ADDON_COMPETENCY_SERVICES,
...ADDON_MESSAGEOUTPUT_SERVICES, ...ADDON_MESSAGEOUTPUT_SERVICES,
...ADDON_MESSAGES_SERVICES, ...ADDON_MESSAGES_SERVICES,

View File

@ -61,9 +61,8 @@ export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBa
return [{ return [{
action: (siteId): void => { action: (siteId): void => {
CoreNavigator.navigateToSitePath('course/list-mod-type', { CoreNavigator.navigateToSitePath('course/' + params.id + '/list-mod-type', {
params: { params: {
courseId: params.id,
modName: this.modName, modName: this.modName,
title: this.title || Translate.instant('addon.mod_' + this.modName + '.modulenameplural'), title: this.title || Translate.instant('addon.mod_' + this.modName + '.modulenameplural'),
}, },

View File

@ -22,18 +22,23 @@ const routes: Routes = [
pathMatch: 'full', pathMatch: 'full',
}, },
{ {
path: 'index', path: ':courseId',
loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule), loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule),
}, },
{ {
path: 'unsupported-module', path: ':courseId/unsupported-module',
loadChildren: () => import('./pages/unsupported-module/unsupported-module.module') loadChildren: () => import('./pages/unsupported-module/unsupported-module.module')
.then( m => m.CoreCourseUnsupportedModulePageModule), .then( m => m.CoreCourseUnsupportedModulePageModule),
}, },
{ {
path: 'list-mod-type', path: ':courseId/list-mod-type',
loadChildren: () => import('./pages/list-mod-type/list-mod-type.module').then(m => m.CoreCourseListModTypePageModule), loadChildren: () => import('./pages/list-mod-type/list-mod-type.module').then(m => m.CoreCourseListModTypePageModule),
}, },
{
path: ':courseId/preview',
loadChildren: () =>
import('./pages/preview/preview.module').then(m => m.CoreCoursePreviewPageModule),
},
]; ];
@NgModule({ @NgModule({

View File

@ -460,7 +460,10 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
* Open the course summary * Open the course summary
*/ */
openCourseSummary(): void { openCourseSummary(): void {
CoreNavigator.navigateToSitePath('/courses/preview', { params: { course: this.course, avoidOpenCourse: true } }); CoreNavigator.navigateToSitePath(
'/course/' + this.course.id + '/preview',
{ params: { course: this.course, avoidOpenCourse: true } },
);
} }
/** /**

View File

@ -17,7 +17,7 @@ import { RouterModule, ROUTES, Routes } from '@angular/router';
import { resolveModuleRoutes } from '@/app/app-routing.module'; import { resolveModuleRoutes } from '@/app/app-routing.module';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseIndexPage } from './index'; import { CoreCourseIndexPage } from './index.page';
import { COURSE_INDEX_ROUTES } from './index-routing.module'; import { COURSE_INDEX_ROUTES } from './index-routing.module';
function buildRoutes(injector: Injector): Routes { function buildRoutes(injector: Injector): Routes {

View File

@ -123,7 +123,6 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
handlers.forEach((handler, index) => { handlers.forEach((handler, index) => {
handler.data.page = CoreTextUtils.concatenatePaths(this.currentPagePath, handler.data.page); handler.data.page = CoreTextUtils.concatenatePaths(this.currentPagePath, handler.data.page);
handler.data.pageParams = handler.data.pageParams || {}; handler.data.pageParams = handler.data.pageParams || {};
handler.data.pageParams.courseId = this.course!.id;
// Check if this handler should be the first selected tab. // Check if this handler should be the first selected tab.
if (this.firstTabName && handler.name == this.firstTabName) { if (this.firstTabName && handler.name == this.firstTabName) {

View File

@ -16,7 +16,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseListModTypePage } from './list-mod-type'; import { CoreCourseListModTypePage } from './list-mod-type.page';
import { CoreCourseComponentsModule } from '@features/course/components/components.module'; import { CoreCourseComponentsModule } from '@features/course/components/components.module';
const routes: Routes = [ const routes: Routes = [

View File

@ -13,8 +13,8 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="dataLoaded"> <core-loading [hideUntil]="dataLoaded">
<div class="core-course-thumb-parallax"> <div *ngIf="courseImageUrl" class="core-course-thumb-parallax">
<div *ngIf="courseImageUrl" (click)="openCourse()" class="core-course-thumb"> <div (click)="openCourse()" class="core-course-thumb">
<img [src]="courseImageUrl" core-external-content alt=""/> <img [src]="courseImageUrl" core-external-content alt=""/>
</div> </div>
</div> </div>

View File

@ -16,13 +16,12 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreCoursesCoursePreviewPage } from './course-preview'; import { CoreCoursePreviewPage } from './preview.page';
import { CoreCoursesComponentsModule } from '../../components/components.module';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: CoreCoursesCoursePreviewPage, component: CoreCoursePreviewPage,
}, },
]; ];
@ -30,11 +29,10 @@ const routes: Routes = [
imports: [ imports: [
RouterModule.forChild(routes), RouterModule.forChild(routes),
CoreSharedModule, CoreSharedModule,
CoreCoursesComponentsModule,
], ],
declarations: [ declarations: [
CoreCoursesCoursePreviewPage, CoreCoursePreviewPage,
], ],
exports: [RouterModule], exports: [RouterModule],
}) })
export class CoreCoursesCoursePreviewPageModule { } export class CoreCoursePreviewPageModule { }

View File

@ -15,7 +15,7 @@
import { Component, OnDestroy, NgZone, OnInit } from '@angular/core'; import { Component, OnDestroy, NgZone, OnInit } from '@angular/core';
import { ModalController, IonRefresher } from '@ionic/angular'; import { ModalController, IonRefresher } from '@ionic/angular';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
@ -32,18 +32,18 @@ import { CoreCourse, CoreCourseProvider } from '@features/course/services/course
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreCoursesSelfEnrolPasswordComponent } from '../../components/self-enrol-password/self-enrol-password'; import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
/** /**
* Page that allows "previewing" a course and enrolling in it if enabled and not enrolled. * Page that allows "previewing" a course and enrolling in it if enabled and not enrolled.
*/ */
@Component({ @Component({
selector: 'page-core-courses-course-preview', selector: 'page-core-course-preview',
templateUrl: 'course-preview.html', templateUrl: 'preview.html',
styleUrls: ['course-preview.scss'], styleUrls: ['preview.scss'],
}) })
export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy { export class CoreCoursePreviewPage implements OnInit, OnDestroy {
course?: CoreCourseSearchedData; course?: CoreCourseSearchedData;
isEnrolled = false; isEnrolled = false;
@ -84,7 +84,7 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy {
if (this.downloadCourseEnabled) { if (this.downloadCourseEnabled) {
// Listen for status change in course. // Listen for status change in course.
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data: CoreEventCourseStatusChanged) => { this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
this.updateCourseStatus(data.status); this.updateCourseStatus(data.status);
} }

View File

@ -16,7 +16,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseUnsupportedModulePage } from './unsupported-module'; import { CoreCourseUnsupportedModulePage } from './unsupported-module.page';
import { CoreCourseComponentsModule } from '@features/course/components/components.module'; import { CoreCourseComponentsModule } from '@features/course/components/components.module';
const routes: Routes = [ const routes: Routes = [

View File

@ -1848,7 +1848,7 @@ export class CoreCourseHelperProvider {
params = params || {}; params = params || {};
Object.assign(params, { course: course }); Object.assign(params, { course: course });
await CoreNavigator.navigateToSitePath('course', { siteId, params }); await CoreNavigator.navigateToSitePath('course/' + course.id, { siteId, params });
} }
} }

View File

@ -177,7 +177,7 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
Object.assign(params, { course: course }); Object.assign(params, { course: course });
// Don't return the .push promise, we don't want to display a loading modal during the page transition. // Don't return the .push promise, we don't want to display a loading modal during the page transition.
CoreNavigator.navigateToSitePath('course', { params }); CoreNavigator.navigateToSitePath('course/' + course.id, { params });
} }
/** /**

View File

@ -64,9 +64,9 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
event.stopPropagation(); event.stopPropagation();
options = options || {}; options = options || {};
options.params = { module, courseId }; options.params = { module };
CoreNavigator.navigateToSitePath('course/unsupported-module', options); CoreNavigator.navigateToSitePath('course/' + courseId + '/unsupported-module', options);
}, },
}; };

View File

@ -94,7 +94,10 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
if (this.isEnrolled) { if (this.isEnrolled) {
CoreCourseHelper.openCourse(this.course); CoreCourseHelper.openCourse(this.course);
} else { } else {
CoreNavigator.navigate('courses/preview', { params: { course: this.course } }); CoreNavigator.navigate(
'/course/' + this.course.id + '/preview',
{ params: { course: this.course } },
);
} }
} }

View File

@ -50,12 +50,6 @@ const routes: Routes = [
import('./pages/my-courses/my-courses.module') import('./pages/my-courses/my-courses.module')
.then(m => m.CoreCoursesMyCoursesPageModule), .then(m => m.CoreCoursesMyCoursesPageModule),
}, },
{
path: 'preview',
loadChildren: () =>
import('./pages/course-preview/course-preview.module')
.then(m => m.CoreCoursesCoursePreviewPageModule),
},
]; ];
@NgModule({ @NgModule({

View File

@ -59,18 +59,14 @@ export class CoreCoursesEnrolPushClickHandlerService implements CorePushNotifica
const params: Params = { const params: Params = {
course: result.course, course: result.course,
}; };
let page: string; let page = 'course/' + courseId;
if (notification.contexturl?.indexOf('user/index.php') != -1) { if (notification.contexturl?.indexOf('user/index.php') != -1) {
// Open the participants tab. // Open the participants tab.
page = 'course'; params.selectedTab = 'participants'; // @todo: Set this when participants is done.
params.selectedTab = 'user_participants'; // @todo: Set this when participants is done. } else if (!result.enrolled) {
} else if (result.enrolled) {
// User is still enrolled, open the course.
page = 'course';
} else {
// User not enrolled anymore, open the preview page. // User not enrolled anymore, open the preview page.
page = 'courses/preview';; page += '/preview';
} }
await CoreNavigator.navigateToSitePath(page, { params, siteId: notification.site }); await CoreNavigator.navigateToSitePath(page, { params, siteId: notification.site });

View File

@ -75,14 +75,11 @@ export class CoreCoursesRequestPushClickHandlerService implements CorePushNotifi
const params: Params = { const params: Params = {
course: result.course, course: result.course,
}; };
let page: string; let page = 'course/' + courseId;
if (result.enrolled) { if (!result.enrolled) {
// User is still enrolled, open the course.
page = 'course';
} else {
// User not enrolled (shouldn't happen), open the preview page. // User not enrolled (shouldn't happen), open the preview page.
page = 'courses/preview'; page += '/preview';
} }
await CoreNavigator.navigateToSitePath(page, { params, siteId: notification.site }); await CoreNavigator.navigateToSitePath(page, { params, siteId: notification.site });

View File

@ -21,8 +21,8 @@ import { CoreSharedModule } from '@/core/shared.module';
import { CoreGradesCoursePage } from './pages/course/course.page'; import { CoreGradesCoursePage } from './pages/course/course.page';
import { CoreGradesCoursePageModule } from './pages/course/course.module'; import { CoreGradesCoursePageModule } from './pages/course/course.module';
import { CoreGradesCoursesPage } from './pages/courses/courses'; import { CoreGradesCoursesPage } from './pages/courses/courses.page';
import { CoreGradesGradePage } from './pages/grade/grade'; import { CoreGradesGradePage } from './pages/grade/grade.page';
const mobileRoutes: Routes = [ const mobileRoutes: Routes = [
{ {

View File

@ -39,6 +39,10 @@ const routes: Routes = [
path: CoreGradesMainMenuHandlerService.PAGE_NAME, path: CoreGradesMainMenuHandlerService.PAGE_NAME,
loadChildren: () => import('@features/grades/grades-lazy.module').then(m => m.CoreGradesLazyModule), loadChildren: () => import('@features/grades/grades-lazy.module').then(m => m.CoreGradesLazyModule),
}, },
{
path: 'user-grades/:courseId',
loadChildren: () => import('@features/grades/grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule),
},
]; ];
const courseIndexRoutes: Routes = [ const courseIndexRoutes: Routes = [

View File

@ -12,7 +12,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="grades.loaded" class="safe-area-page"> <core-loading [hideUntil]="grades.loaded" class="safe-area-page">
<core-empty-box *ngIf="grades.empty" icon="stats" [message]="'core.grades.nogradesreturned' | translate"> <core-empty-box *ngIf="grades.empty" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate">
</core-empty-box> </core-empty-box>
<div *ngIf="!grades.empty" class="core-grades-container"> <div *ngIf="!grades.empty" class="core-grades-container">
<table cellspacing="0" cellpadding="0" class="core-grades-table"> <table cellspacing="0" cellpadding="0" class="core-grades-table">

View File

@ -45,9 +45,9 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
constructor(route: ActivatedRoute) { constructor(protected route: ActivatedRoute) {
const courseId = parseInt(route.snapshot.params.courseId ?? route.snapshot.queryParams.courseId); const courseId = CoreNavigator.getRouteNumberParam('courseId', { route })!;
const userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.getCurrentSiteUserId()); const userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId();
const useSplitView = route.snapshot.data.useSplitView ?? true; const useSplitView = route.snapshot.data.useSplitView ?? true;
const outsideGradesTab = route.snapshot.data.outsideGradesTab ?? false; const outsideGradesTab = route.snapshot.data.outsideGradesTab ?? false;

View File

@ -14,7 +14,7 @@
<core-loading [hideUntil]="courses.loaded"> <core-loading [hideUntil]="courses.loaded">
<core-empty-box <core-empty-box
*ngIf="courses.empty" *ngIf="courses.empty"
icon="stats" icon="fas-chart-bar"
[message]="'core.grades.nogradesreturned' | translate" [message]="'core.grades.nogradesreturned' | translate"
></core-empty-box> ></core-empty-box>

View File

@ -11,7 +11,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="gradeLoaded"> <core-loading [hideUntil]="gradeLoaded">
<core-empty-box *ngIf="!grade" icon="stats" [message]="'core.grades.nogradesreturned' | translate"></core-empty-box> <core-empty-box *ngIf="!grade" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate"></core-empty-box>
<ion-list *ngIf="grade"> <ion-list *ngIf="grade">
<ion-item *ngIf="grade.itemname && grade.link" class="ion-text-wrap" detail="true" [href]="grade.link" core-link <ion-item *ngIf="grade.itemname && grade.link" class="ion-text-wrap" detail="true" [href]="grade.link" core-link

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular'; import { IonRefresher } from '@ionic/angular';
@ -21,6 +20,7 @@ import { CoreGrades } from '@features/grades/services/grades';
import { CoreGradesFormattedRow, CoreGradesHelper } from '@features/grades/services/grades-helper'; import { CoreGradesFormattedRow, CoreGradesHelper } from '@features/grades/services/grades-helper';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator';
/** /**
* Page that displays activity grade. * Page that displays activity grade.
@ -37,10 +37,10 @@ export class CoreGradesGradePage implements OnInit {
grade?: CoreGradesFormattedRow | null; grade?: CoreGradesFormattedRow | null;
gradeLoaded = false; gradeLoaded = false;
constructor(route: ActivatedRoute) { constructor() {
this.courseId = parseInt(route.snapshot.params.courseId ?? route.snapshot.parent?.params.courseId); this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.gradeId = parseInt(route.snapshot.params.gradeId); this.gradeId = CoreNavigator.getRouteNumberParam('gradeId')!;
this.userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.getCurrentSiteUserId()); this.userId = CoreNavigator.getRouteNumberParam('userId') ?? CoreSites.getCurrentSiteUserId();
} }
/** /**

View File

@ -19,6 +19,8 @@ import { CorePushNotifications } from '@features/pushnotifications/services/push
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreWSExternalWarning } from '@services/ws'; import { CoreWSExternalWarning } from '@services/ws';
import { CoreSiteWSPreSets } from '@classes/site';
import { CoreError } from '@classes/errors/error';
/** /**
* Service to provide grade functionalities. * Service to provide grade functionalities.
@ -114,8 +116,8 @@ export class CoreGradesProvider {
const items = await this.getCourseGradesItems(courseId, userId, groupId, siteId, ignoreCache); const items = await this.getCourseGradesItems(courseId, userId, groupId, siteId, ignoreCache);
return items; return items;
} catch (error) { } catch {
// Ignore while solving MDL-57255 // Ignore while solving MDL-57255 (fixed on 3.2.1)
} }
} }
@ -151,13 +153,13 @@ export class CoreGradesProvider {
userid: userId, userid: userId,
groupid: groupId, groupid: groupId,
}; };
const preSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getCourseGradesItemsCacheKey(courseId, userId, groupId), cacheKey: this.getCourseGradesItemsCacheKey(courseId, userId, groupId),
}; };
if (ignoreCache) { if (ignoreCache) {
preSets['getFromCache'] = 0; preSets.getFromCache = false;
preSets['emergencyCache'] = 0; preSets.emergencyCache = false;
} }
const grades = await site.read<CoreGradesGetUserGradeItemsWSResponse>( const grades = await site.read<CoreGradesGetUserGradeItemsWSResponse>(
@ -167,7 +169,7 @@ export class CoreGradesProvider {
); );
if (!grades?.usergrades?.[0]) { if (!grades?.usergrades?.[0]) {
throw new Error('Couldn\'t get course grades items'); throw new CoreError('Couldn\'t get course grades items');
} }
return grades.usergrades[0].gradeitems; return grades.usergrades[0].gradeitems;
@ -198,19 +200,19 @@ export class CoreGradesProvider {
courseid: courseId, courseid: courseId,
userid: userId, userid: userId,
}; };
const preSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getCourseGradesCacheKey(courseId, userId), cacheKey: this.getCourseGradesCacheKey(courseId, userId),
}; };
if (ignoreCache) { if (ignoreCache) {
preSets['getFromCache'] = 0; preSets.getFromCache = false;
preSets['emergencyCache'] = 0; preSets.emergencyCache = false;
} }
const table = await site.read<CoreGradesGetUserGradesTableWSResponse>('gradereport_user_get_grades_table', params, preSets); const table = await site.read<CoreGradesGetUserGradesTableWSResponse>('gradereport_user_get_grades_table', params, preSets);
if (!table?.tables?.[0]) { if (!table?.tables?.[0]) {
throw new Error('Coudln\'t get course grades table'); throw new CoreError('Coudln\'t get course grades table');
} }
return table.tables[0]; return table.tables[0];
@ -228,7 +230,7 @@ export class CoreGradesProvider {
this.logger.debug('Get course grades'); this.logger.debug('Get course grades');
const params: CoreGradesGetOverviewCourseGradesWSParams = {}; const params: CoreGradesGetOverviewCourseGradesWSParams = {};
const preSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getCoursesGradesCacheKey(), cacheKey: this.getCoursesGradesCacheKey(),
}; };
@ -252,9 +254,10 @@ export class CoreGradesProvider {
* @param siteId Site ID (empty for current site). * @param siteId Site ID (empty for current site).
* @return Promise resolved when the data is invalidated. * @return Promise resolved when the data is invalidated.
*/ */
invalidateAllCourseGradesData(courseId: number, siteId?: string): Promise<void> { async invalidateAllCourseGradesData(courseId: number, siteId?: string): Promise<void> {
return CoreSites.getSite(siteId) const site = await CoreSites.getSite(siteId);
.then((site) => site.invalidateWsCacheForKeyStartingWith(this.getCourseGradesPrefixCacheKey(courseId)));
await site.invalidateWsCacheForKeyStartingWith(this.getCourseGradesPrefixCacheKey(courseId));
} }
/** /**
@ -265,12 +268,11 @@ export class CoreGradesProvider {
* @param siteId Site id (empty for current site). * @param siteId Site id (empty for current site).
* @return Promise resolved when the data is invalidated. * @return Promise resolved when the data is invalidated.
*/ */
invalidateCourseGradesData(courseId: number, userId?: number, siteId?: string): Promise<void> { async invalidateCourseGradesData(courseId: number, userId?: number, siteId?: string): Promise<void> {
return CoreSites.getSite(siteId).then((site) => { const site = await CoreSites.getSite(siteId);
userId = userId || site.getUserId(); userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getCourseGradesCacheKey(courseId, userId)); await site.invalidateWsCacheForKey(this.getCourseGradesCacheKey(courseId, userId));
});
} }
/** /**
@ -279,8 +281,10 @@ export class CoreGradesProvider {
* @param siteId Site id (empty for current site). * @param siteId Site id (empty for current site).
* @return Promise resolved when the data is invalidated. * @return Promise resolved when the data is invalidated.
*/ */
invalidateCoursesGradesData(siteId?: string): Promise<void> { async invalidateCoursesGradesData(siteId?: string): Promise<void> {
return CoreSites.getSite(siteId).then((site) => site.invalidateWsCacheForKey(this.getCoursesGradesCacheKey())); const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getCoursesGradesCacheKey());
} }
/** /**
@ -292,9 +296,10 @@ export class CoreGradesProvider {
* @param siteId Site id (empty for current site). * @param siteId Site id (empty for current site).
* @return Promise resolved when the data is invalidated. * @return Promise resolved when the data is invalidated.
*/ */
invalidateCourseGradesItemsData(courseId: number, userId: number, groupId?: number, siteId?: string): Promise<void> { async invalidateCourseGradesItemsData(courseId: number, userId: number, groupId?: number, siteId?: string): Promise<void> {
return CoreSites.getSite(siteId) const site = await CoreSites.getSite(siteId);
.then((site) => site.invalidateWsCacheForKey(this.getCourseGradesItemsCacheKey(courseId, userId, groupId)));
await site.invalidateWsCacheForKey(this.getCourseGradesItemsCacheKey(courseId, userId, groupId));
} }
/** /**
@ -304,16 +309,17 @@ export class CoreGradesProvider {
* @return Resolve with true if plugin is enabled, false otherwise. * @return Resolve with true if plugin is enabled, false otherwise.
* @since Moodle 3.2 * @since Moodle 3.2
*/ */
isCourseGradesEnabled(siteId?: string): Promise<boolean> { async isCourseGradesEnabled(siteId?: string): Promise<boolean> {
return CoreSites.getSite(siteId).then((site) => { const site = await CoreSites.getSite(siteId);
if (!site.wsAvailable('gradereport_overview_get_course_grades')) { if (!site.wsAvailable('gradereport_overview_get_course_grades')) {
return false; return false;
} }
// Now check that the configurable mygradesurl is pointing to the gradereport_overview plugin. // Now check that the configurable mygradesurl is pointing to the gradereport_overview plugin.
const url = site.getStoredConfig('mygradesurl') || ''; const url = site.getStoredConfig('mygradesurl') || '';
return url.indexOf('/grade/report/overview/') !== -1; return url.indexOf('/grade/report/overview/') !== -1;
});
} }
/** /**
@ -323,13 +329,14 @@ export class CoreGradesProvider {
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
*/ */
isPluginEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> { async isPluginEnabledForCourse(courseId?: number, siteId?: string): Promise<boolean> {
if (!courseId) { if (!courseId) {
return Promise.reject(null); return false;
} }
return CoreCourses.getUserCourse(courseId, true, siteId) const course = await CoreCourses.getUserCourse(courseId, true, siteId);
.then((course) => !(course && typeof course.showgrades != 'undefined' && !course.showgrades));
return !(course && typeof course.showgrades != 'undefined' && !course.showgrades);
} }
/** /**
@ -373,9 +380,11 @@ export class CoreGradesProvider {
CorePushNotifications.logViewEvent(courseId, name, 'grades', wsName, { userid: userId }); CorePushNotifications.logViewEvent(courseId, name, 'grades', wsName, { userid: userId });
} }
const site = await CoreSites.getCurrentSite(); const site = CoreSites.getCurrentSite();
await site?.write(wsName, { courseid: courseId, userid: userId }); const params: CoreGradesGradereportViewGradeReportWSParams = { courseid: courseId, userid: userId };
await site?.write(wsName, params);
} }
/** /**
@ -389,13 +398,13 @@ export class CoreGradesProvider {
courseId = CoreSites.getCurrentSiteHomeId(); courseId = CoreSites.getCurrentSiteHomeId();
} }
const params = { const params: CoreGradesGradereportViewGradeReportWSParams = {
courseid: courseId, courseid: courseId,
}; };
CorePushNotifications.logViewListEvent('grades', 'gradereport_overview_view_grade_report', params); CorePushNotifications.logViewListEvent('grades', 'gradereport_overview_view_grade_report', params);
const site = await CoreSites.getCurrentSite(); const site = CoreSites.getCurrentSite();
await site?.write('gradereport_overview_view_grade_report', params); await site?.write('gradereport_overview_view_grade_report', params);
} }
@ -404,6 +413,14 @@ export class CoreGradesProvider {
export const CoreGrades = makeSingleton(CoreGradesProvider); export const CoreGrades = makeSingleton(CoreGradesProvider);
/**
* Params of gradereport_user_view_grade_report and gradereport_overview_view_grade_report WS.
*/
type CoreGradesGradereportViewGradeReportWSParams = {
courseid: number; // Id of the course.
userid?: number; // Id of the user, 0 means current user.
};
/** /**
* Params of gradereport_user_get_grade_items WS. * Params of gradereport_user_get_grade_items WS.
*/ */

View File

@ -54,8 +54,8 @@ export class CoreGradesCourseOptionHandlerService implements CoreCourseOptionsHa
* *
* @return Whether or not the handler is enabled on a site level. * @return Whether or not the handler is enabled on a site level.
*/ */
isEnabled(): Promise<boolean> { async isEnabled(): Promise<boolean> {
return Promise.resolve(true); return true;
} }
/** /**

View File

@ -44,7 +44,7 @@ export class CoreGradesMainMenuHandlerService implements CoreMainMenuHandler {
*/ */
getDisplayData(): CoreMainMenuHandlerData { getDisplayData(): CoreMainMenuHandlerData {
return { return {
icon: 'stats-chart', icon: 'fas-chart-bar',
title: 'core.grades.grades', title: 'core.grades.grades',
page: CoreGradesMainMenuHandlerService.PAGE_NAME, page: CoreGradesMainMenuHandlerService.PAGE_NAME,
class: 'core-grades-coursesgrades-handler', class: 'core-grades-coursesgrades-handler',

View File

@ -34,79 +34,41 @@ export class CoreGradesUserHandlerService implements CoreUserProfileHandler {
name = 'CoreGrades:viewGrades'; name = 'CoreGrades:viewGrades';
priority = 400; priority = 400;
type = CoreUserDelegateService.TYPE_NEW_PAGE; type = CoreUserDelegateService.TYPE_NEW_PAGE;
viewGradesEnabledCache = {}; cacheEnabled = true;
/** /**
* Clear view grades cache. * @inheritdoc
* If a courseId and userId are specified, it will only delete the entry for that user and course.
*
* @param courseId Course ID.
* @param userId User ID.
*/ */
clearViewGradesCache(courseId?: number, userId?: number): void { async isEnabled(): Promise<boolean> {
if (courseId && userId) { return true;
delete this.viewGradesEnabledCache[this.getCacheKey(courseId, userId)];
} else {
this.viewGradesEnabledCache = {};
}
} }
/** /**
* Get a cache key to identify a course and a user. * @inheritdoc
*
* @param courseId Course ID.
* @param userId User ID.
* @return Cache key.
*/ */
protected getCacheKey(courseId: number, userId: number): string { async isEnabledForCourse(courseId?: number): Promise<boolean> {
return courseId + '#' + userId; return CoreUtils.ignoreErrors(CoreGrades.isPluginEnabledForCourse(courseId), false);
} }
/** /**
* Check if handler is enabled. * @inheritdoc
*
* @return Always enabled.
*/ */
isEnabled(): Promise<boolean> { async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise<boolean> {
return Promise.resolve(true); return CoreUtils.promiseWorks(CoreGrades.getCourseGradesTable(courseId!, user.id));
} }
/** /**
* Check if handler is enabled for this user in this context. * @inheritdoc
*
* @param user User to check.
* @param courseId Course ID.
* @return Promise resolved with true if enabled, resolved with false otherwise.
*/
async isEnabledForUser(user: CoreUserProfile, courseId: number): Promise<boolean> {
const cacheKey = this.getCacheKey(courseId, user.id);
const cache = this.viewGradesEnabledCache[cacheKey];
if (typeof cache != 'undefined') {
return cache;
}
const enabled = await CoreUtils.ignoreErrors(CoreGrades.isPluginEnabledForCourse(courseId), false);
this.viewGradesEnabledCache[cacheKey] = enabled;
return enabled;
}
/**
* Returns the data needed to render the handler.
*
* @return Data needed to render the handler.
*/ */
getDisplayData(): CoreUserProfileHandlerData { getDisplayData(): CoreUserProfileHandlerData {
return { return {
icon: 'stats-chart', icon: 'fas-chart-bar',
title: 'core.grades.grades', title: 'core.grades.grades',
class: 'core-grades-user-handler', class: 'core-grades-user-handler',
action: (event, user, courseId): void => { action: (event, user, courseId): void => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
CoreNavigator.navigateToSitePath(`/grades/${courseId}`, { CoreNavigator.navigateToSitePath(`/user-grades/${courseId}`, {
params: { userId: user.id }, params: { userId: user.id },
}); });
}, },

View File

@ -55,24 +55,12 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle
/** /**
* @inheritdoc * @inheritdoc
*/ */
async isEnabledForUser( async isEnabledForCourse(
user: CoreUserProfile,
courseId?: number, courseId?: number,
): Promise<boolean> { ): Promise<boolean> {
// First check if it's enabled for the user.
const enabledForUser = CoreSitePlugins.isHandlerEnabledForUser(
user.id,
this.handlerSchema.restricttocurrentuser,
this.initResult?.restrict,
);
if (!enabledForUser) {
return false;
}
courseId = courseId || CoreSites.getCurrentSiteHomeId(); courseId = courseId || CoreSites.getCurrentSiteHomeId();
// Enabled for user, check if it's enabled for the course. // Check if it's enabled for the course.
return CoreSitePlugins.isHandlerEnabledForCourse( return CoreSitePlugins.isHandlerEnabledForCourse(
courseId, courseId,
this.handlerSchema.restricttoenrolledcourses, this.handlerSchema.restricttoenrolledcourses,
@ -80,6 +68,19 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle
); );
} }
/**
* @inheritdoc
*/
async isEnabledForUser(
user: CoreUserProfile,
): Promise<boolean> {
return CoreSitePlugins.isHandlerEnabledForUser(
user.id,
this.handlerSchema.restricttocurrentuser,
this.initResult?.restrict,
);
}
/** /**
* @inheritdoc * @inheritdoc
*/ */

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot } from '@angular/router';
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { IonRefresher } from '@ionic/angular'; import { IonRefresher } from '@ionic/angular';
@ -43,8 +43,8 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
constructor(route: ActivatedRoute) { constructor() {
const courseId = parseInt(route.snapshot.queryParams.courseId); const courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId); this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId);
} }

View File

@ -31,35 +31,23 @@ export class CoreUserProfileMailHandlerService implements CoreUserProfileHandler
type = CoreUserDelegateService.TYPE_COMMUNICATION; type = CoreUserDelegateService.TYPE_COMMUNICATION;
/** /**
* Check if handler is enabled. * @inheritdoc
*
* @return Always enabled.
*/ */
async isEnabled(): Promise<boolean> { async isEnabled(): Promise<boolean> {
return true; return true;
} }
/** /**
* Check if handler is enabled for this user in this context. * @inheritdoc
*
* @param user User to check.
* @param courseId Course ID.
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return Promise resolved with true if enabled, resolved with false otherwise.
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars async isEnabledForUser(user: CoreUserProfile): Promise<boolean> {
async isEnabledForUser(user: CoreUserProfile, courseId: number, navOptions?: unknown, admOptions?: unknown): Promise<boolean> {
return user.id != CoreSites.getCurrentSiteUserId() && !!user.email; return user.id != CoreSites.getCurrentSiteUserId() && !!user.email;
} }
/** /**
* Returns the data needed to render the handler. * @inheritdoc
*
* @return Data needed to render the handler.
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars getDisplayData(): CoreUserProfileHandlerData {
getDisplayData(user: CoreUserProfile, courseId: number): CoreUserProfileHandlerData {
return { return {
icon: 'mail', icon: 'mail',
title: 'core.user.sendemail', title: 'core.user.sendemail',

View File

@ -18,7 +18,7 @@ import { Subject, BehaviorSubject } from 'rxjs';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreUserProfile } from './user'; import { CoreUserProfile, CoreUserProvider } from './user';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreCourses, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; import { CoreCourses, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
@ -42,21 +42,36 @@ export interface CoreUserProfileHandler extends CoreDelegateHandler {
type: string; type: string;
/** /**
* Whether or not the handler is enabled for a user. * If isEnabledForUser Cache should be enabled.
*/
cacheEnabled?: boolean;
/**
* Whether or not the handler is enabled for a course.
* *
* @param user User object.
* @param courseId Course ID where to show. * @param courseId Course ID where to show.
* @param navOptions Navigation options for the course. * @param navOptions Navigation options for the course.
* @param admOptions Admin options for the course. * @param admOptions Admin options for the course.
* @return Whether or not the handler is enabled for a user. * @return Whether or not the handler is enabled for a user.
*/ */
isEnabledForUser( isEnabledForCourse?(
user: CoreUserProfile,
courseId?: number, courseId?: number,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed, navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
admOptions?: CoreCourseUserAdminOrNavOptionIndexed, admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<boolean>; ): Promise<boolean>;
/**
* Whether or not the handler is enabled for a user.
*
* @param user User object.
* @param courseId Course ID where to show.
* @return Whether or not the handler is enabled for a user.
*/
isEnabledForUser?(
user: CoreUserProfile,
courseId?: number,
): Promise<boolean>;
/** /**
* Returns the data needed to render the handler. * Returns the data needed to render the handler.
* *
@ -156,6 +171,11 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
*/ */
static readonly UPDATE_HANDLER_EVENT = 'CoreUserDelegate_update_handler_event'; static readonly UPDATE_HANDLER_EVENT = 'CoreUserDelegate_update_handler_event';
/**
* Cache object that checks enabled for use.
*/
protected enabledForUserCache: Record<string, Record<string, boolean>> = {};
protected featurePrefix = 'CoreUserDelegate_'; protected featurePrefix = 'CoreUserDelegate_';
// Hold the handlers and the observable to notify them for each user. // Hold the handlers and the observable to notify them for each user.
@ -186,6 +206,14 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
Object.assign(handler.data, data.data); Object.assign(handler.data, data.data);
this.userHandlers[data.userId].observable.next(this.userHandlers[data.userId].handlers); this.userHandlers[data.userId].observable.next(this.userHandlers[data.userId].handlers);
}); });
CoreEvents.on(CoreEvents.LOGOUT, () => {
this.clearHandlerCache();
});
CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
this.clearHandlerCache(data.courseId, data.userId);
});
} }
/** /**
@ -267,7 +295,7 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
const handler = this.handlers[name]; const handler = this.handlers[name];
try { try {
const enabled = await handler.isEnabledForUser(user, courseId, navOptions, admOptions); const enabled = await this.getAndCacheEnabledForUserFromHandler(handler, user, courseId, navOptions, admOptions);
if (enabled) { if (enabled) {
userData.handlers.push({ userData.handlers.push({
@ -288,6 +316,91 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
userData.observable.next(userData.handlers); userData.observable.next(userData.handlers);
} }
/**
* Helper funtion to get enabled for user from the handler.
*
* @param handler Handler object.
* @param user User object.
* @param courseId Course ID where to show.
* @param navOptions Navigation options for the course.
* @param admOptions Admin options for the course.
* @return Whether or not the handler is enabled for a user.
*/
protected async getAndCacheEnabledForUserFromHandler(
handler: CoreUserProfileHandler,
user: CoreUserProfile,
courseId?: number,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<boolean> {
if (handler.isEnabledForCourse) {
const enabledOnCourse = await handler.isEnabledForCourse(courseId, navOptions, admOptions);
if (!enabledOnCourse) {
// If is not enabled in the course, is not enabled for the user.
// Do not cache if this is false.
return false;
}
}
if (!handler.cacheEnabled) {
if (!handler.isEnabledForUser) {
// True by default.
return true;
}
return handler.isEnabledForUser(user, courseId);
}
if (typeof this.enabledForUserCache[handler.name] == 'undefined') {
this.enabledForUserCache[handler.name] = {};
}
const cacheKey = this.getCacheKey(courseId, user.id);
const cache = this.enabledForUserCache[handler.name][cacheKey];
if (typeof cache != 'undefined') {
return cache;
}
let enabled = true; // Default value.
if (handler.isEnabledForUser) {
enabled = await handler.isEnabledForUser(user, courseId);
}
this.enabledForUserCache[handler.name][cacheKey] = enabled;
return enabled;
}
/**
* Clear handler enabled for user cache.
* If a courseId and userId are specified, it will only delete the entry for that user and course.
*
* @param courseId Course ID.
* @param userId User ID.
*/
protected clearHandlerCache(courseId?: number, userId?: number): void {
if (courseId && userId) {
Object.keys(this.enabledHandlers).forEach((name) => {
delete this.enabledForUserCache[name][this.getCacheKey(courseId, userId)];
});
} else {
this.enabledForUserCache = {};
}
}
/**
* Get a cache key to identify a course and a user.
*
* @param courseId Course ID.
* @param userId User ID.
* @return Cache key.
*/
protected getCacheKey(courseId = 0, userId = 0): string {
return courseId + '#' + userId;
}
} }
export const CoreUserDelegate = makeSingleton(CoreUserDelegateService); export const CoreUserDelegate = makeSingleton(CoreUserDelegateService);

View File

@ -820,9 +820,22 @@ export class CoreUserProvider {
} }
} }
export const CoreUser = makeSingleton(CoreUserProvider); export const CoreUser = makeSingleton(CoreUserProvider);
declare module '@singletons/events' {
/**
* Augment CoreEventsData interface with events specific to this service.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
[CoreUserProvider.PROFILE_REFRESHED]: CoreUserProfileRefreshedData;
[CoreUserProvider.PROFILE_PICTURE_UPDATED]: CoreUserProfilePictureUpdatedData;
}
}
/** /**
* Data passed to PROFILE_REFRESHED event. * Data passed to PROFILE_REFRESHED event.
*/ */

View File

@ -18,7 +18,7 @@ import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module'; import { CoreSearchComponentsModule } from '@features/search/components/components.module';
import { CoreUserParticipantsPage } from './pages/participants/participants'; import { CoreUserParticipantsPage } from './pages/participants/participants.page';
const routes: Routes = [ const routes: Routes = [
{ {

View File

@ -52,10 +52,11 @@ export type CoreNavigationOptions = {
}; };
/** /**
* Options for CoreNavigatorService#getCurrentRoute method. * Route options to get route or params values.
*/ */
type GetCurrentRouteOptions = Partial<{ export type CoreNavigatorCurrentRouteOptions = Partial<{
parentRoute: ActivatedRoute; params: Params; // Params to get the value from.
route: ActivatedRoute; // Current Route.
pageComponent: unknown; pageComponent: unknown;
}>; }>;
@ -246,8 +247,8 @@ export class CoreNavigatorService {
/** /**
* Iterately get the params checking parent routes. * Iterately get the params checking parent routes.
* *
* @param route Current route.
* @param name Name of the parameter. * @param name Name of the parameter.
* @param route Current route.
* @return Value of the parameter, undefined if not found. * @return Value of the parameter, undefined if not found.
*/ */
protected getRouteSnapshotParam<T = unknown>(name: string, route?: ActivatedRoute): T | undefined { protected getRouteSnapshotParam<T = unknown>(name: string, route?: ActivatedRoute): T | undefined {
@ -270,18 +271,21 @@ export class CoreNavigatorService {
* unless there's a new navigation to the page. * unless there's a new navigation to the page.
* *
* @param name Name of the parameter. * @param name Name of the parameter.
* @param params Optional params to get the value from. If missing, it will autodetect. * @param routeOptions Optional routeOptions to get the params or route value from. If missing, it will autodetect.
* @return Value of the parameter, undefined if not found. * @return Value of the parameter, undefined if not found.
*/ */
getRouteParam<T = unknown>(name: string, params?: Params): T | undefined { getRouteParam<T = unknown>(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): T | undefined {
let value: any; let value: any;
if (!params) { if (!routeOptions.params) {
const route = this.getCurrentRoute(); let route = this.getCurrentRoute();
if (!route?.snapshot && routeOptions.route) {
route = routeOptions.route;
}
value = this.getRouteSnapshotParam(name, route); value = this.getRouteSnapshotParam(name, route);
} else { } else {
value = params[name]; value = routeOptions.params[name];
} }
if (typeof value == 'undefined') { if (typeof value == 'undefined') {
@ -309,11 +313,11 @@ export class CoreNavigatorService {
* Angular router automatically converts numbers to string, this function automatically converts it back to number. * Angular router automatically converts numbers to string, this function automatically converts it back to number.
* *
* @param name Name of the parameter. * @param name Name of the parameter.
* @param params Optional params to get the value from. If missing, it will autodetect. * @param routeOptions Optional routeOptions to get the params or route value from. If missing, it will autodetect.
* @return Value of the parameter, undefined if not found. * @return Value of the parameter, undefined if not found.
*/ */
getRouteNumberParam(name: string, params?: Params): number | undefined { getRouteNumberParam(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): number | undefined {
const value = this.getRouteParam<string>(name, params); const value = this.getRouteParam<string>(name, routeOptions);
return value !== undefined ? Number(value) : value; return value !== undefined ? Number(value) : value;
} }
@ -323,13 +327,25 @@ export class CoreNavigatorService {
* Angular router automatically converts booleans to string, this function automatically converts it back to boolean. * Angular router automatically converts booleans to string, this function automatically converts it back to boolean.
* *
* @param name Name of the parameter. * @param name Name of the parameter.
* @param params Optional params to get the value from. If missing, it will autodetect. * @param routeOptions Optional routeOptions to get the params or route value from. If missing, it will autodetect.
* @return Value of the parameter, undefined if not found. * @return Value of the parameter, undefined if not found.
*/ */
getRouteBooleanParam(name: string, params?: Params): boolean | undefined { getRouteBooleanParam(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): boolean | undefined {
const value = this.getRouteParam<string>(name, params); const value = this.getRouteParam<string>(name, routeOptions);
return value !== undefined ? Boolean(value) : value; if (typeof value == 'undefined') {
return value;
}
if (CoreUtils.isTrueOrOne(value)) {
return true;
}
if (CoreUtils.isFalseOrZero(value)) {
return false;
}
return Boolean(value);
} }
/** /**
@ -345,25 +361,25 @@ export class CoreNavigatorService {
* Get current activated route. * Get current activated route.
* *
* @param options * @param options
* - parent: Parent route, if this isn't provided the current active route will be used. * - route: Parent route, if this isn't provided the current active route will be used.
* - pageComponent: Page component of the route to find, if this isn't provided the deepest route in the hierarchy * - pageComponent: Page component of the route to find, if this isn't provided the deepest route in the hierarchy
* will be returned. * will be returned.
* @return Current activated route. * @return Current activated route.
*/ */
getCurrentRoute(): ActivatedRoute; getCurrentRoute(): ActivatedRoute;
getCurrentRoute(options: GetCurrentRouteOptions): ActivatedRoute | null; getCurrentRoute(options: CoreNavigatorCurrentRouteOptions): ActivatedRoute | null;
getCurrentRoute({ parentRoute, pageComponent }: GetCurrentRouteOptions = {}): ActivatedRoute | null { getCurrentRoute({ route, pageComponent }: CoreNavigatorCurrentRouteOptions = {}): ActivatedRoute | null {
parentRoute = parentRoute ?? Router.routerState.root; route = route ?? Router.routerState.root;
if (pageComponent && parentRoute.component === pageComponent) { if (pageComponent && route.component === pageComponent) {
return parentRoute; return route;
} }
if (parentRoute.firstChild) { if (route.firstChild) {
return this.getCurrentRoute({ parentRoute: parentRoute.firstChild, pageComponent }); return this.getCurrentRoute({ route: route.firstChild, pageComponent });
} }
return pageComponent ? null : parentRoute; return pageComponent ? null : route;
} }
/** /**

View File

@ -309,7 +309,8 @@ img[alt] {
// Activity modules // Activity modules
.core-module-icon { .core-module-icon {
--size: 24px; --size: 24px;
width: auto; width: var(--size);
height: var(--size);
max-width: var(--size); max-width: var(--size);
max-height: var(--size); max-height: var(--size);
} }