MDL-60579 enrolments: Applied filters lost on certain actions
authorMihail Geshoski <mihail@moodle.com>
Tue, 7 Nov 2017 08:17:00 +0000 (16:17 +0800)
committerMihail Geshoski <mihail@moodle.com>
Tue, 7 Nov 2017 08:17:00 +0000 (16:17 +0800)
AMOS BEGIN
    CPY [invalidrequest,enrol_lti],[invalidrequest,enrol]
AMOS END

13 files changed:
enrol/externallib.php
enrol/tests/externallib_test.php
lang/en/enrol.php
lib/db/services.php
user/amd/build/name_page_filter.min.js [new file with mode: 0644]
user/amd/build/status_field.min.js
user/amd/build/unified_filter.min.js
user/amd/src/name_page_filter.js [new file with mode: 0644]
user/amd/src/status_field.js
user/amd/src/unified_filter.js
user/index.php
user/renderer.php
version.php

index 670be50..ea5f971 100644 (file)
@@ -909,6 +909,89 @@ class core_enrol_external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of unenrol_user_enrolment() parameters
+     *
+     * @return external_function_parameters
+     */
+    public static function unenrol_user_enrolment_parameters() {
+        return new external_function_parameters(
+            array(
+                'ueid' => new external_value(PARAM_INT, 'User enrolment ID')
+            )
+        );
+    }
+
+    /**
+     * External function that unenrols a given user enrolment.
+     *
+     * @param int $ueid The user enrolment ID.
+     * @return array An array consisting of the processing result, errors.
+     */
+    public static function unenrol_user_enrolment($ueid) {
+        global $CFG, $DB, $PAGE;
+
+        $params = self::validate_parameters(self::unenrol_user_enrolment_parameters(), [
+            'ueid' => $ueid
+        ]);
+
+        $result = false;
+        $errors = [];
+
+        $userenrolment = $DB->get_record('user_enrolments', ['id' => $params['ueid']], '*');
+        if ($userenrolment) {
+            $userid = $userenrolment->userid;
+            $enrolid = $userenrolment->enrolid;
+            $enrol = $DB->get_record('enrol', ['id' => $enrolid], '*', MUST_EXIST);
+            $courseid = $enrol->courseid;
+            $course = get_course($courseid);
+            $context = context_course::instance($course->id);
+            self::validate_context($context);
+        } else {
+            $validationerrors['invalidrequest'] = get_string('invalidrequest', 'enrol');
+        }
+
+        // If the userenrolment exists, unenrol the user.
+        if (!isset($validationerrors)) {
+            require_once($CFG->dirroot . '/enrol/locallib.php');
+            $manager = new course_enrolment_manager($PAGE, $course);
+            $result = $manager->unenrol_user($userenrolment);
+        } else {
+            foreach ($validationerrors as $key => $errormessage) {
+                $errors[] = (object)[
+                    'key' => $key,
+                    'message' => $errormessage
+                ];
+            }
+        }
+
+        return [
+            'result' => $result,
+            'errors' => $errors,
+        ];
+    }
+
+    /**
+     * Returns description of unenrol_user_enrolment() result value
+     *
+     * @return external_description
+     */
+    public static function unenrol_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'
+                ),
+            )
+        );
+    }
 }
 
 /**
index 8b787a1..d85f4d3 100644 (file)
@@ -764,4 +764,69 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
         $ue = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
         $this->assertEquals(ENROL_USER_SUSPENDED, $ue->status);
     }
+
+    /**
+     * Test for core_enrol_external::unenrol_user_enrolment().
+     */
+    public function test_unenerol_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);
+
+        // Invalid data by passing invalid ueid.
+        $data = core_enrol_external::unenrol_user_enrolment(101010);
+        $data = external_api::clean_returnvalue(core_enrol_external::unenrol_user_enrolment_returns(), $data);
+        $this->assertFalse($data['result']);
+        $this->assertNotEmpty($data['errors']);
+
+        // Valid data.
+        $data = core_enrol_external::unenrol_user_enrolment($ueid);
+        $data = external_api::clean_returnvalue(core_enrol_external::unenrol_user_enrolment_returns(), $data);
+        $this->assertTrue($data['result']);
+        $this->assertEmpty($data['errors']);
+
+        // Check unenrol user enrolment.
+        $ue = $DB->count_records('user_enrolments', ['id' => $ueid]);
+        $this->assertEquals(0, $ue);
+    }
 }
