Merge branch 'MDL-69751-master' of git://github.com/mihailges/moodle
authorJake Dallimore <jake@moodle.com>
Wed, 21 Oct 2020 01:59:48 +0000 (09:59 +0800)
committerJake Dallimore <jake@moodle.com>
Wed, 21 Oct 2020 02:05:16 +0000 (10:05 +0800)
38 files changed:
admin/tool/usertours/classes/manager.php
backup/cc/cc2moodle.php
backup/cc/entities.class.php
blocks/site_main_menu/block_site_main_menu.php
blocks/social_activities/block_social_activities.php
calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php
calendar/tests/raw_event_retrieval_strategy_test.php
course/renderer.php
grade/edit/tree/item.php
grade/edit/tree/item_form.php
grade/edit/tree/lib.php
grade/report/grader/module.js
grade/report/grader/tests/behat/ajax_grader.feature
h5p/classes/player.php
lib/adminlib.php
lib/db/upgrade.php
lib/dml/moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/form/filemanager.js
lib/moodlelib.php
lib/upgrade.txt
mod/lti/classes/external.php
mod/lti/db/install.xml
mod/lti/db/upgrade.php
mod/lti/locallib.php
mod/lti/tests/externallib_test.php
mod/lti/upgrade.txt
mod/lti/version.php
repository/tests/behat/behat_filepicker.php
search/tests/behat/setup_search_engine.feature [new file with mode: 0644]
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/icons.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
version.php

index 437b416..9d76b2e 100644 (file)
@@ -637,11 +637,13 @@ class manager {
         $tours = cache::get_matching_tourdata($pageurl);
 
         $matches = [];
-        $filters = helper::get_all_filters();
-        foreach ($tours as $record) {
-            $tour = tour::load_from_record($record);
-            if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context, $filters)) {
-                $matches[] = $tour;
+        if ($tours) {
+            $filters = helper::get_all_filters();
+            foreach ($tours as $record) {
+                $tour = tour::load_from_record($record);
+                if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context, $filters)) {
+                    $matches[] = $tour;
+                }
             }
         }
 
index 88895a9..245065a 100644 (file)
@@ -794,7 +794,7 @@ class cc2moodle {
         }
     }
 
