MOBILE-4323 enrol: Create enrol delegates

Co-authored: dpalou
main
Pau Ferrer Ocaña 2023-07-20 13:03:29 +02:00
parent 47af5a252e
commit b975e6f0d8
36 changed files with 1597 additions and 496 deletions

View File

@ -237,6 +237,14 @@
"addon.coursecompletion.requirement": "block_completionstatus",
"addon.coursecompletion.status": "moodle",
"addon.coursecompletion.viewcoursereport": "completion",
"addon.enrol_guest.guestaccess_withoutpassword": "enrol_guest",
"addon.enrol_guest.guestaccess_withpassword": "enrol_guest",
"addon.enrol_guest.passwordinvalid": "enrol_guest",
"addon.enrol_self.confirmselfenrol": "local_moodlemobileapp",
"addon.enrol_self.errorselfenrol": "local_moodlemobileapp",
"addon.enrol_self.nopassword": "enrol_self",
"addon.enrol_self.password": "enrol_self",
"addon.enrol_self.pluginname": "enrol_self",
"addon.messageoutput_airnotifier.processorsettingsdesc": "local_moodlemobileapp",
"addon.messageoutput_airnotifier.pushdisabledwarning": "local_moodlemobileapp",
"addon.messages.acceptandaddcontact": "message",
@ -1590,9 +1598,6 @@
"core.course.errordownloadingsection": "local_moodlemobileapp",
"core.course.errorgetmodule": "local_moodlemobileapp",
"core.course.failed": "completion",
"core.course.guestaccess": "enrol_guest/pluginname",
"core.course.guestaccess_passwordinvalid": "enrol_guest/passwordinvalid",
"core.course.guestaccess_withpassword": "enrol_guest",
"core.course.hiddenfromstudents": "moodle",
"core.course.hiddenoncoursepage": "moodle",
"core.course.highlighted": "moodle",
@ -1623,7 +1628,6 @@
"core.coursedetails": "moodle",
"core.coursenogroups": "local_moodlemobileapp",
"core.courses.addtofavourites": "block_myoverview",
"core.courses.allowguests": "enrol_guest",
"core.courses.aria:coursecategory": "course",
"core.courses.aria:coursename": "course",
"core.courses.aria:courseprogress": "block_myoverview",
@ -1633,7 +1637,6 @@
"core.courses.cannotretrievemorecategories": "local_moodlemobileapp",
"core.courses.categories": "moodle",
"core.courses.completeenrolmentbrowser": "local_moodlemobileapp",
"core.courses.confirmselfenrol": "local_moodlemobileapp",
"core.courses.courses": "moodle",
"core.courses.downloadcourses": "local_moodlemobileapp",
"core.courses.enrolme": "local_moodlemobileapp",
@ -1641,7 +1644,6 @@
"core.courses.errorloadcourses": "local_moodlemobileapp",
"core.courses.errorloadplugins": "local_moodlemobileapp",
"core.courses.errorsearching": "local_moodlemobileapp",
"core.courses.errorselfenrol": "local_moodlemobileapp",
"core.courses.favourite": "course",
"core.courses.filtermycourses": "local_moodlemobileapp",
"core.courses.frontpage": "admin",
@ -1663,7 +1665,6 @@
"core.courses.search": "moodle",
"core.courses.searchcourses": "moodle",
"core.courses.searchcoursesadvice": "local_moodlemobileapp",
"core.courses.selfenrolment": "local_moodlemobileapp",
"core.courses.show": "block_myoverview",
"core.courses.showonlyenrolled": "local_moodlemobileapp",
"core.courses.therearecourses": "moodle",

View File

@ -20,6 +20,7 @@ import { AddonBlogModule } from './blog/blog.module';
import { AddonCalendarModule } from './calendar/calendar.module';
import { AddonCompetencyModule } from './competency/competency.module';
import { AddonCourseCompletionModule } from './coursecompletion/coursecompletion.module';
import { AddonEnrolModule } from './enrol/enrol.module';
import { AddonFilterModule } from './filter/filter.module';
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
import { AddonMessagesModule } from './messages/messages.module';
@ -42,6 +43,7 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield
AddonCalendarModule,
AddonCompetencyModule,
AddonCourseCompletionModule,
AddonEnrolModule,
AddonFilterModule,
AddonMessageOutputModule,
AddonMessagesModule,

View File

@ -50,8 +50,8 @@ export class AddonCompetencyCourseOptionHandlerService implements CoreCourseOpti
accessData: CoreCourseAccess,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<boolean> {
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guests.
if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guest access.
}
if (navOptions && navOptions.competencies !== undefined) {

View File

@ -43,8 +43,8 @@ export class AddonCourseCompletionCourseOptionHandlerService implements CoreCour
* @inheritdoc
*/
async isEnabledForCourse(courseId: number, accessData: CoreCourseAccess): Promise<boolean> {
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guests.
if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guest access.
}
const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(courseId);

View File

@ -0,0 +1,30 @@
// (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 { AddonEnrolFeeModule } from './fee/fee.module';
import { AddonEnrolGuestModule } from './guest/guest.module';
import { AddonEnrolPaypalModule } from './paypal/paypal.module';
import { AddonEnrolSelfModule } from './self/self.module';
@NgModule({
imports: [
AddonEnrolFeeModule,
AddonEnrolGuestModule,
AddonEnrolPaypalModule,
AddonEnrolSelfModule,
],
})
export class AddonEnrolModule {}

View File

@ -0,0 +1,30 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonEnrolFeeHandler } from './services/enrol-handler';
import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate';
@NgModule({
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreEnrolDelegate.registerHandler(AddonEnrolFeeHandler.instance);
},
},
],
})
export class AddonEnrolFeeModule {}

View File

@ -0,0 +1,38 @@
// (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 { CoreEnrolAction, CoreEnrolHandler } from '@features/enrol/services/enrol-delegate';
import { makeSingleton } from '@singletons';
/**
* Enrol handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonEnrolFeeHandlerService implements CoreEnrolHandler {
name = 'AddonEnrolFee';
type = 'fee';
enrolmentAction = CoreEnrolAction.BROWSER;
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonEnrolFeeHandler = makeSingleton(AddonEnrolFeeHandlerService);

View File

@ -0,0 +1,30 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonEnrolGuestHandler } from './services/enrol-handler';
import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate';
@NgModule({
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreEnrolDelegate.registerHandler(AddonEnrolGuestHandler.instance);
},
},
],
})
export class AddonEnrolGuestModule {}

View File

@ -0,0 +1,5 @@
{
"guestaccess_withpassword": "Guest access requires password",
"guestaccess_withoutpassword": "Guest access",
"passwordinvalid": "Incorrect access password, please try again"
}

View File

@ -0,0 +1,142 @@
// (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 { CoreEnrolAction, CoreEnrolGuestHandler, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate';
import { makeSingleton } from '@singletons';
import { AddonEnrolGuest } from './guest';
import { CorePasswordModalResponse } from '@components/password-modal/password-modal';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreWSError } from '@classes/errors/wserror';
import { CoreEnrol, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol';
/**
* Enrol handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonEnrolGuestHandlerService implements CoreEnrolGuestHandler {
name = 'AddonEnrolGuest';
type = 'guest';
enrolmentAction = <CoreEnrolAction.GUEST> CoreEnrolAction.GUEST;
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
async getInfoIcons(courseId: number): Promise<CoreEnrolInfoIcon[]> {
const guestEnrolments = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { type: this.type });
for (const guestEnrolment of guestEnrolments) {
const info = await AddonEnrolGuest.getGuestEnrolmentInfo(guestEnrolment.id);
// Don't allow guest access if it requires a password if not supported.
if (!info.passwordrequired) {
return [{
label: 'addon.enrol_guest.guestaccess_withoutpassword',
icon: 'fas-unlock',
}];
} else {
return [{
label: 'addon.enrol_guest.guestaccess_withpassword',
icon: 'fas-key',
}];
}
}
return [];
}
/**
* @inheritdoc
*/
async canAccess(method: CoreEnrolEnrolmentMethod): Promise<boolean> {
const info = await AddonEnrolGuest.getGuestEnrolmentInfo(method.id);
return info.status && (!info.passwordrequired || AddonEnrolGuest.isValidateGuestAccessPasswordAvailable());
}
/**
* @inheritdoc
*/
async validateAccess(method: CoreEnrolEnrolmentMethod): Promise<boolean> {
const info = await AddonEnrolGuest.getGuestEnrolmentInfo(method.id);
if (!info.status) {
return false;
}
if (!info.passwordrequired) {
return true;
}
if (!AddonEnrolGuest.isValidateGuestAccessPasswordAvailable()) {
return false;
}
const validatePassword = async (password: string): Promise<CorePasswordModalResponse> => {
const modal = await CoreDomUtils.showModalLoading('core.loading', true);
try {
const response = await AddonEnrolGuest.validateGuestAccessPassword(method.id, password);
let error = response.hint;
if (!response.validated && !error) {
error = 'addon.enrol_guest.passwordinvalid';
}
return {
password, validated: response.validated, error,
};
} finally {
modal.dismiss();
}
};
try {
const response = await CoreDomUtils.promptPassword<CorePasswordModalResponse>({
title: method.name,
validator: validatePassword,
});
if (!response.validated) {
return false;
}
} catch (error) {
if (error instanceof CoreWSError) {
throw error;
}
// Cancelled, return
return false;
}
return true;
}
/**
* @inheritdoc
*/
async invalidate(method: CoreEnrolEnrolmentMethod): Promise<void> {
return AddonEnrolGuest.invalidateGuestEnrolmentInfo(method.id);
}
}
export const AddonEnrolGuestHandler = makeSingleton(AddonEnrolGuestHandlerService);

