Merge branch 'MDL-70066-master' of git://github.com/lameze/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 5 Nov 2020 02:00:33 +0000 (10:00 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 5 Nov 2020 02:00:33 +0000 (10:00 +0800)
29 files changed:
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/tests/behat/enrolments.feature [new file with mode: 0644]
admin/tool/uploadcourse/tests/fixtures/enrolment_delete.csv [new file with mode: 0644]
admin/tool/uploadcourse/tests/fixtures/enrolment_disable.csv [new file with mode: 0644]
admin/tool/uploadcourse/tests/fixtures/enrolment_enable.csv [new file with mode: 0644]
backup/controller/restore_controller.class.php
backup/moodle2/restore_stepslib.php
badges/renderer.php
contentbank/templates/bankcontent.mustache
contentbank/templates/renamecontent.mustache
dataformat/pdf/classes/writer.php
enrol/tests/enrollib_test.php
lib/enrollib.php
lib/moodlelib.php
mod/data/classes/external.php
mod/data/field/textarea/field.class.php
mod/data/tests/externallib_test.php
question/type/ddimageortext/amd/build/form.min.js
question/type/ddimageortext/amd/build/form.min.js.map
question/type/ddimageortext/amd/src/form.js
question/type/ddmarker/amd/build/form.min.js
question/type/ddmarker/amd/build/form.min.js.map
question/type/ddmarker/amd/src/form.js
question/type/edit_question_form.php
theme/boost/scss/moodle/bootstrap-rtl.scss
theme/boost/scss/moodle/modules.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/action_redir.php