-    protected function critical_error ($text) {
+    protected static function critical_error ($text) {
 
         $path_to_log = static::log_file();
 
index 4a8ef06..712b84f 100644 (file)
@@ -76,7 +76,7 @@ class entities {
         cc2moodle::log_action('Load the XML resource file: '.$path_to_file);
 
         if (!$resource->load($path_to_file)) {
-            cc2moodle::log_action('Cannot load the XML resource file: ' . $path_to_file, true);
+            cc2moodle::log_action('Cannot load the XML resource file: ' . $path_to_file, false);
         }
 
         return $resource;
index c5a692f..a0de2d1 100644 (file)
@@ -88,7 +88,6 @@ class block_site_main_menu extends block_list {
         $section = $modinfo->get_section_info(0);
 
         if ($ismoving) {
-            $strmovehere = get_string('movehere');
             $strmovefull = strip_tags(get_string('movefull', '', "'$USER->activitycopyname'"));
             $strcancel= get_string('cancel');
         } else {
@@ -128,8 +127,9 @@ class block_site_main_menu extends block_list {
                         if ($mod->id == $USER->activitycopy) {
                             continue;
                         }
-                        $this->content->items[] = '<a title="'.$strmovefull.'" href="'.$CFG->wwwroot.'/course/mod.php?moveto='.$mod->id.'&amp;sesskey='.sesskey().'">'.
-                            '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->image_url('movehere') . '" alt="'.$strmovehere.'" /></a>';
+                        $movingurl = new moodle_url('/course/mod.php', array('moveto' => $mod->id, 'sesskey' => sesskey()));
+                        $this->content->items[] = html_writer::link($movingurl, '', array('title' => $strmovefull,
+                            'class' => 'movehere'));
                         $this->content->icons[] = '';
                     }
                     if ($mod->indent > 0) {
@@ -148,8 +148,8 @@ class block_site_main_menu extends block_list {
         }
 
         if ($ismoving) {
-            $this->content->items[] = '<a title="'.$strmovefull.'" href="'.$CFG->wwwroot.'/course/mod.php?movetosection='.$section->id.'&amp;sesskey='.sesskey().'">'.
-                                      '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->image_url('movehere') . '" alt="'.$strmovehere.'" /></a>';
+            $movingurl = new moodle_url('/course/mod.php', array('movetosection' => $section->id, 'sesskey' => sesskey()));
+            $this->content->items[] = html_writer::link($movingurl, '', array('title' => $strmovefull, 'class' => 'movehere'));
             $this->content->icons[] = '';
         }
 
index 2e5ec90..35c651b 100644 (file)
@@ -83,7 +83,6 @@ class block_social_activities extends block_list {
         $section = $modinfo->get_section_info(0);
 
         if ($ismoving) {
-            $strmovehere = get_string('movehere');
             $strmovefull = strip_tags(get_string('movefull', '', "'$USER->activitycopyname'"));
             $strcancel= get_string('cancel');
         } else {
@@ -92,7 +91,8 @@ class block_social_activities extends block_list {
 
         if ($ismoving) {
             $this->content->icons[] = '&nbsp;' . $OUTPUT->pix_icon('t/move', get_string('move'));
-            $this->content->items[] = $USER->activitycopyname.'&nbsp;(<a href="'.$CFG->wwwroot.'/course/mod.php?cancelcopy=true&amp;sesskey='.sesskey().'">'.$strcancel.'</a>)';
+            $cancelurl = new moodle_url('/course/mod.php', array('cancelcopy' => 'true', 'sesskey' => sesskey()));
+            $this->content->items[] = $USER->activitycopyname . '&nbsp;(<a href="' . $cancelurl . '">' . $strcancel . '</a>)';
         }
 
         if (!empty($modinfo->sections[0])) {
@@ -123,8 +123,9 @@ class block_social_activities extends block_list {
                         if ($mod->id == $USER->activitycopy) {
                             continue;
                         }
-                        $this->content->items[] = '<a title="'.$strmovefull.'" href="'.$CFG->wwwroot.'/course/mod.php?moveto='.$mod->id.'&amp;sesskey='.sesskey().'">'.
-                            '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->image_url('movehere') . '" alt="'.$strmovehere.'" /></a>';
+                        $movingurl = new moodle_url('/course/mod.php', array('moveto' => $mod->id, 'sesskey' => sesskey()));
+                        $this->content->items[] = html_writer::link($movingurl, '', array('title' => $strmovefull,
+                            'class' => 'movehere'));
                         $this->content->icons[] = '';
                     }
                     if (!$mod->url) {
@@ -140,8 +141,8 @@ class block_social_activities extends block_list {
         }
 
         if ($ismoving) {
-            $this->content->items[] = '<a title="'.$strmovefull.'" href="'.$CFG->wwwroot.'/course/mod.php?movetosection='.$section->id.'&amp;sesskey='.sesskey().'">'.
-                                      '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->image_url('movehere') . '" alt="'.$strmovehere.'" /></a>';
+            $movingurl = new moodle_url('/course/mod.php', array('movetosection' => $section->id, 'sesskey' => sesskey()));
+            $this->content->items[] = html_writer::link($movingurl, '', array('title' => $strmovefull, 'class' => 'movehere'));
             $this->content->icons[] = '';
         }
 
index f442a64..46b42d7 100644 (file)
@@ -281,7 +281,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
             $unionstartquery = "SELECT modulename, instance, eventtype, priority
                                   FROM {event} ev
                                  WHERE ";
-            $subqueryunion = $unionstartquery . implode(" UNION $unionstartquery ", $subqueryconditions);
+            $subqueryunion = '('.$unionstartquery . implode(" UNION $unionstartquery ", $subqueryconditions).')';
         } else {
             $subqueryunion = '{event}';
         }
@@ -296,7 +296,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
                             ev.instance,
                             ev.eventtype,
                             MIN(ev.priority) as priority
-                       FROM ($subqueryunion) ev
+                       FROM $subqueryunion ev
                    GROUP BY ev.modulename, ev.instance, ev.eventtype";
 
         // Build the main query.
index 82cd7a8..b195eea 100644 (file)
@@ -447,4 +447,15 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
                 array_column($events, 'name'),
                 '', 0.0, 10, true);
     }
+
+    /**
+     * Test retrieval strategy with empty filters.
+     * This covers a edge case not covered elsewhere to ensure its SQL is cross
+     * db compatible. The test is ensuring we don't get a DML Exception with
+     * the filters setup this way.
+     */
+    public function test_get_raw_events_with_empty_user_and_category_lists() {
+        $retrievalstrategy = new raw_event_retrieval_strategy;
+        $retrievalstrategy->get_raw_events([], null, null, []);
+    }
 }
index 89b7e0c..8d25c22 100644 (file)
@@ -1004,7 +1004,6 @@ class core_course_renderer extends plugin_renderer_base {
         // check if we are currently in the process of moving a module with JavaScript disabled
         $ismoving = $this->page->user_is_editing() && ismoving($course->id);
         if ($ismoving) {
-            $movingpix = new pix_icon('movehere', get_string('movehere'), 'moodle', array('class' => 'movetarget'));
             $strmovefull = strip_tags(get_string("movefull", "", "'$USER->activitycopyname'"));
         }
 
@@ -1032,7 +1031,7 @@ class core_course_renderer extends plugin_renderer_base {
                 if ($ismoving) {
                     $movingurl = new moodle_url('/course/mod.php', array('moveto' => $modnumber, 'sesskey' => sesskey()));
                     $sectionoutput .= html_writer::tag('li',
-                            html_writer::link($movingurl, $this->output->render($movingpix), array('title' => $strmovefull)),
+                            html_writer::link($movingurl, '', array('title' => $strmovefull, 'class' => 'movehere')),
                             array('class' => 'movehere'));
                 }
 
@@ -1042,7 +1041,7 @@ class core_course_renderer extends plugin_renderer_base {
             if ($ismoving) {
                 $movingurl = new moodle_url('/course/mod.php', array('movetosection' => $section->id, 'sesskey' => sesskey()));
                 $sectionoutput .= html_writer::tag('li',
-                        html_writer::link($movingurl, $this->output->render($movingpix), array('title' => $strmovefull)),
+                        html_writer::link($movingurl, '', array('title' => $strmovefull, 'class' => 'movehere')),
                         array('class' => 'movehere'));
             }
         }
index 2ece98f..64ddf7f 100644 (file)
@@ -133,8 +133,11 @@ if ($mform->is_cancelled()) {
         $data->grademin = 0;
     }
 
-    $hidden      = empty($data->hidden) ? 0: $data->hidden;
-    $hiddenuntil = empty($data->hiddenuntil) ? 0: $data->hiddenuntil;
+    $hide = empty($data->hiddenuntil) ? 0 : $data->hiddenuntil;
+    if (!$hide) {
+        $hide = empty($data->hidden) ? 0 : $data->hidden;
+    }
+
     unset($data->hidden);
     unset($data->hiddenuntil);
 
@@ -155,45 +158,43 @@ if ($mform->is_cancelled()) {
         $data->aggregationcoef2 = $defaults['aggregationcoef2'];
     }
 
-    $grade_item = new grade_item(array('id'=>$id, 'courseid'=>$courseid));
-    $oldmin = $grade_item->grademin;
-    $oldmax = $grade_item->grademax;
-    grade_item::set_properties($grade_item, $data);
-    $grade_item->outcomeid = null;
+    $gradeitem = new grade_item(array('id' => $id, 'courseid' => $courseid));
+    $oldmin = $gradeitem->grademin;
+    $oldmax = $gradeitem->grademax;
+    grade_item::set_properties($gradeitem, $data);
+    $gradeitem->outcomeid = null;
 
     // Handle null decimals value
     if (!property_exists($data, 'decimals') or $data->decimals < 0) {
-        $grade_item->decimals = null;
+        $gradeitem->decimals = null;
     }
 
-    if (empty($grade_item->id)) {
-        $grade_item->itemtype = 'manual'; // all new items to be manual only
-        $grade_item->insert();
+    if (empty($gradeitem->id)) {
+        $gradeitem->itemtype = 'manual'; // All new items to be manual only.
+        $gradeitem->insert();
 
         // set parent if needed
         if (isset($data->parentcategory)) {
-            $grade_item->set_parent($data->parentcategory, false);
+            $gradeitem->set_parent($data->parentcategory, false);
         }
 
     } else {
-        $grade_item->update();
+        $gradeitem->update();
 
         if (!empty($data->rescalegrades) && $data->rescalegrades == 'yes') {
-            $newmin = $grade_item->grademin;
-            $newmax = $grade_item->grademax;
-            $grade_item->rescale_grades_keep_percentage($oldmin, $oldmax, $newmin, $newmax, 'gradebook');
+            $newmin = $gradeitem->grademin;
+            $newmax = $gradeitem->grademax;
+            $gradeitem->rescale_grades_keep_percentage($oldmin, $oldmax, $newmin, $newmax, 'gradebook');
         }
     }
 
-    // update hiding flag
-    if ($hiddenuntil) {
-        $grade_item->set_hidden($hiddenuntil, false);
-    } else {
-        $grade_item->set_hidden($hidden, false);
+    if ($item->cancontrolvisibility) {
+        // Update hiding flag.
+        $gradeitem->set_hidden($hide, false);
     }
 
-    $grade_item->set_locktime($locktime); // locktime first - it might be removed when unlocking
-    $grade_item->set_locked($locked, false, true);
+    $gradeitem->set_locktime($locktime); // Locktime first - it might be removed when unlocking.
+    $gradeitem->set_locked($locked, false, true);
 
     redirect($returnurl);
 }
index f762cc7..fe5f58a 100644 (file)
@@ -181,10 +181,9 @@ class edit_item_form extends moodleform {
 
         /// hiding
         if ($item->cancontrolvisibility) {
-            // advcheckbox is not compatible with disabledIf!
-            $mform->addElement('checkbox', 'hidden', get_string('hidden', 'grades'));
+            $mform->addElement('advcheckbox', 'hidden', get_string('hidden', 'grades'), '', [], [0, 1]);
             $mform->addElement('date_time_selector', 'hiddenuntil', get_string('hiddenuntil', 'grades'), array('optional'=>true));
-            $mform->disabledIf('hidden', 'hiddenuntil[off]', 'notchecked');
+            $mform->disabledIf('hidden', 'hiddenuntil[enabled]', 'checked');
         } else {
             $mform->addElement('static', 'hidden', get_string('hidden', 'grades'),
                     get_string('componentcontrolsvisibility', 'grades'));
index 4216129..f34b69a 100644 (file)
@@ -233,7 +233,6 @@ class grade_edit_tree {
                 if ($this->moving && $this->moving != $child_eid) {
 
                     $strmove     = get_string('move');
-                    $strmovehere = get_string('movehere');
                     $actions = $moveaction = ''; // no action icons when moving
 
                     $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'move', 'eid' => $this->moving, 'moveafter' => $child_eid, 'sesskey' => sesskey()));
@@ -245,8 +244,7 @@ class grade_edit_tree {
                     $cell->colspan = 12;
                     $cell->attributes['class'] = 'movehere level' . ($level + 1) . ' level' . ($level % 2 ? 'even' : 'odd');
 
-                    $icon = new pix_icon('movehere', $strmovehere, null, array('class'=>'movetarget'));
-                    $cell->text = $OUTPUT->action_icon($aurl, $icon);
+                    $cell->text = html_writer::link($aurl, '', array('title' => get_string('movehere'), 'class' => 'movehere'));
 
                     $moveto = new html_table_row(array($cell));
                 }
index 46d51db..4f60c57 100644 (file)
@@ -569,7 +569,7 @@ M.gradereport_grader.classes.ajax.prototype.submission_outcome = function(tid, o
         var p = args.properties;
         if (args.type == 'grade') {
             var oldgrade = args.values.oldgrade;
-            p.cell.one('.gradevalue').set('innerHTML',oldgrade);
+            p.cell.one('input.text').set('value', oldgrade);
         } else if (args.type == 'feedback') {
             this.report.update_feedback(p.userid, p.itemid, args.values.oldfeedback);
         }
index b7cbe9e..358f9a5 100644 (file)
@@ -234,3 +234,18 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe
       | Student 2  | 10.00 | 30.00 | 20.00 | 5.00 | 45.00 | 110.00 | 110.00 |
     And I click on student "Student 2" for grade item "Item 1"
     And the field "ajaxfeedback" matches value "Some feedback"
+
+  @javascript
+  Scenario: Teacher can see an error when an incorrect grade is given using the grader report with editing and AJAX on
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "View > Grader report" in the course gradebook
+    And I turn editing mode on
+    And I give the grade "66.00" to the user "Student 2" for the grade item "Item VU"
+    And I click away from student "Student 2" and grade item "Item VU" value
+    When I give the grade "999.00" to the user "Student 2" for the grade item "Item VU"
+    And I click away from student "Student 2" and grade item "Item VU" value
+    Then I should see "The grade entered for Item VU for Student 2 is more than the maximum allowed"
+    And I click on "The grade entered for Item VU for Student 2 is more than the maximum allowed" "text"
+    And I should not see "The grade entered for Item VU for Student 2 is more than the maximum allowed"
+    And the grade for "Student 2" in grade item "Item VU" should match "66.00"
index 6647594..7cc30a4 100644 (file)
@@ -353,6 +353,9 @@ class player {
         $settings['moodleLibraryPaths'] = $this->core->get_dependency_roots($this->h5pid);
         // Add also the Moodle component where the results will be tracked.
         $settings['moodleComponent'] = $this->component;
+        if (!empty($settings['moodleComponent'])) {
+            $settings['reportingIsEnabled'] = true;
+        }
 
         $cid = $this->get_cid();
         // The filterParameters function should be called before getting the dependencyfiles because it rebuild content
index bfdd787..4d55c9f 100644 (file)
@@ -11118,7 +11118,7 @@ class admin_setting_searchsetupinfo extends admin_setting {
      * @return string
      */
     public function output_html($data, $query='') {
-        global $CFG, $OUTPUT;
+        global $CFG, $OUTPUT, $ADMIN;
 
         $return = '';
         $brtag = html_writer::empty_tag('br');
@@ -11180,9 +11180,13 @@ class admin_setting_searchsetupinfo extends admin_setting {
             $row[0] = '3. ' . get_string('setupsearchengine', 'admin');
             $row[1] = html_writer::tag('span', get_string('no'), array('class' => 'badge badge-danger'));
         } else {
-            $url = new moodle_url('/admin/settings.php?section=search' . $CFG->searchengine);
-            $row[0] = '3. ' . html_writer::tag('a', get_string('setupsearchengine', 'admin'),
-                            array('href' => $url));
+            if ($ADMIN->locate('search' . $CFG->searchengine)) {
+                $url = new moodle_url('/admin/settings.php?section=search' . $CFG->searchengine);
+                $row[0] = '3. ' . html_writer::link($url, get_string('setupsearchengine', 'core_admin'));
+            } else {
+                $row[0] = '3. ' . get_string('setupsearchengine', 'core_admin');
+            }
+
             // Check the engine status.
             $searchengine = \core_search\manager::search_engine_instance();
             try {
index c7f3e0c..697d9de 100644 (file)
@@ -2783,6 +2783,39 @@ function xmldb_main_upgrade($oldversion) {
     }
 
     if ($oldversion < 2021052500.26) {
+        // Delete orphaned course_modules_completion rows; these were not deleted properly
+        // by remove_course_contents function.
+        $DB->delete_records_subquery('course_modules_completion', 'id', 'id',
+               "SELECT cmc.id
+                  FROM {course_modules_completion} cmc
+             LEFT JOIN {course_modules} cm ON cm.id = cmc.coursemoduleid
+                 WHERE cm.id IS NULL");
+        upgrade_main_savepoint(true, 2021052500.26);
+    }
+
+    if ($oldversion < 2021052500.27) {
+        // Script to fix incorrect records of "hidden" field in existing grade items.
+        $sql = "SELECT cm.instance, cm.course
+                  FROM {course_modules} cm
+                  JOIN {modules} m ON m.id = cm.module
+                 WHERE m.name = :module AND cm.visible = :visible";
+        $hidequizlist = $DB->get_recordset_sql($sql, ['module' => 'quiz', 'visible' => 0]);
+
+        foreach ($hidequizlist as $hidequiz) {
+            $params = [
+                'itemmodule'    => 'quiz',
+                'courseid'      => $hidequiz->course,
+                'iteminstance'  => $hidequiz->instance,
+            ];
+
+            $DB->set_field('grade_items', 'hidden', 1, $params);
+        }
+        $hidequizlist->close();
+
+        upgrade_main_savepoint(true, 2021052500.27);
+    }
+
+    if ($oldversion < 2021052500.29) {
         // Get the current guest user which is also set as 'deleted'.
         $guestuser = $DB->get_record('user', ['id' => $CFG->siteguest, 'deleted' => 1]);
         // If there is a deleted guest user, reset the user to not be deleted and make sure the related
@@ -2815,7 +2848,7 @@ function xmldb_main_upgrade($oldversion) {
         }
 
         // Main savepoint reached.
-        upgrade_main_savepoint(true, 2021052500.26);
+        upgrade_main_savepoint(true, 2021052500.29);
     }
 
     return true;
index c46ed82..40a154d 100644 (file)
@@ -2038,6 +2038,29 @@ abstract class moodle_database {
         return $this->delete_records_select($table, $select, $params);
     }
 
+    /**
+     * Deletes records from a table using a subquery. The subquery should return a list of values
+     * in a single column, which match one field from the table being deleted.
+     *
+     * The $alias parameter must be set to the name of the single column in your subquery result
+     * (e.g. if the subquery is 'SELECT id FROM whatever', then it should be 'id'). This is not
+     * needed on most databases, but MySQL requires it.
+     *
+     * (On database where the subquery is inefficient, it is implemented differently.)
+     *
+     * @param string $table Table to delete from
+     * @param string $field Field in table to match
+     * @param string $alias Name of single column in subquery e.g. 'id'
+     * @param string $subquery Subquery that will return values of the field to delete
+     * @param array $params Parameters for subquery
+     * @throws dml_exception If there is any error
+     * @since Moodle 3.10
+     */
+    public function delete_records_subquery(string $table, string $field, string $alias,
+            string $subquery, array $params = []): void {
+        $this->delete_records_select($table, $field . ' IN (' . $subquery . ')', $params);
+    }
+
     /**
      * Delete one or more records from a table which match a particular WHERE clause.
      *
index 4bfa04c..599913a 100644 (file)
@@ -1688,6 +1688,23 @@ class mysqli_native_moodle_database extends moodle_database {
         return true;
     }
 
+    /**
+     * Deletes records using a subquery, which is done with a strange DELETE...JOIN syntax in MySQL
+     * because it performs very badly with normal subqueries.
+     *
+     * @param string $table Table to delete from
+     * @param string $field Field in table to match
+     * @param string $alias Name of single column in subquery e.g. 'id'
+     * @param string $subquery Query that will return values of the field to delete
+     * @param array $params Parameters for query
+     * @throws dml_exception If there is any error
+     */
+    public function delete_records_subquery(string $table, string $field, string $alias, string $subquery, array $params = []): void {
+        // Aliases mysql_deltable and mysql_subquery are chosen to be unlikely to conflict.
+        $this->execute("DELETE mysql_deltable FROM {" . $table . "} mysql_deltable JOIN " .
+                "($subquery) mysql_subquery ON mysql_subquery.$alias = mysql_deltable.$field", $params);
+    }
+
     public function sql_cast_char2int($fieldname, $text=false) {
         return ' CAST(' . $fieldname . ' AS SIGNED) ';
     }
index fd6c81c..e23a456 100644 (file)
@@ -3491,6 +3491,29 @@ EOD;
         $this->assertEquals(1, $DB->count_records($tablename));
     }
 
+    public function test_delete_records_subquery() {
+        $DB = $this->tdb;
+        $dbman = $DB->get_manager();
+
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $DB->insert_record($tablename, array('course' => 3));
+        $DB->insert_record($tablename, array('course' => 2));
+        $DB->insert_record($tablename, array('course' => 2));
+
+        // This is not a useful scenario for using a subquery, but it will be sufficient for testing.
+        // Use the 'frog' alias just to make it clearer when we are testing the alias parameter.
+        $DB->delete_records_subquery($tablename, 'id', 'frog',
+                'SELECT id AS frog FROM {' . $tablename . '} WHERE course = ?', [2]);
+        $this->assertEquals(1, $DB->count_records($tablename));
+    }
+
     public function test_delete_records_list() {
         $DB = $this->tdb;
         $dbman = $DB->get_manager();
index 036233b..e70d24d 100644 (file)
@@ -924,10 +924,12 @@ M.form_filemanager.init = function(Y, options) {
             }, this);
             selectnode.one('.fp-file-delete').on('click', function(e) {
                 e.preventDefault();
-                var dialog_options = {};
+                var dialog_options = {
+                    scope: this,
+                    header: M.util.get_string('confirm', 'moodle'),
+                };
                 var params = {};
                 var fileinfo = this.selectui.fileinfo;
-                dialog_options.scope = this;
                 params.filepath = fileinfo.filepath;
                 if (fileinfo.type == 'folder') {
                     params.filename = '.';
index 8634bfb..b571bb7 100644 (file)
@@ -5306,6 +5306,7 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
                     if ($cm->id) {
                         // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
                         context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
+                        $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
                         $DB->delete_records('course_modules', array('id' => $cm->id));
                         rebuild_course_cache($cm->course, true);
                     }
@@ -5327,9 +5328,8 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
     // Remove all data from availability and completion tables that is associated
     // with course-modules belonging to this course. Note this is done even if the
     // features are not enabled now, in case they were enabled previously.
-    $DB->delete_records_select('course_modules_completion',
-           'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
-           array($courseid));
+    $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
+            'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
 
     // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
     $cms = $DB->get_records('course_modules', array('course' => $course->id));
index 2d2536e..40aa910 100644 (file)
@@ -56,6 +56,8 @@ information provided here is intended especially for developers.
   a callback to be provided to determine whether page can be accessed.
 * New setting $CFG->localtempdir overrides which defaults to sys_get_temp_dir()
 * Function redirect() now emits a line of backtrace into the X-Redirect-By header when debugging is on
+* New DML function $DB->delete_records_subquery() to delete records based on a subquery in a way
+  that will work across databases.
 
 === 3.9 ===
 * Following function has been deprecated, please use \core\task\manager::run_from_cli().
index dad4370..dcb6613 100644 (file)
@@ -138,22 +138,18 @@ class mod_lti_external extends external_api {
      * @throws moodle_exception
      */
     public static function get_tool_proxies($orphanedonly) {
-        global $PAGE;
         $params = self::validate_parameters(self::get_tool_proxies_parameters(),
                                             array(
                                                 'orphanedonly' => $orphanedonly
                                             ));
         $orphanedonly = $params['orphanedonly'];
 
-        $proxies = array();
         $context = context_system::instance();
 
         self::validate_context($context);
         require_capability('moodle/site:config', $context);
 
-        $proxies = lti_get_tool_proxies($orphanedonly);
-
-        return array_map('serialise_tool_proxy', $proxies);
+        return lti_get_tool_proxies($orphanedonly);
     }
 
     /**
@@ -164,7 +160,7 @@ class mod_lti_external extends external_api {
      */
     public static function get_tool_proxies_returns() {
         return new external_multiple_structure(
-            self::tool_type_return_structure()
+            self::tool_proxy_return_structure()
         );
     }
 
index 2c5af4c..b154923 100644 (file)
@@ -20,7 +20,7 @@
         <FIELD NAME="instructorchoicesendemailaddr" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Send user's email"/>
         <FIELD NAME="instructorchoiceallowroster" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Allow the roster to be retrieved"/>
         <FIELD NAME="instructorchoiceallowsetting" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Allow a tool to store a setting"/>
-        <FIELD NAME="instructorcustomparameters" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Additional custom parameters provided by the instructor"/>
+        <FIELD NAME="instructorcustomparameters" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Additional custom parameters provided by the instructor"/>
         <FIELD NAME="instructorchoiceacceptgrades" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Accept grades from tool"/>
         <FIELD NAME="grade" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="100" SEQUENCE="false" COMMENT="Grade scale"/>
         <FIELD NAME="launchcontainer" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Launch external tool in a pop-up"/>
index 5712845..7190e27 100644 (file)
@@ -169,5 +169,19 @@ function xmldb_lti_upgrade($oldversion) {
     // Automatically generated Moodle v3.9.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2021052501) {
+
+        // Changing type of field instructorcustomparameters on table lti to text.
+        $table = new xmldb_table('lti');
+        $field = new xmldb_field('instructorcustomparameters', XMLDB_TYPE_TEXT, null, null, null, null, null,
+                'instructorchoiceallowsetting');
+
+        // Launch change of type for field value.
+        $dbman->change_field_type($table, $field);
+
+        // Lti savepoint reached.
+        upgrade_mod_savepoint(true, 2021052501, 'lti');
+    }
+
     return true;
 }
index fe42828..a80f1b1 100644 (file)
@@ -4173,9 +4173,14 @@ function serialise_tool_type(stdClass $type) {
  *
  * @param stdClass $proxy The tool proxy
  *
+ * @deprecated since Moodle 3.10
+ * @todo This will be finally removed for Moodle 4.2 as part of MDL-69976.
  * @return array An array of values representing this type
  */
 function serialise_tool_proxy(stdClass $proxy) {
+    $deprecatedtext = __FUNCTION__ . '() is deprecated. Please remove all references to this method.';
+    debugging($deprecatedtext, DEBUG_DEVELOPER);
+
     return array(
         'id' => $proxy->id,
         'name' => $proxy->name,
index 2b088a0..f56dc83 100644 (file)
@@ -88,6 +88,75 @@ class mod_lti_external_testcase extends externallib_advanced_testcase {
         ];
     }
 
+    /**
+     * Generate a tool type.
+     *
+     * @param string $uniqueid Each tool type needs a different base url. Provide a unique string for every tool type created.
+     * @param int|null $toolproxyid Optional proxy to associate with tool type.
+     * @return stdClass A tool type.
+     */
+    protected function generate_tool_type(string $uniqueid, int $toolproxyid = null) : stdClass {
+        // Create a tool type.
+        $type = new stdClass();
+        $type->state = LTI_TOOL_STATE_CONFIGURED;
+        $type->name = "Test tool $uniqueid";
+        $type->description = "Example description $uniqueid";
+        $type->toolproxyid = $toolproxyid;
+        $type->baseurl = $this->getExternalTestFileUrl("/test$uniqueid.html");
+        lti_add_type($type, new stdClass());
+        return $type;
+    }
+
+    /**
+     * Generate a tool proxy.
+     *
+     * @param string $uniqueid Each tool proxy needs a different reg url. Provide a unique string for every tool proxy created.
+     * @return stdClass A tool proxy.
+     */
+    protected function generate_tool_proxy(string $uniqueid) : stdClass {
+        // Create a tool proxy.
+        $proxy = mod_lti_external::create_tool_proxy("Test proxy $uniqueid",
+                $this->getExternalTestFileUrl("/proxy$uniqueid.html"), array(), array());
+        $proxy = (object)external_api::clean_returnvalue(mod_lti_external::create_tool_proxy_returns(), $proxy);
+        return $proxy;
+    }
+
+    /**
+     * Test get_tool_proxies.
+     */
+    public function test_mod_lti_get_tool_proxies() {
+        // Create two tool proxies. One to associate with tool, and one to leave orphaned.
+        $this->setAdminUser();
+        $proxy = $this->generate_tool_proxy("1");
+        $orphanedproxy = $this->generate_tool_proxy("2");
+        $this->generate_tool_type("1", $proxy->id); // Associate proxy 1 with tool type.
+
+        // Fetch all proxies.
+        $proxies = mod_lti_external::get_tool_proxies(false);
+        $proxies = external_api::clean_returnvalue(mod_lti_external::get_tool_proxies_returns(), $proxies);
+
+        $this->assertCount(2, $proxies);
+        $this->assertEqualsCanonicalizing([(array) $proxy, (array) $orphanedproxy], $proxies);
+    }
+
+    /**
+     * Test get_tool_proxies with orphaned proxies only.
+     */
+    public function test_mod_lti_get_orphaned_tool_proxies() {
+        // Create two tool proxies. One to associate with tool, and one to leave orphaned.
+        $this->setAdminUser();
+        $proxy = $this->generate_tool_proxy("1");
+        $orphanedproxy = $this->generate_tool_proxy("2");
+        $this->generate_tool_type("1", $proxy->id); // Associate proxy 1 with tool type.
+
+        // Fetch all proxies.
+        $proxies = mod_lti_external::get_tool_proxies(true);
+        $proxies = external_api::clean_returnvalue(mod_lti_external::get_tool_proxies_returns(), $proxies);
+
+        $this->assertCount(1, $proxies);
+        $this->assertEqualsCanonicalizing([(array) $orphanedproxy], $proxies);
+    }
+
     /**
      * Test get_tool_launch_data.
      */
index a3c01f4..318c625 100644 (file)
@@ -4,6 +4,8 @@ This files describes API changes in the lti code.
 
 * Select Content supports multiple, allowing a tool to return more than one link at a time.
   Parameter multiple in function lti_build_content_item_selection_request() is now set to true.
+* Deprecated unused function after external function, 'get_tool_proxies()', was refactored:
+    - serialise_tool_proxy()
 
 === 3.8 ===
 
index 00f7e08..364087d 100644 (file)
@@ -48,7 +48,7 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2021052500;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2021052501;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2021052500;    // Requires this Moodle version.
 $plugin->component = 'mod_lti';     // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 0;
index 2a56474..ca8fffe 100644 (file)
@@ -172,9 +172,7 @@ class behat_filepicker extends behat_base {
         $this->perform_on_element('delete', $exception);
 
         // Yes, we are sure.
-        // Using xpath + click instead of pressButton as 'Ok' it is a common string.
-        $okbutton = $this->find('css', 'div.fp-dlg button.fp-dlg-butconfirm');
-        $okbutton->click();
+        $this->execute('behat_general::i_click_on_in_the', [get_string('ok'), 'button', get_string('confirm'), 'dialogue']);
     }
 
     /**
diff --git a/search/tests/behat/setup_search_engine.feature b/search/tests/behat/setup_search_engine.feature
new file mode 100644 (file)
index 0000000..0d90631
--- /dev/null
@@ -0,0 +1,22 @@
+@core @core_search
+Feature: Plugins > Search > Search setup contains Setup search engine only if the target section actually exists
+  In order to set up the selected search engine
+  As an admin
+  I need to be able to click the link 'Setup search engine' but only if the target section actually exists
+
+  Scenario: Selected search engine has an admin section
+    Given the following config values are set as admin:
+      | enableglobalsearch | 1        |
+      | searchengine       | solr     |
+    And I log in as "admin"
+    When I navigate to "Plugins > Search" in site administration
+    Then "Setup search engine" "link" should exist
+
+  Scenario: Selected search engine does not have an admin section
+    Given the following config values are set as admin:
+      | enableglobalsearch | 1        |
+      | searchengine       | simpledb |
+    And I log in as "admin"
+    When I navigate to "Plugins > Search" in site administration
+    Then I should see "Setup search engine"
+    And "Setup search engine" "link" should not exist
index 235b190..7a9d4bf 100644 (file)
@@ -364,4 +364,13 @@ body.drawer-open-left #region-main.has-blocks {
         border-left: 0;
         border-right: 0;
     }
-}
\ No newline at end of file
+}
+
+.block_social_activities li a.movehere,
+.block_site_main_menu li a.movehere {
+    display: block;
+    width: 100%;
+    height: 2rem;
+    border: 2px dashed $gray-800;
+    margin: 4px 0;
+}
index e49aaae..4ed9701 100644 (file)
@@ -219,6 +219,18 @@ div.dropdown-item {
     padding: 0;
 }
 
+.section li.movehere a {
+    display: block;
+    width: 100%;
+    height: 2rem;
+    border: 2px dashed $gray-800;
+}
+
+.editing .course-content .hidden.sectionname {
+    visibility: hidden;
+    display: initial;
+}
+
 .inline,
 .inline-list li {
     display: inline;
index a97af4d..a543b74 100644 (file)
         padding-left: 24px;
     }
 
+    td.movehere {
+        padding: 0;
+    }
+
+    td.movehere a.movehere {
+        display: block;
+        width: 100%;
+        height: 2rem;
+        border: 2px dashed $gray-800;
+    }
+
     .category input[type="text"],
     .category .column-range,
     .categoryitem,
index da5d30a..b78839a 100644 (file)
@@ -41,10 +41,6 @@ $iconsizes: map-merge((
         height: $icon-big-height;
         font-size: $icon-big-height;
     }
-
-    &.movetarget {
-        width: 80px;
-    }
 }
 
 .navbar-dark a .icon {
index 82cbcd9..bebc19e 100644 (file)
@@ -9840,6 +9840,16 @@ div.dropdown-item:focus-within {
   margin: 0;
   padding: 0; }
 
+.section li.movehere a {
+  display: block;
+  width: 100%;
+  height: 2rem;
+  border: 2px dashed #343a40; }
+
+.editing .course-content .hidden.sectionname {
+  visibility: hidden;
+  display: initial; }
+
 .inline,
 .inline-list li {
   display: inline; }
@@ -11874,8 +11884,6 @@ input[disabled] {
     width: 64px;
     height: 64px;
     font-size: 64px; }
-  .icon.movetarget {
-    width: 80px; }
 
 .navbar-dark a .icon {
   color: rgba(255, 255, 255, 0.5) !important;
@@ -12737,6 +12745,14 @@ input[disabled] {
     border-left: 0;
     border-right: 0; } }
 
+.block_social_activities li a.movehere,
+.block_site_main_menu li a.movehere {
+  display: block;
+  width: 100%;
+  height: 2rem;
+  border: 2px dashed #343a40;
+  margin: 4px 0; }
+
 .navbar {
   max-height: 50px; }
 
@@ -18351,6 +18367,15 @@ p.arrow_button {
 .path-grade-edit-tree .setup-grades td.column-name {
   padding-left: 24px; }
 
+.path-grade-edit-tree .setup-grades td.movehere {
+  padding: 0; }
+
+.path-grade-edit-tree .setup-grades td.movehere a.movehere {
+  display: block;
+  width: 100%;
+  height: 2rem;
+  border: 2px dashed #343a40; }
+
 .path-grade-edit-tree .setup-grades .category input[type="text"],
 .path-grade-edit-tree .setup-grades .category .column-range,
 .path-grade-edit-tree .setup-grades .categoryitem,
index 95385c8..7451c2f 100644 (file)
@@ -10044,6 +10044,16 @@ div.dropdown-item:focus-within {
   margin: 0;
   padding: 0; }
 
+.section li.movehere a {
+  display: block;
+  width: 100%;
+  height: 2rem;
+  border: 2px dashed #343a40; }
+
+.editing .course-content .hidden.sectionname {
+  visibility: hidden;
+  display: initial; }
+
 .inline,
 .inline-list li {
   display: inline; }
@@ -12087,8 +12097,6 @@ input[disabled] {
     width: 64px;
     height: 64px;
     font-size: 64px; }
-  .icon.movetarget {
-    width: 80px; }
 
 .navbar-dark a .icon {
   color: rgba(255, 255, 255, 0.5) !important;
@@ -12951,6 +12959,14 @@ input[disabled] {
     border-left: 0;
     border-right: 0; } }
 
+.block_social_activities li a.movehere,
+.block_site_main_menu li a.movehere {
+  display: block;
+  width: 100%;
+  height: 2rem;
+  border: 2px dashed #343a40;
+  margin: 4px 0; }
+
 .navbar {
   max-height: 50px; }
 
@@ -18583,6 +18599,15 @@ p.arrow_button {
 .path-grade-edit-tree .setup-grades td.column-name {
   padding-left: 24px; }
 
+.path-grade-edit-tree .setup-grades td.movehere {
+  padding: 0; }
+
+.path-grade-edit-tree .setup-grades td.movehere a.movehere {
+  display: block;
+  width: 100%;
+  height: 2rem;
+  border: 2px dashed #343a40; }
+
 .path-grade-edit-tree .setup-grades .category input[type="text"],
 .path-grade-edit-tree .setup-grades .category .column-range,
 .path-grade-edit-tree .setup-grades .categoryitem,
index eab1c5f..13d2577 100644 (file)
@@ -29,9 +29,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2021052500.26;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2021052500.29;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
-$release  = '4.0dev (Build: 20201016)'; // Human-friendly version name
+$release  = '4.0dev (Build: 20201021)'; // Human-friendly version name
 $branch   = '400';                      // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.