View File

@ -0,0 +1,158 @@
// (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 { CoreSiteWSPreSets, CoreSite } from '@classes/site';
import { CoreEnrolEnrolmentInfo } from '@features/enrol/services/enrol';
import { CoreSites } from '@services/sites';
import { CoreWSExternalWarning } from '@services/ws';
import { makeSingleton } from '@singletons';
/**
* Service that provides some features to manage guest enrolment.
*/
@Injectable({ providedIn: 'root' })
export class AddonEnrolGuestService {
protected static readonly ROOT_CACHE_KEY = 'AddonEnrolGuest:';
/**
* Get info from a course guest enrolment method.
*
* @param instanceId Guest instance ID.
* @param siteId Site ID. If not defined, use current site.
* @returns Promise resolved when the info is retrieved.
*/
async getGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise<AddonEnrolGuestInfo> {
const site = await CoreSites.getSite(siteId);
const params: AddonEnrolGuestGetInstanceInfoWSParams = {
instanceid: instanceId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getGuestEnrolmentInfoCacheKey(instanceId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
const response =
await site.read<AddonEnrolGuestGetInstanceInfoWSResponse>('enrol_guest_get_instance_info', params, preSets);
return response.instanceinfo;
}
/**
* Get cache key for get course guest enrolment methods WS call.
*
* @param instanceId Guest instance ID.
* @returns Cache key.
*/
protected getGuestEnrolmentInfoCacheKey(instanceId: number): string {
return AddonEnrolGuestService.ROOT_CACHE_KEY + instanceId;
}
/**
* Invalidates get course guest enrolment info WS call.
*
* @param instanceId Guest instance ID.
* @param siteId Site Id. If not defined, use current site.
* @returns Promise resolved when the data is invalidated.
*/
async invalidateGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await Promise.all([
site.invalidateWsCacheForKey(this.getGuestEnrolmentInfoCacheKey(instanceId)),
site.invalidateWsCacheForKey(`mmCourses:guestinfo:${instanceId}`), // @todo Remove after 4.3 release.
]);
}
/**
* Check if guest password validation WS is available on the current site.
*
* @returns Whether guest password validation WS is available.
*/
isValidateGuestAccessPasswordAvailable(): boolean {
return CoreSites.wsAvailableInCurrentSite('enrol_guest_validate_password');
}
/**
* Perform password validation of guess access.
*
* @param enrolmentInstanceId Instance id of guest enrolment plugin.
* @param password Course Password.
* @returns Wether the password is valid.
*/
async validateGuestAccessPassword(
enrolmentInstanceId: number,
password: string,
): Promise<AddonEnrolGuestValidatePasswordWSResponse> {
const site = CoreSites.getCurrentSite();
if (!site) {
return {
validated: false,
};
}
const params: AddonEnrolGuestValidatePasswordWSParams = {
instanceid: enrolmentInstanceId,
password,
};
return await site.write<AddonEnrolGuestValidatePasswordWSResponse>('enrol_guest_validate_password', params);
}
}
export const AddonEnrolGuest = makeSingleton(AddonEnrolGuestService);
/**
* Params of enrol_guest_get_instance_info WS.
*/
type AddonEnrolGuestGetInstanceInfoWSParams = {
instanceid: number; // Instance id of guest enrolment plugin.
};
/**
* Data returned by enrol_guest_get_instance_info WS.
*/
export type AddonEnrolGuestGetInstanceInfoWSResponse = {
instanceinfo: AddonEnrolGuestInfo;
warnings?: CoreWSExternalWarning[];
};
/**
* Course guest enrolment method.
*/
export type AddonEnrolGuestInfo = CoreEnrolEnrolmentInfo & {
passwordrequired: boolean; // Is a password required?
status: boolean; // Is the enrolment enabled?
};
/**
* Params of enrol_guest_validate_password WS.
*/
type AddonEnrolGuestValidatePasswordWSParams = {
instanceid: number; // instance id of guest enrolment plugin
password: string; // the course password
};
/**
* Data returned by enrol_guest_get_instance_info WS.
*/
export type AddonEnrolGuestValidatePasswordWSResponse = {
validated: boolean; // Whether the password was successfully validated
hint?: string; // Password hint (if enabled)
warnings?: CoreWSExternalWarning[];
};

View File

@ -0,0 +1,30 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonEnrolPaypalHandler } from './services/enrol-handler';
import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate';
@NgModule({
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreEnrolDelegate.registerHandler(AddonEnrolPaypalHandler.instance);
},
},
],
})
export class AddonEnrolPaypalModule {}

View File

@ -0,0 +1,38 @@
// (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 { CoreEnrolAction, CoreEnrolHandler } from '@features/enrol/services/enrol-delegate';
import { makeSingleton } from '@singletons';
/**
* Enrol handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonEnrolPaypalHandlerService implements CoreEnrolHandler {
name = 'AddonEnrolPaypal';
type = 'paypal';
enrolmentAction = CoreEnrolAction.BROWSER;
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonEnrolPaypalHandler = makeSingleton(AddonEnrolPaypalHandlerService);

View File

@ -0,0 +1,7 @@
{
"pluginname": "Self enrolment",
"confirmselfenrol": "Are you sure you want to enrol yourself in this course?",
"errorselfenrol": "An error occurred while self enrolling.",
"nopassword": "No enrolment key required.",
"password": "Enrolment key"
}

View File

@ -0,0 +1,30 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonEnrolSelfHandler } from './services/enrol-handler';
import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate';
@NgModule({
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreEnrolDelegate.registerHandler(AddonEnrolSelfHandler.instance);
},
},
],
})
export class AddonEnrolSelfModule {}

View File

@ -0,0 +1,176 @@
// (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 { CoreEnrolAction, CoreEnrolSelfHandler, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate';
import { Translate, makeSingleton } from '@singletons';
import { AddonEnrolSelf } from './self';
import { CorePasswordModalResponse } from '@components/password-modal/password-modal';
import { CoreCoursesProvider } from '@features/courses/services/courses';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreEnrol, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol';
/**
* Enrol handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonEnrolSelfHandlerService implements CoreEnrolSelfHandler {
name = 'AddonEnrolSelf';
type = 'self';
enrolmentAction = <CoreEnrolAction.SELF> CoreEnrolAction.SELF;
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
async getInfoIcons(courseId: number): Promise<CoreEnrolInfoIcon[]> {
const selfEnrolments = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { type: this.type });
let passwordRequired = false;
let noPasswordRequired = false;
for (const selfEnrolment of selfEnrolments) {
const info = await AddonEnrolSelf.getSelfEnrolmentInfo(selfEnrolment.id);
// Don't allow self access if it requires a password if not supported.
if (!info.enrolpassword) {
noPasswordRequired = true;
} else {
passwordRequired = true;
}
if (noPasswordRequired && passwordRequired) {
break;
}
}
const icons: CoreEnrolInfoIcon[] = [];
if (noPasswordRequired) {
icons.push({
label: 'addon.enrol_self.pluginname',
icon: 'fas-right-to-bracket',
});
}
if (passwordRequired) {
icons.push({
label: 'addon.enrol_self.pluginname',
icon: 'fas-key',
});
}
return icons;
}
/**
* @inheritdoc
*/
async enrol(method: CoreEnrolEnrolmentMethod): Promise<boolean> {
const info = await AddonEnrolSelf.getSelfEnrolmentInfo(method.id);
// Don't allow self access if it requires a password if not supported.
if (!info.enrolpassword) {
try {
await CoreDomUtils.showConfirm(
Translate.instant('addon.enrol_self.confirmselfenrol') + '<br>' +
Translate.instant('addon.enrol_self.nopassword'),
method.name,
);
} catch {
// User cancelled.
return false;
}
}
try {
return await this.performEnrol(method);
} catch {
return false;
}
}
/**
* Self enrol in a course.
*
* @param method Enrolment method
* @returns Promise resolved when self enrolled.
*/
protected async performEnrol(method: CoreEnrolEnrolmentMethod): Promise<boolean> {
const validatePassword = async (password = ''): Promise<CorePasswordModalResponse> => {
const modal = await CoreDomUtils.showModalLoading('core.loading', true);
const response: CorePasswordModalResponse = {
password,
};
try {
response.validated = await AddonEnrolSelf.selfEnrol(method.courseid, password, method.id);
} catch (error) {
if (error && error.errorcode === CoreCoursesProvider.ENROL_INVALID_KEY) {
response.validated = false;
response.error = error.message;
} else {
CoreDomUtils.showErrorModalDefault(error, 'addon.enrol_self.errorselfenrol', true);
throw error;
}
} finally {
modal.dismiss();
}
return response;
};
let response: CorePasswordModalResponse | undefined;
try {
response = await validatePassword();
} catch {
return false;
}
if (!response.validated) {
try {
const response = await CoreDomUtils.promptPassword({
validator: validatePassword,
title: method.name,
placeholder: 'addon.enrol_self.password',
submit: 'core.courses.enrolme',
});
if (!response.validated) {
return false;
}
} catch {
// Cancelled, return
return false;
}
}
return true;
}
/**
* @inheritdoc
*/
async invalidate(method: CoreEnrolEnrolmentMethod): Promise<void> {
return AddonEnrolSelf.invalidateSelfEnrolmentInfo(method.id);
}
}
export const AddonEnrolSelfHandler = makeSingleton(AddonEnrolSelfHandlerService);

