From 9e4aa9e32409a562d6db629bf7018e6d6a2fae99 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Tue, 18 Feb 2025 13:33:17 +0000 Subject: [PATCH 1/5] MDL-84142 core_enrol: new template for self enrolment widgets --- .upgradenotes/MDL-84142-2025021801283101.yml | 9 +++ enrol/classes/output/enrol_page.php | 70 ++++++++++++++++++++ enrol/templates/enrol_page.mustache | 52 +++++++++++++++ lib/enrollib.php | 10 ++- 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 .upgradenotes/MDL-84142-2025021801283101.yml create mode 100644 enrol/classes/output/enrol_page.php create mode 100644 enrol/templates/enrol_page.mustache diff --git a/.upgradenotes/MDL-84142-2025021801283101.yml b/.upgradenotes/MDL-84142-2025021801283101.yml new file mode 100644 index 00000000000..206b9a8ae65 --- /dev/null +++ b/.upgradenotes/MDL-84142-2025021801283101.yml @@ -0,0 +1,9 @@ +issueNumber: MDL-84142 +notes: + core_enrol: + - message: >- + Plugins implementing enrol_page_hook() method are encouraged to use the + renderable \core_enrol\output\enrol_page to produce HTML for the + enrolment page. Forms should be displayed in a modal dialogue. See + enrol_self plugin as an example. + type: improved diff --git a/enrol/classes/output/enrol_page.php b/enrol/classes/output/enrol_page.php new file mode 100644 index 00000000000..dcfaa7dc30b --- /dev/null +++ b/enrol/classes/output/enrol_page.php @@ -0,0 +1,70 @@ +. + +declare(strict_types=1); + +namespace core_enrol\output; + +use core\output\named_templatable; +use core\output\renderable; +use core\output\single_button; + +/** + * Allows to render a widget provided by enrol_plugin::enrol_page_hook() + * + * @package core_enrol + * @copyright Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_page implements named_templatable, renderable { + + /** + * Constructor + * + * @param \stdClass $instance + * @param string|null $header + * @param string|null $body + * @param array $buttons + */ + public function __construct( + /** @var \stdClass */ + protected \stdClass $instance, + /** @var string|null */ + protected ?string $header = null, + /** @var string|null */ + protected ?string $body = null, + /** @var single_button[] */ + protected array $buttons = [] + ) { + } + + #[\Override] + public function export_for_template(\core\output\renderer_base $output) { + return [ + 'enrol' => $this->instance->enrol, + 'instanceid' => $this->instance->id, + 'header' => $this->header, + 'body' => $this->body, + 'buttons' => array_map(fn($b) => $b->export_for_template($output), $this->buttons), + 'hasbuttons' => !empty($this->buttons), + ]; + } + + #[\Override] + public function get_template_name(\core\output\renderer_base $renderer): string { + return 'core_enrol/enrol_page'; + } +} diff --git a/enrol/templates/enrol_page.mustache b/enrol/templates/enrol_page.mustache new file mode 100644 index 00000000000..aa95afdc42e --- /dev/null +++ b/enrol/templates/enrol_page.mustache @@ -0,0 +1,52 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_enrol/enrol_page + + Recommended template for displaying the enrolment plugin call-to-action on the enrol/index.php page + + Usually to be used with the \core_enrol\output\enrol_page and returned from the enrol_plugin::enrol_page_hook() + + Example context (json): + { + "enrol": "self", + "instanceid": 1, + "header": "Self-enrolment", + "body": "You can enrol yourself in this course.", + "hasbuttons": true, + "buttons": [{ + "method" : "get", + "id": "buttonid-123", + "type": "primary", + "url" : "#", + "label" : "Enrol me" + }] + } +}} +
+
+ {{#header}}

{{{header}}}

{{/header}} +
{{{body}}}
+ {{#hasbuttons}} + + {{/hasbuttons}} +
+
diff --git a/lib/enrollib.php b/lib/enrollib.php index 45a2cbf2436..b34b6521df9 100644 --- a/lib/enrollib.php +++ b/lib/enrollib.php @@ -2807,11 +2807,15 @@ abstract class enrol_plugin { } /** - * Creates course enrol form, checks if form submitted - * and enrols user if necessary. It can also redirect. + * Creates a widget to display on the course enrolment page. It can also redirect. + * + * It is recommended that all plugins use the same template for the consistent output. Example: + * + * $obj = new \core_enrol\output\enrol_page($instance, ...); + * return $OUTPUT->render($obj); * * @param stdClass $instance - * @return string html text, usually a form in a text box + * @return string|null html to display on the enrolment page */ public function enrol_page_hook(stdClass $instance) { return null; From 7c196005c7598648d34c0a4a5cf3dd53311ccf16 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Tue, 18 Feb 2025 13:34:00 +0000 Subject: [PATCH 2/5] MDL-84142 enrol_self: use new template for the self enrolment widget --- .upgradenotes/MDL-84142-2025021805225963.yml | 7 + course/tests/behat/keyholder.feature | 2 + enrol/self/amd/build/enrol_page.min.js | 10 + enrol/self/amd/build/enrol_page.min.js.map | 1 + enrol/self/amd/src/enrol_page.js | 63 ++++++ enrol/self/classes/form/enrol_form.php | 204 ++++++++++++++++++ enrol/self/lang/en/enrol_self.php | 1 + enrol/self/lib.php | 83 ++++--- enrol/self/locallib.php | 23 ++ enrol/self/tests/behat/key_holder.feature | 4 +- enrol/self/tests/behat/self_enrolment.feature | 12 +- 11 files changed, 373 insertions(+), 37 deletions(-) create mode 100644 .upgradenotes/MDL-84142-2025021805225963.yml create mode 100644 enrol/self/amd/build/enrol_page.min.js create mode 100644 enrol/self/amd/build/enrol_page.min.js.map create mode 100644 enrol/self/amd/src/enrol_page.js create mode 100644 enrol/self/classes/form/enrol_form.php diff --git a/.upgradenotes/MDL-84142-2025021805225963.yml b/.upgradenotes/MDL-84142-2025021805225963.yml new file mode 100644 index 00000000000..15a40636093 --- /dev/null +++ b/.upgradenotes/MDL-84142-2025021805225963.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-84142 +notes: + enrol_self: + - message: >- + Class enrol_self_enrol_form is deprecated, use + enrol_self\form\enrol_form instead + type: deprecated diff --git a/course/tests/behat/keyholder.feature b/course/tests/behat/keyholder.feature index e7a7e958507..968b6ecd545 100644 --- a/course/tests/behat/keyholder.feature +++ b/course/tests/behat/keyholder.feature @@ -40,6 +40,7 @@ Feature: Keyholder role is listed as course contact And I follow "Course 1" Then I should see "Keyholder 1" + @javascript Scenario: Keyholder assigned to a category Given the following "role assigns" exist: | user | role | contextlevel | reference | @@ -50,4 +51,5 @@ Feature: Keyholder role is listed as course contact When I log in as "student1" And I am on site homepage And I follow "Course 1" + And I press "Enrol me" Then I should see "Keyholder 1" diff --git a/enrol/self/amd/build/enrol_page.min.js b/enrol/self/amd/build/enrol_page.min.js new file mode 100644 index 00000000000..bff131c3ad4 --- /dev/null +++ b/enrol/self/amd/build/enrol_page.min.js @@ -0,0 +1,10 @@ +define("enrol_self/enrol_page",["exports","core_form/modalform","core/str","core/prefetch","core/url"],(function(_exports,_modalform,_str,_prefetch,_url){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Functions for the enrol_self plugin + * + * @module enrol_self/enrol_page + * @copyright Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initEnrol=function(instanceId){(0,_prefetch.prefetchStrings)("enrol_self",["enrolme"]);const button=document.querySelector('button[type="submit"][data-instance="'+instanceId+'"]');button&&button.addEventListener("click",(e=>{e.preventDefault();const modalForm=new _modalform.default({modalConfig:{title:button.dataset.title,large:!1},formClass:button.dataset.form,args:{id:button.dataset.id,instance:instanceId},saveButtonText:(0,_str.getString)("enrolme","enrol_self"),returnFocus:button});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{window.location.href=event.detail?event.detail:_url.default.relativeUrl("/course/view.php",{id:button.dataset.id})})),modalForm.show()}))},_modalform=_interopRequireDefault(_modalform),_url=_interopRequireDefault(_url)})); + +//# sourceMappingURL=enrol_page.min.js.map \ No newline at end of file diff --git a/enrol/self/amd/build/enrol_page.min.js.map b/enrol/self/amd/build/enrol_page.min.js.map new file mode 100644 index 00000000000..408645325d1 --- /dev/null +++ b/enrol/self/amd/build/enrol_page.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"enrol_page.min.js","sources":["../src/enrol_page.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Functions for the enrol_self plugin\n *\n * @module enrol_self/enrol_page\n * @copyright Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport {getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\nimport Url from 'core/url';\n\n/**\n * Initialise widget on the course enrolment page - clicking on the button should submit the form\n *\n * @param {Number} instanceId\n */\nexport function initEnrol(instanceId) {\n prefetchStrings('enrol_self', [\n 'enrolme',\n ]);\n\n const button = document.querySelector('button[type=\"submit\"][data-instance=\"' + instanceId + '\"]');\n if (button) {\n button.addEventListener('click', (e) => {\n e.preventDefault();\n const modalForm = new ModalForm({\n modalConfig: {\n title: button.dataset.title,\n large: false, // This is a very small form that does not need a large popup.\n },\n formClass: button.dataset.form,\n args: {id: button.dataset.id, instance: instanceId},\n saveButtonText: getString('enrolme', 'enrol_self'),\n returnFocus: button,\n });\n\n // Redirect to the course page when the form is submitted.\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {\n window.location.href = event.detail ? event.detail :\n Url.relativeUrl('/course/view.php', {id: button.dataset.id});\n });\n\n modalForm.show();\n });\n }\n}\n"],"names":["instanceId","button","document","querySelector","addEventListener","e","preventDefault","modalForm","ModalForm","modalConfig","title","dataset","large","formClass","form","args","id","instance","saveButtonText","returnFocus","events","FORM_SUBMITTED","event","window","location","href","detail","Url","relativeUrl","show"],"mappings":";;;;;;;yFAiC0BA,0CACN,aAAc,CAC1B,kBAGEC,OAASC,SAASC,cAAc,wCAA0CH,WAAa,MACzFC,QACAA,OAAOG,iBAAiB,SAAUC,IAC9BA,EAAEC,uBACIC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,MAAOT,OAAOU,QAAQD,MACtBE,OAAO,GAEXC,UAAWZ,OAAOU,QAAQG,KAC1BC,KAAM,CAACC,GAAIf,OAAOU,QAAQK,GAAIC,SAAUjB,YACxCkB,gBAAgB,kBAAU,UAAW,cACrCC,YAAalB,SAIjBM,UAAUH,iBAAiBG,UAAUa,OAAOC,gBAAgBC,QACxDC,OAAOC,SAASC,KAAOH,MAAMI,OAASJ,MAAMI,OACxCC,aAAIC,YAAY,mBAAoB,CAACZ,GAAIf,OAAOU,QAAQK,QAGhET,UAAUsB"} \ No newline at end of file diff --git a/enrol/self/amd/src/enrol_page.js b/enrol/self/amd/src/enrol_page.js new file mode 100644 index 00000000000..54b240070a5 --- /dev/null +++ b/enrol/self/amd/src/enrol_page.js @@ -0,0 +1,63 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Functions for the enrol_self plugin + * + * @module enrol_self/enrol_page + * @copyright Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import ModalForm from 'core_form/modalform'; +import {getString} from 'core/str'; +import {prefetchStrings} from 'core/prefetch'; +import Url from 'core/url'; + +/** + * Initialise widget on the course enrolment page - clicking on the button should submit the form + * + * @param {Number} instanceId + */ +export function initEnrol(instanceId) { + prefetchStrings('enrol_self', [ + 'enrolme', + ]); + + const button = document.querySelector('button[type="submit"][data-instance="' + instanceId + '"]'); + if (button) { + button.addEventListener('click', (e) => { + e.preventDefault(); + const modalForm = new ModalForm({ + modalConfig: { + title: button.dataset.title, + large: false, // This is a very small form that does not need a large popup. + }, + formClass: button.dataset.form, + args: {id: button.dataset.id, instance: instanceId}, + saveButtonText: getString('enrolme', 'enrol_self'), + returnFocus: button, + }); + + // Redirect to the course page when the form is submitted. + modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => { + window.location.href = event.detail ? event.detail : + Url.relativeUrl('/course/view.php', {id: button.dataset.id}); + }); + + modalForm.show(); + }); + } +} diff --git a/enrol/self/classes/form/enrol_form.php b/enrol/self/classes/form/enrol_form.php new file mode 100644 index 00000000000..ff605fc608c --- /dev/null +++ b/enrol/self/classes/form/enrol_form.php @@ -0,0 +1,204 @@ +. + +declare(strict_types=1); + +namespace enrol_self\form; + +use core\context\course as context_course; +use core\context\system as context_system; +use core_form\dynamic_form; +use core_text; +use html_writer; +use moodle_url; + +/** + * Form for entering password for self enrolment + * + * @package enrol_self + * @copyright Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_form extends dynamic_form { + /** @var \stdClass */ + protected $instance; + /** @var \enrol_self_plugin */ + protected $plugin = null; + + /** + * Returns the enrolment method + * + * @return \enrol_self_plugin + */ + protected function get_plugin(): \enrol_self_plugin { + global $CFG; + require_once($CFG->dirroot . '/lib/enrollib.php'); + if ($this->plugin === null) { + $this->plugin = enrol_get_plugin('self'); + } + return $this->plugin; + } + + /** + * Returns the instance of the enrolment method + * + * @return \stdClass + */ + protected function get_instance(): \stdClass { + global $DB, $CFG; + require_once($CFG->dirroot . '/lib/enrollib.php'); + if ($this->instance === null) { + // Method enrol_get_instances() will also validate that the enrolment method and the instance are enabled. + $courseid = $this->optional_param('id', 0, PARAM_INT); + $instanceid = $this->optional_param('instance', 0, PARAM_INT); + $instances = enrol_get_instances($courseid, true); + $this->instance = $instances[$instanceid] ?? null; + if (empty($this->instance) || $this->instance->enrol !== 'self') { + throw new \moodle_exception('invalidenrolinstance', 'enrol'); + } + } + return $this->instance; + } + + #[\Override] + public function definition() { + global $USER, $OUTPUT, $CFG; + + $mform = $this->_form; + + $mform->addElement('password', 'enrolpassword', get_string('password', 'enrol_self')); + + // Display keyholders - list of users who have 'enrol/self:holdkey' capability. + $context = context_course::instance($this->instance->courseid); + $userfieldsapi = \core_user\fields::for_userpic(); + $ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; + $keyholders = get_users_by_capability($context, 'enrol/self:holdkey', $ufields); + $keyholdercount = 0; + foreach ($keyholders as $keyholder) { + $keyholdercount++; + if ($keyholdercount === 1) { + $mform->addElement('static', 'keyholder', '', get_string('keyholder', 'enrol_self')); + } + if ($USER->id == $keyholder->id + || has_capability('moodle/user:viewdetails', context_system::instance()) + || has_coursecontact_role($keyholder->id)) { + $profileurl = new moodle_url('/user/profile.php', ['id' => $keyholder->id, 'course' => $this->instance->courseid]); + $profilelink = html_writer::link($profileurl, fullname($keyholder)); + } else { + $profilelink = fullname($keyholder); + } + $profilepic = $OUTPUT->user_picture($keyholder, ['size' => 35, 'courseid' => $this->instance->courseid]); + $mform->addElement('static', 'keyholder' . $keyholdercount, '', $profilepic . $profilelink); + } + + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + + $mform->addElement('hidden', 'instance'); + $mform->setType('instance', PARAM_INT); + } + + #[\Override] + public function validation($data, $files) { + global $DB, $CFG; + require_once($CFG->dirroot.'/enrol/self/locallib.php'); + + $errors = parent::validation($data, $files); + $instance = $this->get_instance(); + if ($data['enrolpassword'] !== $instance->password) { + if ($instance->customint1) { + // Check group enrolment key. + if (!enrol_self_check_group_enrolment_key($instance->courseid, $data['enrolpassword'])) { + // We can not hint because there are probably multiple passwords. + $errors['enrolpassword'] = get_string('passwordinvalid', 'enrol_self'); + } + } else { + $plugin = enrol_get_plugin('self'); + if ($plugin->get_config('showhint')) { + $hint = core_text::substr($instance->password, 0, 1); + $errors['enrolpassword'] = get_string('passwordinvalidhint', 'enrol_self', $hint); + } else { + $errors['enrolpassword'] = get_string('passwordinvalid', 'enrol_self'); + } + } + } + + return $errors; + } + + #[\Override] + protected function check_access_for_dynamic_submission(): void { + global $USER, $CFG; + $instance = $this->get_instance(); + $courseid = $instance->courseid; + $course = get_course($courseid); + $context = context_course::instance($instance->courseid); + if (!\core_course_category::can_view_course_info($course) && !is_enrolled($context, $USER, '', true)) { + throw new \moodle_exception('coursehidden', '', $CFG->wwwroot . '/'); + } + if (isguestuser()) { + throw new \moodle_exception('noguestaccess', 'enrol'); + } + $canselfenrol = $this->get_plugin()->can_self_enrol($instance); + if ($canselfenrol !== true) { + throw new \moodle_exception($canselfenrol); + } + if (!$instance->password) { + throw new \moodle_exception('nopassword', 'enrol_self'); + } + } + + #[\Override] + protected function get_context_for_dynamic_submission(): \context { + // This form is used for users who are not yet enrolled in the course and do not have access to the course. + // For the purpose of permission checks they must be able to access the course category for this course. + return context_course::instance($this->get_instance()->courseid)->get_parent_context(); + } + + #[\Override] + protected function get_page_url_for_dynamic_submission(): moodle_url { + $instance = $this->get_instance(); + return new moodle_url('/enrol/index.php', ['id' => $instance->courseid, 'instance' => $instance->id]); + } + + /** + * Process the form submission, used if form was submitted via AJAX + * + * Enrols the user in the course and returns the URL to redirect to + * + * @return string + */ + public function process_dynamic_submission() { + global $CFG, $SESSION; + $this->get_plugin()->enrol_self($this->get_instance(), $this->get_data()); + + // Go to the originally requested page. + if (!empty($SESSION->wantsurl)) { + $destination = $SESSION->wantsurl; + unset($SESSION->wantsurl); + } else { + require_once($CFG->dirroot . '/course/lib.php'); + $destination = course_get_url($this->get_instance()->courseid); + } + return $destination; + } + + #[\Override] + public function set_data_for_dynamic_submission(): void { + $instance = $this->get_instance(); + $this->set_data(['id' => $instance->courseid, 'instance' => $instance->id]); + } +} diff --git a/enrol/self/lang/en/enrol_self.php b/enrol/self/lang/en/enrol_self.php index f681f512266..b77a06ae2cf 100644 --- a/enrol/self/lang/en/enrol_self.php +++ b/enrol/self/lang/en/enrol_self.php @@ -36,6 +36,7 @@ $string['editselectedusers'] = 'Edit selected user enrolments'; $string['enrolenddate'] = 'End date'; $string['enrolenddate_help'] = 'If enabled, users can enrol themselves until this date only.'; $string['enrolenddaterror'] = 'Enrolment end date cannot be earlier than start date'; +$string['enrolkeyrequired'] = 'An enrolment key will be required'; $string['enrolme'] = 'Enrol me'; $string['enrolperiod'] = 'Enrolment duration'; $string['enrolperiod_desc'] = 'Default length of time that the enrolment is valid. If set to zero, the enrolment duration will be unlimited by default.'; diff --git a/enrol/self/lib.php b/enrol/self/lib.php index 30721dcc3ce..d50851ab5b2 100644 --- a/enrol/self/lib.php +++ b/enrol/self/lib.php @@ -22,6 +22,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use core\output\single_button; +use core_enrol\output\enrol_page; + /** * Self enrolment plugin implementation. * @author Petr Skoda @@ -164,7 +167,6 @@ class enrol_self_plugin extends enrol_plugin { * * @param stdClass $instance enrolment instance * @param stdClass $data data needed for enrolment. - * @return bool|array true if enroled else eddor code and messege */ public function enrol_self(stdClass $instance, $data = null) { global $DB, $USER, $CFG; @@ -202,46 +204,61 @@ class enrol_self_plugin extends enrol_plugin { } } - /** - * Creates course enrol form, checks if form submitted - * and enrols user if necessary. It can also redirect. - * - * @param stdClass $instance - * @return string html text, usually a form in a text box - */ + #[\Override] public function enrol_page_hook(stdClass $instance) { - global $CFG, $OUTPUT, $USER; + global $CFG, $OUTPUT, $USER, $PAGE; - require_once("$CFG->dirroot/enrol/self/locallib.php"); + $buttonurl = null; + $buttontext = ''; + $buttonattrs = []; + $body = ''; + $title = $this->get_instance_name($instance); $enrolstatus = $this->can_self_enrol($instance); - - if (true === $enrolstatus) { - // This user can self enrol using this instance. - $form = new enrol_self_enrol_form(null, $instance); - $instanceid = optional_param('instance', 0, PARAM_INT); - if ($instance->id == $instanceid) { - if ($data = $form->get_data()) { - $this->enrol_self($instance, $data); + if ($enrolstatus === true) { + if ($instance->password) { + // Self-enrolment with password. Display a button to open a form in a modal. + $body = get_string('enrolkeyrequired', 'enrol_self'); + $buttonurl = $PAGE->url; + $buttonattrs = [ + 'data-id' => $instance->courseid, + 'data-instance' => $instance->id, + 'data-form' => enrol_self\form\enrol_form::class, + 'data-title' => $title, + ]; + $PAGE->requires->js_call_amd('enrol_self/enrol_page', 'initEnrol', [$instance->id]); + } else { + // Self-enrolment without password. Display a button to self enrol. If button is pressed - enrol the user. + if (optional_param('action', null, PARAM_TEXT) === 'enrol' && confirm_sesskey()) { + $this->enrol_self($instance, (object)[]); + return ''; } + $body = get_string('nopassword', 'enrol_self'); + $buttonurl = new moodle_url($PAGE->url, ['action' => 'enrol', 'sesskey' => sesskey()]); } + $buttontext = get_string('enrolme', 'enrol_self'); + } else if (isguestuser()) { + // User is not logged in. Display a button to login. + $buttonurl = new moodle_url(get_login_url()); + $body = get_string('noguestaccess', 'enrol'); + $buttontext = get_string('continue'); + } else if (!$enrolstatus) { + // No reason why user can not use this method, do not display anything. + return ''; } else { - // This user can not self enrol using this instance. Using an empty form to keep - // the UI consistent with other enrolment plugins that returns a form. - $data = new stdClass(); - $data->header = $this->get_instance_name($instance); - $data->info = $enrolstatus; - - // The can_self_enrol call returns a button to the login page if the user is a - // guest, setting the login url to the form if that is the case. - $url = isguestuser() ? get_login_url() : null; - $form = new enrol_self_empty_form($url, $data); + $body = $enrolstatus; } - ob_start(); - $form->display(); - $output = ob_get_clean(); - return $OUTPUT->box($output); + $notification = new \core\output\notification($body, 'info', false); + $notification->set_extra_classes(['mb-0']); + $enrolpage = new enrol_page( + instance: $instance, + header: $title, + body: $OUTPUT->render($notification), + buttons: $buttonurl ? + [new single_button($buttonurl, $buttontext, 'get', single_button::BUTTON_PRIMARY, $buttonattrs)] : + []); + return $OUTPUT->render($enrolpage); } /** @@ -285,7 +302,7 @@ class enrol_self_plugin extends enrol_plugin { * This function doesn't check user capabilities. Use can_self_enrol to check capabilities. * * @param stdClass $instance enrolment instance - * @return bool - true means "Enrol me in this course" link could be available + * @return bool|string - true means "Enrol me in this course" link could be available */ public function is_self_enrol_available(stdClass $instance) { global $CFG, $DB, $USER; diff --git a/enrol/self/locallib.php b/enrol/self/locallib.php index 98f73c84c5a..b0684160b10 100644 --- a/enrol/self/locallib.php +++ b/enrol/self/locallib.php @@ -53,10 +53,33 @@ function enrol_self_check_group_enrolment_key($courseid, $enrolpassword) { return $found; } +/** + * Old class for displaying the self enrolment form + * + * @deprecated since Moodle 5.0 - please use {@see enrol_self\form\enrol_form} + */ +#[\core\attribute\deprecated(replacement: enrol_self\form\enrol_form::class, since: '5.0', reason: 'Now a dynamic form is used')] class enrol_self_enrol_form extends moodleform { protected $instance; protected $toomany = false; + /** + * Constructor + * + * @param mixed $action + * @param mixed $customdata + * @param string $method + * @param string $target + * @param mixed $attributes + * @param bool $editable + * @param array $ajaxformdata + */ + public function __construct($action=null, $customdata=null, $method='post', $target='', $attributes=null, $editable=true, + $ajaxformdata=null) { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); + parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata); + } + /** * Overriding this function to get unique form id for multiple self enrolments. * diff --git a/enrol/self/tests/behat/key_holder.feature b/enrol/self/tests/behat/key_holder.feature index e6f110067e5..f761c1496b9 100644 --- a/enrol/self/tests/behat/key_holder.feature +++ b/enrol/self/tests/behat/key_holder.feature @@ -34,11 +34,13 @@ Feature: Users can be defined as key holders in courses where self enrolment is And I log out And I log in as "student1" And I am on "Course 1" course homepage + And I should see "An enrolment key will be required" + And I press "Enrol me" And I should see "You should have received this enrolment key from:" And I should see "Manager 1" And I set the following fields to these values: | Enrolment key | moodle_rules | - And I press "Enrol me" + And I click on "Enrol me" "button" in the "Test student enrolment" "dialogue" Then I should see "New section" And I should not see "Enrolment options" And I should not see "Enrol me in this course" diff --git a/enrol/self/tests/behat/self_enrolment.feature b/enrol/self/tests/behat/self_enrolment.feature index 5813c7c61ed..db0617563fb 100644 --- a/enrol/self/tests/behat/self_enrolment.feature +++ b/enrol/self/tests/behat/self_enrolment.feature @@ -42,6 +42,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe Then I should see "New section" And I should not see "Enrolment options" + @javascript Scenario: Self-enrolment enabled requiring an enrolment key Given I log in as "teacher1" When I add "Self enrolment" enrolment method in "Course 1" with: @@ -50,9 +51,11 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe And I log out And I log in as "student1" And I am on "Course 1" course homepage + And I should see "An enrolment key will be required" + And I press "Enrol me" And I set the following fields to these values: | Enrolment key | moodle_rules | - And I press "Enrol me" + And I click on "Enrol me" "button" in the "Test student enrolment" "dialogue" Then I should see "New section" And I should not see "Enrolment options" And I should not see "Enrol me in this course" @@ -62,6 +65,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe When I am on "Course 1" course homepage Then I should see "You cannot enrol yourself in this course" + @javascript Scenario: Self-enrolment enabled requiring a group enrolment key Given I log in as "teacher1" When I add "Self enrolment" enrolment method in "Course 1" with: @@ -77,16 +81,18 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe And I log out And I log in as "student1" And I am on "Course 1" course homepage + And I press "Enrol me" And I set the following fields to these values: | Enrolment key | Test-groupenrolkey1 | - And I press "Enrol me" + And I click on "Enrol me" "button" in the "Test student enrolment" "dialogue" Then I should see "New section" And I should not see "Enrolment options" And I should not see "Enrol me in this course" And I am on the "Course 1" course page logged in as student2 + And I press "Enrol me" And I set the following fields to these values: | Enrolment key | moodle_rules | - And I press "Enrol me" + And I click on "Enrol me" "button" in the "Test student enrolment" "dialogue" And I am on the "Course 1" course page logged in as teacher1 And I navigate to course participants And the following should exist in the "participants" table: From 58a1667f928ff8dda12876f75dbdf2d98f193b9f Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Tue, 18 Feb 2025 13:34:13 +0000 Subject: [PATCH 3/5] MDL-84142 enrol_guest: use new template for the self enrolment widget --- .upgradenotes/MDL-84142-2025021805284040.yml | 7 + enrol/guest/amd/build/enrol_page.min.js | 10 ++ enrol/guest/amd/build/enrol_page.min.js.map | 1 + enrol/guest/amd/src/enrol_page.js | 63 ++++++++ enrol/guest/classes/form/enrol_form.php | 150 +++++++++++++++++++ enrol/guest/lang/en/enrol_guest.php | 1 + enrol/guest/lib.php | 77 +++++----- enrol/guest/locallib.php | 34 ++++- enrol/guest/tests/behat/guest_access.feature | 4 +- 9 files changed, 303 insertions(+), 44 deletions(-) create mode 100644 .upgradenotes/MDL-84142-2025021805284040.yml create mode 100644 enrol/guest/amd/build/enrol_page.min.js create mode 100644 enrol/guest/amd/build/enrol_page.min.js.map create mode 100644 enrol/guest/amd/src/enrol_page.js create mode 100644 enrol/guest/classes/form/enrol_form.php diff --git a/.upgradenotes/MDL-84142-2025021805284040.yml b/.upgradenotes/MDL-84142-2025021805284040.yml new file mode 100644 index 00000000000..b994c8b8c05 --- /dev/null +++ b/.upgradenotes/MDL-84142-2025021805284040.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-84142 +notes: + enrol_guest: + - message: >- + Class enrol_guest_enrol_form is deprecated, use + enrol_guest\form\enrol_form instead + type: deprecated diff --git a/enrol/guest/amd/build/enrol_page.min.js b/enrol/guest/amd/build/enrol_page.min.js new file mode 100644 index 00000000000..30c2454a44b --- /dev/null +++ b/enrol/guest/amd/build/enrol_page.min.js @@ -0,0 +1,10 @@ +define("enrol_guest/enrol_page",["exports","core_form/modalform","core/str","core/prefetch","core/url"],(function(_exports,_modalform,_str,_prefetch,_url){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Functions for the enrol_guest plugin + * + * @module enrol_guest/enrol_page + * @copyright Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initEnrol=function(instanceId){(0,_prefetch.prefetchStrings)("moodle",["loginguest"]);const button=document.querySelector('button[type="submit"][data-instance="'+instanceId+'"]');button&&button.addEventListener("click",(e=>{e.preventDefault();const modalForm=new _modalform.default({modalConfig:{title:button.dataset.title,large:!1},formClass:button.dataset.form,args:{id:button.dataset.id,instance:instanceId},saveButtonText:(0,_str.getString)("loginguest","moodle"),returnFocus:button});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{window.location.href=event.detail?event.detail:_url.default.relativeUrl("/course/view.php",{id:button.dataset.id})})),modalForm.show()}))},_modalform=_interopRequireDefault(_modalform),_url=_interopRequireDefault(_url)})); + +//# sourceMappingURL=enrol_page.min.js.map \ No newline at end of file diff --git a/enrol/guest/amd/build/enrol_page.min.js.map b/enrol/guest/amd/build/enrol_page.min.js.map new file mode 100644 index 00000000000..04289bba68f --- /dev/null +++ b/enrol/guest/amd/build/enrol_page.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"enrol_page.min.js","sources":["../src/enrol_page.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Functions for the enrol_guest plugin\n *\n * @module enrol_guest/enrol_page\n * @copyright Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport {getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\nimport Url from 'core/url';\n\n/**\n * Initialise widget on the course enrolment page - clicking on the button should submit the form\n *\n * @param {Number} instanceId\n */\nexport function initEnrol(instanceId) {\n prefetchStrings('moodle', [\n 'loginguest',\n ]);\n\n const button = document.querySelector('button[type=\"submit\"][data-instance=\"' + instanceId + '\"]');\n if (button) {\n button.addEventListener('click', (e) => {\n e.preventDefault();\n const modalForm = new ModalForm({\n modalConfig: {\n title: button.dataset.title,\n large: false, // This is a very small form that does not need a large popup.\n },\n formClass: button.dataset.form,\n args: {id: button.dataset.id, instance: instanceId},\n saveButtonText: getString('loginguest', 'moodle'),\n returnFocus: button,\n });\n\n // Redirect to the course page when the form is submitted.\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {\n window.location.href = event.detail ? event.detail :\n Url.relativeUrl('/course/view.php', {id: button.dataset.id});\n });\n\n modalForm.show();\n });\n }\n}\n"],"names":["instanceId","button","document","querySelector","addEventListener","e","preventDefault","modalForm","ModalForm","modalConfig","title","dataset","large","formClass","form","args","id","instance","saveButtonText","returnFocus","events","FORM_SUBMITTED","event","window","location","href","detail","Url","relativeUrl","show"],"mappings":";;;;;;;yFAiC0BA,0CACN,SAAU,CACtB,qBAGEC,OAASC,SAASC,cAAc,wCAA0CH,WAAa,MACzFC,QACAA,OAAOG,iBAAiB,SAAUC,IAC9BA,EAAEC,uBACIC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,MAAOT,OAAOU,QAAQD,MACtBE,OAAO,GAEXC,UAAWZ,OAAOU,QAAQG,KAC1BC,KAAM,CAACC,GAAIf,OAAOU,QAAQK,GAAIC,SAAUjB,YACxCkB,gBAAgB,kBAAU,aAAc,UACxCC,YAAalB,SAIjBM,UAAUH,iBAAiBG,UAAUa,OAAOC,gBAAgBC,QACxDC,OAAOC,SAASC,KAAOH,MAAMI,OAASJ,MAAMI,OACxCC,aAAIC,YAAY,mBAAoB,CAACZ,GAAIf,OAAOU,QAAQK,QAGhET,UAAUsB"} \ No newline at end of file diff --git a/enrol/guest/amd/src/enrol_page.js b/enrol/guest/amd/src/enrol_page.js new file mode 100644 index 00000000000..b792d5089be --- /dev/null +++ b/enrol/guest/amd/src/enrol_page.js @@ -0,0 +1,63 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Functions for the enrol_guest plugin + * + * @module enrol_guest/enrol_page + * @copyright Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import ModalForm from 'core_form/modalform'; +import {getString} from 'core/str'; +import {prefetchStrings} from 'core/prefetch'; +import Url from 'core/url'; + +/** + * Initialise widget on the course enrolment page - clicking on the button should submit the form + * + * @param {Number} instanceId + */ +export function initEnrol(instanceId) { + prefetchStrings('moodle', [ + 'loginguest', + ]); + + const button = document.querySelector('button[type="submit"][data-instance="' + instanceId + '"]'); + if (button) { + button.addEventListener('click', (e) => { + e.preventDefault(); + const modalForm = new ModalForm({ + modalConfig: { + title: button.dataset.title, + large: false, // This is a very small form that does not need a large popup. + }, + formClass: button.dataset.form, + args: {id: button.dataset.id, instance: instanceId}, + saveButtonText: getString('loginguest', 'moodle'), + returnFocus: button, + }); + + // Redirect to the course page when the form is submitted. + modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => { + window.location.href = event.detail ? event.detail : + Url.relativeUrl('/course/view.php', {id: button.dataset.id}); + }); + + modalForm.show(); + }); + } +} diff --git a/enrol/guest/classes/form/enrol_form.php b/enrol/guest/classes/form/enrol_form.php new file mode 100644 index 00000000000..c3d43976c22 --- /dev/null +++ b/enrol/guest/classes/form/enrol_form.php @@ -0,0 +1,150 @@ +. + +declare(strict_types=1); + +namespace enrol_guest\form; + +use core\context\course as context_course; +use core_form\dynamic_form; +use core_text; +use moodle_url; + +/** + * Form for entering password for guest enrolment + * + * @package enrol_guest + * @copyright Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_form extends dynamic_form { + /** @var \stdClass */ + protected $instance; + + /** + * Returns the instance of the enrolment method + * + * @throws \moodle_exception + * @return \stdClass + */ + protected function get_instance(): \stdClass { + global $DB, $CFG; + require_once($CFG->dirroot . '/lib/enrollib.php'); + if ($this->instance === null) { + $courseid = $this->optional_param('id', 0, PARAM_INT); + $instanceid = $this->optional_param('instance', 0, PARAM_INT); + // We need enrol_get_instances() to validate that the enrolment method is enabled. + $instances = enrol_get_instances($courseid, true); + if (empty($instances[$instanceid]) || $instances[$instanceid]->enrol !== 'guest') { + throw new \moodle_exception('invalidenrolinstance', 'enrol'); + } + $this->instance = $instances[$instanceid] ?? null; + } + return $this->instance; + } + + #[\Override] + public function definition() { + $mform = $this->_form; + + $mform->addElement('password', 'guestpassword', get_string('password', 'enrol_guest')); + + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + + $mform->addElement('hidden', 'instance'); + $mform->setType('instance', PARAM_INT); + } + + #[\Override] + public function validation($data, $files) { + global $DB, $CFG; + + $errors = parent::validation($data, $files); + $instance = $this->get_instance(); + + if ($instance->password !== '') { + if ($data['guestpassword'] !== $instance->password) { + $plugin = enrol_get_plugin('guest'); + if ($plugin->get_config('showhint')) { + $hint = core_text::substr($instance->password, 0, 1); + $errors['guestpassword'] = get_string('passwordinvalidhint', 'enrol_guest', $hint); + } else { + $errors['guestpassword'] = get_string('passwordinvalid', 'enrol_guest'); + } + } + } + + return $errors; + } + + #[\Override] + protected function check_access_for_dynamic_submission(): void { + global $USER, $CFG; + $courseid = $this->get_instance()->courseid; + $course = get_course($courseid); + $context = context_course::instance($this->get_instance()->courseid); + if (!\core_course_category::can_view_course_info($course) && !is_enrolled($context, $USER, '', true)) { + throw new \moodle_exception('coursehidden', '', $CFG->wwwroot . '/'); + } + } + + #[\Override] + protected function get_context_for_dynamic_submission(): \context { + // This form is used for users who are not yet enrolled in the course and do not have access to the course. + // For the purpose of permission checks they must be able to access the course category for this course. + return context_course::instance($this->get_instance()->courseid)->get_parent_context(); + } + + #[\Override] + protected function get_page_url_for_dynamic_submission(): moodle_url { + $instance = $this->get_instance(); + return new moodle_url('/enrol/index.php', ['id' => $instance->courseid, 'instance' => $instance->id]); + } + + /** + * Process the form submission, used if form was submitted via AJAX + * + * Enrols the user in the course and returns the URL to redirect to + * + * @return string + */ + public function process_dynamic_submission() { + global $USER, $CFG, $SESSION; + + /** @var \enrol_guest_plugin $enrol */ + $enrol = enrol_get_plugin('guest'); + $instance = $this->get_instance(); + + $enrol->mark_user_as_enrolled($instance, $this->get_data()->guestpassword); + + // Go to the originally requested page. + if (!empty($SESSION->wantsurl)) { + $destination = $SESSION->wantsurl; + unset($SESSION->wantsurl); + } else { + require_once($CFG->dirroot . '/course/lib.php'); + $destination = course_get_url($instance->courseid); + } + return $destination; + } + + #[\Override] + public function set_data_for_dynamic_submission(): void { + $instance = $this->get_instance(); + $this->set_data(['id' => $instance->courseid, 'instance' => $instance->id]); + } +} diff --git a/enrol/guest/lang/en/enrol_guest.php b/enrol/guest/lang/en/enrol_guest.php index bed0a5a85f2..bdd3f930124 100644 --- a/enrol/guest/lang/en/enrol_guest.php +++ b/enrol/guest/lang/en/enrol_guest.php @@ -31,6 +31,7 @@ $string['password_help'] = 'A password allows guest access to the course to be r $string['passwordinvalid'] = 'Incorrect access password, please try again'; $string['passwordinvalidhint'] = 'That access password was incorrect, please try again
(Here\'s a hint - it starts with \'{$a}\')'; +$string['passwordrequired'] = 'A password will be required'; $string['pluginname'] = 'Guest access'; $string['pluginname_desc'] = 'Guest access plugin is only granting temporary access to courses, it is not actually enrolling users.'; $string['requirepassword'] = 'Require guest access password'; diff --git a/enrol/guest/lib.php b/enrol/guest/lib.php index 1af049f19d1..e32c8754dec 100644 --- a/enrol/guest/lib.php +++ b/enrol/guest/lib.php @@ -25,7 +25,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); +use core\output\single_button; +use core_enrol\output\enrol_page; /** * Class enrol_guest_plugin @@ -148,14 +149,28 @@ class enrol_guest_plugin extends enrol_plugin { } /** - * Creates course enrol form, checks if form submitted - * and enrols user if necessary. It can also redirect. + * Enrol a user using the guest enrolment method * * @param stdClass $instance - * @return string html text, usually a form in a text box + * @param string $guestpassword + * @return void */ + public function mark_user_as_enrolled(stdClass $instance, string $guestpassword): void { + global $USER, $CFG; + + // Add guest role. + $context = \core\context\course::instance($instance->courseid); + $USER->enrol_guest_passwords[$instance->id] = $guestpassword; + if (isset($USER->enrol['tempguest'][$instance->courseid])) { + remove_temp_course_roles($context); + } + load_temp_course_role($context, $CFG->guestroleid); + $USER->enrol['tempguest'][$instance->courseid] = ENROL_MAX_TIMESTAMP; + } + + #[\Override] public function enrol_page_hook(stdClass $instance) { - global $CFG, $OUTPUT, $SESSION, $USER; + global $CFG, $OUTPUT, $SESSION, $USER, $PAGE; if ($instance->password === '') { return null; @@ -166,37 +181,27 @@ class enrol_guest_plugin extends enrol_plugin { return null; } - require_once("$CFG->dirroot/enrol/guest/locallib.php"); - $form = new enrol_guest_enrol_form(NULL, $instance); - $instanceid = optional_param('instance', 0, PARAM_INT); - - if ($instance->id == $instanceid) { - if ($data = $form->get_data()) { - // add guest role - $context = context_course::instance($instance->courseid); - $USER->enrol_guest_passwords[$instance->id] = $data->guestpassword; // this is a hack, ideally we should not add stuff to $USER... - if (isset($USER->enrol['tempguest'][$instance->courseid])) { - remove_temp_course_roles($context); - } - load_temp_course_role($context, $CFG->guestroleid); - $USER->enrol['tempguest'][$instance->courseid] = ENROL_MAX_TIMESTAMP; - - // go to the originally requested page - if (!empty($SESSION->wantsurl)) { - $destination = $SESSION->wantsurl; - unset($SESSION->wantsurl); - } else { - $destination = "$CFG->wwwroot/course/view.php?id=$instance->courseid"; - } - redirect($destination); - } - } - - ob_start(); - $form->display(); - $output = ob_get_clean(); - - return $OUTPUT->box($output, 'generalbox'); + $title = $this->get_instance_name($instance); + $notification = new \core\output\notification(get_string('passwordrequired', 'enrol_guest'), 'info', false); + $notification->set_extra_classes(['mb-0']); + $button = new single_button( + $PAGE->url, + get_string('loginguest', 'moodle'), + 'get', + single_button::BUTTON_PRIMARY, + [ + 'data-id' => $instance->courseid, + 'data-instance' => $instance->id, + 'data-form' => enrol_guest\form\enrol_form::class, + 'data-title' => $title, + ]); + $PAGE->requires->js_call_amd('enrol_guest/enrol_page', 'initEnrol', [$instance->id]); + $enrolpage = new enrol_page( + instance: $instance, + header: $title, + body: $OUTPUT->render($notification), + buttons: [$button]); + return $OUTPUT->render($enrolpage); } /** diff --git a/enrol/guest/locallib.php b/enrol/guest/locallib.php index 934d47a7e70..65f5a247cd7 100644 --- a/enrol/guest/locallib.php +++ b/enrol/guest/locallib.php @@ -14,21 +14,41 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Guest access plugin implementation. - * - * @package enrol_guest - * @copyright 2010 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ defined('MOODLE_INTERNAL') || die(); require_once("$CFG->libdir/formslib.php"); +/** + * Guest access plugin implementation. + * + * @deprecated since Moodle 5.0 - please use {@see enrol_guest\form\enrol_form} + * + * @package enrol_guest + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\core\attribute\deprecated(replacement: enrol_guest\form\enrol_form::class, since: '5.0', reason: 'Now a dynamic form is used')] class enrol_guest_enrol_form extends moodleform { protected $instance; + /** + * Constructor + * + * @param mixed $action + * @param mixed $customdata + * @param string $method + * @param string $target + * @param mixed $attributes + * @param bool $editable + * @param array $ajaxformdata + */ + public function __construct($action=null, $customdata=null, $method='post', $target='', $attributes=null, $editable=true, + $ajaxformdata=null) { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); + parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata); + } + public function definition() { $mform = $this->_form; $instance = $this->_customdata; diff --git a/enrol/guest/tests/behat/guest_access.feature b/enrol/guest/tests/behat/guest_access.feature index ce8d696b198..f5f6232af04 100644 --- a/enrol/guest/tests/behat/guest_access.feature +++ b/enrol/guest/tests/behat/guest_access.feature @@ -30,6 +30,7 @@ Feature: Guest users can auto-enrol themself in courses where guest access is al When I am on the "Test forum name" "forum activity" page logged in as student1 Then I should not see "Subscribe to this forum" + @javascript Scenario: Allow guest access with password Given I click on "Edit" "link" in the "Guest access" "table_row" And I set the following fields to these values: @@ -38,7 +39,8 @@ Feature: Guest users can auto-enrol themself in courses where guest access is al And I press "Save changes" When I am on the "Course 1" course page logged in as student1 Then I should see "Guest access" + And I press "Access as a guest" And I set the following fields to these values: | Password | moodle_rules | - And I press "Submit" + And I click on "Access as a guest" "button" in the "Guest access" "dialogue" And I should see "Test forum name" From 129ab8007ec818e8ecaa85c9beeca9efd46a1752 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Tue, 18 Feb 2025 13:34:26 +0000 Subject: [PATCH 4/5] MDL-84142 enrol_fee: use new template for the self enrolment widget --- enrol/fee/classes/plugin.php | 101 ++++++++++---------- enrol/fee/templates/enrol_page.mustache | 30 ++++++ enrol/fee/templates/payment_region.mustache | 81 ---------------- enrol/fee/tests/behat/fee.feature | 6 +- lang/en/moodle.php | 2 +- 5 files changed, 82 insertions(+), 138 deletions(-) create mode 100644 enrol/fee/templates/enrol_page.mustache delete mode 100644 enrol/fee/templates/payment_region.mustache diff --git a/enrol/fee/classes/plugin.php b/enrol/fee/classes/plugin.php index 3ca18f0b699..c8603a4fa43 100644 --- a/enrol/fee/classes/plugin.php +++ b/enrol/fee/classes/plugin.php @@ -24,6 +24,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use core\output\single_button; +use core_enrol\output\enrol_page; + /** * Fee enrolment plugin implementation. * @@ -172,53 +175,20 @@ class enrol_fee_plugin extends enrol_plugin { return parent::update_instance($instance, $data); } - /** - * Creates course enrol form, checks if form submitted - * and enrols user if necessary. It can also redirect. - * - * @param stdClass $instance - * @return string html text, usually a form in a text box - */ + #[\Override] public function enrol_page_hook(stdClass $instance) { - return $this->show_payment_info($instance); - } - - /** - * Returns optional enrolment instance description text. - * - * This is used in detailed course information. - * - * - * @param object $instance - * @return string short html text - */ - public function get_description_text($instance) { - return $this->show_payment_info($instance); - } - - /** - * Generates payment information to display on enrol/info page. - * - * @param stdClass $instance - * @return false|string - * @throws coding_exception - * @throws dml_exception - */ - private function show_payment_info(stdClass $instance) { - global $USER, $OUTPUT, $DB; - - ob_start(); + global $USER, $OUTPUT, $DB, $PAGE; if ($DB->record_exists('user_enrolments', array('userid' => $USER->id, 'enrolid' => $instance->id))) { - return ob_get_clean(); + return ''; } if ($instance->enrolstartdate != 0 && $instance->enrolstartdate > time()) { - return ob_get_clean(); + return ''; } if ($instance->enrolenddate != 0 && $instance->enrolenddate < time()) { - return ob_get_clean(); + return ''; } $course = $DB->get_record('course', array('id' => $instance->courseid)); @@ -230,26 +200,51 @@ class enrol_fee_plugin extends enrol_plugin { $cost = (float) $instance->cost; } + $name = !empty($instance->name) ? + format_string($instance->name, true, ['context' => $context]) : + get_string('paymentrequired'); + if (abs($cost) < 0.01) { // No cost, other enrolment methods (instances) should be used. - echo '

'.get_string('nocost', 'enrol_fee').'

'; + $notification = new \core\output\notification(get_string('nocost', 'enrol_fee'), 'error', false); + $notification->set_extra_classes(['mb-0']); + $enrolpage = new enrol_page( + instance: $instance, + header: $name, + body: $OUTPUT->render($notification)); + return $OUTPUT->render($enrolpage); } else { + if (isguestuser() || !isloggedin()) { + $button = new single_button(new moodle_url(get_login_url()), get_string('loginsite'), + 'get', single_button::BUTTON_PRIMARY); + } else { + $PAGE->requires->js_call_amd('core_payment/gateways_modal', 'init'); + $button = new single_button( + $PAGE->url, + get_string('sendpaymentbutton', 'enrol_fee'), + 'post', + single_button::BUTTON_PRIMARY, + [ + 'data-action' => 'core_payment/triggerPayment', + 'data-component' => 'enrol_fee', + 'data-paymentarea' => 'fee', + 'data-itemid' => $instance->id, + 'data-cost' => $cost, + 'data-successurl' => \enrol_fee\payment\service_provider::get_success_url('fee', $instance->id)->out(false), + 'data-description' => get_string('purchasedescription', 'enrol_fee', + format_string($course->fullname, true, ['context' => $context])), + ]); + } - $name = !empty($instance->name) ? - format_string($instance->name, true, ['context' => $context]) : - get_string('paymentrequired'); - $data = [ - 'name' => $name, - 'isguestuser' => isguestuser() || !isloggedin(), + $body = $OUTPUT->render_from_template('enrol_fee/enrol_page', [ 'cost' => \core_payment\helper::get_cost_as_string($cost, $instance->currency), - 'instanceid' => $instance->id, - 'description' => get_string('purchasedescription', 'enrol_fee', - format_string($course->fullname, true, ['context' => $context])), - 'successurl' => \enrol_fee\payment\service_provider::get_success_url('fee', $instance->id)->out(false), - ]; - echo $OUTPUT->render_from_template('enrol_fee/payment_region', $data); + ]); + $enrolpage = new enrol_page( + instance: $instance, + header: $name, + body: $body, + buttons: [$button]); + return $OUTPUT->render($enrolpage); } - - return $OUTPUT->box(ob_get_clean()); } /** diff --git a/enrol/fee/templates/enrol_page.mustache b/enrol/fee/templates/enrol_page.mustache new file mode 100644 index 00000000000..fbb0275dfc4 --- /dev/null +++ b/enrol/fee/templates/enrol_page.mustache @@ -0,0 +1,30 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template enrol_fee/enrol_page + + Contents of the enrolment widget on the course enrolment page + + Example context (json): + { + "cost": "10.00", + "currency": "USD" + } +}} + +{{#str}} labelvalue, core, {"label": {{#quote}}{{#str}} cost {{/str}}{{/quote}}, "value": {{#quote}}{{cost}}{{/quote}} }{{/str}} + diff --git a/enrol/fee/templates/payment_region.mustache b/enrol/fee/templates/payment_region.mustache deleted file mode 100644 index 9ad10514bd7..00000000000 --- a/enrol/fee/templates/payment_region.mustache +++ /dev/null @@ -1,81 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template enrol_fee/payment_region - - This template will render information about course fee along with a button for payment. - - Classes required for JS: - * none - - Data attributes required for JS: - * data-component - * data-paymentarea - * data-itemid - * data-cost - * data-description - * data-successurl - - Context variables required for this template: - * cost - Human readable cost string including amount and currency - * instanceid - Id of the enrolment instance - * description - The description for this purchase - * successurl - The URL of the course - - Example context (json): - { - "cost": "$108.50", - "name": "This course requires a payment for entry.", - "instanceid": 11, - "description": "Enrolment in course Introduction to algorithms", - "successurl": "https://moodlesite/course/view.php?id=2", - "isguestuser": false - } - -}} -
- {{#isguestuser}} -
-

{{{name}}}

-

{{cost}}

-

{{# str }} loginsite {{/ str }}

-
- {{/isguestuser}} - {{^isguestuser}} -

{{{name}}}

-

{{cost}}

- - {{/isguestuser}} -
-{{#js}} - require(['core_payment/gateways_modal'], function(modal) { - modal.init(); - }); -{{/js}} diff --git a/enrol/fee/tests/behat/fee.feature b/enrol/fee/tests/behat/fee.feature index 94cf3ad7039..8896b716cea 100644 --- a/enrol/fee/tests/behat/fee.feature +++ b/enrol/fee/tests/behat/fee.feature @@ -36,7 +36,7 @@ Feature: Signing up for a course with a fee enrolment method When I log in as "student1" And I am on course index And I follow "Course 1" - Then I should see "This course requires a payment for entry." + Then I should see "This course requires a payment for entry" And I should see "123.45" And I press "Select payment type" And I should see "PayPal" in the "Select payment type" "dialogue" @@ -46,7 +46,7 @@ Feature: Signing up for a course with a fee enrolment method When I log in as "guest" And I am on course index And I follow "Course 1" - Then I should see "This course requires a payment for entry." + Then I should see "This course requires a payment for entry" And I should see "123.45" And I should see "Log in to the site" @@ -66,6 +66,6 @@ Feature: Signing up for a course with a fee enrolment method When I log in as "student1" And I am on course index And I follow "Course 1" - Then I should not see "This course requires a payment for entry." + Then I should not see "This course requires a payment for entry" Then I should see "Lifetime access" Then I should not see "Only for teachers" diff --git a/lang/en/moodle.php b/lang/en/moodle.php index de2625673e2..8aed1cfa9a6 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1658,7 +1658,7 @@ $string['pathnotexists'] = 'Path doesn\'t exist in your server!'; $string['pathslasherror'] = 'Path can\'t end with a slash!!'; $string['paymentinstant'] = 'Use the button below to pay and be enrolled within minutes!'; $string['paymentpending'] = '({$a} pending)'; -$string['paymentrequired'] = 'This course requires a payment for entry.'; +$string['paymentrequired'] = 'This course requires a payment for entry'; $string['payments'] = 'Payments'; $string['paymentsorry'] = 'Thank you for your payment! Unfortunately your payment has not yet been fully processed, and you are not yet registered to enter the course "{$a->fullname}". Please try continuing to the course in a few seconds, but if you continue to have trouble then please alert the {$a->teacher} or the site administrator'; $string['paymentthanks'] = 'Thank you for your payment! You are now enrolled in your course:
"{$a}"'; From 0f75ab1d0edd38aaea6c54b795a78519fd6ae575 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Tue, 18 Feb 2025 13:34:37 +0000 Subject: [PATCH 5/5] MDL-84142 enrol_paypal: use new template for the self enrolment widget --- enrol/paypal/enrol.html | 46 --------- enrol/paypal/lib.php | 112 ++++++++++++--------- enrol/paypal/templates/enrol_page.mustache | 34 +++++++ 3 files changed, 100 insertions(+), 92 deletions(-) delete mode 100644 enrol/paypal/enrol.html create mode 100644 enrol/paypal/templates/enrol_page.mustache diff --git a/enrol/paypal/enrol.html b/enrol/paypal/enrol.html deleted file mode 100644 index c9d5865f404..00000000000 --- a/enrol/paypal/enrol.html +++ /dev/null @@ -1,46 +0,0 @@ -
- -

-

-

currency} {$localisedcost}"; ?>

-

<?php print_string('paypalaccepted', 'enrol_paypal') ?>

-

-usepaypalsandbox) ? 'https://www.paypal.com/cgi-bin/webscr' : 'https://www.sandbox.paypal.com/cgi-bin/webscr'; -?> -
- - - - - - - -" /> - -id}-{$course->id}-{$instance->id}" ?>" /> - - - - - - - -wwwroot/enrol/paypal/ipn.php"?>" /> -wwwroot/enrol/paypal/return.php?id=$course->id" ?>" /> - - -" /> - - - - - - - - -" /> - -
- -
diff --git a/enrol/paypal/lib.php b/enrol/paypal/lib.php index 254b0836e73..754fd4edf83 100644 --- a/enrol/paypal/lib.php +++ b/enrol/paypal/lib.php @@ -24,7 +24,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); +use core\output\single_button; +use core_enrol\output\enrol_page; /** * Paypal enrolment plugin implementation. @@ -147,84 +148,103 @@ class enrol_paypal_plugin extends enrol_plugin { return parent::update_instance($instance, $data); } - /** - * Creates course enrol form, checks if form submitted - * and enrols user if necessary. It can also redirect. - * - * @param stdClass $instance - * @return string html text, usually a form in a text box - */ - function enrol_page_hook(stdClass $instance) { + #[\Override] + public function enrol_page_hook(stdClass $instance) { global $CFG, $USER, $OUTPUT, $PAGE, $DB; - ob_start(); - - if ($DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id))) { - return ob_get_clean(); + if ($DB->record_exists('user_enrolments', ['userid' => $USER->id, 'enrolid' => $instance->id])) { + return ''; } if ($instance->enrolstartdate != 0 && $instance->enrolstartdate > time()) { - return ob_get_clean(); + return ''; } if ($instance->enrolenddate != 0 && $instance->enrolenddate < time()) { - return ob_get_clean(); + return ''; } - $course = $DB->get_record('course', array('id'=>$instance->courseid)); + $course = $DB->get_record('course', ['id' => $instance->courseid]); $context = context_course::instance($course->id); - $shortname = format_string($course->shortname, true, array('context' => $context)); - $strloginto = get_string("loginto", "", $shortname); - $strcourses = get_string("courses"); - - // Pass $view=true to filter hidden caps if the user cannot see them - if ($users = get_users_by_capability($context, 'moodle/course:update', 'u.*', 'u.id ASC', - '', '', '', '', false, true)) { - $users = sort_by_roleassignment_authority($users, $context); - $teacher = array_shift($users); - } else { - $teacher = false; - } - if ( (float) $instance->cost <= 0 ) { $cost = (float) $this->get_config('cost'); } else { $cost = (float) $instance->cost; } - if (abs($cost) < 0.01) { // no cost, other enrolment methods (instances) should be used - echo '

'.get_string('nocost', 'enrol_paypal').'

'; - } else { + $name = $this->get_instance_name($instance); + if (abs($cost) < 0.01) { + // No cost, other enrolment methods (instances) should be used. + $notification = new \core\output\notification(get_string('nocost', 'enrol_paypal'), 'error', false); + $notification->set_extra_classes(['mb-0']); + $enrolpage = new enrol_page( + instance: $instance, + header: $name, + body: $OUTPUT->render($notification)); + return $OUTPUT->render($enrolpage); + } else { // Calculate localised and "." cost, make sure we send PayPal the same value, // please note PayPal expects amount with 2 decimal places and "." separator. $localisedcost = format_float($cost, 2, true); $cost = format_float($cost, 2, false); - if (isguestuser()) { // force login only for guest user, not real users with guest role - $wwwroot = $CFG->wwwroot; - echo '

'.get_string('paymentrequired').'

'; - echo '

'.get_string('cost').": $instance->currency $localisedcost".'

'; - echo '

'.get_string('loginsite').'

'; - echo '
'; + $body = $OUTPUT->render_from_template('enrol_paypal/enrol_page', + ['currency' => $instance->currency, 'cost' => $localisedcost]); + if (isguestuser() || !isloggedin()) { + $button = new single_button(new moodle_url(get_login_url()), get_string('loginsite'), 'get', + single_button::BUTTON_PRIMARY); } else { - //Sanitise some fields before building the PayPal form - $coursefullname = format_string($course->fullname, true, array('context'=>$context)); - $courseshortname = $shortname; + // Sanitise some fields before building the PayPal form. + $coursefullname = format_string($course->fullname, true, ['context' => $context]); + $courseshortname = format_string($course->shortname, true, ['context' => $context]); $userfullname = fullname($USER); $userfirstname = $USER->firstname; $userlastname = $USER->lastname; $useraddress = $USER->address; $usercity = $USER->city; - $instancename = $this->get_instance_name($instance); - - include($CFG->dirroot.'/enrol/paypal/enrol.html'); + $buttonurl = new moodle_url(empty($CFG->usepaypalsandbox) ? + 'https://www.paypal.com/cgi-bin/webscr' : + 'https://www.sandbox.paypal.com/cgi-bin/webscr', + [ + 'cmd' => '_xclick', + 'charset' => 'utf-8', + 'business' => $this->get_config('paypalbusiness'), + 'item_name' => $coursefullname, + 'item_number' => $courseshortname, + 'quantity' => 1, + 'on0' => get_string("user"), + 'os0' => $userfullname, + 'custom' => "{$USER->id}-{$course->id}-{$instance->id}", + 'currency_code' => $instance->currency, + 'amount' => $cost, + 'for_auction' => 'false', + 'no_note' => 1, + 'no_shipping' => 1, + 'notify_url' => "$CFG->wwwroot/enrol/paypal/ipn.php", + 'return' => "$CFG->wwwroot/enrol/paypal/return.php?id={$course->id}", + 'cancel_return' => $CFG->wwwroot, + 'rm' => 2, + 'cbt' => get_string("continuetocourse"), + 'first_name' => $userfirstname, + 'last_name' => $userlastname, + 'address' => $useraddress, + 'city' => $usercity, + 'email' => $USER->email, + 'country' => $USER->country, + ]); + $button = new single_button($buttonurl, get_string("sendpaymentbutton", "enrol_paypal"), + 'get', single_button::BUTTON_PRIMARY); } + $enrolpage = new enrol_page( + instance: $instance, + header: $name, + body: $body, + buttons: [$button]); + return $OUTPUT->render($enrolpage); } - - return $OUTPUT->box(ob_get_clean()); } /** diff --git a/enrol/paypal/templates/enrol_page.mustache b/enrol/paypal/templates/enrol_page.mustache new file mode 100644 index 00000000000..54031cc2a37 --- /dev/null +++ b/enrol/paypal/templates/enrol_page.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template enrol_paypal/enrol_page + + Contents of the enrolment widget on the course enrolment page + + Example context (json): + { + "cost": "10.00", + "currency": "USD" + } +}} +
+
{{#str}}cost{{/str}}: {{currency}} {{cost}}
+
+ {{#quote}}{{#str}}paypalaccepted, +
+