Merge branch 'MDL-65651-master' of https://github.com/lucaboesch/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 6 Aug 2019 03:57:56 +0000 (11:57 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 6 Aug 2019 03:57:56 +0000 (11:57 +0800)
43 files changed:
.travis.yml
admin/roles/allow.php
admin/roles/classes/allow_assign_page.php
admin/roles/classes/allow_override_page.php
admin/roles/classes/allow_role_page.php
admin/roles/classes/allow_switch_page.php
admin/roles/classes/allow_view_page.php
admin/roles/classes/define_role_table_advanced.php
admin/roles/define.php
admin/roles/override.php
admin/tool/dataprivacy/classes/form/purpose.php
babel-plugin-add-module-to-define.js
enrol/editenrolment.php
enrol/editenrolment_form.php
enrol/manual/ajax.php
enrol/manual/classes/enrol_users_form.php
enrol/manual/manage.php
enrol/manual/tests/behat/quickenrolment.feature
enrol/tests/enrollib_test.php
lang/en/deprecated.txt
lang/en/enrol.php
lang/en/role.php
lib/accesslib.php
lib/classes/event/capability_assigned.php [new file with mode: 0644]
lib/classes/event/capability_unassigned.php [new file with mode: 0644]
lib/classes/event/role_allow_assign_updated.php
lib/classes/event/role_allow_override_updated.php
lib/classes/event/role_allow_switch_updated.php
lib/classes/event/role_allow_view_updated.php
lib/classes/event/role_capabilities_updated.php
lib/classes/event/role_updated.php [new file with mode: 0644]
lib/classes/output/mustache_template_source_loader.php
lib/classes/task/task_log_cleanup_task.php
lib/enrollib.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_forms.php
question/format.php
user/amd/build/status_field.min.js
user/amd/build/status_field.min.js.map
user/amd/src/status_field.js
user/lib.php
user/renderer.php
user/tests/behat/filter_participants.feature

index 56a20e8..90edc4f 100644 (file)
@@ -11,7 +11,10 @@ notifications:
 
 language: php
 
-dist: trusty
+dist: xenial
+
+services:
+    - mysql
 
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
@@ -20,10 +23,6 @@ php:
 
 addons:
   postgresql: "9.6"
-  packages:
-    - mysql-server-5.6
-    - mysql-client-core-5.6
-    - mysql-client-5.6
 
 env:
     # Although we want to run these jobs and see failures as quickly as possible, we also want to get the slowest job to
@@ -71,10 +70,10 @@ install:
         then
             sudo mkdir /mnt/ramdisk
             sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk
-            sudo stop mysql
+            sudo service mysql stop
             sudo mv /var/lib/mysql /mnt/ramdisk
             sudo ln -s /mnt/ramdisk/mysql /var/lib/mysql
-            sudo start mysql
+            sudo service mysql restart
         fi
     - >
         if [ "$DB" = 'pgsql' ];
index 88609cf..643e6ce 100644 (file)
@@ -46,25 +46,6 @@ $controller = new $classformode[$mode]();
 
 if (optional_param('submit', false, PARAM_BOOL) && data_submitted() && confirm_sesskey()) {
     $controller->process_submission();
-    $event = null;
-    // Create event depending on mode.
-    switch ($mode) {
-        case 'assign':
-            $event = \core\event\role_allow_assign_updated::create(array('context' => $syscontext));
-            break;
-        case 'override':
-            $event = \core\event\role_allow_override_updated::create(array('context' => $syscontext));
-            break;
-        case 'switch':
-            $event = \core\event\role_allow_switch_updated::create(array('context' => $syscontext));
-            break;
-        case 'view':
-            $event = \core\event\role_allow_view_updated::create(array('context' => $syscontext));
-            break;
-    }
-    if ($event) {
-        $event->trigger();
-    }
     redirect($baseurl);
 }
 
index a89228f..002d944 100644 (file)
@@ -46,4 +46,8 @@ class core_role_allow_assign_page extends core_role_allow_role_page {
     public function get_intro_text() {
         return get_string('configallowassign', 'core_admin');
     }
+
+    protected function get_eventclass() {
+        return \core\event\role_allow_assign_updated::class;
+    }
 }
index 4b160ee..16226a6 100644 (file)
@@ -46,4 +46,8 @@ class core_role_allow_override_page extends core_role_allow_role_page {
     public function get_intro_text() {
         return get_string('configallowoverride2', 'core_admin');
     }
+
+    protected function get_eventclass() {
+        return \core\event\role_allow_override_updated::class;
+    }
 }