index 8c870a6..1d29ca6 100644 (file)
@@ -96,6 +96,7 @@ $string['instanceeditselfwarning'] = 'Warning:';
 $string['instanceeditselfwarningtext'] = 'You are enrolled into this course through this enrolment method, changes may affect your access to this course.';
 $string['invalidenrolinstance'] = 'Invalid enrolment instance';
 $string['invalidrole'] = 'Invalid role';
+$string['invalidrequest'] = 'Invalid request';
 $string['manageenrols'] = 'Manage enrol plugins';
 $string['manageinstance'] = 'Manage';
 $string['migratetomanual'] = 'Migrate to manual enrolments';
index 34425f6..a581ba3 100644 (file)
@@ -546,6 +546,14 @@ $functions = array(
         'type' => 'write',
         'ajax' => true,
     ),
+    'core_enrol_unenrol_user_enrolment' => array(
+        'classname' => 'core_enrol_external',
+        'methodname' => 'unenrol_user_enrolment',
+        'classpath' => 'enrol/externallib.php',
+        'description' => 'External function that unenrols a given user enrolment',
+        'type' => 'write',
+        'ajax' => true,
+    ),
     'core_fetch_notifications' => array(
         'classname' => 'core_external',
         'methodname' => 'fetch_notifications',
diff --git a/user/amd/build/name_page_filter.min.js b/user/amd/build/name_page_filter.min.js
new file mode 100644 (file)
index 0000000..2d4fcac
Binary files /dev/null and b/user/amd/build/name_page_filter.min.js differ
index 29f8eef..2079e47 100644 (file)
Binary files a/user/amd/build/status_field.min.js and b/user/amd/build/status_field.min.js differ
index bb8a603..eca6bea 100644 (file)
Binary files a/user/amd/build/unified_filter.min.js and b/user/amd/build/unified_filter.min.js differ
diff --git a/user/amd/src/name_page_filter.js b/user/amd/src/name_page_filter.js
new file mode 100644 (file)
index 0000000..8ebf4e7
--- /dev/null
@@ -0,0 +1,64 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Name and page filter JS module for the course participants page.
+ *
+ * @module     core_user/name_page_filter
+ * @package    core_user
+ * @copyright  2017 Mihail Geshoski
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core_user/unified_filter'],
+        function($, UnifiedFilter) {
+
+    /**
+     * Selectors.
+     *
+     * @access private
+     * @type {{NAME_FILTERS: string, PAGE_FILTERS: string}}
+     */
+    var SELECTORS = {
+        NAME_FILTERS: 'a.letter',
+        PAGE_FILTERS: 'a.page-link'
+    };
+
+    /**
+     * Init function.
+     *
+     * @method init
+     * @private
+     */
+    var init = function() {
+        $(SELECTORS.NAME_FILTERS + ', ' + SELECTORS.PAGE_FILTERS).on('click', function(e) {
+            e.preventDefault();
+            var href = $(this).attr('href');
+            UnifiedFilter.getForm().attr('action', href);
+            UnifiedFilter.getForm().submit();
+        });
+    };
+
+    return /** @alias module:core/form-autocomplete */ {
+        // Public variables and functions.
+        /**
+         * Initialise the name and page user filter.
+         *
+         * @method init
+         */
+        'init': function() {
+            init();
+        }
+    };
+});
index c17844f..23f5b7f 100644 (file)
@@ -126,6 +126,8 @@ define(['core/templates',
          * @private
          */
         StatusFieldActions.prototype.bindUnenrol = function() {
+            var statusFieldInstsance = this;
+
             $(SELECTORS.UNENROL).click(function(e) {
                 e.preventDefault();
                 var unenrolLink = $(this);
@@ -161,12 +163,12 @@ define(['core/templates',
                     modal.getRoot().on(ModalEvents.save, function() {
                         // Build params.
                         var unenrolParams = {
-                            confirm: 1,
-                            sesskey: Config.sesskey,
-                            ue: $(unenrolLink).attr('rel')
+                            'ueid': $(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);
+                        // Don't close the modal yet.
+                        e.preventDefault();
+                        // Submit data.
+                        statusFieldInstsance.submitUnenrolFormAjax(modal, unenrolParams);
                     });
 
                     // Handle hidden event.
@@ -306,7 +308,6 @@ define(['core/templates',
                         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());
@@ -315,6 +316,37 @@ define(['core/templates',
             }).fail(Notification.exception);
         };
 
+         /**
+         * Private method
+         *
+         * @method submitUnenrolFormAjax
+         * @param {Object} modal The the AMD modal object containing the form.
+         * @param {Object} unenrolParams The unenrol parameters.
+         * @private
+         */
+        StatusFieldActions.prototype.submitUnenrolFormAjax = function(modal, unenrolParams) {
+            var request = {
+                methodname: 'core_enrol_unenrol_user_enrolment',
+                args: unenrolParams
+            };
+
+            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 {
+                    // Display an alert containing the error message
+                    Notification.alert(data.errors[0].key, data.errors[0].message);
+                }
+            }).fail(Notification.exception);
+        };
+
         /**
          * Private method
          *
index bfb2df1..2fddee3 100644 (file)
@@ -104,6 +104,17 @@ define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'],
         });
     };
 
+    /**
+     * Return the unified user filter form.
+     *
+     * @method getForm
+     * @private
+     * @return {Form object}
+     */
+    var getForm = function() {
+        return $(SELECTORS.UNIFIED_FILTERS).closest('form');
+    };
+
     return /** @alias module:core/form-autocomplete */ {
         // Public variables and functions.
         /**
@@ -113,6 +124,15 @@ define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'],
          */
         'init': function() {
             init();
+        },
+        /**
+         * Return the unified user filter form.
+         *
+         * @method getForm
+         * @return {Form object}
+         */
+        'getForm': function() {
+            return getForm();
         }
     };
 });
index e89aab7..8f13c12 100644 (file)
@@ -249,6 +249,8 @@ if ($bulkoperations) {
 
 echo $participanttablehtml;
 
+$PAGE->requires->js_call_amd('core_user/name_page_filter', 'init');
+
 $perpageurl = clone($baseurl);
 $perpageurl->remove_params('perpage');
 if ($perpage == SHOW_ALL_PAGE_SIZE && $participanttable->totalrows > DEFAULT_PAGE_SIZE) {
index 89ceb2b..bf775b4 100644 (file)
@@ -212,31 +212,34 @@ class core_user_renderer extends plugin_renderer_base {
             // Days.
             for ($i = 1; $i < 7; $i++) {
                 $timestamp = strtotime('-' . $i . ' days', $now);
-                if ($timestamp >= $minlastaccess) {
-                    $value = get_string('numdays', 'moodle', $i);
-                    $timeoptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $value);
+                if ($timestamp < $minlastaccess) {
+                    break;
                 }
+                $value = get_string('numdays', 'moodle', $i);
+                $timeoptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $value);
             }
             // Weeks.
             for ($i = 1; $i < 10; $i++) {
                 $timestamp = strtotime('-'.$i.' weeks', $now);
-                if ($timestamp >= $minlastaccess) {
-                    $value = get_string('numweeks', 'moodle', $i);
-                    $timeoptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $value);
+                if ($timestamp < $minlastaccess) {
+                    break;
                 }
+                $value = get_string('numweeks', 'moodle', $i);
+                $timeoptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $value);
             }
             // Months.
             for ($i = 2; $i < 12; $i++) {
                 $timestamp = strtotime('-'.$i.' months', $now);
-                if ($timestamp >= $minlastaccess) {
-                    $value = get_string('nummonths', 'moodle', $i);
-                    $timeoptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $value);
+                if ($timestamp < $minlastaccess) {
+                    break;
                 }
+                $value = get_string('nummonths', 'moodle', $i);
+                $timeoptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $value);
             }
             // Try a year.
-            $timestamp = strtotime('-'.$i.' year', $now);
+            $timestamp = strtotime('-1 year', $now);
             if ($timestamp >= $minlastaccess) {
-                $value = get_string('lastyear', 'moodle');
+                $value = get_string('numyear', 'moodle', 1);
                 $timeoptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $value);
             }
             if (!empty($lastaccess0exists)) {
@@ -298,6 +301,9 @@ class core_user_renderer extends plugin_renderer_base {
                 get_string('inactive'));
         }
 
+        // Add missing applied filters to the filter options.
+        $filteroptions = $this->handle_missing_applied_filters($filtersapplied, $filteroptions);
+
         $indexpage = new \core_user\output\unified_filter($filteroptions, $filtersapplied);
         $context = $indexpage->export_for_template($this->output);
 
@@ -318,6 +324,71 @@ class core_user_renderer extends plugin_renderer_base {
         $optionvalue = "$filtertype:$value";
         return [$optionvalue => $optionlabel];
     }
+
+    /**
+     * Handles cases when after reloading the applied filters are missing in the filter options.
+     *
+     * @param array $filtersapplied The applied filters.
+     * @param array $filteroptions The filter options.
+     * @return array The formatted options with the ['filtertype:value' => 'criteria: label'] format.
+     */
+    private function handle_missing_applied_filters($filtersapplied, $filteroptions) {
+        global $DB;
+
+        foreach ($filtersapplied as $filter) {
+            if (!array_key_exists($filter, $filteroptions)) {
+                $filtervalue = explode(':', $filter);
+                $key = $filtervalue[0];
+                $value = $filtervalue[1];
+
+                switch($key) {
+                    case USER_FILTER_LAST_ACCESS:
+                        $now = usergetmidnight(time());
+                        $criteria = get_string('usersnoaccesssince');
+                        // Days.
+                        for ($i = 1; $i < 7; $i++) {
+                            $timestamp = strtotime('-' . $i . ' days', $now);
+                            if ($timestamp < $value) {
+                                break;
+                            }
+                            $val = get_string('numdays', 'moodle', $i);
+                            $filteroptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $val);
+                        }
+                        // Weeks.
+                        for ($i = 1; $i < 10; $i++) {
+                            $timestamp = strtotime('-'.$i.' weeks', $now);
+                            if ($timestamp < $value) {
+                                break;
+                            }
+                            $val = get_string('numweeks', 'moodle', $i);
+                            $filteroptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $val);
+                        }
+                        // Months.
+                        for ($i = 2; $i < 12; $i++) {
+                            $timestamp = strtotime('-'.$i.' months', $now);
+                            if ($timestamp < $value) {
+                                break;
+                            }
+                            $val = get_string('nummonths', 'moodle', $i);
+                            $filteroptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $val);
+                        }
+                        // Try a year.
+                        $timestamp = strtotime('-1 year', $now);
+                        if ($timestamp >= $value) {
+                            $val = get_string('numyear', 'moodle', 1);
+                            $filteroptions += $this->format_filter_option(USER_FILTER_LAST_ACCESS, $criteria, $timestamp, $val);
+                        }
+                    case USER_FILTER_ROLE:
+                        $criteria = get_string('role');
+                        if ($role = $DB->get_record('role', array('id' => $value))) {
+                            $role = role_get_name($role);
+                            $filteroptions += $this->format_filter_option(USER_FILTER_ROLE, $criteria, $value, $role);
+                        }
+                }
+            }
+        }
+        return $filteroptions;
+    }
 }
 
 /**
index 1a90c1f..90ea999 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017110300.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017110100.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.