View File

@ -0,0 +1,152 @@
// (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 { CoreWSError } from '@classes/errors/wserror';
import { CoreSiteWSPreSets, CoreSite } from '@classes/site';
import { CoreCoursesProvider } from '@features/courses/services/courses';
import { CoreSites } from '@services/sites';
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
import { makeSingleton } from '@singletons';
/**
* Service that provides some features to manage self enrolment.
*/
@Injectable({ providedIn: 'root' })
export class AddonEnrolSelfService {
protected static readonly ROOT_CACHE_KEY = 'AddonEnrolSelf:';
/**
* Get info from a course self enrolment method.
*
* @param instanceId Self instance ID.
* @param siteId Site ID. If not defined, use current site.
* @returns Promise resolved when the info is retrieved.
*/
async getSelfEnrolmentInfo(instanceId: number, siteId?: string): Promise<AddonEnrolSelfGetInstanceInfoWSResponse> {
const site = await CoreSites.getSite(siteId);
const params: AddonEnrolSelfGetInstanceInfoWSParams = {
instanceid: instanceId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getSelfEnrolmentInfoCacheKey(instanceId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
return await site.read<AddonEnrolSelfGetInstanceInfoWSResponse>('enrol_self_get_instance_info', params, preSets);
}
/**
* Get cache key for get course self enrolment methods WS call.
*
* @param instanceId Self instance ID.
* @returns Cache key.
*/
protected getSelfEnrolmentInfoCacheKey(instanceId: number): string {
return AddonEnrolSelfService.ROOT_CACHE_KEY + instanceId;
}
/**
* Invalidates get course self enrolment info WS call.
*
* @param instanceId Self instance ID.
* @param siteId Site Id. If not defined, use current site.
* @returns Promise resolved when the data is invalidated.
*/
async invalidateSelfEnrolmentInfo(instanceId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getSelfEnrolmentInfoCacheKey(instanceId));
}
/**
* Self enrol current user in a certain course.
*
* @param courseId Course ID.
* @param password Password to use.
* @param instanceId Enrol instance ID.
* @param siteId Site ID. If not defined, use current site.
* @returns Promise resolved if the user is enrolled. If the password is invalid, the promise is rejected
* with an object with errorcode = CoreCoursesProvider.ENROL_INVALID_KEY.
*/
async selfEnrol(courseId: number, password: string = '', instanceId?: number, siteId?: string): Promise<boolean> {
const site = await CoreSites.getSite(siteId);
const params: AddonEnrolSelfEnrolUserWSParams = {
courseid: courseId,
password: password,
};
if (instanceId) {
params.instanceid = instanceId;
}
const response = await site.write<CoreStatusWithWarningsWSResponse>('enrol_self_enrol_user', params);
if (!response) {
throw Error('WS enrol_self_enrol_user failed');
}
if (response.status) {
return true;
}
if (response.warnings && response.warnings.length) {
// Invalid password warnings.
const warning = response.warnings.find((warning) =>
warning.warningcode == '2' || warning.warningcode == '3' || warning.warningcode == '4');
if (warning) {
throw new CoreWSError({ errorcode: CoreCoursesProvider.ENROL_INVALID_KEY, message: warning.message });
} else {
throw new CoreWSError(response.warnings[0]);
}
}
throw Error('WS enrol_self_enrol_user failed without warnings');
}
}
export const AddonEnrolSelf = makeSingleton(AddonEnrolSelfService);
/**
* Params of enrol_self_get_instance_info WS.
*/
type AddonEnrolSelfGetInstanceInfoWSParams = {
instanceid: number; // Instance id of self enrolment plugin.
};
/**
* Data returned by enrol_self_get_instance_info WS.
*/
export type AddonEnrolSelfGetInstanceInfoWSResponse = {
id: number; // Id of course enrolment instance.
courseid: number; // Id of course.
type: string; // Type of enrolment plugin.
name: string; // Name of enrolment plugin.
status: string; // Status of enrolment plugin.
enrolpassword?: string; // Password required for enrolment.
};
/**
* Params of enrol_self_enrol_user WS.
*/
type AddonEnrolSelfEnrolUserWSParams = {
courseid: number; // Id of the course.
password?: string; // Enrolment key.
instanceid?: number; // Instance id of self enrolment plugin.
};

View File

@ -47,8 +47,8 @@ export class AddonNotesCourseOptionHandlerService implements CoreCourseOptionsHa
accessData: CoreCourseAccess,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<boolean> {
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guests.
if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guest access.
}
if (navOptions && navOptions.notes !== undefined) {

View File

@ -61,7 +61,12 @@ export class CorePasswordModalComponent {
ModalController.dismiss(response);
}
this.error = response.error;
if (typeof response.error === 'string') {
this.error = response.error;
} else if (response.error) {
ModalController.dismiss(response.error);
}
}
/**

View File

@ -76,7 +76,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
@Input() initialSectionId?: number; // The section to load first (by ID).
@Input() initialSectionNumber?: number; // The section to load first (by number).
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
@Input() isGuest?: boolean; // If user is accessing as a guest.
@Input() isGuest?: boolean; // If user is accessing using an ACCESS_GUEST enrolment method.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent<any>>;

View File

@ -35,9 +35,6 @@
"errordownloadingsection": "Error downloading section.",
"errorgetmodule": "Error getting activity data.",
"failed": "Failed",
"guestaccess_passwordinvalid": "Incorrect access password, please try again",
"guestaccess_withpassword": "Guest access requires password",
"guestaccess": "Guest access",
"hiddenfromstudents": "Hidden from students",
"hiddenoncoursepage": "Available but not shown on course page",
"highlighted": "Highlighted",

View File

@ -138,26 +138,18 @@
<ion-label>{{item.data.title | translate }}</ion-label>
</ion-button>
</ng-container>
<ng-container *ngIf="!isEnrolled">
<ion-button expand="block" (click)="enrolMe()" *ngIf="selfEnrolInstances.length || hasBrowserEnrolments" class="ion-text-wrap">
{{ 'core.courses.enrolme' | translate }}
</ion-button>
<ion-button expand="block" (click)="enrolMe()" *ngIf="!isEnrolled && (selfEnrolInstances.length > 0 || otherEnrolments)"
class="ion-text-wrap">
{{ 'core.courses.enrolme' | translate }}
</ion-button>
<ion-card class="core-info-card ion-text-wrap" *ngIf="!isEnrolled && !selfEnrolInstances.length && !otherEnrolments">
<ion-item>
<ion-icon name="fas-circle-info" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.courses.notenrollable' | translate }}</ion-label>
</ion-item>
</ion-card>
<ion-card class="core-info-card ion-text-wrap" *ngIf="!isEnrolled && useGuestAccess && guestAccessPasswordRequired">
<ion-item>
<ion-icon name="fas-key" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.course.guestaccess_withpassword' | translate }}</ion-label>
</ion-item>
</ion-card>
<ion-card class="core-info-card ion-text-wrap" *ngIf="!selfEnrolInstances.length && !hasBrowserEnrolments">
<ion-item>
<ion-icon name="fas-circle-info" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.courses.notenrollable' | translate }}</ion-label>
</ion-item>
</ion-card>
</ng-container>
<ion-button (click)="openCourse()" *ngIf="!isModal && canAccessCourse" expand="block" fill="outline" class="ion-text-wrap">
<ion-icon name="fas-eye" slot="start" aria-hidden="true"></ion-icon>
{{ 'core.course.viewcourse' | translate }}

View File

@ -19,7 +19,6 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import {
CoreCourseCustomField,
CoreCourseEnrolmentMethod,
CoreCourses,
CoreCourseSearchedData,
CoreCoursesProvider,
@ -37,13 +36,12 @@ import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '@features/course
import { Subscription } from 'rxjs';
import { CoreColors } from '@singletons/colors';
import { CorePath } from '@singletons/path';
import { CorePromisedValue } from '@classes/promised-value';
import { CorePlatform } from '@services/platform';
import { CorePasswordModalResponse } from '@components/password-modal/password-modal';
import { CoreTime } from '@singletons/time';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
const ENROL_BROWSER_METHODS = ['fee', 'paypal'];
import { CoreEnrolHelper } from '@features/enrol/services/enrol-helper';
import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate';
import { CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol';
/**
* Page that shows the summary of a course including buttons to enrol and other available options.
@ -61,21 +59,21 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
@ViewChild('courseThumb') courseThumb?: ElementRef;
isEnrolled = false;
canAccessCourse = true;
selfEnrolInstances: CoreCourseEnrolmentMethod[] = [];
otherEnrolments = false;
useGuestAccess = false;
selfEnrolInstances: CoreEnrolEnrolmentMethod[] = [];
guestEnrolInstances: CoreEnrolEnrolmentMethod[] = [];
hasBrowserEnrolments = false;
dataLoaded = false;
isModal = false;
contactsExpanded = false;
useGuestAccess = false;
guestAccessPasswordRequired = false;
courseUrl = '';
progress?: number;
courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
protected actionSheet?: HTMLIonActionSheetElement;
protected guestInstanceId = new CorePromisedValue<number | undefined>();
protected courseData = new CorePromisedValue<CoreCourseSummaryData | undefined>();
protected waitStart = 0;
protected enrolUrl = '';
protected pageDestroyed = false;
@ -143,40 +141,14 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
await this.getCourse();
}
/**
* Check if the user can access as guest.
*
* @returns Promise resolved if can access as guest, rejected otherwise. Resolve param indicates if
* password is required for guest access.
*/
protected async canAccessAsGuest(): Promise<boolean> {
const guestInstanceId = await this.guestInstanceId;
if (guestInstanceId === undefined) {
return false;
}
const info = await CoreCourses.getCourseGuestEnrolmentInfo(guestInstanceId);
// Don't allow guest access if it requires a password if not supported.
this.guestAccessPasswordRequired = info.passwordrequired;
return info.status && (!info.passwordrequired || CoreCourses.isValidateGuestAccessPasswordAvailable());
}
/**
* Convenience function to get course. We use this to determine if a user can see the course or not.
*
* @param refresh If it's refreshing content.
*/
protected async getCourse(refresh = false): Promise<void> {
this.otherEnrolments = false;
try {
await Promise.all([
this.getEnrolmentMethods(),
this.getCourseData(),
this.loadCourseExtraData(),
]);
await this.getCourseData();
this.logView();
} catch (error) {
@ -201,40 +173,13 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
this.dataLoaded = true;
}
/**
* Get course enrolment methods.
*/
protected async getEnrolmentMethods(): Promise<void> {
this.selfEnrolInstances = [];
this.guestInstanceId.reset();
const enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.courseId);
enrolmentMethods.forEach((method) => {
if (!CoreUtils.isTrueOrOne(method.status)) {
return;
}
if (method.type === 'self') {
this.selfEnrolInstances.push(method);
} else if (method.type === 'guest') {
this.guestInstanceId.resolve(method.id);
} else {
// Other enrolments that comes from that WS should need user action.
this.otherEnrolments = true;
}
});
if (!this.guestInstanceId.isSettled()) {
// No guest instance found.
this.guestInstanceId.resolve(undefined);
}
}
/**
* Get course data.
*/
protected async getCourseData(): Promise<void> {
this.canAccessCourse = false;
this.useGuestAccess = false;
try {
// Check if user is enrolled in the course.
try {
@ -250,40 +195,51 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
this.canAccessCourse = true;
this.useGuestAccess = false;
} catch {
// The user is not an admin/manager. Check if we can provide guest access to the course.
this.canAccessCourse = await this.canAccessAsGuest();
this.useGuestAccess = this.canAccessCourse;
// Ignore errors.
}
this.courseData.resolve(this.course);
const courseByField = await CoreUtils.ignoreErrors(CoreCourses.getCourseByField('id', this.courseId));
if (courseByField) {
if (this.course) {
this.course.customfields = courseByField.customfields;
this.course.contacts = courseByField.contacts;
this.course.displayname = courseByField.displayname;
this.course.categoryname = courseByField.categoryname;
this.course.overviewfiles = courseByField.overviewfiles;
} else {
this.course = courseByField;
}
}
await this.getEnrolmentInfo();
}
/**
* Load some extra data for the course.
* Get course enrolment methods.
*/
protected async loadCourseExtraData(): Promise<void> {
try {
const courseByField = await CoreCourses.getCourseByField('id', this.courseId);
const courseData = await this.courseData;
protected async getEnrolmentInfo(): Promise<void> {
if (this.isEnrolled) {
return;
}
if (courseData) {
courseData.customfields = courseByField.customfields;
courseData.contacts = courseByField.contacts;
courseData.displayname = courseByField.displayname;
courseData.categoryname = courseByField.categoryname;
courseData.overviewfiles = courseByField.overviewfiles;
} else {
this.course = courseByField;
this.courseData.resolve(courseByField);
}
const enrolByType = await CoreEnrolHelper.getEnrolmentsByType(this.courseId);
// enrollmentmethods contains ALL enrolment methods including manual.
if (!this.isEnrolled && courseByField.enrollmentmethods?.some((method) => ENROL_BROWSER_METHODS.includes(method))) {
this.otherEnrolments = true;
}
this.hasBrowserEnrolments = enrolByType.hasBrowser;
this.selfEnrolInstances = enrolByType.self;
this.guestEnrolInstances = enrolByType.guest;
} catch {
// Ignore errors.
if (!this.canAccessCourse) {
// The user is not an admin/manager. Check if we can provide guest access to the course.
const promises = this.guestEnrolInstances.map(async (method) => {
const canAccess = await CoreEnrolDelegate.canAccess(method);
if (canAccess) {
this.canAccessCourse = true;
}
});
await Promise.all(promises);
this.useGuestAccess = this.canAccessCourse;
}
}
@ -302,6 +258,33 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
await CoreCourseOptionsDelegate.getMenuHandlersToDisplay(this.course, refresh, this.useGuestAccess);
}
/**
* Validates if the user has access to the course and opens it.
*
* @param enrolMethod The enrolment method.
* @param replaceCurrentPage If current place should be replaced in the navigation stack.
*/
async validateAccessAndOpen(enrolMethod: CoreEnrolEnrolmentMethod, replaceCurrentPage: boolean): Promise<void> {
if (!this.canAccessCourse || !this.course || this.isModal) {
return;
}
let validated = false;
try {
validated = await CoreEnrolDelegate.validateAccess(enrolMethod);
} catch {
this.refreshData();
return;
}
if (!validated) {
return;
}
CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage });
}
/**
* Open the course.
*
@ -312,50 +295,34 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
return;
}
const guestInstanceId = await this.guestInstanceId;
if (this.useGuestAccess && this.guestAccessPasswordRequired && guestInstanceId) {
// Check if the user has access to the course as guest with a previous sent password.
let validated = await CoreCourseHelper.userHasAccessToCourse(this.courseId);
const hasAccess = await CoreCourseHelper.userHasAccessToCourse(this.courseId);
if (!hasAccess && this.guestEnrolInstances.length) {
if (this.guestEnrolInstances.length == 1) {
this.validateAccessAndOpen(this.guestEnrolInstances[0], replaceCurrentPage);
if (!validated) {
try {
type ValidatorResponse = CorePasswordModalResponse & { cancel?: boolean };
const validatePassword = async (password: string): Promise<ValidatorResponse> => {
try {
const response = await CoreCourses.validateGuestAccessPassword(guestInstanceId, password);
validated = response.validated;
let error = response.hint;
if (!validated && !error) {
error = 'core.course.guestaccess_passwordinvalid';
}
return {
password, validated, error,
};
} catch {
this.refreshData();
return {
password,
cancel: true,
};
}
};
const response = await CoreDomUtils.promptPassword<ValidatorResponse>({
title: 'core.course.guestaccess',
validator: validatePassword,
});
if (!response.validated || response.cancel) {
return;
}
} catch {
// Cancelled, return
return;
}
return;
}
const buttons: ActionSheetButton[] = this.guestEnrolInstances.map((enrolMethod) => ({
text: enrolMethod.name,
handler: (): void => {
this.validateAccessAndOpen(enrolMethod, replaceCurrentPage);
},
}));
buttons.push({
text: Translate.instant('core.cancel'),
role: 'cancel',
});
this.actionSheet = await ActionSheetController.create({
header: Translate.instant('core.course.viewcourse'),
buttons: buttons,
});
await this.actionSheet.present();
return;
}
CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage });
@ -389,74 +356,22 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
}
/**
* Confirm user to Self enrol in course.
* Self enrol in a course.
*
* @param enrolMethod The enrolment method.
*/
async selfEnrolConfirm(enrolMethod: CoreCourseEnrolmentMethod): Promise<void> {
async selfEnrolInCourse(enrolMethod: CoreEnrolEnrolmentMethod): Promise<void> {
let enrolled = false;
try {
await CoreDomUtils.showConfirm(Translate.instant('core.courses.confirmselfenrol'), enrolMethod.name);
this.selfEnrolInCourse(enrolMethod.id);
enrolled = await CoreEnrolDelegate.enrol(enrolMethod);
} catch {
// User cancelled.
}
}
this.refreshData();
/**
* Self enrol in a course.
*
* @param instanceId The instance ID.
* @returns Promise resolved when self enrolled.
*/
async selfEnrolInCourse(instanceId: number): Promise<void> {
const validatePassword = async (password = ''): Promise<CorePasswordModalResponse> => {
const response: CorePasswordModalResponse = {
password,
};
try {
response.validated = await CoreCourses.selfEnrol(this.courseId, password, instanceId);
} catch (error) {
if (error && error.errorcode === CoreCoursesProvider.ENROL_INVALID_KEY) {
response.validated = false;
response.error = error.message;
} else {
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorselfenrol', true);
throw error;
}
}
return response;
};
const modal = await CoreDomUtils.showModalLoading('core.loading', true);
let response: CorePasswordModalResponse | undefined;
try {
response = await validatePassword();
} catch {
return;
} finally {
modal.dismiss();
}
if (!response.validated) {
try {
const response = await CoreDomUtils.promptPassword({
validator: validatePassword,
title: 'core.courses.selfenrolment',
placeholder: 'core.courses.password',
submit: 'core.courses.enrolme',
});
if (!response.validated) {
return;
}
} catch {
// Cancelled, return
return;
}
if (!enrolled) {
return;
}
// Refresh data.
@ -488,12 +403,18 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
promises.push(CoreCourses.invalidateUserCourses());
promises.push(CoreCourses.invalidateCourse(this.courseId));
promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId));
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.courseId));
promises.push(CoreCourses.invalidateCoursesByField('id', this.courseId));
if (this.guestInstanceId.value) {
promises.push(CoreCourses.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId.value));
}
promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId));
this.selfEnrolInstances.forEach((method) => {
promises.push(CoreEnrolDelegate.invalidate(method));
});
this.guestEnrolInstances.forEach((method) => {
promises.push(CoreEnrolDelegate.invalidate(method));
});
await Promise.all(promises).finally(() => this.getCourse()).finally(() => {
refresher?.complete();
@ -587,13 +508,13 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
* Open enrol action sheet.
*/
async enrolMe(): Promise<void> {
if (this.selfEnrolInstances.length == 1 && !this.otherEnrolments) {
this.selfEnrolConfirm(this.selfEnrolInstances[0]);
if (this.selfEnrolInstances.length == 1 && !this.hasBrowserEnrolments) {
this.selfEnrolInCourse(this.selfEnrolInstances[0]);
return;
}
if (this.selfEnrolInstances.length == 0 && this.otherEnrolments) {
if (this.selfEnrolInstances.length == 0 && this.hasBrowserEnrolments) {
this.browserEnrol();
return;
@ -602,11 +523,11 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy {
const buttons: ActionSheetButton[] = this.selfEnrolInstances.map((enrolMethod) => ({
text: enrolMethod.name,
handler: (): void => {
this.selfEnrolConfirm(enrolMethod);
this.selfEnrolInCourse(enrolMethod);
},
}));
if (this.otherEnrolments) {
if (this.hasBrowserEnrolments) {
buttons.push({
text: Translate.instant('core.courses.completeenrolmentbrowser'),
handler: (): void => {

View File

@ -74,6 +74,8 @@ import { CoreCourseWithImageAndColor } from '@features/courses/services/courses-
import { CoreCourseSummaryPage } from '../pages/course-summary/course-summary.page';
import { CoreRemindersPushNotificationData } from '@features/reminders/services/reminders';
import { CoreLocalNotifications } from '@services/local-notifications';
import { AddonEnrolGuest } from '@addons/enrol/guest/services/guest';
import { CoreEnrol } from '@features/enrol/services/enrol';
/**
* Prefetch info of a module.
@ -622,19 +624,16 @@ export class CoreCourseHelperProvider {
}
// Check if guest access is enabled.
const enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(courseId, siteId);
const method = enrolmentMethods.find((method) => method.type === 'guest');
if (!method) {
const enrolmentMethods = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { type: 'guest', siteId });
if (!enrolmentMethods) {
return { guestAccess: false };
}
const info = await CoreCourses.getCourseGuestEnrolmentInfo(method.id);
const info = await AddonEnrolGuest.getGuestEnrolmentInfo(enrolmentMethods[0].id);
// Don't allow guest access if it requires a password and it's available.
return {
guestAccess: info.status && (!info.passwordrequired || CoreCourses.isValidateGuestAccessPasswordAvailable()),
guestAccess: info.status && (!info.passwordrequired || AddonEnrolGuest.isValidateGuestAccessPasswordAvailable()),
passwordRequired: info.passwordrequired,
};
} catch {
@ -2151,7 +2150,7 @@ export type CoreCoursePrefetchCourseOptions = {
sections?: CoreCourseWSSection[]; // List of course sections.
courseHandlers?: CoreCourseOptionsHandlerToDisplay[]; // List of course handlers.
menuHandlers?: CoreCourseOptionsMenuHandlerToDisplay[]; // List of course menu handlers.
isGuest?: boolean; // Whether the user is guest.
isGuest?: boolean; // Whether the user is using an ACCESS_GUEST enrolment method.
};
/**

View File

@ -357,7 +357,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
*
* @param course The course object.
* @param refresh True if it should refresh the list.
* @param isGuest Whether it's guest.
* @param isGuest Whether user is using an ACCESS_GUEST enrolment method.
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @returns Promise resolved with array of handlers.
@ -379,7 +379,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
*
* @param course The course object.
* @param refresh True if it should refresh the list.
* @param isGuest Whether it's guest.
* @param isGuest Whether user is using an ACCESS_GUEST enrolment method.
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @returns Promise resolved with array of handlers.
@ -402,7 +402,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
* @param menu If true, gets menu handlers; false, gets tab handlers
* @param course The course object.
* @param refresh True if it should refresh the list.
* @param isGuest Whether it's guest.
* @param isGuest Whether user is using an ACCESS_GUEST enrolment method.
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @returns Promise resolved with array of handlers.

View File

@ -61,7 +61,6 @@ Feature: Test basic usage of guest access course in app
And I press "Course 1" in the app
Then I should find "Course summary" in the app
And I should find "Course" in the app
And I should find "Guest access requires password" in the app
When I press "View course" "ion-button" in the app
And I set the following fields to these values in the app:

View File

@ -57,7 +57,7 @@
<span *ngIf="(layout == 'list' || layout == 'listwithenrol') && !isEnrolled" class="core-course-enrol-icons">
<ion-icon *ngFor="let icon of enrolmentIcons" color="medium" [name]="icon.icon" [title]="icon.label | translate"
[attr.aria-label]="icon.label | translate">
[attr.aria-label]="icon.label | translate" [ngClass]="[icon.className]">
</ion-icon>
</span>

View File

@ -26,6 +26,7 @@ import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@si
import { CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../services/courses';
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu';
import { CoreEnrolHelper } from '@features/enrol/services/enrol-helper';
/**
* This directive is meant to display an item for a list of courses.
@ -98,33 +99,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
this.initPrefetchCourse();
} else if ('enrollmentmethods' in this.course) {
this.enrolmentIcons = [];
this.course.enrollmentmethods.forEach((instance) => {
if (instance === 'self') {
this.enrolmentIcons.push({
label: 'core.courses.selfenrolment',
icon: 'fas-key',
});
} else if (instance === 'guest') {
this.enrolmentIcons.push({
label: 'core.courses.allowguests',
icon: 'fas-unlock',
});
} else if (instance === 'paypal' || instance === 'fee') {
this.enrolmentIcons.push({
label: 'core.courses.otherenrolments',
icon: 'fas-up-right-from-square',
});
}
});
if (this.enrolmentIcons.length == 0) {
this.enrolmentIcons.push({
label: 'core.courses.notenrollable',
icon: 'fas-lock',
});
}
this.enrolmentIcons = await CoreEnrolHelper.getEnrolmentIcons(this.course.enrollmentmethods, this.course.id);
}
}

View File

@ -1,6 +1,5 @@
{
"addtofavourites": "Star this course",
"allowguests": "This course allows guest users to enter",
"aria:coursecategory": "Course category",
"aria:coursename": "Course name",
"aria:courseprogress": "Course progress:",
@ -10,7 +9,6 @@
"cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.",
"categories": "Course categories",
"completeenrolmentbrowser": "Complete enrolment in browser",
"confirmselfenrol": "Are you sure you want to enrol yourself in this course?",
"courses": "Courses",
"downloadcourses": "Download all courses",
"enrolme": "Enrol me",
@ -18,7 +16,6 @@
"errorloadcourses": "An error occurred while loading courses.",
"errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.",
"errorsearching": "An error occurred while searching.",
"errorselfenrol": "An error occurred while self enrolling.",
"favourite": "Starred course",
"filtermycourses": "Filter my courses",
"frontpage": "Site home",
@ -40,7 +37,6 @@
"search": "Search",
"searchcourses": "Search courses",
"searchcoursesadvice": "You can use the search courses button to find courses to access as a guest or enrol yourself in courses that allow it.",
"selfenrolment": "Self enrolment",
"show": "Restore to view",
"showonlyenrolled": "Show only my courses",
"therearecourses": "There are {{$a}} courses",

View File

@ -16,13 +16,15 @@ import { Injectable } from '@angular/core';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSite, CoreSiteWSPreSets, WSObservable } from '@classes/site';
import { makeSingleton } from '@singletons';
import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { CoreEvents } from '@singletons/events';
import { CoreWSError } from '@classes/errors/wserror';
import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper';
import { asyncObservable, firstValueFrom, ignoreErrors, zipIncludingComplete } from '@/core/utils/rxjs';
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
import { AddonEnrolGuest, AddonEnrolGuestInfo } from '@addons/enrol/guest/services/guest';
import { AddonEnrolSelf } from '@addons/enrol/self/services/self';
import { CoreEnrol, CoreEnrolEnrolmentInfo, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol';
const ROOT_CACHE_KEY = 'mmCourses:';
@ -309,33 +311,13 @@ export class CoreCoursesProvider {
/**
* Get the enrolment methods from a course.
*
* @param id ID of the course.
* @param courseId ID of the course.
* @param siteId Site ID. If not defined, use current site.
* @returns Promise resolved with the methods.
* @deprecated since 4.3. Use CoreEnrol.getSupportedCourseEnrolmentMethods instead.
*/
async getCourseEnrolmentMethods(id: number, siteId?: string): Promise<CoreCourseEnrolmentMethod[]> {
const site = await CoreSites.getSite(siteId);
const params: CoreEnrolGetCourseEnrolmentMethodsWSParams = {
courseid: id,
};
const preSets = {
cacheKey: this.getCourseEnrolmentMethodsCacheKey(id),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
return site.read<CoreEnrolGetCourseEnrolmentMethodsWSResponse>
('core_enrol_get_course_enrolment_methods', params, preSets);
}
/**
* Get cache key for get course enrolment methods WS call.
*
* @param id Course ID.
* @returns Cache key.
*/
protected getCourseEnrolmentMethodsCacheKey(id: number): string {
return ROOT_CACHE_KEY + 'enrolmentmethods:' + id;
async getCourseEnrolmentMethods(courseId: number, siteId?: string): Promise<CoreEnrolEnrolmentMethod[]> {
return CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { siteId });
}
/**
@ -344,70 +326,10 @@ export class CoreCoursesProvider {
* @param instanceId Guest instance ID.
* @param siteId Site ID. If not defined, use current site.
* @returns Promise resolved when the info is retrieved.
* @deprecated since 4.3 use AddonEnrolGuest.getCourseGuestEnrolmentInfo instead.
*/
async getCourseGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise<CoreCourseEnrolmentGuestMethod> {
const site = await CoreSites.getSite(siteId);
const params: EnrolGuestGetInstanceInfoWSParams = {
instanceid: instanceId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getCourseGuestEnrolmentInfoCacheKey(instanceId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
const response = await site.read<EnrolGuestGetInstanceInfoWSResponse>('enrol_guest_get_instance_info', params, preSets);
return response.instanceinfo;
}
/**
* Get cache key for get course guest enrolment methods WS call.
*
* @param instanceId Guest instance ID.
* @returns Cache key.
*/
protected getCourseGuestEnrolmentInfoCacheKey(instanceId: number): string {
return ROOT_CACHE_KEY + 'guestinfo:' + instanceId;
}
/**
* Check if guest password validation WS is available on the current site.
*
* @returns Whether guest password validation WSget courses by field is available.
*/
isValidateGuestAccessPasswordAvailable(): boolean {
return CoreSites.wsAvailableInCurrentSite('enrol_guest_validate_password');
}
/**
* Perform password validation of guess access.
*
* @param enrolmentInstanceId Instance id of guest enrolment plugin.
* @param password Course Password.
* @returns Wether the password is valid.
*/
async validateGuestAccessPassword(
enrolmentInstanceId: number,
password: string,
): Promise<EnrolGuestValidatePasswordWSResponse> {
const site = CoreSites.getCurrentSite();
if (!site) {
return {
validated: false,
};
}
const preSets: CoreSiteWSPreSets = {
getFromCache: false,
saveToCache: false,
emergencyCache: false,
};
const params: EnrolGuestValidatePasswordWSParams = {
instanceid: enrolmentInstanceId,
password,
};
return await site.read<EnrolGuestValidatePasswordWSResponse>('enrol_guest_validate_password', params, preSets);
async getCourseGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise<AddonEnrolGuestInfo> {
return AddonEnrolGuest.getGuestEnrolmentInfo(instanceId, siteId);
}
/**
@ -1116,14 +1038,13 @@ export class CoreCoursesProvider {
/**
* Invalidates get course enrolment methods WS call.
*
* @param id Course ID.
* @param courseId Course ID.
* @param siteId Site Id. If not defined, use current site.
* @returns Promise resolved when the data is invalidated.
* @deprecated since 4.3, use CoreEnrol.invalidateCourseEnrolmentMethods instead.
*/
async invalidateCourseEnrolmentMethods(id: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getCourseEnrolmentMethodsCacheKey(id));
async invalidateCourseEnrolmentMethods(courseId: number, siteId?: string): Promise<void> {
return CoreEnrol.invalidateCourseEnrolmentMethods(courseId, siteId);
}
/**
@ -1132,11 +1053,10 @@ export class CoreCoursesProvider {
* @param instanceId Guest instance ID.
* @param siteId Site Id. If not defined, use current site.
* @returns Promise resolved when the data is invalidated.
* @deprecated since 4.3, use CoreEnrolDelegate.invalidate instead.
*/
async invalidateCourseGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getCourseGuestEnrolmentInfoCacheKey(instanceId));
return AddonEnrolGuest.invalidateGuestEnrolmentInfo(instanceId, siteId);
}
/**
@ -1270,16 +1190,6 @@ export class CoreCoursesProvider {
await site.invalidateWsCacheForKey(this.getUserNavigationOptionsCacheKey(courseIds));
}
/**
* Check if WS to retrieve guest enrolment data is available.
*
* @returns Whether guest WS is available.
* @deprecated since app 3.9.5
*/
isGuestWSAvailable(): boolean {
return true;
}
/**
* Report a dashboard or my courses page view event.
*
@ -1339,42 +1249,10 @@ export class CoreCoursesProvider {
* @param siteId Site ID. If not defined, use current site.
* @returns Promise resolved if the user is enrolled. If the password is invalid, the promise is rejected
* with an object with errorcode = CoreCoursesProvider.ENROL_INVALID_KEY.
* @deprecated since 4.3, use CoreEnrolDelegate.enrol instead.
*/
async selfEnrol(courseId: number, password: string = '', instanceId?: number, siteId?: string): Promise<boolean> {
const site = await CoreSites.getSite(siteId);
const params: EnrolSelfEnrolUserWSParams = {
courseid: courseId,
password: password,
};
if (instanceId) {
params.instanceid = instanceId;
}
const response = await site.write<CoreStatusWithWarningsWSResponse>('enrol_self_enrol_user', params);
if (!response) {
throw Error('WS enrol_self_enrol_user failed');
}
if (response.status) {
return true;
}
if (response.warnings && response.warnings.length) {
// Invalid password warnings.
const warning = response.warnings.find((warning) =>
warning.warningcode == '2' || warning.warningcode == '3' || warning.warningcode == '4');
if (warning) {
throw new CoreWSError({ errorcode: CoreCoursesProvider.ENROL_INVALID_KEY, message: warning.message });
} else {
throw new CoreWSError(response.warnings[0]);
}
}
throw Error('WS enrol_self_enrol_user failed without warnings');
return AddonEnrolSelf.selfEnrol(courseId, password, instanceId, siteId);
}
/**
@ -1823,50 +1701,19 @@ export type CoreCourseUserAdminOrNavOptionIndexed = {
boolean; // Whether the option is available or not.
};
/**
* Params of core_enrol_get_course_enrolment_methods WS.
*/
type CoreEnrolGetCourseEnrolmentMethodsWSParams = {
courseid: number; // Course id.
};
/**
* Data returned by core_enrol_get_course_enrolment_methods WS.
*/
type CoreEnrolGetCourseEnrolmentMethodsWSResponse = CoreCourseEnrolmentMethod[];
/**
* Course enrolment basic info.
*
* @deprecated since 4.3 use CoreEnrolEnrolmentInfo instead.
*/
export type CoreCourseEnrolmentInfo = {
id: number; // Id of course enrolment instance.
courseid: number; // Id of course.
type: string; // Type of enrolment plugin.
name: string; // Name of enrolment plugin.
};
export type CoreCourseEnrolmentInfo = CoreEnrolEnrolmentInfo;
/**
* Course enrolment method.
*
* @deprecated since 4.3 use CoreEnrolEnrolmentMethod instead.
*/
export type CoreCourseEnrolmentMethod = CoreCourseEnrolmentInfo & {
wsfunction?: string; // Webservice function to get more information.
status: string; // Status of enrolment plugin. True if successful, else error message or false.
};
/**
* Params of enrol_guest_get_instance_info WS.
*/
type EnrolGuestGetInstanceInfoWSParams = {
instanceid: number; // Instance id of guest enrolment plugin.
};
/**
* Data returned by enrol_guest_get_instance_info WS.
*/
export type EnrolGuestGetInstanceInfoWSResponse = {
instanceinfo: CoreCourseEnrolmentGuestMethod;
warnings?: CoreWSExternalWarning[];
};
export type CoreCourseEnrolmentMethod = CoreEnrolEnrolmentMethod;
/**
* Params of core_course_get_recent_courses WS.
@ -1888,23 +1735,6 @@ export type CoreCourseGetRecentCoursesOptions = CoreSitesCommonWSOptions & {
sort?: string; // Sort string.
};
/**
* Course guest enrolment method.
*/
export type CoreCourseEnrolmentGuestMethod = CoreCourseEnrolmentInfo & {
passwordrequired: boolean; // Is a password required?
status: boolean; // Is the enrolment enabled?
};
/**
* Params of enrol_self_enrol_user WS.
*/
type EnrolSelfEnrolUserWSParams = {
courseid: number; // Id of the course.
password?: string; // Enrolment key.
instanceid?: number; // Instance id of self enrolment plugin.
};
/**
* Params of core_course_set_favourite_courses WS.
*/
@ -1928,23 +1758,6 @@ export type CoreCourseAnyCourseDataWithOptions = CoreCourseAnyCourseData & {
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
};
/**
* Params of enrol_guest_validate_password WS.
*/
type EnrolGuestValidatePasswordWSParams = {
instanceid: number; // instance id of guest enrolment plugin
password: string; // the course password
};
/**
* Data returned by enrol_guest_get_instance_info WS.
*/
export type EnrolGuestValidatePasswordWSResponse = {
validated: boolean; // Whether the password was successfully validated
hint?: string; // Password hint (if enabled)
warnings?: CoreWSExternalWarning[];
};
/**
* Params of core_my_view_page WS.
*/

View File

@ -0,0 +1,24 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule, Type } from '@angular/core';
import { CoreEnrolDelegateService } from './services/enrol-delegate';
export const CORE_ENROL_SERVICES: Type<unknown>[] = [
CoreEnrolDelegateService,
];
@NgModule({})
export class CoreEnrolModule {}

View File

@ -0,0 +1,240 @@
// (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 { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { makeSingleton } from '@singletons';
import { CoreEnrolEnrolmentMethod } from './enrol';
/**
* Enrolment actions.
*/
export enum CoreEnrolAction {
BROWSER = 'browser', // User should use the browser to enrol. Ie. paypal
SELF = 'self', // User can enrol himself or herself.
GUEST = 'guest', // User can view the course without enrolling, like guest enrolment.
NOT_SUPPORTED = 'not_supported', // Enrolment method is not supported by the app.
}
/**
* Interface that all enrolment plugins must implement.
*/
export interface CoreEnrolHandler extends CoreDelegateHandler {
/**
* Name of the enrol the handler supports. E.g. 'self'.
*/
type: string;
/**
* Action to take when enroling.
*/
enrolmentAction: CoreEnrolAction;
/**
* Returns the data needed to render the icon.
*
* @param courseId Course Id.
* @returns Icons data.
*/
getInfoIcons?(courseId: number): Promise<CoreEnrolInfoIcon[]>;
/**
* Invalidates the enrolment info.
*
* @param method Course enrolment method.
* @returns Promise resolved when done
*/
invalidate?(method: CoreEnrolEnrolmentMethod): Promise<void>;
}
/**
* Interface that all self enrolment plugins must implement.
*/
export interface CoreEnrolSelfHandler extends CoreEnrolHandler {
/**
* @inheritdoc
*/
enrolmentAction: CoreEnrolAction.SELF;
/**
* Enrols the user and returns if it has been enrolled or not.
*
* @param method Course enrolment method.
* @returns If the user has been enrolled.
*/
enrol(method: CoreEnrolEnrolmentMethod): Promise<boolean>;
}
/**
* Interface that all guest enrolment plugins must implement.
*/
export interface CoreEnrolGuestHandler extends CoreEnrolHandler {
/**
* @inheritdoc
*/
enrolmentAction: CoreEnrolAction.GUEST;
/**
* Check if the user can access to the course.
*
* @param method Course enrolment method.
* @returns Whether the user can access.
*/
canAccess(method: CoreEnrolEnrolmentMethod): Promise<boolean>;
/**
* Validates the access to a course
*
* @param method Course enrolment method.
* @returns Whether the user has validated the access to the course.
*/
validateAccess(method: CoreEnrolEnrolmentMethod): Promise<boolean>;
}
/**
* Data needed to render a enrolment icons. It's returned by the handler.
*/
export interface CoreEnrolInfoIcon {
label: string;
icon: string;
className?: string;
}
/**
* Delegate to register enrol handlers.
*/
@Injectable({ providedIn: 'root' })
export class CoreEnrolDelegateService extends CoreDelegate<CoreEnrolHandler> {
protected handlerNameProperty = 'type';
protected featurePrefix = 'CoreEnrolDelegate_';
constructor() {
super('CoreEnrolDelegate', true);
}
/**
* Check if an enrolment plugin is supported.
*
* @param methodType Enrol method type.
* @returns Whether it's supported.
*/
isEnrolSupported(methodType: string): boolean {
return this.hasHandler(methodType, true);
}
/**
* Get enrolment action.
*
* @param methodType Enrol method type.
* @returns The enrolment action to take.
*/
getEnrolmentAction(methodType: string): CoreEnrolAction {
const handler = this.getHandler(methodType, false);
if (!handler) {
return CoreEnrolAction.NOT_SUPPORTED;
}
return handler.enrolmentAction;
}
/**
* Get the enrol icon for a certain enrolment method.
*
* @param methodType The methodType to get the icon.
* @param courseId Course Id.
* @returns Promise resolved with the display data.
*/
async getInfoIcons(methodType: string, courseId: number): Promise<CoreEnrolInfoIcon[]> {
const icons = await this.executeFunctionOnEnabled<CoreEnrolInfoIcon[]>(
methodType,
'getInfoIcons',
[courseId],
);
icons?.forEach((icon) => {
if (!icon.className) {
icon.className = `addon-enrol-${methodType}`;
}
});
return icons || [];
}
/**
* Enrols the user and returns if it has been enrolled or not.
*
* @param method Course enrolment method.
* @returns If the user has been enrolled.
*/
async enrol(method: CoreEnrolEnrolmentMethod): Promise<boolean> {
const enrolled = await this.executeFunctionOnEnabled<boolean>(
method.type,
'enrol',
[method],
);
return !!enrolled;
}
/**
* Check if the user can access to the course.
*
* @param method Course enrolment method.
* @returns Whether the user can access.
*/
async canAccess(method: CoreEnrolEnrolmentMethod): Promise<boolean> {
const canAccess = await this.executeFunctionOnEnabled<boolean>(
method.type,
'canAccess',
[method],
);
return !!canAccess;
}
/**
* Validates the access to a course.
*
* @param method Course enrolment method.
* @returns Whether the user has validated the access to the course.
*/
async validateAccess(method: CoreEnrolEnrolmentMethod): Promise<boolean> {
const validated = await this.executeFunctionOnEnabled<boolean>(
method.type,
'validateAccess',
[method],
);
return !!validated;
}
/**
* Invalidates the enrolment info.
*
* @param method Course enrolment method.
* @returns Promise resolved when done
*/
async invalidate(method: CoreEnrolEnrolmentMethod): Promise<void> {
await this.executeFunctionOnEnabled<boolean>(
method.type,
'invalidate',
[method],
);
}
}
export const CoreEnrolDelegate = makeSingleton(CoreEnrolDelegateService);

View File

@ -0,0 +1,128 @@
// (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 { makeSingleton } from '@singletons';
import { CoreEnrolAction, CoreEnrolDelegate, CoreEnrolInfoIcon } from './enrol-delegate';
import { CoreUtils } from '@services/utils/utils';
import { CoreEnrol, CoreEnrolEnrolmentMethod } from './enrol';
/**
* Service that provides helper functions for enrolment plugins.
*/
@Injectable({ providedIn: 'root' })
export class CoreEnrolHelperService {
/**
* Get enrolment icons to show enrol status.
*
* @param methodTypes List of enrolment types to show.
* @param courseId Course Id.
* @returns Enrolment icons to show.
*/
async getEnrolmentIcons(methodTypes: string[], courseId: number): Promise<CoreEnrolInfoIcon[]> {
methodTypes = CoreUtils.uniqueArray(methodTypes);
let enrolmentIcons: CoreEnrolInfoIcon[] = [];
let addBrowserOption = false;
const promises = methodTypes.map(async (type) => {
const enrolIcons = await CoreEnrolDelegate.getInfoIcons(type, courseId);
if (enrolIcons.length) {
enrolmentIcons = enrolmentIcons.concat(enrolIcons);
return;
}
const action = CoreEnrolDelegate.getEnrolmentAction(type);
addBrowserOption = addBrowserOption || action === CoreEnrolAction.BROWSER;
});
await Promise.all(promises);
if (addBrowserOption) {
enrolmentIcons.push({
className: 'enrol_browser',
label: 'core.courses.otherenrolments',
icon: 'fas-up-right-from-square',
});
}
if (enrolmentIcons.length == 0) {
enrolmentIcons.push({
className: 'enrol_locked',
label: 'core.courses.notenrollable',
icon: 'fas-lock',
});
}
return enrolmentIcons;
}
/**
* Get enrolment methods divided by type.
*
* @param courseId Course Id.
* @returns Enrolment info divided by types.
*/
async getEnrolmentsByType(courseId: number): Promise<CoreEnrolmentsByType> {
const enrolmentMethods = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId);
const self: CoreEnrolEnrolmentMethod[] = [];
const guest: CoreEnrolEnrolmentMethod[] = [];
let hasBrowser = false;
let hasNotSupported = false;
enrolmentMethods.forEach((method) => {
if (!CoreUtils.isTrueOrOne(method.status)) {
return;
}
const action = CoreEnrolDelegate.getEnrolmentAction(method.type);
switch (action) {
case CoreEnrolAction.SELF:
self.push(method);
break;
case CoreEnrolAction.GUEST:
guest.push(method);
break;
case CoreEnrolAction.BROWSER:
hasBrowser = true;
break;
case CoreEnrolAction.NOT_SUPPORTED:
hasNotSupported = true;
break;
}
});
return {
self,
guest,
hasBrowser,
hasNotSupported,
};
}
}
export const CoreEnrolHelper = makeSingleton(CoreEnrolHelperService);
export type CoreEnrolmentsByType = {
self: CoreEnrolEnrolmentMethod[];
guest: CoreEnrolEnrolmentMethod[];
hasBrowser: boolean;
hasNotSupported: boolean;
};

View File

@ -0,0 +1,138 @@
// (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 { makeSingleton } from '@singletons';
import { CoreSite } from '@classes/site';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreEnrolAction, CoreEnrolDelegate } from './enrol-delegate';
/**
* Service that provides functions for enrolment plugins.
*/
@Injectable({ providedIn: 'root' })
export class CoreEnrolService {
protected static readonly ROOT_CACHE_KEY = 'CoreEnrol:';
/**
* Get the enrolment methods from a course.
*
* @param courseId ID of the course.
* @param siteId Site ID. If not defined, use current site.
* @returns Promise resolved with the methods.
*/
protected async getCourseEnrolmentMethods(courseId: number, siteId?: string): Promise<CoreEnrolEnrolmentMethod[]> {
const site = await CoreSites.getSite(siteId);
const params: CoreEnrolGetCourseEnrolmentMethodsWSParams = {
courseid: courseId,
};
const preSets = {
cacheKey: this.getCourseEnrolmentMethodsCacheKey(courseId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
return site.read<CoreEnrolGetCourseEnrolmentMethodsWSResponse>('core_enrol_get_course_enrolment_methods', params, preSets);
}
/**
* Get the enrolment methods from a course that are enabled and supported by the app.
*
* @param courseId ID of the course.
* @param options Options.
* @returns Promise resolved with the methods.
*/
async getSupportedCourseEnrolmentMethods(
courseId: number,
options: CoreEnrolGetSupportedMethodsOptions = {},
): Promise<CoreEnrolEnrolmentMethod[]> {
const methods = await CoreEnrol.getCourseEnrolmentMethods(courseId, options.siteId);
return methods.filter((method) => {
if (options.type && method.type !== options.type) {
return false;
}
return CoreEnrolDelegate.isEnrolSupported(method.type) && CoreUtils.isTrueOrOne(method.status) &&
(!options.action || CoreEnrolDelegate.getEnrolmentAction(method.type) === options.action);
});
}
/**
* Get cache key for get course enrolment methods WS call.
*
* @param courseId Course ID.
* @returns Cache key.
*/
protected getCourseEnrolmentMethodsCacheKey(courseId: number): string {
return CoreEnrolService.ROOT_CACHE_KEY + 'enrolmentmethods:' + courseId;
}
/**
* Invalidates get course enrolment methods WS call.
*
* @param courseId Course ID.
* @param siteId Site Id. If not defined, use current site.
* @returns Promise resolved when the data is invalidated.
*/
async invalidateCourseEnrolmentMethods(courseId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await Promise.all([
site.invalidateWsCacheForKey(this.getCourseEnrolmentMethodsCacheKey(courseId)),
site.invalidateWsCacheForKey(`mmCourses:enrolmentmethods:${courseId}`), // @todo: Remove after 4.3 release.
]);
}
}
export const CoreEnrol = makeSingleton(CoreEnrolService);
/**
* Params of core_enrol_get_course_enrolment_methods WS.
*/
type CoreEnrolGetCourseEnrolmentMethodsWSParams = {
courseid: number; // Course id.
};
/**
* Data returned by core_enrol_get_course_enrolment_methods WS.
*/
type CoreEnrolGetCourseEnrolmentMethodsWSResponse = CoreEnrolEnrolmentMethod[];
/**
* Course enrolment method.
*/
export type CoreEnrolEnrolmentMethod = CoreEnrolEnrolmentInfo & {
wsfunction?: string; // Webservice function to get more information.
status: string; // Status of enrolment plugin. True if successful, else error message or false.
};
/**
* Course enrolment basic info.
*/
export type CoreEnrolEnrolmentInfo = {
id: number; // Id of course enrolment instance.
courseid: number; // Id of course.
type: string; // Type of enrolment plugin.
name: string; // Name of enrolment plugin.
};
export type CoreEnrolGetSupportedMethodsOptions = {
type?: string; // If set, only get methods of a certain type.
action?: CoreEnrolAction; // If set, only get methods that use a certain action.
siteId?: string; // Site ID. If not defined, use current site.
};

View File

@ -21,6 +21,7 @@ import { CoreCourseModule } from './course/course.module';
import { CoreCoursesModule } from './courses/courses.module';
import { CoreEditorModule } from './editor/editor.module';
import { CoreEmulatorModule } from './emulator/emulator.module';
import { CoreEnrolModule } from './enrol/enrol.module';
import { CoreFileUploaderModule } from './fileuploader/fileuploader.module';
import { CoreFilterModule } from './filter/filter.module';
import { CoreGradesModule } from './grades/grades.module';
@ -53,6 +54,7 @@ import { CoreReportBuilderModule } from './reportbuilder/reportbuilder.module';
CoreCourseModule,
CoreCoursesModule,
CoreEditorModule,
CoreEnrolModule,
CoreFileUploaderModule,
CoreFilterModule,
CoreGradesModule,

View File

@ -60,6 +60,7 @@ import { CorePlatform } from '@services/platform';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreLang } from '@services/lang';
import { CorePasswordModalParams, CorePasswordModalResponse } from '@components/password-modal/password-modal';
import { CoreWSError } from '@classes/errors/wserror';
/*
* "Utils" service with helper functions for UI, DOM elements and HTML code.
@ -1903,6 +1904,8 @@ export class CoreDomUtilsProvider {
if (modalData === undefined) {
throw new CoreCanceledError();
} else if (modalData instanceof CoreWSError) {
throw modalData;
}
return modalData;