index c84d01a..376489f 100644 (file)
@@ -59,12 +59,36 @@ abstract class core_role_allow_role_page {
      */
     public function process_submission() {
         global $DB;
+
+        $context = context_system::instance();
+        $this->load_current_settings();
+
         // Delete all records, then add back the ones that should be allowed.
         $DB->delete_records($this->tablename);
         foreach ($this->roles as $fromroleid => $notused) {
             foreach ($this->roles as $targetroleid => $alsonotused) {
+                $isallowed = $this->allowed[$fromroleid][$targetroleid];
                 if (optional_param('s_' . $fromroleid . '_' . $targetroleid, false, PARAM_BOOL)) {
                     $this->set_allow($fromroleid, $targetroleid);
+                    // Only trigger events if this role allow relationship did not exist and the checkbox element
+                    // has been submitted.
+                    if (!$isallowed) {
+                        $eventclass = $this->get_eventclass();
+                        $eventclass::create([
+                            'context' => $context,
+                            'objectid' => $fromroleid,
+                            'other' => ['targetroleid' => $targetroleid, 'allow' => true]
+                        ])->trigger();
+                    }
+                } else if ($isallowed) {
+                    // When the user has deselect an existing role allow checkbox but it is in the list of roles
+                    // allowances.
+                    $eventclass = $this->get_eventclass();
+                    $eventclass::create([
+                        'context' => $context,
+                        'objectid' => $fromroleid,
+                        'other' => ['targetroleid' => $targetroleid, 'allow' => false]
+                    ])->trigger();
                 }
             }
         }
@@ -161,4 +185,10 @@ abstract class core_role_allow_role_page {
      * @return string
      */
     public abstract function get_intro_text();
+
+    /**
+     * Returns the allow class respective event class name.
+     * @return string
+     */
+    protected abstract function get_eventclass();
 }
index 5b22e1e..195aab7 100644 (file)
@@ -58,4 +58,8 @@ class core_role_allow_switch_page extends core_role_allow_role_page {
     public function get_intro_text() {
         return get_string('configallowswitch', 'core_admin');
     }
+
+    protected function get_eventclass() {
+        return \core\event\role_allow_switch_updated::class;
+    }
 }
index f1a1031..d331322 100644 (file)
@@ -74,4 +74,8 @@ class core_role_allow_view_page extends core_role_allow_role_page {
     public function get_intro_text() {
         return get_string('configallowview', 'core_admin');
     }
+
+    protected function get_eventclass() {
+        return \core\event\role_allow_view_updated::class;
+    }
 }
index d5f6c18..265119f 100644 (file)
@@ -434,7 +434,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     }
 
     public function save_changes() {
-        global $DB;
+        global $DB, $USER;
 
         if (!$this->roleid) {
             // Creating role.
@@ -444,6 +444,20 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             // Updating role.
             $DB->update_record('role', $this->role);
 
+            // Trigger role updated event.
+            \core\event\role_updated::create([
+                'userid' => $USER->id,
+                'objectid' => $this->role->id,
+                'context' => $this->context,
+                'other' => [
+                    'name' => $this->role->name,
+                    'shortname' => $this->role->shortname,
+                    'description' => $this->role->description,
+                    'archetype' => $this->role->archetype,
+                    'contextlevels' => $this->contextlevels
+                ]
+            ])->trigger();
+
             // This will ensure the course contacts cache is purged so name changes get updated in
             // the UI. It would be better to do this only when we know that fields affected are
             // updated. But thats getting into the weeds of the coursecat cache and role edits
@@ -473,10 +487,17 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         $addfunction = "core_role_set_{$type}_allowed";
         $deltable = 'role_allow_'.$type;
         $field = 'allow'.$type;
+        $eventclass = "\\core\\event\\role_allow_" . $type . "_updated";
+        $context = context_system::instance();
 
         foreach ($current as $roleid) {
             if (!in_array($roleid, $wanted)) {
                 $DB->delete_records($deltable, array('roleid'=>$this->roleid, $field=>$roleid));
+                $eventclass::create([
+                    'context' => $context,
+                    'objectid' => $this->roleid,
+                    'other' => ['targetroleid' => $roleid, 'allow' => false]
+                ])->trigger();
                 continue;
             }
             $key = array_search($roleid, $wanted);
@@ -488,6 +509,14 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
                 $roleid = $this->roleid;
             }
             $addfunction($this->roleid, $roleid);
+
+            if (in_array($roleid, $wanted)) {
+                $eventclass::create([
+                    'context' => $context,
+                    'objectid' => $this->roleid,
+                    'other' => ['targetroleid' => $roleid, 'allow' => true]
+                ])->trigger();
+            }
         }
     }
 
index 1a90cdd..fdf144d 100644 (file)
@@ -200,19 +200,6 @@ if (optional_param('cancel', false, PARAM_BOOL)) {
 if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey() && $definitiontable->is_submission_valid()) {
     $definitiontable->save_changes();
     $tableroleid = $definitiontable->get_role_id();
-    // Trigger event.
-    $event = \core\event\role_capabilities_updated::create(
-        array(
-            'context' => $systemcontext,
-            'objectid' => $tableroleid
-        )
-    );
-    $event->set_legacy_logdata(array(SITEID, 'role', $action, 'admin/roles/define.php?action=view&roleid=' . $tableroleid,
-        $definitiontable->get_role_name(), '', $USER->id));
-    if (!empty($role)) {
-        $event->add_record_snapshot('role', $role);
-    }
-    $event->trigger();
 
     if ($action === 'add') {
         redirect(new moodle_url('/admin/roles/define.php', array('action'=>'view', 'roleid'=>$definitiontable->get_role_id())));
index 878c820..f3d393b 100644 (file)
@@ -134,22 +134,6 @@ $overridestable->read_submitted_permissions();
 if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
     $overridestable->save_changes();
     $rolename = $overridableroles[$roleid];
-    // Trigger event.
-    $event = \core\event\role_capabilities_updated::create(
-        array(
-            'context' => $context,
-            'objectid' => $roleid,
-        )
-    );
-
-    $event->set_legacy_logdata(
-        array(
-            $course->id, 'role', 'override', 'admin/roles/override.php?contextid=' . $context->id . '&roleid=' . $roleid,
-            $rolename, '', $USER->id
-        )
-    );
-    $event->add_record_snapshot('role', $role);
-    $event->trigger();
 
     redirect($returnurl);
 }
index 72472e5..1de9317 100644 (file)
@@ -440,6 +440,9 @@ class purpose extends persistent {
         }
         if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) {
             $data->sensitivedatareasons = implode(',', $data->sensitivedatareasons);
+        } else {
+            // Nothing selected. Set default value of null.
+            $data->sensitivedatareasons = null;
         }
 
         // A single value.
index 2169dae..133df3f 100644 (file)
@@ -36,6 +36,7 @@
 
 module.exports = ({ template, types }) => {
     const fs = require('fs');
+    const path = require('path');
     const glob = require('glob');
     const cwd = process.cwd();
 
@@ -72,7 +73,7 @@ module.exports = ({ template, types }) => {
             var rawContents = fs.readFileSync(file);
             var subplugins = JSON.parse(rawContents);
 
-            for (const [component, path] of Object.entries(subplugins)) {
+            for (const [component, path] of Object.entries(subplugins.plugintypes)) {
                 if (path) {
                     moodlePlugins[path] = component;
                 }
@@ -92,7 +93,7 @@ module.exports = ({ template, types }) => {
      */
     function getModuleNameFromFileName(searchFileName) {
         searchFileName = fs.realpathSync(searchFileName);
-        const relativeFileName = searchFileName.replace(`${cwd}/`, '');
+        const relativeFileName = searchFileName.replace(`${cwd}${path.sep}`, '').replace(/\\/g, '/');
         const [componentPath, file] = relativeFileName.split('/amd/src/');
         const fileName = file.replace('.js', '');
 
@@ -202,4 +203,4 @@ module.exports = ({ template, types }) => {
             }
         }
     };
-};
\ No newline at end of file
+};
index f9f4cf4..8c358b2 100644 (file)
@@ -87,6 +87,9 @@ if ($mform->is_cancelled()) {
     redirect($returnurl);
 
 } else if ($data = $mform->get_data()) {
+    if ($data->duration && $data->timeend == 0) {
+        $data->timeend = $data->timestart + $data->duration;
+    }
     if ($manager->edit_enrolment($ue, $data)) {
         redirect($returnurl);
     }
index d1ee2ec..f90e733 100644 (file)
@@ -22,6 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+use core_enrol\enrol_helper;
+
 defined('MOODLE_INTERNAL') || die();
 
 require_once("$CFG->libdir/formslib.php");
@@ -34,6 +36,9 @@ class enrol_user_enrolment_form extends moodleform {
         $instancename = $this->_customdata['enrolinstancename'];
         $modal = !empty($this->_customdata['modal']);
 
+        $periodmenu = enrol_get_period_list();
+        $duration = enrol_calculate_duration($ue->timestart, $ue->timeend);
+
         $mform->addElement('static', 'enrolmentmethod', get_string('enrolmentmethod', 'enrol'), $instancename);
 
         $options = array(ENROL_USER_ACTIVE    => get_string('participationactive', 'enrol'),
@@ -44,6 +49,11 @@ class enrol_user_enrolment_form extends moodleform {
 
         $mform->addElement('date_time_selector', 'timestart', get_string('enroltimestart', 'enrol'), array('optional' => true));
 
+        $mform->addElement('select', 'duration', get_string('enrolperiod', 'enrol'), $periodmenu);
+        $mform->setDefault('duration', $duration);
+        $mform->disabledIf('duration', 'timestart[enabled]', 'notchecked', 1);
+        $mform->disabledIf('duration', 'timeend[enabled]', 'checked', 1);
+
         $mform->addElement('date_time_selector', 'timeend', get_string('enroltimeend', 'enrol'), array('optional' => true));
 
         $mform->addElement('static', 'timecreated', get_string('enroltimecreated', 'enrol'), userdate($ue->timecreated));
index 3f182fd..5378aae 100644 (file)
@@ -32,6 +32,7 @@ require_once($CFG->dirroot.'/enrol/locallib.php');
 require_once($CFG->dirroot.'/group/lib.php');
 require_once($CFG->dirroot.'/enrol/manual/locallib.php');
 require_once($CFG->dirroot.'/cohort/lib.php');
+require_once($CFG->dirroot . '/enrol/manual/classes/enrol_users_form.php');
 
 $id      = required_param('id', PARAM_INT); // Course id.
 $action  = required_param('action', PARAM_ALPHANUMEXT);
@@ -94,6 +95,7 @@ switch ($action) {
         $duration = optional_param('duration', 0, PARAM_INT);
         $startdate = optional_param('startdate', 0, PARAM_INT);
         $recovergrades = optional_param('recovergrades', 0, PARAM_INT);
+        $timeend = optional_param_array('timeend', [], PARAM_INT);
 
         if (empty($roleid)) {
             $roleid = null;
@@ -122,12 +124,25 @@ switch ($action) {
                 $timestart = $today;
                 break;
         }
-        if ($duration <= 0) {
+        if ($timeend) {
+            $timeend = make_timestamp($timeend['year'], $timeend['month'], $timeend['day'], $timeend['hour'], $timeend['minute']);
+        } else if ($duration <= 0) {
             $timeend = 0;
         } else {
             $timeend = $timestart + $duration;
         }
 
+        $mform = new enrol_manual_enrol_users_form(null, (object)["context" => $context]);
+        $userenroldata = [
+                'startdate' => $timestart,
+                'timeend' => $timeend,
+        ];
+        $mform->set_data($userenroldata);
+        $validationerrors = $mform->validation($userenroldata, null);
+        if (!empty($validationerrors)) {
+            throw new enrol_ajax_exception('invalidenrolduration');
+        }
+
         $instances = $manager->get_enrolment_instances();
         $plugins = $manager->get_enrolment_plugins(true); // Do not allow actions on disabled plugins.
         if (!array_key_exists($enrolid, $instances)) {
index 165a6e9..b7fa663 100644 (file)
@@ -60,14 +60,7 @@ class enrol_manual_enrol_users_form extends moodleform {
         $mform = $this->_form;
         $mform->setDisableShortforms();
         $mform->disable_form_change_checker();
-        // Build the list of options for the enrolment period dropdown.
-        $unlimitedperiod = get_string('unlimited');
-        $periodmenu = array();
-        $periodmenu[''] = $unlimitedperiod;
-        for ($i=1; $i<=365; $i++) {
-            $seconds = $i * 86400;
-            $periodmenu[$seconds] = get_string('numdays', '', $i);
-        }
+        $periodmenu = enrol_get_period_list();
         // Work out the apropriate default settings.
         $defaultperiod = $instance->enrolperiod;
         if ($instance->enrolperiod > 0 && !isset($periodmenu[$instance->enrolperiod])) {
@@ -131,13 +124,15 @@ class enrol_manual_enrol_users_form extends moodleform {
         $mform->addElement('checkbox', 'recovergrades', get_string('recovergrades', 'enrol'));
         $mform->setAdvanced('recovergrades');
         $mform->setDefault('recovergrades', $CFG->recovergradesdefault);
-        $mform->addElement('select', 'duration', get_string('defaultperiod', 'enrol_manual'), $periodmenu);
-        $mform->setDefault('duration', $defaultperiod);
-        $mform->setAdvanced('duration');
         $mform->addElement('select', 'startdate', get_string('startingfrom'), $basemenu);
         $mform->setDefault('startdate', $extendbase);
         $mform->setAdvanced('startdate');
-
+        $mform->addElement('select', 'duration', get_string('enrolperiod', 'enrol'), $periodmenu);
+        $mform->setDefault('duration', $defaultperiod);
+        $mform->setAdvanced('duration');
+        $mform->disabledIf('duration', 'timeend[enabled]', 'checked', 1);
+        $mform->addElement('date_time_selector', 'timeend', get_string('enroltimeend', 'enrol'), ['optional' => true]);
+        $mform->setAdvanced('timeend');
         $mform->addElement('hidden', 'id', $course->id);
         $mform->setType('id', PARAM_INT);
         $mform->addElement('hidden', 'action', 'enrol');
@@ -145,4 +140,22 @@ class enrol_manual_enrol_users_form extends moodleform {
         $mform->addElement('hidden', 'enrolid', $instance->id);
         $mform->setType('enrolid', PARAM_INT);
     }
+
+    /**
+     * Validate the submitted form data.
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array of "element_name"=>"error_description" if there are errors,
+     *         or an empty array if everything is OK (true allowed for backwards compatibility too).
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        if (!empty($data['startdate']) && !empty($data['timeend'])) {
+            if ($data['startdate'] >= $data['timeend']) {
+                $errors['timeend'] = get_string('enroltimeendinvalid', 'enrol');
+            }
+        }
+        return $errors;
+    }
 }
index 9eb8758..0df80cf 100644 (file)
@@ -29,6 +29,7 @@ $enrolid      = required_param('enrolid', PARAM_INT);
 $roleid       = optional_param('roleid', -1, PARAM_INT);
 $extendperiod = optional_param('extendperiod', 0, PARAM_INT);
 $extendbase   = optional_param('extendbase', 0, PARAM_INT);
+$timeend      = optional_param_array('timeend', [], PARAM_INT);
 
 $instance = $DB->get_record('enrol', array('id'=>$enrolid, 'enrol'=>'manual'), '*', MUST_EXIST);
 $course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_EXIST);
@@ -135,7 +136,10 @@ if ($canenrol && optional_param('add', false, PARAM_BOOL) && confirm_sesskey())
                     break;
             }
 
-            if ($extendperiod <= 0) {
+            if ($timeend) {
+                $timeend = make_timestamp($timeend['year'], $timeend['month'], $timeend['day'], $timeend['hour'],
+                        $timeend['minute']);
+            } else if ($extendperiod <= 0) {
                 $timeend = 0;
             } else {
                 $timeend = $timestart + $extendperiod;
index 2afa506..a6a7af7 100644 (file)
@@ -108,11 +108,11 @@ Feature: Teacher can search and enrol users one by one into the course
       | student098  | Student   | 098      | student098@example.com  |
       | student099  | Student   | 099      | student099@example.com  |
     And the following "courses" exist:
-      | fullname    | shortname |
-      | Course 001  | C001      |
+      | fullname   | shortname | format | startdate       |
+      | Course 001 | C001      | weeks  | ##1 month ago## |
     And the following "course enrolments" exist:
-      | user        | course    | role            |
-      | teacher001  | C001      | editingteacher  |
+      | user       | course | role           | timestart       |
+      | teacher001 | C001   | editingteacher | ##1 month ago## |
     And I log in as "teacher001"
     And I am on "Course 001" course homepage
 
@@ -178,3 +178,47 @@ Feature: Teacher can search and enrol users one by one into the course
     When I set the field "Select users" to "student100@example.com"
     And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
     Then I should see "student100@example.com, 1234567892, 1234567893, ABC1, ABC2"
+
+  @javascript
+  Scenario: Enrol user from participants page
+    Given I navigate to course participants
+    # Enrol user to course
+    And I press "Enrol users"
+    And I set the field "Select users" to "example.com"
+    And I expand the "Select users" autocomplete
+    When I click on "Student 099" item in the autocomplete list
+    Then I should see "Student 099" in the list of options for the "Select users" autocomplete
+    And I click on "Show more" "button"
+    # Fill data to input duration
+    And "input[name='timeend[enabled]'][checked=checked]" "css_element" should not exist
+    And the "Enrolment duration" "select" should be enabled
+    And I set the field "duration" to "2"
+    # Fill data to input end time
+    And I set the field "Starting from" to "2"
+    And I set the field "timeend[enabled]" to "1"
+    And I set the field "timeend[day]" to "10"
+    And the "Enrolment duration" "select" should be disabled
+    And I click on "Enrol users" "button" in the "Enrol users" "dialogue"
+    And I am on "Course 001" course homepage
+    And I navigate to course participants
+    And I should see "Student 099" in the "participants" "table"
+    And I click on "Edit enrolment" "icon" in the "Student 099" "table_row"
+    And the field "timeend[day]" matches value "10"
+
+  @javascript
+  Scenario: Update Enrol user
+    Given I am on "Course 001" course homepage
+    And I navigate to course participants
+    When I click on "Edit enrolment" "icon" in the "Teacher 001" "table_row"
+    Then the "Enrolment duration" "select" should be enabled
+    # Fill duration
+    And "input[name='timeend[enabled]'][checked=checked]" "css_element" should not exist
+    And the "Enrolment duration" "select" should be enabled
+    And I set the field "duration" to "2"
+    # Fill end time
+    And I set the field "timeend[enabled]" to "1"
+    And I set the field "timeend[day]" to "28"
+    And the "Enrolment duration" "select" should be disabled
+    And I press "Save changes"
+    And I click on "Edit enrolment" "icon" in the "Teacher 001" "table_row"
+    And the field "timeend[day]" matches value "28"
index 801f176..e44cef6 100644 (file)
@@ -1080,4 +1080,23 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertArrayHasKey($roles['student'], $return[$user2->id]);
         $this->assertArrayNotHasKey($roles['teacher'], $return[$user2->id]);
     }
+
+    /**
+     * Test enrol_calculate_duration function
+     */
+    public function test_enrol_calculate_duration() {
+        // Start time 07/01/2019 @ 12:00am (UTC).
+        $timestart = 1561939200;
+        // End time 07/05/2019 @ 12:00am (UTC).
+        $timeend = 1562284800;
+        $duration = enrol_calculate_duration($timestart, $timeend);
+        $durationinday = $duration / DAYSECS;
+        $this->assertEquals(4, $durationinday);
+
+        // End time 07/10/2019 @ 12:00am (UTC).
+        $timeend = 1562716800;
+        $duration = enrol_calculate_duration($timestart, $timeend);
+        $durationinday = $duration / DAYSECS;
+        $this->assertEquals(9, $durationinday);
+    }
 }
index 14eafe9..8158d35 100644 (file)
@@ -160,3 +160,4 @@ nobackpackcollections,core_badges
 error:nogroups,core_badges
 purgedefinitionsuccess,core_cache
 purgestoresuccess,core_cache
+eventrolecapabilitiesupdated,core_role
\ No newline at end of file
index ea1bb8e..bdb64e4 100644 (file)
@@ -96,6 +96,7 @@ $string['instanceadded'] = 'Method added';
 $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['invalidenrolduration'] = 'Invalid enrolment duration';
 $string['invalidrole'] = 'Invalid role';
 $string['invalidrequest'] = 'Invalid request';
 $string['manageenrols'] = 'Manage enrol plugins';
index a01ff27..ead0c9e 100644 (file)
@@ -225,14 +225,16 @@ $string['errorbadroleshortname'] = 'Incorrect role short name';
 $string['errorexistsrolename'] = 'Role name already exists';
 $string['errorexistsroleshortname'] = 'Role name already exists';
 $string['errorroleshortnametoolong'] = 'The short name must not exceed 100 characters';
+$string['eventcapabilityassigned'] = 'Capability assigned';
+$string['eventcapabilityunassigned'] = 'Capability unassigned';
 $string['eventroleallowassignupdated'] = 'Allow role assignment';
 $string['eventroleallowoverrideupdated'] = 'Allow role override';
 $string['eventroleallowswitchupdated'] = 'Allow role switch';
 $string['eventroleallowviewupdated'] = 'Allow role view';
 $string['eventroleassigned'] = 'Role assigned';
-$string['eventrolecapabilitiesupdated'] = 'Role capabilities updated';
 $string['eventroledeleted'] = 'Role deleted';
 $string['eventroleunassigned'] = 'Role unassigned';
+$string['eventroleupdated'] = 'Role updated';
 $string['existingadmins'] = 'Current site administrators';
 $string['existingusers'] = '{$a} existing users';
 $string['explanation'] = 'Explanation';
@@ -495,3 +497,6 @@ $string['privacy:metadata:role_capabilities:tableexplanation'] = 'The capabiliti
 $string['privacy:metadata:role_capabilities:timemodified'] = 'The date when the capability was created or modified.';
 $string['privacy:metadata:role_cohortroles'] = 'Roles to cohort';
 $string['course:togglecompletion'] = 'Manually mark activities as complete';
+
+// Deprecated since Moodle 3.8.
+$string['eventrolecapabilitiesupdated'] = 'Role capabilities updated';
\ No newline at end of file
index 856c556..2383769 100644 (file)
@@ -1379,6 +1379,18 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
         }
     }
 
+    // Trigger capability_assigned event.
+    \core\event\capability_assigned::create([
+        'userid' => $cap->modifierid,
+        'context' => $context,
+        'objectid' => $roleid,
+        'other' => [
+            'capability' => $capability,
+            'oldpermission' => $existing->permission ?? CAP_INHERIT,
+            'permission' => $permission
+        ]
+    ])->trigger();
+
     // Reset any cache of this role, including MUC.
     accesslib_clear_role_cache($roleid);
 
@@ -1394,7 +1406,7 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
  * @return boolean true or exception
  */
 function unassign_capability($capability, $roleid, $contextid = null) {
-    global $DB;
+    global $DB, $USER;
 
     // Capability must exist.
     if (!$capinfo = get_capability_info($capability)) {
@@ -1413,6 +1425,16 @@ function unassign_capability($capability, $roleid, $contextid = null) {
         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
     }
 
+    // Trigger capability_assigned event.
+    \core\event\capability_unassigned::create([
+        'userid' => $USER->id,
+        'context' => $context ?? context_system::instance(),
+        'objectid' => $roleid,
+        'other' => [
+            'capability' => $capability,
+        ]
+    ])->trigger();
+
     // Reset any cache of this role, including MUC.
     accesslib_clear_role_cache($roleid);
 
diff --git a/lib/classes/event/capability_assigned.php b/lib/classes/event/capability_assigned.php
new file mode 100644 (file)
index 0000000..a5b4d4a
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+// 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/>.
+
+/**
+ * Capability assigned event.
+ *
+ * @package    core
+ * @since      Moodle 3.8
+ * @copyright  2019 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Capability assigned event class.
+ *
+ * @package    core
+ * @since      Moodle 3.8
+ * @copyright  2019 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class capability_assigned extends base {
+    /**
+     * Initialise event parameters.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'role_capabilities';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcapabilityassigned', 'role');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+
+        $strpermissions = [
+            CAP_INHERIT => get_string('notset', 'role'),
+            CAP_ALLOW => get_string('allow', 'role'),
+            CAP_PREVENT => get_string('prevent', 'role'),
+            CAP_PROHIBIT => get_string('prohibit', 'role')
+        ];
+
+        $capability = $this->other['capability'];
+        $oldpermission = $this->other['oldpermission'];
+        $permission = $this->other['permission'];
+
+        if ($oldpermission == CAP_INHERIT && $permission == CAP_ALLOW) {
+            $description = "The user id '$this->userid' assigned the '$capability' capability for " .
+                "role '$this->objectid' with '$strpermissions[$permission]' permission";
+        } else {
+            $description = "The user id '$this->userid' changed the '$capability' capability permission for " .
+            "role '$this->objectid' from '$strpermissions[$oldpermission]' to '$strpermissions[$permission]'";
+        }
+
+        return $description;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->contextlevel == CONTEXT_SYSTEM) {
+            return new \moodle_url('/admin/roles/define.php', ['action' => 'edit', 'roleid' => $this->objectid]);
+        } else {
+            return new \moodle_url('/admin/roles/override.php', ['contextid' => $this->contextid,
+                'roleid' => $this->objectid]);
+        }
+    }
+}
diff --git a/lib/classes/event/capability_unassigned.php b/lib/classes/event/capability_unassigned.php
new file mode 100644 (file)
index 0000000..928795f
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+// 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/>.
+
+/**
+ * Capability unassigned event.
+ *
+ * @package    core
+ * @since      Moodle 3.8
+ * @copyright  2019 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Capability unassigned event class.
+ *
+ * @package    core
+ * @since      Moodle 3.8
+ * @copyright  2019 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class capability_unassigned extends base {
+    /**
+     * Initialise event parameters.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'role_capabilities';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcapabilityunassigned', 'role');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $capability = $this->other['capability'];
+
+        return "The user id id '$this->userid' has unassigned the '$capability' capability for role '$this->objectid'";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->contextlevel == CONTEXT_SYSTEM) {
+            return new \moodle_url('/admin/roles/define.php', ['action' => 'view', 'roleid' => $this->objectid]);
+        } else {
+            return new \moodle_url('/admin/roles/override.php', ['contextid' => $this->contextid,
+                'roleid' => $this->objectid]);
+        }
+    }
+}
index 5cdb7e2..850f894 100644 (file)
@@ -42,6 +42,7 @@ class role_allow_assign_updated extends base {
     protected function init() {
         $this->data['crud'] = 'u';
         $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'role_allow_assign';
     }
 
     /**
@@ -59,7 +60,9 @@ class role_allow_assign_updated extends base {
      * @return string
      */
     public function get_description() {
-        return "The user with id '$this->userid' updated Allow role assignments.";
+        $action = ($this->other['allow']) ? 'allow' : 'stop allowing';
+        return "The user with id '$this->userid' modified the role with id '" . $this->other['targetroleid']
+            . "' to $action users with that role from assigning the role with id '" . $this->objectid . "' to users";
     }
 
     /**
index 84960fb..a405116 100644 (file)
@@ -42,6 +42,7 @@ class role_allow_override_updated extends base {
     protected function init() {
         $this->data['crud'] = 'u';
         $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'role_allow_override';
     }
 
     /**
@@ -59,7 +60,9 @@ class role_allow_override_updated extends base {
      * @return string
      */
     public function get_description() {
-        return "The user with id '$this->userid' updated Allow role overrides.";
+        $action = ($this->other['allow']) ? 'allow' : 'stop allowing';
+        return "The user with id '$this->userid' modified the role with id '" . $this->other['targetroleid']
+            . "' to $action users with that role from overriding the role with id '" . $this->objectid . "' to users";
     }
 
     /**
index 7115efc..70b8c36 100644 (file)
@@ -42,6 +42,7 @@ class role_allow_switch_updated extends base {
     protected function init() {
         $this->data['crud'] = 'u';
         $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'role_allow_switch';
     }
 
     /**
@@ -59,7 +60,9 @@ class role_allow_switch_updated extends base {
      * @return string
      */
     public function get_description() {
-        return "The user with id '$this->userid' updated Allow role switches.";
+        $action = ($this->other['allow']) ? 'allow' : 'stop allowing';
+        return "The user with id '$this->userid' modified the role with id '" . $this->other['targetroleid']
+            . "' to $action users with that role from switching the role with id '" . $this->objectid . "' to users";
     }
 
     /**
index 1d08df9..7603988 100644 (file)
@@ -42,6 +42,7 @@ class role_allow_view_updated extends base {
     protected function init() {
         $this->data['crud'] = 'u';
         $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'role_allow_view';
     }
 
     /**
@@ -59,7 +60,9 @@ class role_allow_view_updated extends base {
      * @return string
      */
     public function get_description() {
-        return "The user with id '$this->userid' updated Allow role views.";
+        $action = ($this->other['allow']) ? 'allow' : 'stop allowing';
+        return "The user with id '$this->userid' modified the role with id '" . $this->other['targetroleid']
+            . "' to $action users with that role from viewing the role with id '" . $this->objectid . "' to users";
     }
 
     /**
index e94e768..28f14d8 100644 (file)
@@ -27,6 +27,9 @@ namespace core\event;
 
 defined('MOODLE_INTERNAL') || die();
 
+debugging('core\\event\\role_capabilities_updated has been deprecated. Please use
+        core\\event\\capability_assigned instead', DEBUG_DEVELOPER);
+
 /**
  * Role updated event class.
  *
@@ -102,4 +105,14 @@ class role_capabilities_updated extends base {
     public static function get_objectid_mapping() {
         return array('db' => 'role', 'restore' => 'role');
     }
+
+
+    /**
+     * This event has been deprecated.
+     *
+     * @return boolean
+     */
+    public static function is_deprecated() {
+        return true;
+    }
 }
diff --git a/lib/classes/event/role_updated.php b/lib/classes/event/role_updated.php
new file mode 100644 (file)
index 0000000..dd6e948
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+// 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/>.
+
+/**
+ * Role updated event.
+ *
+ * @package    core
+ * @copyright  2019 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Role updated event class.
+ *
+ * @package    core
+ * @since      Moodle 3.8
+ * @copyright  2019 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class role_updated extends base {
+    /**
+     * Initialise event parameters.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'role';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventroleupdated', 'role');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated the role with id '$this->objectid'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/roles/define.php', ['action' => 'edit', 'roleid' => $this->objectid]);
+    }
+
+    /**
+     * Returns array of parameters to be passed to legacy add_to_log() function.
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return [SITEID, 'role', 'update', 'admin/roles/manage.php?action=edit&roleid=' . $this->objectid,
+            $this->other['shortname'], ''];
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['shortname'])) {
+            throw new \coding_exception('The \'shortname\' value must be set in other.');
+        }
+    }
+}
index 22cde76..e340326 100644 (file)
@@ -158,7 +158,7 @@ class mustache_template_source_loader {
         // Get the requested template source.
         $templatesource = $this->load($templatecomponent, $templatename, $themename, $includecomments);
         // This is a helper function to save a value in one of the result arrays (either $templates or $strings).
-        $save = function(array $results, array $seenlist, string $component, string $id, $value) {
+        $save = function(array $results, array $seenlist, string $component, string $id, $value) use ($lang) {
             if (!isset($results[$component])) {
                 // If the results list doesn't already contain this component then initialise it.
                 $results[$component] = [];
@@ -173,7 +173,7 @@ class mustache_template_source_loader {
         };
         // This is a helper function for processing a dependency. Does stuff like ignore duplicate processing,
         // common result formatting etc.
-        $handler = function(array $dependency, array $ignorelist, callable $processcallback) {
+        $handler = function(array $dependency, array $ignorelist, callable $processcallback) use ($lang) {
             foreach ($dependency as $component => $ids) {
                 foreach ($ids as $id) {
                     $dependencyid = "$component/$id";
@@ -223,7 +223,8 @@ class mustache_template_source_loader {
                 &$seenstrings,
                 &$templates,
                 &$strings,
-                $save
+                $save,
+                $lang
             ) {
                 // We haven't seen this template yet so load it and it's dependencies.
                 $subdependencies = $this->load_with_dependencies(
@@ -232,7 +233,8 @@ class mustache_template_source_loader {
                     $themename,
                     $includecomments,
                     $seentemplates,
-                    $seenstrings
+                    $seenstrings,
+                    $lang
                 );
 
                 foreach ($subdependencies['templates'] as $component => $ids) {
index 188171a..5a6380f 100644 (file)
@@ -25,6 +25,9 @@ namespace core\task;
 
 defined('MOODLE_INTERNAL') || die();
 
+use core\task\database_logger;
+use core\task\logmanager;
+
 /**
  * A task to cleanup log entries for tasks.
  *
@@ -46,8 +49,9 @@ class task_log_cleanup_task extends scheduled_task {
      * Perform the cleanup task.
      */
     public function execute() {
-        if (\core\task\database_logger::class == \core\task\logmanager::get_logger_classname()) {
-            \core\task\database_logger::cleanup();
+        $logger = logmanager::get_logger_classname();
+        if (is_a($logger, database_logger::class, true)) {
+            $logger::cleanup();
         }
     }
 }
index 69ec99f..aac1cac 100644 (file)
@@ -1701,6 +1701,33 @@ function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfi
     return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
 }
 
+/**
+ * Get the list of options for the enrolment period dropdown
+ *
+ * @return array List of options for the enrolment period dropdown
+ */
+function enrol_get_period_list() {
+    $periodmenu = [];
+    $periodmenu[''] = get_string('unlimited');
+    for ($i = 1; $i <= 365; $i++) {
+        $seconds = $i * DAYSECS;
+        $periodmenu[$seconds] = get_string('numdays', '', $i);
+    }
+    return $periodmenu;
+}
+
+/**
+ * Calculate duration base on start time and end time
+ *
+ * @param int $timestart Time start
+ * @param int $timeend Time end
+ * @return float|int Calculated duration
+ */
+function enrol_calculate_duration($timestart, $timeend) {
+    $duration = floor(($timeend - $timestart) / DAYSECS) * DAYSECS;
+    return $duration;
+}
+
 /**
  * Enrolment plugins abstract class.
  *
index e6834a4..8e6ce40 100644 (file)
@@ -368,7 +368,7 @@ class core_accesslib_testcase extends advanced_testcase {
      * Test adding of capabilities to roles.
      */
     public function test_assign_capability() {
-        global $DB;
+        global $DB, $USER;
 
         $this->resetAfterTest();
 
@@ -408,34 +408,40 @@ class core_accesslib_testcase extends advanced_testcase {
         $permission = $DB->get_record('role_capabilities', array('contextid'=>$frontcontext->id, 'roleid'=>$student->id, 'capability'=>'moodle/backup:backupcourse'));
         $this->assertEmpty($permission);
 
-        // Test event trigger.
-        $rolecapabilityevent = \core\event\role_capabilities_updated::create(array('context' => $syscontext,
-                                                                                  'objectid' => $student->id,
-                                                                                  'other' => array('name' => $student->shortname)
-                                                                                 ));
-        $expectedlegacylog = array(SITEID, 'role', 'view', 'admin/roles/define.php?action=view&roleid=' . $student->id,
-                            $student->shortname, '', $user->id);
-        $rolecapabilityevent->set_legacy_logdata($expectedlegacylog);
-        $rolecapabilityevent->add_record_snapshot('role', $student);
-
+        // Test event triggered.
         $sink = $this->redirectEvents();
-        $rolecapabilityevent->trigger();
+        $capability = 'moodle/backup:backupcourse';
+        assign_capability($capability, CAP_ALLOW, $student->id, $syscontext);
         $events = $sink->get_events();
         $sink->close();
-        $event = array_pop($events);
-
-        $this->assertInstanceOf('\core\event\role_capabilities_updated', $event);
-        $expectedurl = new moodle_url('/admin/roles/define.php', array('action' => 'view', 'roleid' => $student->id));
-        $this->assertEquals($expectedurl, $event->get_url());
-        $this->assertEventLegacyLogData($expectedlegacylog, $event);
-        $this->assertEventContextNotUsed($event);
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\capability_assigned', $event);
+        $this->assertSame('role_capabilities', $event->objecttable);
+        $this->assertEquals($student->id, $event->objectid);
+        $this->assertEquals($syscontext->id, $event->contextid);
+        $other = ['capability' => $capability, 'oldpermission' => CAP_INHERIT, 'permission' => CAP_ALLOW];
+        $this->assertEquals($other, $event->other);
+        $description = "The user id '$USER->id' assigned the '$capability' capability for " .
+            "role '$student->id' with 'Allow' permission";
+        $this->assertEquals($description, $event->get_description());
+
+        // Test if the event has different description when updating the capability permission.
+        $sink = $this->redirectEvents();
+        assign_capability($capability, CAP_PROHIBIT, $student->id, $syscontext, true);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = $events[0];
+        $description = "The user id '$USER->id' changed the '$capability' capability permission for " .
+            "role '$student->id' from 'Allow' to 'Prohibit'";
+        $this->assertEquals($description, $event->get_description());
     }
 
     /**
      * Test removing of capabilities from roles.
      */
     public function test_unassign_capability() {
-        global $DB;
+        global $DB, $USER;
 
         $this->resetAfterTest();
 
@@ -463,6 +469,22 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertTrue($result);
         $this->assertFalse($DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$manager->id, 'capability'=>'moodle/backup:backupcourse')));
         $this->assertFalse($DB->record_exists('role_capabilities', array('contextid'=>$frontcontext->id, 'roleid'=>$manager->id, 'capability'=>'moodle/backup:backupcourse')));
+
+        // Test event triggered.
+        $sink = $this->redirectEvents();
+        $capability = 'moodle/backup:backupcourse';
+        unassign_capability($capability, CAP_ALLOW, $manager->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\capability_unassigned', $event);
+        $this->assertSame('role_capabilities', $event->objecttable);
+        $this->assertEquals($manager->id, $event->objectid);
+        $this->assertEquals($syscontext->id, $event->contextid);
+        $this->assertEquals($capability, $event->other['capability']);
+        $description = "The user id id '$USER->id' has unassigned the '$capability' capability for role '$manager->id'";
+        $this->assertEquals($description, $event->get_description());
     }
 
     /**
@@ -977,7 +999,11 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('role_allow_assign', array('roleid'=>$otherid, 'allowassign'=>$student->id)));
 
         // Test event trigger.
-        $allowroleassignevent = \core\event\role_allow_assign_updated::create(array('context' => context_system::instance()));
+        $allowroleassignevent = \core\event\role_allow_assign_updated::create([
+            'context' => context_system::instance(),
+            'objectid' => $otherid,
+            'other' => ['targetroleid' => $student->id]
+        ]);
         $sink = $this->redirectEvents();
         $allowroleassignevent->trigger();
         $events = $sink->get_events();
@@ -1006,7 +1032,11 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('role_allow_override', array('roleid'=>$otherid, 'allowoverride'=>$student->id)));
 
         // Test event trigger.
-        $allowroleassignevent = \core\event\role_allow_override_updated::create(array('context' => context_system::instance()));
+        $allowroleassignevent = \core\event\role_allow_override_updated::create([
+            'context' => context_system::instance(),
+            'objectid' => $otherid,
+            'other' => ['targetroleid' => $student->id]
+        ]);
         $sink = $this->redirectEvents();
         $allowroleassignevent->trigger();
         $events = $sink->get_events();
@@ -1035,7 +1065,11 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('role_allow_switch', array('roleid'=>$otherid, 'allowswitch'=>$student->id)));
 
         // Test event trigger.
-        $allowroleassignevent = \core\event\role_allow_switch_updated::create(array('context' => context_system::instance()));
+        $allowroleassignevent = \core\event\role_allow_switch_updated::create([
+            'context' => context_system::instance(),
+            'objectid' => $otherid,
+            'other' => ['targetroleid' => $student->id]
+        ]);
         $sink = $this->redirectEvents();
         $allowroleassignevent->trigger();
         $events = $sink->get_events();
@@ -1064,7 +1098,11 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('role_allow_view', array('roleid' => $otherid, 'allowview' => $student->id)));
 
         // Test event trigger.
-        $allowroleassignevent = \core\event\role_allow_view_updated::create(array('context' => context_system::instance()));
+        $allowroleassignevent = \core\event\role_allow_view_updated::create([
+            'context' => context_system::instance(),
+            'objectid' => $otherid,
+            'other' => ['targetroleid' => $student->id]
+        ]);
         $sink = $this->redirectEvents();
         $allowroleassignevent->trigger();
         $events = $sink->get_events();
index cd532a3..9d89a75 100644 (file)
@@ -546,4 +546,32 @@ class behat_forms extends behat_base {
         $csstarget = ".form-autocomplete-downarrow";
         $this->execute('behat_general::i_click_on', [$csstarget, 'css_element']);
     }
+
+    /**
+     * Expand the given autocomplete list
+     *
+     * @Given /^I expand the "(?P<field_string>(?:[^"]|\\")*)" autocomplete$/
+     *
+     * @param string $field Field name
+     */
+    public function i_expand_the_autocomplete($field) {
+        $csstarget = '.form-autocomplete-downarrow';
+        $node = $this->get_node_in_container('css_element', $csstarget, 'form_row', $field);
+        $this->ensure_node_is_visible($node);
+        $node->click();
+    }
+
+    /**
+     * Assert the given option exist in the given autocomplete list
+     *
+     * @Given /^I should see "(?P<option_string>(?:[^"]|\\")*)" in the list of options for the "(?P<field_string>(?:[^"]|\\")*)" autocomplete$$/
+     *
+     * @param string $option Name of option
+     * @param string $field Field name
+     */
+    public function i_should_see_in_the_list_of_option_for_the_autocomplete($option, $field) {
+        $xpathtarget = "//div[contains(@class, 'form-autocomplete-selection') and contains(.//div, '" . $option . "')]";
+        $node = $this->get_node_in_container('xpath_element', $xpathtarget, 'form_row', $field);
+        $this->ensure_node_is_visible($node);
+    }
 }
index 6cf40e5..5e82530 100644 (file)
@@ -860,6 +860,10 @@ class qformat_default {
     public function exportprocess($checkcapabilities = true) {
         global $CFG, $DB;
 
+        // Raise time and memory, as exporting can be quite intensive.
+        core_php_time_limit::raise();
+        raise_memory_limit(MEMORY_EXTRA);
+
         // Get the parents (from database) for this category.
         $parents = [];
         if ($this->category) {
index b2f3ab5..faeb294 100644 (file)
Binary files a/user/amd/build/status_field.min.js and b/user/amd/build/status_field.min.js differ
index 1ad4665..e0fe66a 100644 (file)
Binary files a/user/amd/build/status_field.min.js.map and b/user/amd/build/status_field.min.js.map differ
index 865f33c..438941b 100644 (file)
@@ -294,6 +294,12 @@ define(['core/templates',
                 params.timeend = timeEnd.getTime() / 1000;
             }
 
+            // Enrol duration.
+            var enrolDuration = $(form).find('[name="duration"]');
+            if (enrolDuration.is(':enabled')) {
+                params.timeend = params.timestart + parseInt(enrolDuration.val());
+            }
+
             var request = {
                 methodname: 'core_enrol_edit_user_enrolment',
                 args: params
index 477e614..d38ad9b 100644 (file)
@@ -1293,7 +1293,7 @@ function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx
  * @param int $courseid The course id
  * @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
  * @param int $accesssince The time since last access, 0 means any time
- * @param int $roleid The role id, 0 means all roles
+ * @param int $roleid The role id, 0 means all roles and -1 no roles
  * @param int $enrolid The enrolment id, 0 means all enrolment methods will be returned.
  * @param int $statusid The user enrolment status, -1 means all enrolments regardless of the status will be returned, if allowed.
  * @param string|array $search The search that was performed, empty means perform no search
@@ -1367,8 +1367,14 @@ function user_get_participants_sql($courseid, $groupid = 0, $accesssince = 0, $r
         list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true),
             SQL_PARAMS_NAMED, 'relatedctx');
 
-        $wheres[] = "u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)";
-        $params = array_merge($params, array('roleid' => $roleid), $relatedctxparams);
+        // Get users without any role.
+        if ($roleid == -1) {
+            $wheres[] = "u.id NOT IN (SELECT userid FROM {role_assignments} WHERE contextid $relatedctxsql)";
+            $params = array_merge($params, $relatedctxparams);
+        } else {
+            $wheres[] = "u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)";
+            $params = array_merge($params, array('roleid' => $roleid), $relatedctxparams);
+        }
     }
 
     if (!empty($search)) {
index 1e8461f..a1f22c1 100644 (file)
@@ -135,7 +135,7 @@ class core_user_renderer extends plugin_renderer_base {
         }
 
         $criteria = get_string('role');
-        $roleoptions = [];
+        $roleoptions = $this->format_filter_option(USER_FILTER_ROLE, $criteria, -1, get_string('noroles', 'role'));
         foreach ($roles as $id => $role) {
             $roleoptions += $this->format_filter_option(USER_FILTER_ROLE, $criteria, $id, $role);
         }
index f0097f2..13d815c 100644 (file)
@@ -98,6 +98,23 @@ Feature: Course participants can be filtered
       | Group: Group A                  | Student 1 | Student 2 |           | Student 3    | XX-IGNORE-XX |
       | Group: Group B                  | Student 2 |           |           | Student 1    | Student 3    |
 
+  @javascript
+  Scenario: Filter users who have no role in a course
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to course participants
+    And I click on "Student 1's role assignments" "link"
+    And I click on ".form-autocomplete-selection [aria-selected=true]" "css_element"
+    And I press key "27" in the field "Student 1's role assignments"
+    And I click on "Save changes" "link"
+    When I open the autocomplete suggestions list
+    And I click on "Role: No roles" item in the autocomplete list
+    Then I should see "Student 1" in the "participants" "table"
+    And I should not see "Student 2" in the "participants" "table"
+    And I should not see "Student 3" in the "participants" "table"
+    And I should not see "Student 4" in the "participants" "table"
+    And I should not see "Teacher 1" in the "participants" "table"
+
   @javascript
   Scenario: Multiple filters applied
     Given I log in as "teacher1"