diff --git a/enrol/cohort/lib.php b/enrol/cohort/lib.php index 179671e122c..7c1eef26803 100644 --- a/enrol/cohort/lib.php +++ b/enrol/cohort/lib.php @@ -236,7 +236,9 @@ class enrol_cohort_plugin extends enrol_plugin { $params['ue'] = $ue->id; if ($this->allow_unenrol_user($instance, $ue) && has_capability('enrol/cohort:unenrol', $context)) { $url = new moodle_url('/enrol/unenroluser.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, array('class'=>'unenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL); + $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, + $actionparams); } return $actions; } diff --git a/enrol/database/lib.php b/enrol/database/lib.php index b21578e5fc2..890942a47bf 100644 --- a/enrol/database/lib.php +++ b/enrol/database/lib.php @@ -97,7 +97,9 @@ class enrol_database_plugin extends enrol_plugin { $params['ue'] = $ue->id; if ($this->allow_unenrol_user($instance, $ue) && has_capability('enrol/database:unenrol', $context)) { $url = new moodle_url('/enrol/unenroluser.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, array('class'=>'unenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL); + $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, + $actionparams); } return $actions; } diff --git a/enrol/editenrolment.php b/enrol/editenrolment.php index 8b6f54efe11..54f18efc46e 100644 --- a/enrol/editenrolment.php +++ b/enrol/editenrolment.php @@ -41,7 +41,7 @@ $instance = $DB->get_record('enrol', array('id'=>$ue->enrolid), '*', MUST_EXIST) $course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_EXIST); // The URL of the enrolled users page for the course. -$usersurl = new moodle_url('/enrol/users.php', array('id' => $course->id)); +$usersurl = new moodle_url('/user/index.php', array('id' => $course->id)); // Do not allow any changes if plugin disabled, not available or not suitable. if (!$plugin = enrol_get_plugin($instance->enrol)) { diff --git a/enrol/editenrolment_form.php b/enrol/editenrolment_form.php index 8bdc46611de..307346c4125 100644 --- a/enrol/editenrolment_form.php +++ b/enrol/editenrolment_form.php @@ -28,15 +28,10 @@ require_once("$CFG->libdir/formslib.php"); class enrol_user_enrolment_form extends moodleform { function definition() { - global $CFG, $DB; - $mform = $this->_form; - $user = $this->_customdata['user']; - $course = $this->_customdata['course']; - $ue = $this->_customdata['ue']; - - $mform->addElement('header','general', ''); + $ue = $this->_customdata['ue']; + $modal = !empty($this->_customdata['modal']); $options = array(ENROL_USER_ACTIVE => get_string('participationactive', 'enrol'), ENROL_USER_SUSPENDED => get_string('participationsuspended', 'enrol')); @@ -56,7 +51,10 @@ class enrol_user_enrolment_form extends moodleform { $mform->addElement('hidden', 'ifilter'); $mform->setType('ifilter', PARAM_ALPHA); - $this->add_action_buttons(); + // Show action buttons if this is not being rendered as a fragment. + if (!$modal) { + $this->add_action_buttons(); + } $this->set_data(array( 'ue' => $ue->id, @@ -71,8 +69,7 @@ class enrol_user_enrolment_form extends moodleform { if (!empty($data['timestart']) and !empty($data['timeend'])) { if ($data['timestart'] >= $data['timeend']) { - $errors['timestart'] = get_string('error'); - $errors['timeend'] = get_string('error'); + $errors['timeend'] = get_string('enroltimeendinvalid', 'enrol'); } } diff --git a/enrol/externallib.php b/enrol/externallib.php index f8244225f84..370141a44ef 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -805,6 +805,107 @@ class core_enrol_external extends external_api { ) ); } + + /** + * Returns description of edit_user_enrolment() parameters + * + * @return external_function_parameters + */ + public static function edit_user_enrolment_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_INT, 'User enrolment ID'), + 'ueid' => new external_value(PARAM_INT, 'User enrolment ID'), + 'status' => new external_value(PARAM_INT, 'Enrolment status'), + 'timestart' => new external_value(PARAM_INT, 'Enrolment start timestamp', VALUE_DEFAULT, 0), + 'timeend' => new external_value(PARAM_INT, 'Enrolment end timestamp', VALUE_DEFAULT, 0), + ) + ); + } + + /** + * External function that updates a given user enrolment. + * + * @param int $courseid The course ID. + * @param int $ueid The user enrolment ID. + * @param int $status The enrolment status. + * @param int $timestart Enrolment start timestamp. + * @param int $timeend Enrolment end timestamp. + * @return array An array consisting of the processing result, errors and form output, if available. + */ + public static function edit_user_enrolment($courseid, $ueid, $status, $timestart = 0, $timeend = 0) { + global $CFG, $DB, $PAGE; + + $params = self::validate_parameters(self::edit_user_enrolment_parameters(), [ + 'courseid' => $courseid, + 'ueid' => $ueid, + 'status' => $status, + 'timestart' => $timestart, + 'timeend' => $timeend, + ]); + + $course = get_course($courseid); + $context = context_course::instance($course->id); + self::validate_context($context); + + $userenrolment = $DB->get_record('user_enrolments', ['id' => $params['ueid']], '*', MUST_EXIST); + $userenroldata = [ + 'status' => $params['status'], + 'timestart' => $params['timestart'], + 'timeend' => $params['timeend'], + ]; + + $result = false; + $errors = []; + + // Validate data against the edit user enrolment form. + require_once("$CFG->dirroot/enrol/editenrolment_form.php"); + $customformdata = [ + 'ue' => $userenrolment, + 'modal' => true, + ]; + $mform = new \enrol_user_enrolment_form(null, $customformdata, 'post', '', null, true, $userenroldata); + $mform->set_data($userenroldata); + $validationerrors = $mform->validation($userenroldata, null); + if (empty($validationerrors)) { + require_once($CFG->dirroot . '/enrol/locallib.php'); + $manager = new course_enrolment_manager($PAGE, $course); + $result = $manager->edit_enrolment($userenrolment, (object)$userenroldata); + } else { + foreach ($validationerrors as $key => $errormessage) { + $errors[] = (object)[ + 'key' => $key, + 'message' => $errormessage + ]; + } + } + + return [ + 'result' => $result, + 'errors' => $errors, + ]; + } + + /** + * Returns description of edit_user_enrolment() result value + * + * @return external_description + */ + public static function edit_user_enrolment_returns() { + return new external_single_structure( + array( + 'result' => new external_value(PARAM_BOOL, 'True if the user\'s enrolment was successfully updated'), + 'errors' => new external_multiple_structure( + new external_single_structure( + array( + 'key' => new external_value(PARAM_TEXT, 'The data that failed the validation'), + 'message' => new external_value(PARAM_TEXT, 'The error message'), + ) + ), 'List of validation errors' + ), + ) + ); + } } /** diff --git a/enrol/flatfile/lib.php b/enrol/flatfile/lib.php index 4bff2a9939a..498584819bb 100644 --- a/enrol/flatfile/lib.php +++ b/enrol/flatfile/lib.php @@ -132,11 +132,14 @@ class enrol_flatfile_plugin extends enrol_plugin { $params['ue'] = $ue->id; if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/flatfile:unenrol", $context)) { $url = new moodle_url('/enrol/unenroluser.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, array('class'=>'unenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL); + $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, + $actionparams); } if ($this->allow_manage($instance) && has_capability("enrol/flatfile:manage", $context)) { $url = new moodle_url('/enrol/editenrolment.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, array('class'=>'editenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT); + $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams); } return $actions; } diff --git a/enrol/lti/lib.php b/enrol/lti/lib.php index 1377b197815..0edfbfbdcac 100644 --- a/enrol/lti/lib.php +++ b/enrol/lti/lib.php @@ -392,13 +392,14 @@ class enrol_lti_plugin extends enrol_plugin { $params['ue'] = $ue->id; if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/lti:unenrol", $context)) { $url = new moodle_url('/enrol/unenroluser.php', $params); + $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL); $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, - array('class' => 'unenrollink', 'rel' => $ue->id)); + $actionparams); } if ($this->allow_manage($instance) && has_capability("enrol/lti:manage", $context)) { $url = new moodle_url('/enrol/editenrolment.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, - array('class' => 'editenrollink', 'rel' => $ue->id)); + $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT); + $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams); } return $actions; } diff --git a/enrol/manual/lib.php b/enrol/manual/lib.php index 1eef9b01cbb..c71c2467050 100644 --- a/enrol/manual/lib.php +++ b/enrol/manual/lib.php @@ -376,11 +376,14 @@ class enrol_manual_plugin extends enrol_plugin { $params['ue'] = $ue->id; if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/manual:unenrol", $context)) { $url = new moodle_url('/enrol/unenroluser.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, array('class'=>'unenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL); + $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, + $actionparams); } if ($this->allow_manage($instance) && has_capability("enrol/manual:manage", $context)) { $url = new moodle_url('/enrol/editenrolment.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, array('class'=>'editenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT); + $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams); } return $actions; } diff --git a/enrol/meta/lib.php b/enrol/meta/lib.php index 1415f57010d..0b6bfd0171e 100644 --- a/enrol/meta/lib.php +++ b/enrol/meta/lib.php @@ -110,7 +110,9 @@ class enrol_meta_plugin extends enrol_plugin { $params['ue'] = $ue->id; if ($this->allow_unenrol_user($instance, $ue) && has_capability('enrol/meta:unenrol', $context)) { $url = new moodle_url('/enrol/unenroluser.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, array('class'=>'unenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL); + $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, + $actionparams); } return $actions; } diff --git a/enrol/meta/tests/behat/enrol_meta.feature b/enrol/meta/tests/behat/enrol_meta.feature index f8ba1731044..447822973a3 100644 --- a/enrol/meta/tests/behat/enrol_meta.feature +++ b/enrol/meta/tests/behat/enrol_meta.feature @@ -16,18 +16,21 @@ Feature: Enrolments are synchronised with meta courses | Course 1 | C1C1 | | Course 2 | C2C2 | | Course 3 | C3C3 | + | Course 4 | C4C4 | And the following "groups" exist: | name | course | idnumber | | Groupcourse 1 | C3C3 | G1 | | Groupcourse 2 | C3C3 | G2 | And the following "course enrolments" exist: - | user | course | role | - | student1 | C1C1 | student | - | student2 | C1C1 | student | - | student3 | C1C1 | student | - | student4 | C1C1 | student | - | student1 | C2C2 | student | - | student2 | C2C2 | student | + | user | course | role | status | + | student1 | C1C1 | student | 0 | + | student2 | C1C1 | student | 0 | + | student3 | C1C1 | student | 0 | + | student4 | C1C1 | student | 0 | + | student1 | C2C2 | student | 0 | + | student2 | C2C2 | student | 0 | + | student1 | C4C4 | student | 0 | + | student2 | C4C4 | student | 1 | And I log in as "admin" And I navigate to "Manage enrol plugins" node in "Site administration > Plugins > Enrolments" And I click on "Enable" "link" in the "Course meta link" "table_row" @@ -105,3 +108,13 @@ Feature: Enrolments are synchronised with meta courses And I should see "Groupcourse 1" in the "Student 4" "table_row" And I should see "Course 2" in the "Student 1" "table_row" And I should not see "Course 2" in the "Student 3" "table_row" + + Scenario: Unenrol a user from the course participants page that was enrolled via course meta link. + Given I am on "Course 3" course homepage + And I add "Course meta link" enrolment method with: + | Link course | C4C4 | + And I navigate to course participants + # Suspended users can be unenrolled. + When I click on "//a[@data-action='unenrol']" "xpath_element" in the "student2" "table_row" + And I click on "Yes" "button" + Then I should not see "Student 2" in the "participants" "table" diff --git a/enrol/paypal/lib.php b/enrol/paypal/lib.php index 87ecfe56ee8..7ec6722175b 100644 --- a/enrol/paypal/lib.php +++ b/enrol/paypal/lib.php @@ -291,11 +291,14 @@ class enrol_paypal_plugin extends enrol_plugin { $params['ue'] = $ue->id; if ($this->allow_unenrol($instance) && has_capability("enrol/paypal:unenrol", $context)) { $url = new moodle_url('/enrol/unenroluser.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, array('class'=>'unenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL); + $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, + $actionparams); } if ($this->allow_manage($instance) && has_capability("enrol/paypal:manage", $context)) { $url = new moodle_url('/enrol/editenrolment.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, array('class'=>'editenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT); + $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams); } return $actions; } diff --git a/enrol/self/lib.php b/enrol/self/lib.php index 90be25aac53..3519f76fe11 100644 --- a/enrol/self/lib.php +++ b/enrol/self/lib.php @@ -536,11 +536,14 @@ class enrol_self_plugin extends enrol_plugin { $params['ue'] = $ue->id; if ($this->allow_unenrol($instance) && has_capability("enrol/self:unenrol", $context)) { $url = new moodle_url('/enrol/unenroluser.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, array('class'=>'unenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL); + $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url, + $actionparams); } if ($this->allow_manage($instance) && has_capability("enrol/self:manage", $context)) { $url = new moodle_url('/enrol/editenrolment.php', $params); - $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, array('class'=>'editenrollink', 'rel'=>$ue->id)); + $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT); + $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams); } return $actions; } diff --git a/enrol/self/tests/behat/self_enrolment.feature b/enrol/self/tests/behat/self_enrolment.feature index b38fb108ae4..332e3af7fdf 100644 --- a/enrol/self/tests/behat/self_enrolment.feature +++ b/enrol/self/tests/behat/self_enrolment.feature @@ -87,3 +87,41 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe Then I should see "Topic 1" And I should not see "Enrolment options" And I should not see "Enrol me in this course" + + @javascript + Scenario: Edit a self-enrolled user's enrolment from the course participants page + Given I log in as "teacher1" + And I am on "Course 1" course homepage + When I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + 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 log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='editenrolment']" "xpath_element" in the "student1" "table_row" + And I should see "Edit Student 1's enrolment" + And I set the field "Status" to "Suspended" + And I click on "Save changes" "button" + Then I should see "Suspended" in the "student1" "table_row" + + @javascript + Scenario: Unenrol a self-enrolled student from the course participants page + Given I log in as "teacher1" + And I am on "Course 1" course homepage + When I add "Self enrolment" enrolment method with: + | Custom instance name | Test student enrolment | + 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 log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='unenrol']" "xpath_element" in the "student1" "table_row" + And I click on "Yes" "button" + Then I should not see "Student 1" in the "participants" "table" diff --git a/enrol/tests/externallib_test.php b/enrol/tests/externallib_test.php index b3e061dda80..8b787a145ec 100644 --- a/enrol/tests/externallib_test.php +++ b/enrol/tests/externallib_test.php @@ -682,4 +682,86 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase { $this->assertEquals($data->student1->id, $expecteduser['id']); } + /** + * Test for core_enrol_external::edit_user_enrolment(). + */ + public function test_edit_user_enrolment() { + global $DB; + + $this->resetAfterTest(true); + $datagen = $this->getDataGenerator(); + + /** @var enrol_manual_plugin $manualplugin */ + $manualplugin = enrol_get_plugin('manual'); + $this->assertNotNull($manualplugin); + + $studentroleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST); + $teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher'], MUST_EXIST); + $course = $datagen->create_course(); + $user = $datagen->create_user(); + $teacher = $datagen->create_user(); + + $instanceid = null; + $instances = enrol_get_instances($course->id, true); + foreach ($instances as $inst) { + if ($inst->enrol == 'manual') { + $instanceid = (int)$inst->id; + break; + } + } + if (empty($instanceid)) { + $instanceid = $manualplugin->add_default_instance($course); + if (empty($instanceid)) { + $instanceid = $manualplugin->add_instance($course); + } + } + $this->assertNotNull($instanceid); + + $instance = $DB->get_record('enrol', ['id' => $instanceid], '*', MUST_EXIST); + $manualplugin->enrol_user($instance, $user->id, $studentroleid, 0, 0, ENROL_USER_ACTIVE); + $manualplugin->enrol_user($instance, $teacher->id, $teacherroleid, 0, 0, ENROL_USER_ACTIVE); + $ueid = (int)$DB->get_field( + 'user_enrolments', + 'id', + ['enrolid' => $instance->id, 'userid' => $user->id], + MUST_EXIST + ); + + // Login as teacher. + $this->setUser($teacher); + + $now = new DateTime(); + $nowtime = $now->getTimestamp(); + + // Invalid data. + $data = core_enrol_external::edit_user_enrolment($course->id, $ueid, ENROL_USER_ACTIVE, $nowtime, $nowtime); + $data = external_api::clean_returnvalue(core_enrol_external::edit_user_enrolment_returns(), $data); + $this->assertFalse($data['result']); + $this->assertNotEmpty($data['errors']); + + // Valid data. + $nextmonth = clone($now); + $nextmonth->add(new DateInterval('P1M')); + $nextmonthtime = $nextmonth->getTimestamp(); + $data = core_enrol_external::edit_user_enrolment($course->id, $ueid, ENROL_USER_ACTIVE, $nowtime, $nextmonthtime); + $data = external_api::clean_returnvalue(core_enrol_external::edit_user_enrolment_returns(), $data); + $this->assertTrue($data['result']); + $this->assertEmpty($data['errors']); + + // Check updated user enrolment. + $ue = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST); + $this->assertEquals(ENROL_USER_ACTIVE, $ue->status); + $this->assertEquals($nowtime, $ue->timestart); + $this->assertEquals($nextmonthtime, $ue->timeend); + + // Suspend user. + $data = core_enrol_external::edit_user_enrolment($course->id, $ueid, ENROL_USER_SUSPENDED); + $data = external_api::clean_returnvalue(core_enrol_external::edit_user_enrolment_returns(), $data); + $this->assertTrue($data['result']); + $this->assertEmpty($data['errors']); + + // Check updated user enrolment. + $ue = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST); + $this->assertEquals(ENROL_USER_SUSPENDED, $ue->status); + } } diff --git a/enrol/unenroluser.php b/enrol/unenroluser.php index 99033919001..fe1fc18d190 100644 --- a/enrol/unenroluser.php +++ b/enrol/unenroluser.php @@ -58,8 +58,7 @@ if (!$plugin->allow_unenrol_user($instance, $ue) or !has_capability("enrol/$inst $manager = new course_enrolment_manager($PAGE, $course, $filter); $table = new course_enrolment_users_table($manager, $PAGE); -$returnurl = new moodle_url('/enrol/users.php', array('id' => $course->id)+$manager->get_url_params()+$table->get_url_params()); -$usersurl = new moodle_url('/enrol/users.php', array('id' => $course->id)); +$usersurl = new moodle_url('/user/index.php', array('id' => $course->id)); $PAGE->set_pagelayout('admin'); navigation_node::override_active_url($usersurl); @@ -67,7 +66,7 @@ navigation_node::override_active_url($usersurl); // If the unenrolment has been confirmed and the sesskey is valid unenrol the user. if ($confirm && confirm_sesskey()) { $plugin->unenrol_user($instance, $ue->userid); - redirect($returnurl); + redirect($usersurl); } $yesurl = new moodle_url($PAGE->url, array('confirm'=>1, 'sesskey'=>sesskey())); @@ -82,5 +81,5 @@ $PAGE->navbar->add($fullname); echo $OUTPUT->header(); echo $OUTPUT->heading($fullname); -echo $OUTPUT->confirm($message, $yesurl, $returnurl); +echo $OUTPUT->confirm($message, $yesurl, $usersurl); echo $OUTPUT->footer(); diff --git a/lang/en/enrol.php b/lang/en/enrol.php index f9c026d5c45..ae18f7ce4fc 100644 --- a/lang/en/enrol.php +++ b/lang/en/enrol.php @@ -43,11 +43,13 @@ $string['deleteinstancenousersconfirm'] = 'You are about to delete the enrolment $string['disableinstanceconfirmself'] = 'Are you really sure you want to disable instance "{$a->name}" that gives you access to this course? It is possible that you will not be able to access this course if you continue.'; $string['durationdays'] = '{$a} days'; $string['editenrolment'] = 'Edit enrolment'; +$string['edituserenrolment'] = 'Edit {$a}\'s enrolment'; $string['enrol'] = 'Enrol'; $string['enrolcandidates'] = 'Not enrolled users'; $string['enrolcandidatesmatching'] = 'Matching not enrolled users'; $string['enrolcohort'] = 'Enrol cohort'; $string['enrolcohortusers'] = 'Enrol users'; +$string['enroldetails'] = 'Enrolment details'; $string['eventenrolinstancecreated'] = 'Enrolment instance created'; $string['eventenrolinstancedeleted'] = 'Enrolment instance deleted'; $string['eventenrolinstanceupdated'] = 'Enrolment instance updated'; @@ -58,6 +60,7 @@ $string['enrolme'] = 'Enrol me in this course'; $string['enrolmentinstances'] = 'Enrolment methods'; $string['enrolmentnew'] = 'New enrolment in {$a}'; $string['enrolmentnewuser'] = '{$a->user} has enrolled in course "{$a->course}"'; +$string['enrolmentmethod'] = 'Enrolment method'; $string['enrolments'] = 'Enrolments'; $string['enrolmentoptions'] = 'Enrolment options'; $string['enrolnotpermitted'] = 'You do not have permission or are not allowed to enrol someone in this course'; @@ -67,6 +70,7 @@ $string['enrolusers'] = 'Enrol users'; $string['enrolxusers'] = 'Enrol {$a} users'; $string['enroltimecreated'] = 'Enrolment created'; $string['enroltimeend'] = 'Enrolment ends'; +$string['enroltimeendinvalid'] = 'Enrolment end date must be after the enrolment start date'; $string['enroltimestart'] = 'Enrolment starts'; $string['errajaxfailedenrol'] = 'Failed to enrol user'; $string['errajaxsearch'] = 'Error when searching users'; @@ -104,6 +108,7 @@ $string['notenrollable'] = 'You can not enrol yourself in this course.'; $string['notenrolledusers'] = 'Other users'; $string['otheruserdesc'] = 'The following users are not enrolled in this course but do have roles, inherited or assigned within it.'; $string['participationactive'] = 'Active'; +$string['participationnotcurrent'] = 'Not current'; $string['participationstatus'] = 'Status'; $string['participationsuspended'] = 'Suspended'; $string['periodend'] = 'until {$a}'; diff --git a/lib/db/services.php b/lib/db/services.php index d483578e06b..14385ae6628 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -482,6 +482,14 @@ $functions = array( 'capabilities' => 'moodle/course:viewparticipants', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_enrol_edit_user_enrolment' => array( + 'classname' => 'core_enrol_external', + 'methodname' => 'edit_user_enrolment', + 'classpath' => 'enrol/externallib.php', + 'description' => 'External function that updates a given user enrolment', + 'type' => 'write', + 'ajax' => true, + ), 'core_fetch_notifications' => array( 'classname' => 'core_external', 'methodname' => 'fetch_notifications', diff --git a/lib/enrollib.php b/lib/enrollib.php index 7157bc5ebbf..93f67dc2890 100644 --- a/lib/enrollib.php +++ b/lib/enrollib.php @@ -87,6 +87,12 @@ define('ENROL_SEND_EMAIL_FROM_KEY_HOLDER', 2); */ define('ENROL_SEND_EMAIL_FROM_NOREPLY', 3); +/** Edit enrolment action. */ +define('ENROL_ACTION_EDIT', 'editenrolment'); + +/** Unenrol action. */ +define('ENROL_ACTION_UNENROL', 'unenrol'); + /** * Returns instances of enrol plugins * @param bool $enabled return enabled only @@ -1414,6 +1420,44 @@ function enrol_send_welcome_email_options() { ]; } +/** + * Serve the user enrolment form as a fragment. + * + * @param array $args List of named arguments for the fragment loader. + * @return string + */ +function enrol_output_fragment_user_enrolment_form($args) { + global $CFG, $DB; + + $args = (object) $args; + $context = $args->context; + require_capability('moodle/course:enrolreview', $context); + + $ueid = $args->ueid; + $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST); + $customdata = [ + 'ue' => $userenrolment, + 'modal' => true, + ]; + + // Set the data if applicable. + $data = []; + if (isset($args->formdata)) { + $serialiseddata = json_decode($args->formdata); + parse_str($serialiseddata, $data); + } + + require_once("$CFG->dirroot/enrol/editenrolment_form.php"); + $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data); + + if (!empty($data)) { + $mform->set_data($data); + $mform->is_validated(); + } + + return $mform->render(); +} + /** * Returns the course where a user enrolment belong to. * diff --git a/lib/upgrade.txt b/lib/upgrade.txt index ef21b435245..ea0eace1434 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -25,6 +25,13 @@ information provided here is intended especially for developers. every form. The method should accept a single parameter with the submitted value. It should return a string with the eventual validation error, or an empty value if the validation passes. * New user_picture attribute $includefullname to determine whether to include the user's full name with the user's picture. +* Enrol plugins which provide enrolment actions can now declare the following "data-action" attributes in their implementation of + enrol_plugin::get_user_enrolment_actions() whenever applicable: + * "editenrolment" - For editing a user'e enrolment details. Defined by constant ENROL_ACTION_EDIT. + * "unenrol" - For unenrolling a student. Defined by constant ENROL_ACTION_UNENROL. + These attributes enable enrol actions to be rendered via modals. If not added, clicking on the enrolment action buttons will still + redirect the user to the appropriate enrolment action page. Though optional, it is recommended to add these attributes for a + better user experience when performing enrol actions. === 3.3.1 === diff --git a/user/amd/build/status_field.min.js b/user/amd/build/status_field.min.js new file mode 100644 index 00000000000..ea0b3656672 --- /dev/null +++ b/user/amd/build/status_field.min.js @@ -0,0 +1 @@ +define(["core/templates","jquery","core/str","core/config","core/notification","core/modal_factory","core/modal_events","core/fragment","core/ajax"],function(a,b,c,d,e,f,g,h,i){var j={EDIT_ENROLMENT:'[data-action="editenrolment"]',SHOW_DETAILS:'[data-action="showdetails"]',UNENROL:'[data-action="unenrol"]'},k=function(a){this.contextid=a.contextid,this.courseid=a.courseid,this.bindEditEnrol(),this.bindUnenrol(),this.bindStatusDetails()};return k.prototype.courseid=0,k.prototype.bindEditEnrol=function(){var a=this;b(j.EDIT_ENROLMENT).click(function(d){d.preventDefault();var h=b(this),i=h.parent(),j=i.data("fullname"),k=h.attr("rel");b.when(c.get_string("edituserenrolment","enrol",j)).then(function(a){return f.create({large:!0,title:a,type:f.types.SAVE_CANCEL})}).done(function(b){b.getRoot().on(g.save,function(c){c.preventDefault(),a.submitEditFormAjax(b)}),b.getRoot().on(g.hidden,function(){b.destroy()}),b.setBody(a.getBody(k)),b.show()}).fail(e.exception)})},k.prototype.bindUnenrol=function(){b(j.UNENROL).click(function(a){a.preventDefault();var h=b(this),i=h.parent(),j=[{key:"unenrol",component:"enrol"},{key:"unenrolconfirm",component:"enrol",param:{user:i.data("fullname"),course:i.data("coursename")}}];b.when(c.get_strings(j)).then(function(a){var b=a[0],c=a[1];return f.create({body:c,large:!0,title:b,type:f.types.CONFIRM})}).done(function(a){a.getRoot().on(g.yes,function(){var a={confirm:1,sesskey:d.sesskey,ue:b(h).attr("rel")};window.location.href=d.wwwroot+"/enrol/unenroluser.php?"+b.param(a)}),a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)})},k.prototype.bindStatusDetails=function(){b(j.SHOW_DETAILS).click(function(d){d.preventDefault();var h=b(this),i=h.parent(),j={fullname:i.data("fullname"),coursename:i.data("coursename"),enrolinstancename:i.data("enrolinstancename"),status:i.data("status"),statusclass:i.find("span").attr("class"),timestart:i.data("timestart"),timeend:i.data("timeend")},k=c.get_string("enroldetails","enrol"),l=f.create({large:!0,type:f.types.CANCEL});b.when(k,l).done(function(b,c){var d=a.render("core_user/status_details",j);c.setTitle(b),c.setBody(d),c.show(),c.getRoot().on(g.hidden,function(){c.destroy()})}).fail(e.exception)})},k.prototype.submitEditFormAjax=function(a){var c=this,d=a.getRoot().find("form"),f=b(d).find('[name="ue"]').val(),g=b(d).find('[name="status"]').val(),h={courseid:this.courseid,ueid:f,status:g},j=b(d).find('[name="timestart[enabled]"]');if(j.is(":checked")){var k=b(d).find('[name="timestart[year]"]').val(),l=b(d).find('[name="timestart[month]"]').val()-1,m=b(d).find('[name="timestart[day]"]').val(),n=b(d).find('[name="timestart[hour]"]').val(),o=b(d).find('[name="timestart[minute]"]').val(),p=new Date(k,l,m,n,o);h.timestart=p.getTime()/1e3}var q=b(d).find('[name="timeend[enabled]"]');if(q.is(":checked")){var r=b(d).find('[name="timeend[year]"]').val(),s=b(d).find('[name="timeend[month]"]').val()-1,t=b(d).find('[name="timeend[day]"]').val(),u=b(d).find('[name="timeend[hour]"]').val(),v=b(d).find('[name="timeend[minute]"]').val(),w=new Date(r,s,t,u,v);h.timeend=w.getTime()/1e3}var x={methodname:"core_enrol_edit_user_enrolment",args:h};i.call([x])[0].done(function(b){if(b.result)a.hide(),"undefined"!=typeof window.M.core_formchangechecker&&window.M.core_formchangechecker.reset_form_dirty_state(),window.location.reload();else{var e=JSON.stringify(d.serialize());a.setBody(c.getBody(f,e))}}).fail(e.exception)},k.prototype.getBody=function(a,b){var c={ueid:a};return"undefined"!=typeof b&&(c.formdata=b),h.loadFragment("enrol","user_enrolment_form",this.contextid,c).fail(e.exception)},{init:function(a){new k(a)}}}); \ No newline at end of file diff --git a/user/amd/src/status_field.js b/user/amd/src/status_field.js new file mode 100644 index 00000000000..e57b7e915b5 --- /dev/null +++ b/user/amd/src/status_field.js @@ -0,0 +1,323 @@ +// 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 . + +/** + * AMD module for the user enrolment status field in the course participants page. + * + * @module core_user/status_field + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['core/templates', + 'jquery', + 'core/str', + 'core/config', + 'core/notification', + 'core/modal_factory', + 'core/modal_events', + 'core/fragment', + 'core/ajax' + ], + function(Template, $, Str, Config, Notification, ModalFactory, ModalEvents, Fragment, Ajax) { + + /** + * Action selectors. + * + * @access private + * @type {{EDIT_ENROLMENT: string, SHOW_DETAILS: string, UNENROL: string}} + */ + var SELECTORS = { + EDIT_ENROLMENT: '[data-action="editenrolment"]', + SHOW_DETAILS: '[data-action="showdetails"]', + UNENROL: '[data-action="unenrol"]' + }; + + /** + * Constructor + * + * @param {Object} options Object containing options. The only valid option at this time is contextid. + * Each call to templates.render gets it's own instance of this class. + */ + var StatusFieldActions = function(options) { + this.contextid = options.contextid; + this.courseid = options.courseid; + + // Bind click event to editenrol buttons. + this.bindEditEnrol(); + + // Bind click event to unenrol buttons. + this.bindUnenrol(); + + // Bind click event to status details buttons. + this.bindStatusDetails(); + }; + // Class variables and functions. + + /** @var {number} courseid The course ID. */ + StatusFieldActions.prototype.courseid = 0; + + /** + * Private method + * + * @method initModal + * @private + */ + StatusFieldActions.prototype.bindEditEnrol = function() { + var statusFieldInstsance = this; + + $(SELECTORS.EDIT_ENROLMENT).click(function(e) { + e.preventDefault(); + + // The particular edit button that was clicked. + var clickedEditTrigger = $(this); + // Get the parent container (it contains the data attributes associated with the status field). + var parentContainer = clickedEditTrigger.parent(); + // Get the name of the user whose enrolment status is being edited. + var fullname = parentContainer.data('fullname'); + // Get the user enrolment ID. + var ueid = clickedEditTrigger.attr('rel'); + + $.when(Str.get_string('edituserenrolment', 'enrol', fullname)).then(function(modalTitle) { + return ModalFactory.create({ + large: true, + title: modalTitle, + type: ModalFactory.types.SAVE_CANCEL + }); + }).done(function(modal) { + // Handle save event. + modal.getRoot().on(ModalEvents.save, function(e) { + // Don't close the modal yet. + e.preventDefault(); + // Submit form data. + statusFieldInstsance.submitEditFormAjax(modal); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + // Set the modal body. + modal.setBody(statusFieldInstsance.getBody(ueid)); + + // Show the modal! + modal.show(); + }).fail(Notification.exception); + }); + }; + + /** + * Private method + * + * @method bindUnenrol + * @private + */ + StatusFieldActions.prototype.bindUnenrol = function() { + $(SELECTORS.UNENROL).click(function(e) { + e.preventDefault(); + var unenrolLink = $(this); + var parentContainer = unenrolLink.parent(); + var strings = [ + { + key: 'unenrol', + component: 'enrol' + }, + { + key: 'unenrolconfirm', + component: 'enrol', + param: { + user: parentContainer.data('fullname'), + course: parentContainer.data('coursename') + } + } + ]; + + $.when(Str.get_strings(strings)).then(function(results) { + var title = results[0]; + var confirmMessage = results[1]; + return ModalFactory.create({ + body: confirmMessage, + large: true, + title: title, + type: ModalFactory.types.CONFIRM + }); + }).done(function(modal) { + // Handle confirm event. + modal.getRoot().on(ModalEvents.yes, function() { + // Build params. + var unenrolParams = { + confirm: 1, + sesskey: Config.sesskey, + ue: $(unenrolLink).attr('rel') + }; + // Send data to unenrol page (which will redirect back to the participants page after unenrol). + window.location.href = Config.wwwroot + '/enrol/unenroluser.php?' + $.param(unenrolParams); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + // Display the delete confirmation modal. + modal.show(); + }).fail(Notification.exception); + }); + }; + + /** + * Private method + * + * @method bindStatusDetails + * @private + */ + StatusFieldActions.prototype.bindStatusDetails = function() { + $(SELECTORS.SHOW_DETAILS).click(function(e) { + e.preventDefault(); + + var detailsButton = $(this); + var parentContainer = detailsButton.parent(); + var context = { + "fullname": parentContainer.data('fullname'), + "coursename": parentContainer.data('coursename'), + "enrolinstancename": parentContainer.data('enrolinstancename'), + "status": parentContainer.data('status'), + "statusclass": parentContainer.find('span').attr('class'), + "timestart": parentContainer.data('timestart'), + "timeend": parentContainer.data('timeend') + }; + + var modalTitlePromise = Str.get_string('enroldetails', 'enrol'); + var modalPromise = ModalFactory.create({large: true, type: ModalFactory.types.CANCEL}); + $.when(modalTitlePromise, modalPromise).done(function(modalTitle, modal) { + var modalBodyPromise = Template.render('core_user/status_details', context); + modal.setTitle(modalTitle); + modal.setBody(modalBodyPromise); + modal.show(); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + }).fail(Notification.exception); + }); + }; + + /** + * Private method + * + * @method submitEditFormAjax + * @param {Object} modal The the AMD modal object containing the form. + * @private + */ + StatusFieldActions.prototype.submitEditFormAjax = function(modal) { + var statusFieldInstsance = this; + var form = modal.getRoot().find('form'); + + // User enrolment ID. + var ueid = $(form).find('[name="ue"]').val(); + // Status. + var status = $(form).find('[name="status"]').val(); + + var params = { + 'courseid': this.courseid, + 'ueid': ueid, + 'status': status + }; + + // Enrol time start. + var timeStartEnabled = $(form).find('[name="timestart[enabled]"]'); + if (timeStartEnabled.is(':checked')) { + var timeStartYear = $(form).find('[name="timestart[year]"]').val(); + var timeStartMonth = $(form).find('[name="timestart[month]"]').val() - 1; + var timeStartDay = $(form).find('[name="timestart[day]"]').val(); + var timeStartHour = $(form).find('[name="timestart[hour]"]').val(); + var timeStartMinute = $(form).find('[name="timestart[minute]"]').val(); + var timeStart = new Date(timeStartYear, timeStartMonth, timeStartDay, timeStartHour, timeStartMinute); + params.timestart = timeStart.getTime() / 1000; + } + + // Enrol time end. + var timeEndEnabled = $(form).find('[name="timeend[enabled]"]'); + if (timeEndEnabled.is(':checked')) { + var timeEndYear = $(form).find('[name="timeend[year]"]').val(); + var timeEndMonth = $(form).find('[name="timeend[month]"]').val() - 1; + var timeEndDay = $(form).find('[name="timeend[day]"]').val(); + var timeEndHour = $(form).find('[name="timeend[hour]"]').val(); + var timeEndMinute = $(form).find('[name="timeend[minute]"]').val(); + var timeEnd = new Date(timeEndYear, timeEndMonth, timeEndDay, timeEndHour, timeEndMinute); + params.timeend = timeEnd.getTime() / 1000; + } + + var request = { + methodname: 'core_enrol_edit_user_enrolment', + args: params + }; + + Ajax.call([request])[0].done(function(data) { + if (data.result) { + // Dismiss the modal. + modal.hide(); + + // Reload the page, don't show changed data warnings. + if (typeof window.M.core_formchangechecker !== "undefined") { + window.M.core_formchangechecker.reset_form_dirty_state(); + } + window.location.reload(); + + } else { + // Serialise the form data and reload the form fragment to show validation errors. + var formData = JSON.stringify(form.serialize()); + modal.setBody(statusFieldInstsance.getBody(ueid, formData)); + } + }).fail(Notification.exception); + }; + + /** + * Private method + * + * @method getBody + * @private + * @param {Number} ueid The user enrolment ID associated with the user. + * @param {string} formData Serialized string of the edit enrolment form data. + * @return {Promise} + */ + StatusFieldActions.prototype.getBody = function(ueid, formData) { + var params = { + 'ueid': ueid + }; + if (typeof formData !== 'undefined') { + params.formdata = formData; + } + return Fragment.loadFragment('enrol', 'user_enrolment_form', this.contextid, params).fail(Notification.exception); + }; + + return /** @alias module:core_user/editenrolment */ { + // Public variables and functions. + /** + * Every call to init creates a new instance of the class with it's own event listeners etc. + * + * @method init + * @public + * @param {object} config - config variables for the module. + */ + init: function(config) { + (new StatusFieldActions(config)); + } + }; + }); \ No newline at end of file diff --git a/user/classes/output/status_field.php b/user/classes/output/status_field.php new file mode 100644 index 00000000000..18d36ba944f --- /dev/null +++ b/user/classes/output/status_field.php @@ -0,0 +1,163 @@ +. + +/** + * Class containing the data necessary for rendering the status field in the course participants page. + * + * @package core_user + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_user\output; + +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use stdClass; +use templatable; +use user_enrolment_action; + +/** + * Class containing the data for the status field. + * + * @package core_user + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class status_field implements renderable, templatable { + + /** Active user enrolment status constant. */ + const STATUS_ACTIVE = 0; + + /** Suspended user enrolment status constant. */ + const STATUS_SUSPENDED = 1; + + /** Not current user enrolment status constant. */ + const STATUS_NOT_CURRENT = 2; + + /** @var string $enrolinstancename The enrolment instance name. */ + protected $enrolinstancename; + + /** @var string $coursename The course's full name. */ + protected $coursename; + + /** @var string $fullname The user's full name. */ + protected $fullname; + + /** @var string $status The user enrolment status. */ + protected $status; + + /** @var int $timestart The timestamp when the user's enrolment starts. */ + protected $timestart; + + /** @var int $timeend The timestamp when the user's enrolment ends. */ + protected $timeend; + + /** @var user_enrolment_action[] $enrolactions Array of enrol action objects for the given enrolment method. */ + protected $enrolactions; + + /** @var bool $statusactive Indicates whether a user enrolment status should be rendered as active. */ + protected $statusactive = false; + + /** @var bool $statusactive Indicates whether a user enrolment status should be rendered as suspended. */ + protected $statussuspended = false; + + /** @var bool $statusactive Indicates whether a user enrolment status should be rendered as not current. */ + protected $statusnotcurrent = false; + + /** + * status_field constructor. + * + * @param string $enrolinstancename The enrolment instance name. + * @param string $coursename The course's full name. + * @param string $fullname The user's full name. + * @param string $status The user enrolment status. + * @param int|null $timestart The timestamp when the user's enrolment starts. + * @param int|null $timeend The timestamp when the user's enrolment ends. + * @param user_enrolment_action[] $enrolactions Array of enrol action objects for the given enrolment method. + */ + public function __construct($enrolinstancename, $coursename, $fullname, $status, $timestart = null, $timeend = null, + $enrolactions = []) { + $this->enrolinstancename = $enrolinstancename; + $this->coursename = $coursename; + $this->fullname = $fullname; + $this->status = $status; + $this->timestart = $timestart; + $this->timeend = $timeend; + $this->enrolactions = $enrolactions; + } + + /** + * Function to export the renderer data in a format that is suitable for a + * mustache template. This means: + * 1. No complex types - only stdClass, array, int, string, float, bool + * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return stdClass|array + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->enrolinstancename = $this->enrolinstancename; + $data->coursename = $this->coursename; + $data->fullname = $this->fullname; + $data->status = $this->status; + $data->active = $this->statusactive; + $data->suspended = $this->statussuspended; + $data->notcurrent = $this->statusnotcurrent; + + if ($this->timestart) { + $data->timestart = userdate($this->timestart); + } + if ($this->timeend) { + $data->timeend = userdate($this->timeend); + } + $data->enrolactions = []; + + foreach ($this->enrolactions as $enrolaction) { + $action = new stdClass(); + $action->url = $enrolaction->get_url()->out(false); + $action->icon = $output->render($enrolaction->get_icon()); + $action->attributes = []; + foreach ($enrolaction->get_attributes() as $name => $value) { + $attribute = (object) [ + 'name' => $name, + 'value' => $value + ]; + $action->attributes[] = $attribute; + } + $data->enrolactions[] = $action; + } + + return $data; + } + + /** + * Status setter. + * + * @param int $status The user enrolment status representing one of this class' STATUS_* constants. + * @return status_field This class' instance. Useful for chaining. + */ + public function set_status($status = self::STATUS_ACTIVE) { + $this->statusactive = $status == static::STATUS_ACTIVE; + $this->statussuspended = $status == static::STATUS_SUSPENDED; + $this->statusnotcurrent = $status == static::STATUS_NOT_CURRENT; + + return $this; + } +} diff --git a/user/classes/participants_table.php b/user/classes/participants_table.php index 85f937a1fbb..becb5e45c0a 100644 --- a/user/classes/participants_table.php +++ b/user/classes/participants_table.php @@ -24,6 +24,10 @@ namespace core_user; +use context; +use core_user\output\status_field; +use DateTime; + defined('MOODLE_INTERNAL') || die; global $CFG; @@ -86,12 +90,12 @@ class participants_table extends \table_sql { protected $extrafields; /** - * @var \stdClass The course details. + * @var \stdClass $course The course details. */ protected $course; /** - * @var \context The course context. + * @var context $context The course context. */ protected $context; @@ -125,6 +129,7 @@ class participants_table extends \table_sql { // Get the context. $this->course = get_course($courseid); $context = \context_course::instance($courseid, MUST_EXIST); + $this->context = $context; // Define the headers and columns. $headers = []; @@ -168,6 +173,13 @@ class participants_table extends \table_sql { $columns[] = 'lastaccess'; } + $canreviewenrol = has_capability('moodle/course:enrolreview', $context); + if ($canreviewenrol) { + $columns[] = 'status'; + $headers[] = get_string('participationstatus', 'enrol'); + $this->no_sorting('status'); + }; + $this->define_columns($columns); $this->define_headers($headers); @@ -192,6 +204,24 @@ class participants_table extends \table_sql { $this->assignableroles = get_assignable_roles($this->context, ROLENAME_ALIAS, false); } + /** + * Render the participants table. + * + * @param int $pagesize Size of page for paginated displayed table. + * @param bool $useinitialsbar Whether to use the initials bar which will only be used if there is a fullname column defined. + * @param string $downloadhelpbutton + */ + public function out($pagesize, $useinitialsbar, $downloadhelpbutton = '') { + global $PAGE; + + parent::out($pagesize, $useinitialsbar, $downloadhelpbutton); + + if (has_capability('moodle/course:enrolreview', $this->context)) { + $params = ['contextid' => $this->context->id, 'courseid' => $this->course->id]; + $PAGE->requires->js_call_amd('core_user/status_field', 'init', [$params]); + } + } + /** * Generate the select column. * @@ -290,6 +320,56 @@ class participants_table extends \table_sql { return get_string('never'); } + /** + * Generate the status column. + * + * @param \stdClass $data The data object. + * @return string + */ + public function col_status($data) { + global $CFG, $OUTPUT, $PAGE; + + $enrolstatusoutput = ''; + $canreviewenrol = has_capability('moodle/course:enrolreview', $this->context); + if ($canreviewenrol) { + $fullname = fullname($data); + $coursename = $this->course->fullname; + require_once($CFG->dirroot . '/enrol/locallib.php'); + $manager = new \course_enrolment_manager($PAGE, $this->course); + $userenrolments = $manager->get_user_enrolments($data->id); + foreach ($userenrolments as $ue) { + $timestart = $ue->timestart; + $timeend = $ue->timeend; + $actions = $ue->enrolmentplugin->get_user_enrolment_actions($manager, $ue); + $instancename = $ue->enrolmentinstancename; + + // Default status field label and value. + $status = get_string('participationactive', 'enrol'); + $statusval = status_field::STATUS_ACTIVE; + switch ($ue->status) { + case ENROL_USER_ACTIVE: + $currentdate = new DateTime(); + $now = $currentdate->getTimestamp(); + // If user enrolment status has not yet started/already ended. + if ($timestart > $now || ($timeend > 0 && $timeend < $now)) { + $status = get_string('participationnotcurrent', 'enrol'); + $statusval = status_field::STATUS_NOT_CURRENT; + } + break; + case ENROL_USER_SUSPENDED: + $status = get_string('participationsuspended', 'enrol'); + $statusval = status_field::STATUS_SUSPENDED; + break; + } + + $statusfield = new status_field($instancename, $coursename, $fullname, $status, $timestart, $timeend, $actions); + $statusfielddata = $statusfield->set_status($statusval)->export_for_template($OUTPUT); + $enrolstatusoutput .= $OUTPUT->render_from_template('core_user/status_field', $statusfielddata); + } + } + return $enrolstatusoutput; + } + /** * This function is used for the extra user fields. * diff --git a/user/lib.php b/user/lib.php index 9231d9c6277..1188d0953d0 100644 --- a/user/lib.php +++ b/user/lib.php @@ -1234,7 +1234,9 @@ function user_get_participants_sql($courseid, $groupid = 0, $accesssince = 0, $r $isfrontpage = ($courseid == SITEID); - list($esql, $params) = get_enrolled_sql($context, null, $groupid, true); + // Show active users only if the user doesn't have the 'moodle/course:enrolreview' capability. + $onlyactive = !has_capability('moodle/course:enrolreview', $context); + list($esql, $params) = get_enrolled_sql($context, null, $groupid, $onlyactive); $joins = array('FROM {user} u'); $wheres = array(); diff --git a/user/templates/status_details.mustache b/user/templates/status_details.mustache new file mode 100644 index 00000000000..079f1cbb3cf --- /dev/null +++ b/user/templates/status_details.mustache @@ -0,0 +1,97 @@ +{{! + 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_user/status_details + + Template for the user enrolment status details. + + Context variables required for this template: + * fullname string - The user's full name. + * coursename string - The course name. + * enrolinstancename string - The enrolment instance name. + * status string - The enrolment status. + * statusclass string - The CSS class for the enrolment status. + * timestart string - Optional. The enrolment time start. + * timeend string - Optional. The enrolment time end. + + Example context (json): + { + "fullname": "Student John", + "coursename": "TC 1", + "enrolinstancename": "Manual enrolment", + "status": "Active", + "statusclass": "label label-success", + "timestart": "1 January 2017", + "timeend": "31 January 2018" + } +}} + + + + + + + + + + + + + + + + + + {{#timestart}} + + + + + {{/timestart}} + {{#timeend}} + + + + + {{/timeend}} +
+ {{#str}}fullname{{/str}} + + {{fullname}} +
+ {{#str}}course{{/str}} + + {{coursename}} +
+ {{#str}}enrolmentmethod, enrol{{/str}} + + {{enrolinstancename}} +
+ {{#str}}participationstatus, enrol{{/str}} + + + {{status}} + +
+ {{#str}}enroltimestart, enrol{{/str}} + + {{timestart}} +
+ {{#str}}enroltimeend, enrol{{/str}} + + {{timeend}} +
diff --git a/user/templates/status_field.mustache b/user/templates/status_field.mustache new file mode 100644 index 00000000000..57fcf8c9101 --- /dev/null +++ b/user/templates/status_field.mustache @@ -0,0 +1,71 @@ +{{! + 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_user/status_field + + Template for the enrolment status field. + + Context variables required for this template: + * fullname string - The user's full name. + * coursename string - The course name. + * enrolinstancename string - The enrolment instance name. + * status string - The enrolment status. + * active boolean - Flag to indicate whether the user has an active enrolment. + * suspended boolean - Flag to indicate whether the user is suspended. + * notcurrent boolean - Flag to indicate whether the user's enrolment is active, but not current. + * timestart string - Optional. The enrolment time start. + * timeend string - Optional. The enrolment time end. + * enrolactions array - Optional. Array of enrol actions consisting of: + * url string - The URL of the enrol action link. + * attributes array - The array of attributes for the enrol action link. + * icon string - The HTML output for the enrol action icon. + + Example context (json): + { + "fullname": "Student John", + "coursename": "TC 1", + "enrolinstancename": "Manual enrolment", + "status": "Active", + "active": true, + "timestart": "1 January 2017", + "timeend": "31 January 2018", + "enrolactions": [ + { + "url": "#", + "attributes": [ + { + "name": "class", + "value": "action-class" + } + ], + "icon": "" + } + ] + } +}} +
+ {{status}} + + {{#pix}}docs, core, {{#str}}enroldetails, enrol{{/str}}{{/pix}} + + {{#enrolactions}} + + {{{icon}}} + + {{/enrolactions}} +
diff --git a/user/tests/behat/edit_user_enrolment.feature b/user/tests/behat/edit_user_enrolment.feature new file mode 100644 index 00000000000..eaeb4be5fea --- /dev/null +++ b/user/tests/behat/edit_user_enrolment.feature @@ -0,0 +1,93 @@ +@core @core_user +Feature: Edit user enrolment + In order to manage students' enrolments + As a teacher + I need to be able to view enrolment details and edit student enrolments in the course participants page + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "course enrolments" exist: + | user | course | role | status | + | teacher1 | C1 | editingteacher | 0 | + | student1 | C1 | student | 0 | + | student2 | C1 | student | 1 | + + @javascript + Scenario: Edit a user's enrolment + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='editenrolment']" "xpath_element" in the "student1" "table_row" + And I should see "Edit Student 1's enrolment" + And I set the field "Status" to "Suspended" + And I click on "Save changes" "button" + And I click on "//a[@data-action='editenrolment']" "xpath_element" in the "student2" "table_row" + And I should see "Edit Student 2's enrolment" + And I set the field "timeend[enabled]" to "1" + And I set the field "timeend[day]" to "1" + And I set the field "timeend[month]" to "January" + And I set the field "timeend[year]" to "2017" + And I set the field "Status" to "Active" + And I click on "Save changes" "button" + Then I should see "Suspended" in the "student1" "table_row" + And I should see "Not current" in the "student2" "table_row" + + @javascript + Scenario: Unenrol a student + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='unenrol']" "xpath_element" in the "student1" "table_row" + And I click on "Yes" "button" + Then I should not see "Student 1" in the "participants" "table" + + @javascript + Scenario: View a student's enrolment details + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='showdetails']" "xpath_element" in the "student1" "table_row" + Then I should see "Enrolment details" + And I should see "Student 1" in the "Full name" "table_row" + And I should see "Active" in the "//td[@class='user-enrol-status']" "xpath_element" + And I press "Cancel" + And I click on "//a[@data-action='showdetails']" "xpath_element" in the "student2" "table_row" + And I should see "Enrolment details" + And I should see "Student 2" in the "Full name" "table_row" + And I should see "Suspended" in the "//td[@class='user-enrol-status']" "xpath_element" + + # Without JS, the user should be redirected to the original edit enrolment form. + Scenario: Edit a user's enrolment without JavaScript + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='editenrolment']" "xpath_element" in the "student1" "table_row" + And I should see "Student 1" + And I set the field "Status" to "Suspended" + And I click on "Save changes" "button" + And I click on "//a[@data-action='editenrolment']" "xpath_element" in the "student2" "table_row" + And I should see "Student 2" + And I set the field "timeend[enabled]" to "1" + And I set the field "timeend[day]" to "1" + And I set the field "timeend[month]" to "January" + And I set the field "timeend[year]" to "2017" + And I set the field "Status" to "Active" + And I click on "Save changes" "button" + Then I should see "Suspended" in the "student1" "table_row" + And I should see "Not current" in the "student2" "table_row" + + # Without JS, the user should be redirected to the original unenrol confirmation page. + Scenario: Unenrol a student + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I navigate to course participants + When I click on "//a[@data-action='unenrol']" "xpath_element" in the "student1" "table_row" + And I click on "Continue" "button" + Then I should not see "Student 1" in the "participants" "table" diff --git a/user/tests/behat/view_participants.feature b/user/tests/behat/view_participants.feature index c9724096243..532d7c9324a 100644 --- a/user/tests/behat/view_participants.feature +++ b/user/tests/behat/view_participants.feature @@ -32,27 +32,27 @@ Feature: View course participants | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: - | user | course | role | - | teacher1 | C1 | editingteacher | - | student1 | C1 | student | - | student2 | C1 | student | - | student3 | C1 | student | - | student4 | C1 | student | - | student5 | C1 | student | - | student6 | C1 | student | - | student7 | C1 | student | - | student8 | C1 | student | - | student9 | C1 | student | - | student10 | C1 | student | - | student11 | C1 | student | - | student12 | C1 | student | - | student13 | C1 | student | - | student14 | C1 | student | - | student15 | C1 | student | - | student16 | C1 | student | - | student17 | C1 | student | - | student18 | C1 | student | - | student19 | C1 | student | + | user | course | role | status | timeend | + | teacher1 | C1 | editingteacher | 0 | 0 | + | student1 | C1 | student | 0 | 0 | + | student2 | C1 | student | 0 | 0 | + | student3 | C1 | student | 0 | 0 | + | student4 | C1 | student | 0 | 0 | + | student5 | C1 | student | 0 | 0 | + | student6 | C1 | student | 0 | 0 | + | student7 | C1 | student | 0 | 0 | + | student8 | C1 | student | 0 | 0 | + | student9 | C1 | student | 0 | 0 | + | student10 | C1 | student | 1 | 0 | + | student11 | C1 | student | 0 | 100 | + | student12 | C1 | student | 0 | 0 | + | student13 | C1 | student | 0 | 0 | + | student14 | C1 | student | 0 | 0 | + | student15 | C1 | student | 0 | 0 | + | student16 | C1 | student | 0 | 0 | + | student17 | C1 | student | 0 | 0 | + | student18 | C1 | student | 0 | 0 | + | student19 | C1 | student | 0 | 0 | @javascript Scenario: Use select and deselect all buttons @@ -222,3 +222,46 @@ Feature: View course participants And the field with xpath "//tbody//tr[19]//input[@class='usercheckbox']" matches value "0" And the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "0" And the field with xpath "//tbody//tr[21]//input[@class='usercheckbox']" matches value "0" + + Scenario: View the participants page as a teacher + Given I log in as "teacher1" + And I am on "Course 1" course homepage + When I navigate to course participants + Then I should see "Active" in the "student1" "table_row" + And I should see "Active" in the "student2" "table_row" + And I should see "Active" in the "student3" "table_row" + And I should see "Active" in the "student4" "table_row" + And I should see "Active" in the "student5" "table_row" + And I should see "Active" in the "student6" "table_row" + And I should see "Active" in the "student7" "table_row" + And I should see "Active" in the "student8" "table_row" + And I should see "Active" in the "student9" "table_row" + And I should see "Suspended" in the "student10" "table_row" + And I should see "Not current" in the "student11" "table_row" + And I should see "Active" in the "student12" "table_row" + And I should see "Active" in the "student13" "table_row" + And I should see "Active" in the "student14" "table_row" + And I should see "Active" in the "student15" "table_row" + And I should see "Active" in the "student16" "table_row" + And I should see "Active" in the "student17" "table_row" + And I should see "Active" in the "student18" "table_row" + And I should see "Active" in the "student19" "table_row" + + Scenario: View the participants page as a student + Given I log in as "student1" + And I am on "Course 1" course homepage + When I navigate to course participants + # Student should not see the status column. + Then I should not see "Status" in the "participants" "table" + # Student should be able to see the other actively-enrolled students. + And I should see "Student 1" in the "participants" "table" + And I should see "Student 2" in the "participants" "table" + And I should see "Student 3" in the "participants" "table" + And I should see "Student 4" in the "participants" "table" + And I should see "Student 5" in the "participants" "table" + And I should see "Student 6" in the "participants" "table" + And I should see "Student 7" in the "participants" "table" + And I should see "Student 8" in the "participants" "table" + # Suspended and non-current students should not be rendered. + And I should not see "Student 10" in the "participants" "table" + And I should not see "Student 11" in the "participants" "table" diff --git a/version.php b/version.php index 7e84112cf7e..75a5d7f1fb6 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017072500.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017072600.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.