index fd7569e..4c45b1a 100644 (file)
@@ -980,55 +980,48 @@ class tool_uploadcourse_course {
             unset($method['delete']);
             unset($method['disable']);
 
-            if (!empty($instance) && $todelete) {
+            if ($todelete) {
                 // Remove the enrolment method.
-                foreach ($instances as $instance) {
-                    if ($instance->enrol == $enrolmethod) {
-                        $plugin = $enrolmentplugins[$instance->enrol];
-
-                        // Ensure user is able to delete the instance.
-                        if ($plugin->can_delete_instance($instance)) {
-                            $plugin->delete_instance($instance);
-                        } else {
-                            $this->error('errorcannotdeleteenrolment',
-                                new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
-                                    $plugin->get_instance_name($instance)));
-                        }
+                if ($instance) {
+                    $plugin = $enrolmentplugins[$instance->enrol];
 
-                        break;
-                    }
-                }
-            } else if (!empty($instance) && $todisable) {
-                // Disable the enrolment.
-                foreach ($instances as $instance) {
-                    if ($instance->enrol == $enrolmethod) {
-                        $plugin = $enrolmentplugins[$instance->enrol];
-
-                        // Ensure user is able to toggle instance status.
-                        if ($plugin->can_hide_show_instance($instance)) {
-                            $plugin->update_status($instance, ENROL_INSTANCE_DISABLED);
-                        } else {
-                            $this->error('errorcannotdisableenrolment',
-                                new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
-                                    $plugin->get_instance_name($instance)));
-                        }
-
-                        break;
+                    // Ensure user is able to delete the instance.
+                    if ($plugin->can_delete_instance($instance)) {
+                        $plugin->delete_instance($instance);
+                    } else {
+                        $this->error('errorcannotdeleteenrolment',
+                            new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
+                                $plugin->get_instance_name($instance)));
                     }
                 }
             } else {
                 // Create/update enrolment.
                 $plugin = $enrolmentplugins[$enrolmethod];
 
-                // Ensure user is able to create/update instance.
+                $status = ($todisable) ? ENROL_INSTANCE_DISABLED : ENROL_INSTANCE_ENABLED;
+
+                // Create a new instance if necessary.
                 if (empty($instance) && $plugin->can_add_instance($course->id)) {
-                    $instance = new stdClass();
-                    $instance->id = $plugin->add_default_instance($course);
+                    $instanceid = $plugin->add_default_instance($course);
+                    $instance = $DB->get_record('enrol', ['id' => $instanceid]);
                     $instance->roleid = $plugin->get_config('roleid');
-                    $instance->status = ENROL_INSTANCE_ENABLED;
-                } else if (!empty($instance) && $plugin->can_edit_instance($instance)) {
-                    $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
-                } else {
+                    // On creation the user can decide the status.
+                    $plugin->update_status($instance, $status);
+                }
+
+                // Check if the we need to update the instance status.
+                if ($instance && $status != $instance->status) {
+                    if ($plugin->can_hide_show_instance($instance)) {
+                        $plugin->update_status($instance, $status);
+                    } else {
+                        $this->error('errorcannotdisableenrolment',
+                            new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
+                                $plugin->get_instance_name($instance)));
+                        break;
+                    }
+                }
+
+                if (empty($instance) || !$plugin->can_edit_instance($instance)) {
                     $this->error('errorcannotcreateorupdateenrolment',
                         new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
                             $plugin->get_instance_name($instance)));
diff --git a/admin/tool/uploadcourse/tests/behat/enrolments.feature b/admin/tool/uploadcourse/tests/behat/enrolments.feature
new file mode 100644 (file)
index 0000000..c8ee54d
--- /dev/null
@@ -0,0 +1,114 @@
+@tool @tool_uploadcourse @_file_upload
+Feature: An admin can update courses enrolments using a CSV file
+  In order to update courses enrolments using a CSV file
+  As an admin
+  I need to be able to upload a CSV file with enrolment methods for the courses
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And I log in as "admin"
+
+  @javascript
+  Scenario: Creating enrolment method by enable it
+    Given I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I click on "Delete" "link" in the "Guest access" "table_row"
+    And I click on "Continue" "button"
+    And I should not see "Guest access" in the "generaltable" "table"
+    And I navigate to "Courses > Upload courses" in site administration
+    And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_enable.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Only update existing courses"
+    And I set the field "Update mode" to "Update with CSV data only"
+    And I set the field "Allow deletes" to "Yes"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And "Disable" "icon" should exist in the "Guest access" "table_row"
+
+  @javascript
+  Scenario: Creating enrolment method by disabling it
+    Given I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I click on "Delete" "link" in the "Guest access" "table_row"
+    And I click on "Continue" "button"
+    And I should not see "Guest access" in the "generaltable" "table"
+    And I navigate to "Courses > Upload courses" in site administration
+    And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_disable.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Only update existing courses"
+    And I set the field "Update mode" to "Update with CSV data only"
+    And I set the field "Allow deletes" to "Yes"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And "Enable" "icon" should exist in the "Guest access" "table_row"
+
+  @javascript
+  Scenario: Enabling enrolment method
+    Given I navigate to "Courses > Upload courses" in site administration
+    And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_enable.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Only update existing courses"
+    And I set the field "Update mode" to "Update with CSV data only"
+    And I set the field "Allow deletes" to "Yes"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And "Disable" "icon" should exist in the "Guest access" "table_row"
+
+  @javascript
+  Scenario: Disable an enrolment method
+    Given I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I click on "Enable" "link" in the "Guest access" "table_row"
+    And "Disable" "icon" should exist in the "Guest access" "table_row"
+    And I navigate to "Courses > Upload courses" in site administration
+    And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_disable.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Only update existing courses"
+    And I set the field "Update mode" to "Update with CSV data only"
+    And I set the field "Allow deletes" to "Yes"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And "Enable" "icon" should exist in the "Guest access" "table_row"
+
+  @javascript
+  Scenario: Delete an enrolment method
+    Given I navigate to "Courses > Upload courses" in site administration
+    And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_delete.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Only update existing courses"
+    And I set the field "Update mode" to "Update with CSV data only"
+    And I set the field "Allow deletes" to "Yes"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I should not see "Guest access" in the "generaltable" "table"
+
+  @javascript
+  Scenario: Delete an unexistent enrolment method (nothing should change)
+    Given I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I click on "Delete" "link" in the "Guest access" "table_row"
+    And I click on "Continue" "button"
+    And I should not see "Guest access" in the "generaltable" "table"
+    And I navigate to "Courses > Upload courses" in site administration
+    And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_delete.csv" file to "File" filemanager
+    And I set the field "Upload mode" to "Only update existing courses"
+    And I set the field "Update mode" to "Update with CSV data only"
+    And I set the field "Allow deletes" to "Yes"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I should not see "Guest access" in the "generaltable" "table"
diff --git a/admin/tool/uploadcourse/tests/fixtures/enrolment_delete.csv b/admin/tool/uploadcourse/tests/fixtures/enrolment_delete.csv
new file mode 100644 (file)
index 0000000..0f219d1
--- /dev/null
@@ -0,0 +1,2 @@
+shortname,category,enrolment_1,enrolment_1_delete
+C1,1,guest,1
\ No newline at end of file
diff --git a/admin/tool/uploadcourse/tests/fixtures/enrolment_disable.csv b/admin/tool/uploadcourse/tests/fixtures/enrolment_disable.csv
new file mode 100644 (file)
index 0000000..0b3ede8
--- /dev/null
@@ -0,0 +1,2 @@
+shortname,category,enrolment_1,enrolment_1_disable
+C1,1,guest,1
\ No newline at end of file
diff --git a/admin/tool/uploadcourse/tests/fixtures/enrolment_enable.csv b/admin/tool/uploadcourse/tests/fixtures/enrolment_enable.csv
new file mode 100644 (file)
index 0000000..2bae81e
--- /dev/null
@@ -0,0 +1,2 @@
+shortname,category,enrolment_1,enrolment_1_disable
+C1,1,guest,0
\ No newline at end of file
index 39c4116..f73cb3f 100644 (file)
@@ -369,6 +369,7 @@ class restore_controller extends base_controller {
             $options = array();
             $options['keep_roles_and_enrolments'] = $this->get_setting_value('keep_roles_and_enrolments');
             $options['keep_groups_and_groupings'] = $this->get_setting_value('keep_groups_and_groupings');
+            $options['userid'] = $this->userid;
             restore_dbops::delete_course_content($this->get_courseid(), $options);
         }
         // If this is not a course restore or single activity restore (e.g. duplicate), inform the plan we are not
index 69fcf19..8108f6e 100644 (file)
@@ -2121,14 +2121,29 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
         $data = (object)$data;
 
         // Check roleid is one of the mapped ones
-        $newroleid = $this->get_mappingid('role', $data->roleid);
+        $newrole = $this->get_mapping('role', $data->roleid);
+        $newroleid = $newrole->newitemid ?? false;
+        $userid = $this->task->get_userid();
+
         // If newroleid and context are valid assign it via API (it handles dupes and so on)
         if ($newroleid && $this->task->get_contextid()) {
-            if (!get_capability_info($data->capability)) {
+            if (!$capability = get_capability_info($data->capability)) {
                 $this->log("Capability '{$data->capability}' was not found!", backup::LOG_WARNING);
             } else {
-                // TODO: assign_capability() needs one userid param to be able to specify our restore userid.
-                assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
+                $context = context::instance_by_id($this->task->get_contextid());
+                $overrideableroles = get_overridable_roles($context, ROLENAME_SHORT);
+                $safecapability = is_safe_capability($capability);
+
+                // Check if the new role is an overrideable role AND if the user performing the restore has the
+                // capability to assign the capability.
+                if (in_array($newrole->info['shortname'], $overrideableroles) &&
+                    ($safecapability && has_capability('moodle/role:safeoverride', $context, $userid) ||
+                        !$safecapability && has_capability('moodle/role:override', $context, $userid))
+                ) {
+                    assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
+                } else {
+                    $this->log("Insufficient capability to assign capability '{$data->capability}' to role!", backup::LOG_WARNING);
+                }
             }
         }
     }
@@ -2148,11 +2163,22 @@ class restore_default_enrolments_step extends restore_execution_step {
         }
 
         $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
+        // Return any existing course enrolment instances.
+        $enrolinstances = enrol_get_instances($course->id, false);
+
+        if ($enrolinstances) {
+            // Something already added instances.
+            // Get the existing enrolment methods in the course.
+            $enrolmethods = array_map(function($enrolinstance) {
+                return $enrolinstance->enrol;
+            }, $enrolinstances);
 
-        if ($DB->record_exists('enrol', array('courseid'=>$this->get_courseid(), 'enrol'=>'manual'))) {
-            // Something already added instances, do not add default instances.
             $plugins = enrol_get_plugins(true);
-            foreach ($plugins as $plugin) {
+            foreach ($plugins as $pluginname => $plugin) {
+                // Make sure all default enrolment methods exist in the course.
+                if (!in_array($pluginname, $enrolmethods)) {
+                    $plugin->course_updated(true, $course, null);
+                }
                 $plugin->restore_sync_course($course);
             }
 
index b561bfe..13415fd 100644 (file)
@@ -805,7 +805,7 @@ class core_badges_renderer extends plugin_renderer_base {
                 );
 
         if (has_capability('moodle/badges:configuredetails', $context)) {
-            $row[] = new tabobject('details',
+            $row[] = new tabobject('badge',
                         new moodle_url('/badges/edit.php', array('id' => $badgeid, 'action' => 'badge')),
                         get_string('bdetails', 'badges')
                     );
@@ -856,7 +856,7 @@ class core_badges_renderer extends plugin_renderer_base {
         if (has_capability('moodle/badges:configuredetails', $context)) {
             $alignments = $DB->count_records_sql("SELECT COUNT(bc.id)
                       FROM {badge_alignment} bc WHERE bc.badgeid = :badgeid", array('badgeid' => $badgeid));
-            $row[] = new tabobject('balignment',
+            $row[] = new tabobject('alignment',
                 new moodle_url('/badges/alignment.php', array('id' => $badgeid)),
                 get_string('balignment', 'badges', $alignments)
             );
index f5c800e..e0b2095 100644 (file)
@@ -146,17 +146,17 @@ data-region="contentbank">
                     </div>
                 {{#contents}}
                     <div class="cb-listitem"
-                        data-file="{{{ title }}}"
-                        data-name="{{{ name }}}"
+                        data-file="{{ title }}"
+                        data-name="{{ name }}"
                         data-bytes="{{ bytes }}"
                         data-timemodified="{{ timemodified }}"
-                        data-type="{{{ type }}}"
-                        data-author="{{{ author }}}">
+                        data-type="{{ type }}"
+                        data-author="{{ author }}">
                         <div class="cb-file cb-column position-relative">
-                            <div class="cb-thumbnail" role="img" aria-label="{{{ name }}}"
+                            <div class="cb-thumbnail" role="img" aria-label="{{ name }}"
                             style="background-image: url('{{{ icon }}}');">
                             </div>
-                            <a href="{{{ link }}}" class="cb-link stretched-link" title="{{{ name }}}">
+                            <a href="{{{ link }}}" class="cb-link stretched-link" title="{{ name }}">
                                 <span class="cb-name word-break-all clamp-2" data-region="cb-content-name">
                                     {{{ name }}}
                                 </span>
index ea638d5..6a72db9 100644 (file)
@@ -26,5 +26,5 @@
 }}
     <div class="form-check w-100 justify-content-start">
         <label for="newname">{{#str}}contentname, core_contentbank{{/str}}</label>
-        <input type="text" size="5" id="newname" name="newname"  value="{{{ name }}}" class="form-control text-ltr">
+        <input type="text" size="5" id="newname" name="newname"  value="{{ name }}" class="form-control text-ltr">
     </div>
index 2203af5..3f916b1 100644 (file)
@@ -135,14 +135,19 @@ class writer extends \core\dataformat\base {
             // height. Solution similar to that at https://stackoverflow.com/a/1943096.
             $pdf2 = clone $this->pdf;
             $pdf2->startTransaction();
+            $numpages = $pdf2->getNumPages();
             $pdf2->AddPage('L');
             $pdf2->writeHTMLCell($this->colwidth, 0, '', '', $cell, 1, 1, false, true, 'L');
-            $rowheight = max($rowheight, $pdf2->getY() - $pdf2->getMargins()['top']);
+            $pagesadded = $pdf2->getNumPages() - $numpages;
+            $pageheight = $pdf2->getPageHeight() - $pdf2->getMargins()['top'] - $pdf2->getMargins()['bottom'];
+            $cellheight = ($pagesadded - 1) * $pageheight + $pdf2->getLastH();
+            $rowheight = max($rowheight, $cellheight);
             $pdf2->rollbackTransaction();
         }
 
         $margins = $this->pdf->getMargins();
-        if ($this->pdf->GetY() + $rowheight + $margins['bottom'] > $this->pdf->getPageHeight()) {
+        if ($this->pdf->getNumPages() > 1 &&
+                ($this->pdf->GetY() + $rowheight + $margins['bottom'] > $this->pdf->getPageHeight())) {
             $this->pdf->AddPage('L');
             $this->print_heading();
         }
index 0965efa..acd3e60 100644 (file)
@@ -228,6 +228,219 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
     }
 
+    /**
+     * Test enrol_course_delete() without passing a user id. When a value for user id is not present, the method
+     * should delete all enrolment related data in the course.
+     */
+    public function test_enrol_course_delete_without_userid() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create users.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+
+        $manual = enrol_get_plugin('manual');
+        $manualinstance = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => 'manual'], '*', MUST_EXIST);
+        // Enrol user1 as a student in the course using manual enrolment.
+        $manual->enrol_user($manualinstance, $user1->id, $studentrole->id);
+
+        $self = enrol_get_plugin('self');
+        $selfinstance = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => 'self'], '*', MUST_EXIST);
+        $self->update_status($selfinstance, ENROL_INSTANCE_ENABLED);
+        // Enrol user2 as a student in the course using self enrolment.
+        $self->enrol_user($selfinstance, $user2->id, $studentrole->id);
+
+        // Delete all enrolment related records in the course.
+        enrol_course_delete($course);
+
+        // The course enrolment of user1 should not exists.
+        $user1enrolment = $DB->get_record('user_enrolments',
+            ['enrolid' => $manualinstance->id, 'userid' => $user1->id]);
+        $this->assertFalse($user1enrolment);
+
+        // The role assignment of user1 should not exists.
+        $user1roleassignment = $DB->get_record('role_assignments',
+            ['roleid' => $studentrole->id, 'userid'=> $user1->id, 'contextid' => $coursecontext->id]
+        );
+        $this->assertFalse($user1roleassignment);
+
+        // The course enrolment of user2 should not exists.
+        $user2enrolment = $DB->get_record('user_enrolments',
+            ['enrolid' => $selfinstance->id, 'userid' => $user2->id]);
+        $this->assertFalse($user2enrolment);
+
+        // The role assignment of user2 should not exists.
+        $user2roleassignment = $DB->get_record('role_assignments',
+            ['roleid' => $studentrole->id, 'userid'=> $user2->id, 'contextid' => $coursecontext->id]);
+        $this->assertFalse($user2roleassignment);
+
+        // All existing course enrolment instances should not exists.
+        $enrolmentinstances = enrol_get_instances($course->id, false);
+        $this->assertCount(0, $enrolmentinstances);
+    }
+
+    /**
+     * Test enrol_course_delete() when user id is present.
+     * When a value for user id is present, the method should make sure the user has the proper capability to
+     * un-enrol users before removing the enrolment data. If the capabilities are missing the data should not be removed.
+     *
+     * @dataProvider enrol_course_delete_with_userid_provider
+     * @param array $excludedcapabilities The capabilities that should be excluded from the user's role
+     * @param bool $expected The expected results
+     */
+    public function test_enrol_course_delete_with_userid($excludedcapabilities, $expected) {
+        global $DB;
+
+        $this->resetAfterTest();
+        // Create users.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+        $editingteacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
+
+        $manual = enrol_get_plugin('manual');
+        $manualinstance = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => 'manual'],
+            '*', MUST_EXIST);
+        // Enrol user1 as a student in the course using manual enrolment.
+        $manual->enrol_user($manualinstance, $user1->id, $studentrole->id);
+        // Enrol user3 as an editing teacher in the course using manual enrolment.
+        // By default, the editing teacher role has the capability to un-enroll users which have been enrolled using
+        // the existing enrolment methods.
+        $manual->enrol_user($manualinstance, $user3->id, $editingteacherrole->id);
+
+        $self = enrol_get_plugin('self');
+        $selfinstance = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => 'self'],
+            '*', MUST_EXIST);
+        $self->update_status($selfinstance, ENROL_INSTANCE_ENABLED);
+        // Enrol user2 as a student in the course using self enrolment.
+        $self->enrol_user($selfinstance, $user2->id, $studentrole->id);
+
+        foreach($excludedcapabilities as $capability) {
+            // Un-assign the given capability from the editing teacher role.
+            unassign_capability($capability, $editingteacherrole->id);
+        }
+
+        // Delete only enrolment related records in the course where user3 has the required capability.
+        enrol_course_delete($course, $user3->id);
+
+        // Check the existence of the course enrolment of user1.
+        $user1enrolmentexists = (bool) $DB->count_records('user_enrolments',
+            ['enrolid' => $manualinstance->id, 'userid' => $user1->id]);
+        $this->assertEquals($expected['User 1 course enrolment exists'], $user1enrolmentexists);
+
+        // Check the existence of the role assignment of user1 in the course.
+        $user1roleassignmentexists = (bool) $DB->count_records('role_assignments',
+            ['roleid' => $studentrole->id, 'userid' => $user1->id, 'contextid' => $coursecontext->id]);
+        $this->assertEquals($expected['User 1 role assignment exists'], $user1roleassignmentexists);
+
+        // Check the existence of the course enrolment of user2.
+        $user2enrolmentexists = (bool) $DB->count_records('user_enrolments',
+            ['enrolid' => $selfinstance->id, 'userid' => $user2->id]);
+        $this->assertEquals($expected['User 2 course enrolment exists'], $user2enrolmentexists);
+
+        // Check the existence of the role assignment of user2 in the course.
+        $user2roleassignmentexists = (bool) $DB->count_records('role_assignments',
+            ['roleid' => $studentrole->id, 'userid' => $user2->id, 'contextid' => $coursecontext->id]);
+        $this->assertEquals($expected['User 2 role assignment exists'], $user2roleassignmentexists);
+
+        // Check the existence of the course enrolment of user3.
+        $user3enrolmentexists = (bool) $DB->count_records('user_enrolments',
+            ['enrolid' => $manualinstance->id, 'userid' => $user3->id]);
+        $this->assertEquals($expected['User 3 course enrolment exists'], $user3enrolmentexists);
+
+        // Check the existence of the role assignment of user3 in the course.
+        $user3roleassignmentexists = (bool) $DB->count_records('role_assignments',
+            ['roleid' => $editingteacherrole->id, 'userid' => $user3->id, 'contextid' => $coursecontext->id]);
+        $this->assertEquals($expected['User 3 role assignment exists'], $user3roleassignmentexists);
+
+        // Check the existence of the manual enrolment instance in the course.
+        $manualinstance = (bool) $DB->count_records('enrol', ['enrol' => 'manual', 'courseid' => $course->id]);
+        $this->assertEquals($expected['Manual course enrolment instance exists'], $manualinstance);
+
+        // Check existence of the self enrolment instance in the course.
+        $selfinstance = (bool) $DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $course->id]);
+        $this->assertEquals($expected['Self course enrolment instance exists'], $selfinstance);
+    }
+
+    /**
+     * Data provider for test_enrol_course_delete_with_userid().
+     *
+     * @return array
+     */
+    public function enrol_course_delete_with_userid_provider() {
+        return [
+            'The teacher can un-enrol users in a course' =>
+                [
+                    'excludedcapabilities' => [],
+                    'results' => [
+                        // Whether certain enrolment related data still exists in the course after the deletion.
+                        // When the user has the capabilities to un-enrol users and the enrolment plugins allow manual
+                        // unenerolment than all course enrolment data should be removed.
+                        'Manual course enrolment instance exists' => false,
+                        'Self course enrolment instance exists' => false,
+                        'User 1 course enrolment exists' => false,
+                        'User 1 role assignment exists' => false,
+                        'User 2 course enrolment exists' => false,
+                        'User 2 role assignment exists' => false,
+                        'User 3 course enrolment exists' => false,
+                        'User 3 role assignment exists' => false
+                    ],
+                ],
+            'The teacher cannot un-enrol self enrolled users'  =>
+                [
+                    'excludedcapabilities' => [
+                        // Exclude the following capabilities for the editing teacher.
+                        'enrol/self:unenrol'
+                    ],
+                    'results' => [
+                        // When the user does not have the capabilities to un-enrol self enrolled users, the data
+                        // related to this enrolment method should not be removed. Everything else should be removed.
+                        'Manual course enrolment instance exists' => false,
+                        'Self course enrolment instance exists' => true,
+                        'User 1 course enrolment exists' => false,
+                        'User 1 role assignment exists' => false,
+                        'User 2 course enrolment exists' => true,
+                        'User 2 role assignment exists' => true,
+                        'User 3 course enrolment exists' => false,
+                        'User 3 role assignment exists' => false
+                    ],
+                ],
+            'The teacher cannot un-enrol self and manually enrolled users' =>
+                [
+                    'excludedcapabilities' => [
+                        // Exclude the following capabilities for the editing teacher.
+                        'enrol/manual:unenrol',
+                        'enrol/self:unenrol'
+                    ],
+                    'results' => [
+                        // When the user does not have the capabilities to un-enrol self and manually enrolled users,
+                        // the data related to these enrolment methods should not be removed.
+                        'Manual course enrolment instance exists' => true,
+                        'Self course enrolment instance exists' => true,
+                        'User 1 course enrolment exists' => true,
+                        'User 1 role assignment exists' => true,
+                        'User 2 course enrolment exists' => true,
+                        'User 2 role assignment exists' => true,
+                        'User 3 course enrolment exists' => true,
+                        'User 3 role assignment exists' => true
+                    ],
+                ],
+        ];
+    }
+
+
     public function test_enrol_user_sees_own_courses() {
         global $DB, $CFG;
 
index 193f0c5..7b40ca0 100644 (file)
@@ -1089,20 +1089,35 @@ function enrol_user_delete($user) {
 
 /**
  * Called when course is about to be deleted.
+ * If a user id is passed, only enrolments that the user has permission to un-enrol will be removed,
+ * otherwise all enrolments in the course will be removed.
+ *
  * @param stdClass $course
+ * @param int|null $userid
  * @return void
  */
-function enrol_course_delete($course) {
+function enrol_course_delete($course, $userid = null) {
     global $DB;
 
+    $context = context_course::instance($course->id);
     $instances = enrol_get_instances($course->id, false);
     $plugins = enrol_get_plugins(true);
+
+    if ($userid) {
+        // If the user id is present, include only course enrolment instances which allow manual unenrolment and
+        // the given user have a capability to perform unenrolment.
+        $instances = array_filter($instances, function($instance) use ($userid, $plugins, $context) {
+            $unenrolcap = "enrol/{$instance->enrol}:unenrol";
+            return $plugins[$instance->enrol]->allow_unenrol($instance) &&
+                has_capability($unenrolcap, $context, $userid);
+        });
+    }
+
     foreach ($instances as $instance) {
         if (isset($plugins[$instance->enrol])) {
             $plugins[$instance->enrol]->delete_instance($instance);
         }
         // low level delete in case plugin did not do it
-        $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
         $DB->delete_records('enrol', array('id'=>$instance->id));
index 590777e..5e09703 100644 (file)
@@ -5371,11 +5371,14 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
     }
     unset($childcontexts);
 
-    // Remove all roles and enrolments by default.
+    // Remove roles and enrolments by default.
     if (empty($options['keep_roles_and_enrolments'])) {
         // This hack is used in restore when deleting contents of existing course.
+        // During restore, we should remove only enrolment related data that the user performing the restore has a
+        // permission to remove.
+        $userid = $options['userid'] ?? null;
+        enrol_course_delete($course, $userid);
         role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
-        enrol_course_delete($course);
         if ($showfeedback) {
             echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
         }
index 4811a6c..0b4f1c2 100644 (file)
@@ -266,6 +266,7 @@ class mod_data_external extends external_api {
             'warnings' => $warnings
         );
 
+        $groupmode = groups_get_activity_groupmode($cm);
         if (!empty($params['groupid'])) {
             $groupid = $params['groupid'];
             // Determine is the group is visible to user.
@@ -274,7 +275,6 @@ class mod_data_external extends external_api {
             }
         } else {
             // Check to see if groups are being used here.
-            $groupmode = groups_get_activity_groupmode($cm);
             if ($groupmode) {
                 $groupid = groups_get_activity_group($cm);
             } else {
@@ -981,10 +981,10 @@ class mod_data_external extends external_api {
         // Check database is open in time.
         data_require_time_available($database, null, $context);
 
+        $groupmode = groups_get_activity_groupmode($cm);
         // Determine default group.
         if (empty($params['groupid'])) {
             // Check to see if groups are being used here.
-            $groupmode = groups_get_activity_groupmode($cm);
             if ($groupmode) {
                 $groupid = groups_get_activity_group($cm);
             } else {
index 69fd9db..6dd703f 100644 (file)
@@ -153,7 +153,8 @@ class data_field_textarea extends data_field_base {
         $editor->use_editor($field, $options, $fpoptions);
         $str .= '<input type="hidden" name="'.$field.'_itemid" value="'.s($draftitemid).'" />';
         $str .= '<div class="mod-data-input">';
-        $str .= '<div><textarea id="'.$field.'" name="'.$field.'" rows="'.$this->field->param3.'" cols="'.$this->field->param2.'" spellcheck="true">'.s($text).'</textarea></div>';
+        $str .= '<div><textarea id="'.$field.'" name="'.$field.'" rows="'.$this->field->param3.'" class="form-control" ' .
+            'cols="'.$this->field->param2.'" spellcheck="true">'.s($text).'</textarea></div>';
         $str .= '<div><label class="accesshide" for="' . $field . '_content1">' . get_string('format') . '</label>';
         $str .= '<select id="' . $field . '_content1" name="'.$field.'_content1">';
         foreach ($formats as $key=>$desc) {
index 815744c..b796df0 100644 (file)
@@ -379,6 +379,45 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(0, $result['entrieslefttoview']);
     }
 
+    /**
+     * Test get_data_access_information with groups.
+     */
+    public function test_get_data_access_information_groups() {
+        global $DB;
+
+        $DB->set_field('course', 'groupmode', VISIBLEGROUPS, ['id' => $this->course->id]);
+
+        // Check I can see my group.
+        $this->setUser($this->student1);
+
+        $result = mod_data_external::get_data_access_information($this->database->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_data_access_information_returns(), $result);
+
+        $this->assertEquals($this->group1->id, $result['groupid']); // My group is correctly found.
+        $this->assertFalse($result['canmanageentries']);
+        $this->assertFalse($result['canapprove']);
+        $this->assertTrue($result['canaddentry']);  // I can entries in my groups.
+        $this->assertTrue($result['timeavailable']);
+        $this->assertFalse($result['inreadonlyperiod']);
+        $this->assertEquals(0, $result['numentries']);
+        $this->assertEquals(0, $result['entrieslefttoadd']);
+        $this->assertEquals(0, $result['entrieslefttoview']);
+
+        // Check the other course group in visible groups mode.
+        $result = mod_data_external::get_data_access_information($this->database->id, $this->group2->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_data_access_information_returns(), $result);
+
+        $this->assertEquals($this->group2->id, $result['groupid']); // The group is correctly found.
+        $this->assertFalse($result['canmanageentries']);
+        $this->assertFalse($result['canapprove']);
+        $this->assertFalse($result['canaddentry']);  // I cannot add entries in other groups.
+        $this->assertTrue($result['timeavailable']);
+        $this->assertFalse($result['inreadonlyperiod']);
+        $this->assertEquals(0, $result['numentries']);
+        $this->assertEquals(0, $result['entrieslefttoadd']);
+        $this->assertEquals(0, $result['entrieslefttoview']);
+    }
+
     /**
      * Helper method to populate the database with some entries.
      *
@@ -1095,6 +1134,16 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         mod_data_external::add_entry($this->database->id, 0, []);
     }
 
+    /**
+     * Test add_entry invalid group.
+     */
+    public function test_add_entry_invalid_group() {
+        $this->setUser($this->student1);
+        $this->expectExceptionMessage(get_string('noaccess', 'data'));
+        $this->expectException('moodle_exception');
+        mod_data_external::add_entry($this->database->id, $this->group2->id, []);
+    }
+
     /**
      * Test update_entry.
      */
index 1753414..f963ee6 100644 (file)
Binary files a/question/type/ddimageortext/amd/build/form.min.js and b/question/type/ddimageortext/amd/build/form.min.js differ
index 1e6304f..bced8d5 100644 (file)
Binary files a/question/type/ddimageortext/amd/build/form.min.js.map and b/question/type/ddimageortext/amd/build/form.min.js.map differ
index 6147e6b..1e0c80f 100644 (file)
@@ -82,7 +82,7 @@ define(['jquery', 'core/dragdrop'], function($, dragDrop) {
             // From now on, when a new file gets loaded into the filepicker, update the preview.
             // This is not in the setupEventHandlers section as it needs to be delayed until
             // after filepicker's javascript has finished.
-            $('form.mform').on('change', '.filepickerhidden', function() {
+            $('form.mform[data-qtype="ddimageortext"]').on('change', '.filepickerhidden', function() {
                 M.util.js_pending('dragDropToImageForm');
                 dragDropToImageForm.loadPreviewImage();
             });
@@ -428,7 +428,7 @@ define(['jquery', 'core/dragdrop'], function($, dragDrop) {
             },
 
             getEl: function(name, indexes) {
-                var form = $('form.mform')[0];
+                var form = $('form.mform[data-qtype="ddimageortext"]')[0];
                 return form.elements[this.toNameWithIndex(name, indexes)];
             },
 
@@ -479,7 +479,7 @@ define(['jquery', 'core/dragdrop'], function($, dragDrop) {
             if (draftItemIdsToName === undefined) {
                 draftItemIdsToName = {};
                 nameToParentNode = {};
-                var fp = $('form.mform input.filepickerhidden');
+                var fp = $('form.mform[data-qtype="ddimageortext"] input.filepickerhidden');
                 fp.each(function(index, filepicker) {
                     draftItemIdsToName[filepicker.value] = filepicker.name;
                     nameToParentNode[filepicker.name] = filepicker.parentNode;
index edf1402..ef09d2c 100644 (file)
Binary files a/question/type/ddmarker/amd/build/form.min.js and b/question/type/ddmarker/amd/build/form.min.js differ
index a8ed51d..8a99a09 100644 (file)
Binary files a/question/type/ddmarker/amd/build/form.min.js.map and b/question/type/ddmarker/amd/build/form.min.js.map differ
index d8f61b2..a06e0b8 100644 (file)
@@ -554,7 +554,7 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes'], function($, dragDro
             // From now on, when a new file gets loaded into the filepicker, update the preview.
             // This is not in the setupEventHandlers section as it needs to be delayed until
             // after filepicker's javascript has finished.
-            $('form.mform').on('change', '#id_bgimage', dragDropForm.loadPreviewImage);
+            $('form.mform[data-qtype="ddmarker"]').on('change', '#id_bgimage', dragDropForm.loadPreviewImage);
 
             dragDropForm.loadPreviewImage();
         },
@@ -645,7 +645,7 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes'], function($, dragDro
             },
 
             getEl: function(name, indexes) {
-                var form = $('form.mform')[0];
+                var form = $('form.mform[data-qtype="ddmarker"]')[0];
                 return form.elements[this.toNameWithIndex(name, indexes)];
             },
 
index 735615a..3bf44cb 100644 (file)
@@ -110,7 +110,7 @@ abstract class question_edit_form extends question_wizard_form {
         $this->category = $category;
         $this->categorycontext = context::instance_by_id($category->contextid);
 
-        parent::__construct($submiturl, null, 'post', '', null, $formeditable);
+        parent::__construct($submiturl, null, 'post', '', ['data-qtype' => $this->qtype()], $formeditable);
     }
 
     /**
index 53f7c3e..6c54f04 100644 (file)
@@ -23,4 +23,8 @@
     left: 0;
     text-align: right;
 }
-*/
\ No newline at end of file
+*/
+
+.dir-rtl .custom-switch .custom-control-input:checked ~ .custom-control-label::after {
+    transform: translateX(-($custom-switch-width - $custom-control-indicator-size));
+}
index 971c187..5218e74 100644 (file)
@@ -1275,7 +1275,7 @@ div#dock {
     position: sticky;
     justify-content: end;
     top: $navbar-height + 5px;
-    z-index: $zindex-modal;
+    z-index: $zindex-sticky;
     #quiz-timer {
         border: $border-width solid $red;
         background-color: $white;
index fcce58b..59f212a 100644 (file)
@@ -9625,6 +9625,9 @@ a.text-dark:hover, a.text-dark:focus {
     text-align: right;
 }
 */
+.dir-rtl .custom-switch .custom-control-input:checked ~ .custom-control-label::after {
+  transform: translateX(-0.9375rem); }
+
 /**
  * Moodle variables
  *
@@ -17569,7 +17572,7 @@ div#dock {
   position: sticky;
   justify-content: end;
   top: 55px;
-  z-index: 1050; }
+  z-index: 1020; }
   #quiz-timer-wrapper #quiz-timer {
     border: 1px solid #ca3120;
     background-color: #fff; }
index a52cae5..cf7cc90 100644 (file)
@@ -9828,6 +9828,9 @@ a.text-dark:hover, a.text-dark:focus {
     text-align: right;
 }
 */
+.dir-rtl .custom-switch .custom-control-input:checked ~ .custom-control-label::after {
+  transform: translateX(-0.75rem); }
+
 /**
  * Moodle variables
  *
@@ -17796,7 +17799,7 @@ div#dock {
   position: sticky;
   justify-content: end;
   top: 55px;
-  z-index: 1050; }
+  z-index: 1020; }
   #quiz-timer-wrapper #quiz-timer {
     border: 1px solid #ca3120;
     background-color: #fff; }
index 72eb305..62bf3c5 100644 (file)
@@ -89,7 +89,6 @@ if ($formaction == 'bulkchange.php') {
                     $columnnames = array(
                         'firstname' => get_string('firstname'),
                         'lastname' => get_string('lastname'),
-                        'email' => get_string('email'),
                     );
 
                     $identityfields = get_extra_user_fields($context);
@@ -104,7 +103,7 @@ if ($formaction == 'bulkchange.php') {
                         list($insql, $inparams) = $DB->get_in_or_equal($userids);
                     }
 
-                    $sql = "SELECT u.firstname, u.lastname, u.email" . $identityfieldsselect . "
+                    $sql = "SELECT u.firstname, u.lastname" . $identityfieldsselect . "
                               FROM {user} u
                              WHERE u.id $insql";