Merge branch 'MDL-65759' of git://github.com/Chocolate-lightning/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 20 Jun 2019 09:41:00 +0000 (11:41 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 20 Jun 2019 09:41:00 +0000 (11:41 +0200)
102 files changed:
admin/tool/log/db/subplugins.json [new file with mode: 0644]
admin/tool/log/db/subplugins.php
admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
admin/tool/policy/readme_moodle.txt
admin/tool/policy/thirdpartylibs.xml
backup/util/plan/backup_structure_step.class.php
backup/util/plan/restore_structure_step.class.php
backup/util/plan/tests/step_test.php
blocks/recentlyaccesseditems/classes/helper.php
blocks/recentlyaccesseditems/tests/helper_test.php [new file with mode: 0644]
course/externallib.php
course/format/renderer.php
course/format/topics/renderer.php
course/format/upgrade.txt
course/tests/behat/app_course_completion.feature [deleted file]
course/tests/behat/app_courselist.feature [deleted file]
course/tests/externallib_test.php
course/upgrade.txt
lib/accesslib.php
lib/adminlib.php
lib/classes/component.php
lib/classes/plugin_manager.php
lib/components.json [new file with mode: 0644]
lib/db/services.php
lib/editor/atto/db/subplugins.json [new file with mode: 0644]
lib/editor/atto/db/subplugins.php
lib/editor/tinymce/db/subplugins.json [new file with mode: 0644]
lib/editor/tinymce/db/subplugins.php
lib/formslib.php
lib/html2text/Html2Text.php
lib/moodlelib.php
lib/outputlib.php
lib/tests/moodlelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js
lib/yui/src/formchangechecker/js/formchangechecker.js
mod/assign/db/subplugins.json [new file with mode: 0644]
mod/assign/db/subplugins.php
mod/assign/lang/en/assign.php
mod/assign/lang/en/deprecated.txt
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/tests/generator.php
mod/assign/tests/lib_test.php
mod/assignment/db/subplugins.json [new file with mode: 0644]
mod/assignment/db/subplugins.php
mod/book/db/subplugins.json [new file with mode: 0644]
mod/book/db/subplugins.php
mod/chat/lib.php
mod/choice/lang/en/choice.php
mod/choice/lang/en/deprecated.txt
mod/choice/lib.php
mod/data/db/subplugins.json [new file with mode: 0644]
mod/data/db/subplugins.php
mod/forum/classes/subscriptions.php
mod/forum/externallib.php
mod/forum/lang/en/deprecated.txt
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/templates/discussion_list.mustache
mod/forum/tests/behat/app_basic_usage.feature [deleted file]
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/tests/mail_test.php
mod/lesson/lang/en/deprecated.txt [new file with mode: 0644]
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lti/db/subplugins.json [new file with mode: 0644]
mod/lti/db/subplugins.php
mod/quiz/attemptlib.php
mod/quiz/db/subplugins.json [new file with mode: 0644]
mod/quiz/db/subplugins.php
mod/quiz/lang/en/deprecated.txt
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/tests/attempt_test.php
mod/quiz/tests/events_test.php
mod/scorm/db/subplugins.json [new file with mode: 0644]
mod/scorm/db/subplugins.php
mod/scorm/lang/en/deprecated.txt [new file with mode: 0644]
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/upgrade.txt
mod/workshop/db/subplugins.json [new file with mode: 0644]
mod/workshop/db/subplugins.php
question/engine/datalib.php
question/engine/tests/datalib_test.php
question/type/ddmarker/amd/build/shapes.min.js
question/type/ddmarker/amd/src/shapes.js
report/stats/user.php
search/engine/solr/classes/engine.php
search/engine/solr/tests/engine_test.php
theme/boost/scss/moodle/blocks.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/editadvanced_form.php
version.php

diff --git a/admin/tool/log/db/subplugins.json b/admin/tool/log/db/subplugins.json
new file mode 100644 (file)
index 0000000..3c8f43f
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "logstore": "admin\/tool\/log\/store"
+    }
+}
index 9d1fe61..538c286 100644 (file)
@@ -22,4 +22,5 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$subplugins = array('logstore' => 'admin/tool/log/store');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index 8ea0109..1ed6d8b 100644 (file)
Binary files a/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js and b/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js differ
index 22688c5..379c3c4 100644 (file)
@@ -1,22 +1,19 @@
 /**\r
- *     \r
+ *\r
  * JQUERY EU COOKIE LAW POPUPS\r
- * version 1.0.1\r
- * \r
+ * version 1.1.1\r
+ *\r
  * Code on Github:\r
  * https://github.com/wimagguc/jquery-eu-cookie-law-popup\r
- * \r
+ *\r
  * To see a live demo, go to:\r
- * http://www.wimagguc.com/2015/03/jquery-eu-cookie-law-popup/\r
- * \r
+ * http://www.wimagguc.com/2018/05/gdpr-compliance-with-the-jquery-eu-cookie-law-plugin/\r
+ *\r
  * by Richard Dancsi\r
  * http://www.wimagguc.com/\r
- * \r
+ *\r
  */\r
-\r
-define(\r
-['jquery'],\r
-function($) {\r
+define(['jquery'], function($) {\r
 \r
 // for ie9 doesn't support debug console >>>\r
 if (!window.console) window.console = {};\r
@@ -30,7 +27,7 @@ $.fn.euCookieLawPopup = (function() {
        ///////////////////////////////////////////////////////////////////////////////////////////////\r
        // PARAMETERS (MODIFY THIS PART) //////////////////////////////////////////////////////////////\r
        _self.params = {\r
-               cookiePolicyUrl : 'http://www.wimagguc.com/?cookie-policy',\r
+               cookiePolicyUrl : '/?cookie-policy',\r
                popupPosition : 'top',\r
                colorStyle : 'default',\r
                compactStyle : false,\r
@@ -137,10 +134,10 @@ $.fn.euCookieLawPopup = (function() {
                        return _self.params.htmlMarkup;\r
                }\r
 \r
-               var html = \r
-                       '<div class="eupopup-container' + \r
-                           ' eupopup-container-' + _self.params.popupPosition + \r
-                           (_self.params.compactStyle ? ' eupopup-style-compact' : '') + \r
+               var html =\r
+                       '<div class="eupopup-container' +\r
+                           ' eupopup-container-' + _self.params.popupPosition +\r
+                           (_self.params.compactStyle ? ' eupopup-style-compact' : '') +\r
                                ' eupopup-color-' + _self.params.colorStyle + '">' +\r
                                '<div class="eupopup-head">' + _self.params.popupTitle + '</div>' +\r
                                '<div class="eupopup-body">' + _self.params.popupText + '</div>' +\r
@@ -181,7 +178,7 @@ $.fn.euCookieLawPopup = (function() {
 \r
                return userAcceptedCookies;\r
        };\r
-       \r
+\r
        var hideContainer = function() {\r
                // $('.eupopup-container').slideUp(200);\r
                $('.eupopup-container').animate({\r
@@ -206,6 +203,7 @@ $.fn.euCookieLawPopup = (function() {
 \r
                        // No need to display this if user already accepted the policy\r
                        if (userAlreadyAcceptedCookies()) {\r
+        $(document).trigger("user_cookie_already_accepted", {'consent': true});\r
                                return;\r
                        }\r
 \r
@@ -239,7 +237,7 @@ $.fn.euCookieLawPopup = (function() {
                        // Ready to start!\r
                        $('.eupopup-container').show();\r
 \r
-                       // In case it's alright to just display the message once \r
+                       // In case it's alright to just display the message once\r
                        if (_self.params.autoAcceptCookiePolicy) {\r
                                setUserAcceptsCookies(true);\r
                        }\r
@@ -250,5 +248,4 @@ $.fn.euCookieLawPopup = (function() {
 \r
        return publicfunc;\r
 });\r
-\r
 });\r
index 74794c5..a4241d9 100644 (file)
@@ -1,4 +1,4 @@
-jQuery EU Cookie Law popups 1.0.1
+jQuery EU Cookie Law popups 1.1.2
 -------------
 https://github.com/wimagguc/jquery-eu-cookie-law-popup
 
@@ -53,4 +53,4 @@ $(document).bind("user_cookie_consent_changed", function(event, object) {
    Add "tool_policy styles" to the end of the styles file.
 
 4. Execute grunt to compile js
-   grunt js
+   grunt amd
index 151961b..9d7a636 100644 (file)
@@ -4,7 +4,7 @@
     <location>amd/src/jquery-eu-cookie-law-popup.js</location>
     <name>jQuery EU Cookie Law popups</name>
     <license>MIT</license>
-    <version>1.0.1</version>
+    <version>1.1.2</version>
     <licenseversion></licenseversion>
   </library>
 </libraries>
index e18ada4..e94cfe8 100644 (file)
@@ -173,7 +173,7 @@ abstract class backup_structure_step extends backup_step {
      * looking for /mod/modulenanme subplugins. This new method is a generalization of the
      * existing one for activities, supporting all subplugins injecting information everywhere.
      *
-     * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.php.
+     * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.json.
      * @param backup_nested_element $element element in the backup tree (anywhere) that
      *                                       we are going to add subplugin information to.
      * @param bool $multiple to define if multiple subplugins can produce information
@@ -206,11 +206,10 @@ abstract class backup_structure_step extends backup_step {
         }
 
         // Check the requested subplugintype is a valid one.
-        $subpluginsfile = core_component::get_component_directory($plugintype . '_' . $pluginname) . '/db/subplugins.php';
-        if (!file_exists($subpluginsfile)) {
-             throw new backup_step_exception('plugin_missing_subplugins_php_file', array($plugintype, $pluginname));
+        $subplugins = core_component::get_subplugins("{$plugintype}_{$pluginname}");
+        if (null === $subplugins) {
+            throw new backup_step_exception('plugin_missing_subplugins_configuration', [$plugintype, $pluginname]);
         }
-        include($subpluginsfile);
         if (!array_key_exists($subplugintype, $subplugins)) {
              throw new backup_step_exception('incorrect_subplugin_type', $subplugintype);
         }
index b59ac82..bffc7b7 100644 (file)
@@ -305,7 +305,7 @@ abstract class restore_structure_step extends restore_step {
      * looking for /mod/modulenanme subplugins. This new method is a generalization of the
      * existing one for activities, supporting all subplugins injecting information everywhere.
      *
-     * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.php.
+     * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.json.
      * @param restore_path_element $element element in the structure restore tree that
      *                              we are going to add subplugin information to.
      * @param string $plugintype type of the plugin.
@@ -336,11 +336,10 @@ abstract class restore_structure_step extends restore_step {
         }
 
         // Check the requested subplugintype is a valid one.
-        $subpluginsfile = core_component::get_component_directory($plugintype . '_' . $pluginname) . '/db/subplugins.php';
-        if (!file_exists($subpluginsfile)) {
-            throw new restore_step_exception('plugin_missing_subplugins_php_file', array($plugintype, $pluginname));
+        $subplugins = core_component::get_subplugins("{$plugintype}_{$pluginname}");
+        if (null === $subplugins) {
+            throw new restore_step_exception('plugin_missing_subplugins_configuration', array($plugintype, $pluginname));
         }
-        include($subpluginsfile);
         if (!array_key_exists($subplugintype, $subplugins)) {
              throw new restore_step_exception('incorrect_subplugin_type', $subplugintype);
         }
index 0cc854b..8b9f6e1 100644 (file)
@@ -241,7 +241,7 @@ class backup_step_testcase extends advanced_testcase {
             $this->assertTrue(false, 'base_step_exception expected');
         } catch (exception $e) {
             $this->assertTrue($e instanceof backup_step_exception);
-            $this->assertEquals('plugin_missing_subplugins_php_file', $e->errorcode);
+            $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
         }
         // Wrong BC (defaulting to mod and modulename) use not having subplugins.
         try {
@@ -250,7 +250,7 @@ class backup_step_testcase extends advanced_testcase {
             $this->assertTrue(false, 'base_step_exception expected');
         } catch (exception $e) {
             $this->assertTrue($e instanceof backup_step_exception);
-            $this->assertEquals('plugin_missing_subplugins_php_file', $e->errorcode);
+            $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
         }
         // Wrong subplugin type.
         try {
@@ -362,7 +362,7 @@ class backup_step_testcase extends advanced_testcase {
             $this->assertTrue(false, 'base_step_exception expected');
         } catch (exception $e) {
             $this->assertTrue($e instanceof restore_step_exception);
-            $this->assertEquals('plugin_missing_subplugins_php_file', $e->errorcode);
+            $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
         }
         // Wrong BC (defaulting to mod and modulename) use not having subplugins.
         try {
@@ -371,7 +371,7 @@ class backup_step_testcase extends advanced_testcase {
             $this->assertTrue(false, 'base_step_exception expected');
         } catch (exception $e) {
             $this->assertTrue($e instanceof restore_step_exception);
-            $this->assertEquals('plugin_missing_subplugins_php_file', $e->errorcode);
+            $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
         }
         // Wrong subplugin type.
         try {
index b7d2b72..d1ac05b 100644 (file)
@@ -53,11 +53,13 @@ class helper {
             return $recentitems;
         }
 
-        // Determine sort sql clause.
-        $sort = 'timeaccess DESC';
-
         $paramsql = array('userid' => $userid);
-        $records = $DB->get_records('block_recentlyaccesseditems', $paramsql, $sort);
+        $sql = "SELECT rai.*
+                  FROM {block_recentlyaccesseditems} rai
+                  JOIN {course} c ON c.id = rai.courseid
+                 WHERE userid = :userid
+                 ORDER BY rai.timeaccess DESC";
+        $records = $DB->get_records_sql($sql, $paramsql);
         $order = 0;
 
         // Get array of items by course. Use $order index to keep sql sorted results.
diff --git a/blocks/recentlyaccesseditems/tests/helper_test.php b/blocks/recentlyaccesseditems/tests/helper_test.php
new file mode 100644 (file)
index 0000000..aed8863
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Block recentlyaccesseditems helper tests.
+ *
+ * @package    block_recentlyaccesseditems
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use block_recentlyaccesseditems\helper;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block Recently accessed helper class tests.
+ *
+ * @package    block_recentlyaccesseditems
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_recentlyaccesseditems_helper_testcase extends advanced_testcase {
+    /**
+     * Tests that the get recent items method can handle getting records when courses have been deleted.
+     */
+    public function test_get_recent_items() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $coursetodelete = self::getDataGenerator()->create_course();
+        $user = self::getDataGenerator()->create_and_enrol($course, 'student');
+        self::getDataGenerator()->enrol_user($user->id, $coursetodelete->id, 'student');
+
+        // Add an activity to each course.
+        $forum = self::getDataGenerator()->create_module('forum', ['course' => $course]);
+        $glossary = self::getDataGenerator()->create_module('glossary', ['course' => $coursetodelete]);
+        self::setUser($user);
+
+        // Get the user to visit the activities.
+        $event1params = ['context' => context_module::instance($forum->cmid), 'objectid' => $forum->id];
+        $event1 = \mod_forum\event\course_module_viewed::create($event1params);
+        $event1->trigger();
+        $event2params = ['context' => context_module::instance($glossary->cmid), 'objectid' => $glossary->id];
+        $event2 = \mod_glossary\event\course_module_viewed::create($event2params);
+        $event2->trigger();
+        $recent1 = helper::get_recent_items();
+        self::assertCount(2, $recent1);
+        $recentlimited = helper::get_recent_items(1);
+        self::assertCount(1, $recentlimited);
+        delete_course($coursetodelete, false);
+
+        // There should be no errors if a course has been deleted.
+        $recent2 = helper::get_recent_items();
+        self::assertCount(1, $recent2);
+    }
+}
index bd44260..c914656 100644 (file)
@@ -2906,121 +2906,6 @@ class core_course_external extends external_api {
         return self::get_course_module_returns();
     }
 
-    /**
-     * Returns description of method parameters
-     *
-     * @deprecated since 3.3
-     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
-     * @return external_function_parameters
-     * @since Moodle 3.2
-     */
-    public static function get_activities_overview_parameters() {
-        return new external_function_parameters(
-            array(
-                'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'Course id.')),
-            )
-        );
-    }
-
-    /**
-     * Return activities overview for the given courses.
-     *
-     * @deprecated since 3.3
-     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
-     * @param array $courseids a list of course ids
-     * @return array of warnings and the activities overview
-     * @since Moodle 3.2
-     * @throws moodle_exception
-     */
-    public static function get_activities_overview($courseids) {
-        global $USER;
-
-        // Parameter validation.
-        $params = self::validate_parameters(self::get_activities_overview_parameters(), array('courseids' => $courseids));
-        $courseoverviews = array();
-
-        list($courses, $warnings) = external_util::validate_courses($params['courseids']);
-
-        if (!empty($courses)) {
-            // Add lastaccess to each course (required by print_overview function).
-            // We need the complete user data, the ws server does not load a complete one.
-            $user = get_complete_user_data('id', $USER->id);
-            foreach ($courses as $course) {
-                if (isset($user->lastcourseaccess[$course->id])) {
-                    $course->lastaccess = $user->lastcourseaccess[$course->id];
-                } else {
-                    $course->lastaccess = 0;
-                }
-            }
-
-            $overviews = array();
-            if ($modules = get_plugin_list_with_function('mod', 'print_overview')) {
-                foreach ($modules as $fname) {
-                    $fname($courses, $overviews);
-                }
-            }
-
-            // Format output.
-            foreach ($overviews as $courseid => $modules) {
-                $courseoverviews[$courseid]['id'] = $courseid;
-                $courseoverviews[$courseid]['overviews'] = array();
-
-                foreach ($modules as $modname => $overviewtext) {
-                    $courseoverviews[$courseid]['overviews'][] = array(
-                        'module' => $modname,
-                        'overviewtext' => $overviewtext // This text doesn't need formatting.
-                    );
-                }
-            }
-        }
-
-        $result = array(
-            'courses' => $courseoverviews,
-            'warnings' => $warnings
-        );
-        return $result;
-    }
-
-    /**
-     * Returns description of method result value
-     *
-     * @deprecated since 3.3
-     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
-     * @return external_description
-     * @since Moodle 3.2
-     */
-    public static function get_activities_overview_returns() {
-        return new external_single_structure(
-            array(
-                'courses' => new external_multiple_structure(
-                    new external_single_structure(
-                        array(
-                            'id' => new external_value(PARAM_INT, 'Course id'),
-                            'overviews' => new external_multiple_structure(
-                                new external_single_structure(
-                                    array(
-                                        'module' => new external_value(PARAM_PLUGIN, 'Module name'),
-                                        'overviewtext' => new external_value(PARAM_RAW, 'Overview text'),
-                                    )
-                                )
-                            )
-                        )
-                    ), 'List of courses'
-                ),
-                'warnings' => new external_warnings()
-            )
-        );
-    }
-
-    /**
-     * Marking the method as deprecated.
-     *
-     * @return bool
-     */
-    public static function get_activities_overview_is_deprecated() {
-        return true;
-    }
-
     /**
      * Returns description of method parameters
      *
index 2f94041..ffdd6db 100644 (file)
@@ -254,39 +254,11 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
     }
 
     /**
-     * Generate the edit controls of a section
-     *
-     * @param stdClass $course The course entry from DB
-     * @param stdClass $section The course_section entry from DB
-     * @param bool $onsectionpage true if being printed on a section page
-     * @return array of links with edit controls
-     * @deprecated since Moodle 3.0 MDL-48947 - please do not use this function any more.
-     * @see format_section_renderer_base::section_edit_control_items()
+     * @deprecated since Moodle 3.0 MDL-48947 - Use format_section_renderer_base::section_edit_control_items() instead
      */
-    protected function section_edit_controls($course, $section, $onsectionpage = false) {
-        global $PAGE;
-
-        if (!$PAGE->user_is_editing()) {
-            return array();
-        }
-
-        $controls = array();
-        $items = $this->section_edit_control_items($course, $section, $onsectionpage);
-
-        foreach ($items as $key => $item) {
-                $url = empty($item['url']) ? '' : $item['url'];
-                $icon = empty($item['icon']) ? '' : $item['icon'];
-                $name = empty($item['name']) ? '' : $item['name'];
-                $attr = empty($item['attr']) ? '' : $item['attr'];
-                $class = empty($item['pixattr']['class']) ? '' : $item['pixattr']['class'];
-                $alt = empty($item['pixattr']['alt']) ? '' : $item['pixattr']['alt'];
-                $controls[$key] = html_writer::link(
-                    new moodle_url($url), $this->output->pix_icon($icon, $alt),
-                    $attr);
-        }
-
-        debugging('section_edit_controls() is deprecated, please use section_edit_control_items() instead.', DEBUG_DEVELOPER);
-        return $controls;
+    protected function section_edit_controls() {
+        throw new coding_exception('section_edit_controls() can not be used anymore. Please use ' .
+            'section_edit_control_items() instead.');
     }
 
     /**
index 1efad30..67551c5 100644 (file)
@@ -44,7 +44,7 @@ class format_topics_renderer extends format_section_renderer_base {
     public function __construct(moodle_page $page, $target) {
         parent::__construct($page, $target);
 
-        // Since format_topics_renderer::section_edit_controls() only displays the 'Set current section' control when editing mode is on
+        // Since format_topics_renderer::section_edit_control_items() only displays the 'Highlight' control when editing mode is on
         // we need to be sure that the link 'Turn editing mode on' is available for a user who does not have any other managing capability.
         $page->set_other_editing_capability('moodle/course:setcurrentsection');
     }
index 68d7194..d549e46 100644 (file)
@@ -2,6 +2,11 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 3.8 ===
+
+* The following functions have been finally deprecated and can not be used anymore:
+  * section_edit_controls()
+
 === 3.6 ===
 * New method validate_format_options() cleans the values of the course/section format options before inserting them
   in the database. Course format options can now be set in tool_uploadcourse and validation of user-submitted data is important.
diff --git a/course/tests/behat/app_course_completion.feature b/course/tests/behat/app_course_completion.feature
deleted file mode 100644 (file)
index f0c30ba..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-@core @core_course @app @javascript
-Feature: Check course completion feature.
-  In order to track the progress of the course on mobile device
-  As a student
-  I need to be able to update the activity completion status.
-
-  Background:
-    Given the following "users" exist:
-      | username | firstname | lastname | email                |
-      | student1 | Student   | 1        | student1@example.com |
-    And the following "courses" exist:
-      | fullname | shortname | category | enablecompletion |
-      | Course 1 | C1        | 0        | 1                |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student1 | C1     | student |
-
-  Scenario: Complete the activity manually by clicking at the completion checkbox.
-    Given the following "activities" exist:
-      | activity | name         | course | idnumber | completion | completionview |
-      | forum    | First forum  | C1     | forum1   | 1          | 0              |
-      | forum    | Second forum | C1     | forum2   | 1          | 0              |
-    When I enter the app
-    And I log in as "student1"
-    And I press "Course 1" near "Recently accessed courses" in the app
-    # Set activities as completed.
-    And I should see "0%"
-    And I press "Not completed: First forum. Select to mark as complete." in the app
-    And I should see "50%"
-    And I press "Not completed: Second forum. Select to mark as complete." in the app
-    And I should see "100%"
-    # Set activities as not completed.
-    And I press "Completed: First forum. Select to mark as not complete." in the app
-    And I should see "50%"
-    And I press "Completed: Second forum. Select to mark as not complete." in the app
-    And I should see "0%"
diff --git a/course/tests/behat/app_courselist.feature b/course/tests/behat/app_courselist.feature
deleted file mode 100644 (file)
index ea68c05..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-@core @core_course @app @javascript
-Feature: Test course list shown on app start tab
-  In order to select a course
-  As a student
-  I need to see the correct list of courses
-
-  Background:
-    Given the following "courses" exist:
-      | fullname | shortname |
-      | Course 1 | C1        |
-      | Course 2 | C2        |
-    And the following "users" exist:
-      | username |
-      | student1 |
-      | student2 |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student1 | C1     | student |
-      | student2 | C1     | student |
-      | student2 | C2     | student |
-
-  Scenario: Student is registered on one course
-    When I enter the app
-    And I log in as "student1"
-    Then I should see "Course 1"
-    And I should not see "Course 2"
-
-  Scenario: Student is registered on two courses (shortnames not displayed)
-    When I enter the app
-    And I log in as "student2"
-    Then I should see "Course 1"
-    And I should see "Course 2"
-    And I should not see "C1"
-    And I should not see "C2"
-
-  Scenario: Student is registered on two courses (shortnames displayed)
-    Given the following config values are set as admin:
-      | courselistshortnames | 1 |
-    When I enter the app
-    And I log in as "student2"
-    Then I should see "Course 1"
-    And I should see "Course 2"
-    And I should see "C1"
-    And I should see "C2"
-
-  Scenario: Student uses course list to enter course, then leaves it again
-    When I enter the app
-    And I log in as "student2"
-    And I press "Course 2" near "Course overview" in the app
-    Then the header should be "Course 2" in the app
-    And I press the back button in the app
-    Then the header should be "Acceptance test site" in the app
-
-  Scenario: Student uses filter feature to reduce course list
-    Given the following config values are set as admin:
-      | courselistshortnames | 1 |
-    And the following "courses" exist:
-      | fullname | shortname |
-      | Frog 3   | C3        |
-      | Frog 4   | C4        |
-      | Course 5 | C5        |
-      | Toad 6   | C6        |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student2 | C3     | student |
-      | student2 | C4     | student |
-      | student2 | C5     | student |
-      | student2 | C6     | student |
-    # Create bogus courses so that the main ones aren't shown in the 'recently accessed' part.
-    # Because these come later in alphabetical order, they may not be displayed in the lower part
-    # which is OK.
-    And the following "courses" exist:
-      | fullname | shortname |
-      | Zogus 1  | Z1        |
-      | Zogus 2  | Z2        |
-      | Zogus 3  | Z3        |
-      | Zogus 4  | Z4        |
-      | Zogus 5  | Z5        |
-      | Zogus 6  | Z6        |
-      | Zogus 7  | Z7        |
-      | Zogus 8  | Z8        |
-      | Zogus 9  | Z9        |
-      | Zogus 10 | Z10       |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student2 | Z1     | student |
-      | student2 | Z2     | student |
-      | student2 | Z3     | student |
-      | student2 | Z4     | student |
-      | student2 | Z5     | student |
-      | student2 | Z6     | student |
-      | student2 | Z7     | student |
-      | student2 | Z8     | student |
-      | student2 | Z9     | student |
-      | student2 | Z10    | student |
-    When I enter the app
-    And I log in as "student2"
-    Then I should see "C1"
-    And I should see "C2"
-    And I should see "C3"
-    And I should see "C4"
-    And I should see "C5"
-    And I should see "C6"
-    And I press "more" near "Course overview" in the app
-    And I press "Filter my courses" in the app
-    And I set the field "Filter my courses" to "fr" in the app
-    Then I should not see "C1"
-    And I should not see "C2"
-    And I should see "C3"
-    And I should see "C4"
-    And I should not see "C5"
-    And I should not see "C6"
-    And I press "more" near "Course overview" in the app
-    And I press "Filter my courses" in the app
-    Then I should see "C1"
-    And I should see "C2"
-    And I should see "C3"
-    And I should see "C4"
-    And I should see "C5"
-    And I should see "C6"
index ac06afa..ac4358a 100644 (file)
@@ -2194,50 +2194,6 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
     }
 
-    /**
-     * Test get_activities_overview
-     */
-    public function test_get_activities_overview() {
-        global $USER;
-
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-        $course2 = self::getDataGenerator()->create_course();
-
-        // Create a viewer user.
-        $viewer = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
-        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
-
-        // Create two forums - one in each course.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course1->id));
-        $forum2 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course2->id));
-
-        $this->setAdminUser();
-        // A standard post in the forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $record->userid = $USER->id;
-        $record->forum = $forum1->id;
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $this->setUser($viewer->id);
-        $courses = array($course1->id , $course2->id);
-
-        $result = core_course_external::get_activities_overview($courses);
-        $this->assertDebuggingCalledCount(8);
-        $result = external_api::clean_returnvalue(core_course_external::get_activities_overview_returns(), $result);
-
-        // There should be one entry for course1, and no others.
-        $this->assertCount(1, $result['courses']);
-        $this->assertEquals($course1->id, $result['courses'][0]['id']);
-        // Check expected overview data for the module.
-        $this->assertEquals('forum', $result['courses'][0]['overviews'][0]['module']);
-        $this->assertContains('1 total unread', $result['courses'][0]['overviews'][0]['overviewtext']);
-    }
-
     /**
      * Test get_user_navigation_options
      */
index 5ebb81b..10407f9 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /course/*,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+* The following functions have been finally deprecated and can not be used any more:
+  - core_course_external::get_activities_overview
+
 === 3.7 ===
 
  * The course pattern function in course_summary_exporter::get_course_pattern has been moved to $OUTPUT->get_generated_image_for_id.
@@ -10,7 +14,6 @@ information provided here is intended especially for developers.
  * External function core_course_external::get_course_contents now returns a new contentsinfo field with summary files information.
  * External function core_course_external::get_course_contents now returns an additional field "tags" returning the content tags.
 
-
 === 3.6 ===
 
  * External function core_course_external::get_course_public_information now returns the roles and the primary role of course
index e80ba19..856c556 100644 (file)
@@ -7010,20 +7010,27 @@ class context_module extends context {
         $module = $DB->get_record('modules', array('id'=>$cm->module));
 
         $subcaps = array();
-        $subpluginsfile = "$CFG->dirroot/mod/$module->name/db/subplugins.php";
-        if (file_exists($subpluginsfile)) {
+
+        $modulepath = "{$CFG->dirroot}/mod/{$module->name}";
+        if (file_exists("{$modulepath}/db/subplugins.json")) {
+            $subplugins = (array) json_decode(file_get_contents("{$modulepath}/db/subplugins.json"))->plugintypes;
+        } else if (file_exists("{$modulepath}/db/subplugins.php")) {
+            debugging('Use of subplugins.php has been deprecated. ' .
+                    'Please update your plugin to provide a subplugins.json file instead.',
+                    DEBUG_DEVELOPER);
             $subplugins = array();  // should be redefined in the file
-            include($subpluginsfile);
-            if (!empty($subplugins)) {
-                foreach (array_keys($subplugins) as $subplugintype) {
-                    foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) {
-                        $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
-                    }
+            include("{$modulepath}/db/subplugins.php");
+        }
+
+        if (!empty($subplugins)) {
+            foreach (array_keys($subplugins) as $subplugintype) {
+                foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) {
+                    $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
                 }
             }
         }
 
-        $modfile = "$CFG->dirroot/mod/$module->name/lib.php";
+        $modfile = "{$modulepath}/lib.php";
         $extracaps = array();
         if (file_exists($modfile)) {
             include_once($modfile);
index 78b1a35..fdb999b 100644 (file)
@@ -132,17 +132,26 @@ function uninstall_plugin($type, $name) {
     $subplugintypes = core_component::get_plugin_types_with_subplugins();
     if (isset($subplugintypes[$type])) {
         $base = core_component::get_plugin_directory($type, $name);
-        if (file_exists("$base/db/subplugins.php")) {
-            $subplugins = array();
-            include("$base/db/subplugins.php");
-            foreach ($subplugins as $subplugintype=>$dir) {
+
+        $subpluginsfile = "{$base}/db/subplugins.json";
+        if (file_exists($subpluginsfile)) {
+            $subplugins = (array) json_decode(file_get_contents($subpluginsfile))->plugintypes;
+        } else if (file_exists("{$base}/db/subplugins.php")) {
+            debugging('Use of subplugins.php has been deprecated. ' .
+                    'Please update your plugin to provide a subplugins.json file instead.',
+                    DEBUG_DEVELOPER);
+            $subplugins = [];
+            include("{$base}/db/subplugins.php");
+        }
+
+        if (!empty($subplugins)) {
+            foreach (array_keys($subplugins) as $subplugintype) {
                 $instances = core_component::get_plugin_list($subplugintype);
                 foreach ($instances as $subpluginname => $notusedpluginpath) {
                     uninstall_plugin($subplugintype, $subpluginname);
                 }
             }
         }
-
     }
 
     $component = $type . '_' . $name;  // eg. 'qtype_multichoice' or 'workshopgrading_accumulative' or 'mod_forum'
index 6c5d842..2f1d336 100644 (file)
@@ -47,6 +47,8 @@ class core_component {
     /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
     protected static $supportsubplugins = array('mod', 'editor', 'tool', 'local');
 
+    /** @var object JSON source of the component data */
+    protected static $componentsource = null;
     /** @var array cache of plugin types */
     protected static $plugintypes = null;
     /** @var array cache of plugin locations */
@@ -416,79 +418,20 @@ $cache = '.var_export($cache, true).';
         global $CFG;
 
         // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
+        $info = [];
+        foreach (self::fetch_component_source('subsystems') as $subsystem => $path) {
+            // Replace admin/ directory with the config setting.
+            if ($CFG->admin !== 'admin') {
+                if ($path === 'admin') {
+                    $path = $CFG->admin;
+                }
+                if (strpos($path, 'admin/') === 0) {
+                    $path = $CFG->admin . substr($path, 0, 5);
+                }
+            }
 
-        $info = array(
-            'access'      => null,
-            'admin'       => $CFG->dirroot.'/'.$CFG->admin,
-            'analytics'   => $CFG->dirroot . '/analytics',
-            'antivirus'   => $CFG->dirroot . '/lib/antivirus',
-            'auth'        => $CFG->dirroot.'/auth',
-            'availability' => $CFG->dirroot . '/availability',
-            'backup'      => $CFG->dirroot.'/backup/util/ui',
-            'badges'      => $CFG->dirroot.'/badges',
-            'block'       => $CFG->dirroot.'/blocks',
-            'blog'        => $CFG->dirroot.'/blog',
-            'bulkusers'   => null,
-            'cache'       => $CFG->dirroot.'/cache',
-            'calendar'    => $CFG->dirroot.'/calendar',
-            'cohort'      => $CFG->dirroot.'/cohort',
-            'comment'     => $CFG->dirroot.'/comment',
-            'competency'  => $CFG->dirroot.'/competency',
-            'completion'  => $CFG->dirroot.'/completion',
-            'countries'   => null,
-            'course'      => $CFG->dirroot.'/course',
-            'currencies'  => null,
-            'customfield' => $CFG->dirroot.'/customfield',
-            'dbtransfer'  => null,
-            'debug'       => null,
-            'editor'      => $CFG->dirroot.'/lib/editor',
-            'edufields'   => null,
-            'enrol'       => $CFG->dirroot.'/enrol',
-            'error'       => null,
-            'favourites'  => $CFG->dirroot . '/favourites',
-            'filepicker'  => null,
-            'fileconverter' => $CFG->dirroot.'/files/converter',
-            'files'       => $CFG->dirroot.'/files',
-            'filters'     => $CFG->dirroot.'/filter',
-            //'fonts'       => null, // Bogus.
-            'form'        => $CFG->dirroot.'/lib/form',
-            'grades'      => $CFG->dirroot.'/grade',
-            'grading'     => $CFG->dirroot.'/grade/grading',
-            'group'       => $CFG->dirroot.'/group',
-            'help'        => null,
-            'hub'         => null,
-            'imscc'       => null,
-            'install'     => null,
-            'iso6392'     => null,
-            'langconfig'  => null,
-            'license'     => null,
-            'mathslib'    => null,
-            'media'       => $CFG->dirroot.'/media',
-            'message'     => $CFG->dirroot.'/message',
-            'mimetypes'   => null,
-            'mnet'        => $CFG->dirroot.'/mnet',
-            //'moodle.org'  => null, // Not used any more.
-            'my'          => $CFG->dirroot.'/my',
-            'notes'       => $CFG->dirroot.'/notes',
-            'pagetype'    => null,
-            'pix'         => null,
-            'plagiarism'  => $CFG->dirroot.'/plagiarism',
-            'plugin'      => null,
-            'portfolio'   => $CFG->dirroot.'/portfolio',
-            'privacy'     => $CFG->dirroot . '/privacy',
-            'question'    => $CFG->dirroot.'/question',
-            'rating'      => $CFG->dirroot.'/rating',
-            'repository'  => $CFG->dirroot.'/repository',
-            'rss'         => $CFG->dirroot.'/rss',
-            'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
-            'search'      => $CFG->dirroot.'/search',
-            'table'       => null,
-            'tag'         => $CFG->dirroot.'/tag',
-            'timezones'   => null,
-            'user'        => $CFG->dirroot.'/user',
-            'userkey'     => $CFG->dirroot.'/lib/userkey',
-            'webservice'  => $CFG->dirroot.'/webservice',
-        );
+            $info[$subsystem] = empty($path) ? null : "{$CFG->dirroot}/{$path}";
+        }
 
         return $info;
     }
@@ -500,43 +443,15 @@ $cache = '.var_export($cache, true).';
     protected static function fetch_plugintypes() {
         global $CFG;
 
-        $types = array(
-            'antivirus'     => $CFG->dirroot . '/lib/antivirus',
-            'availability'  => $CFG->dirroot . '/availability/condition',
-            'qtype'         => $CFG->dirroot.'/question/type',
-            'mod'           => $CFG->dirroot.'/mod',
-            'auth'          => $CFG->dirroot.'/auth',
-            'calendartype'  => $CFG->dirroot.'/calendar/type',
-            'customfield'   => $CFG->dirroot.'/customfield/field',
-            'enrol'         => $CFG->dirroot.'/enrol',
-            'message'       => $CFG->dirroot.'/message/output',
-            'block'         => $CFG->dirroot.'/blocks',
-            'media'         => $CFG->dirroot.'/media/player',
-            'filter'        => $CFG->dirroot.'/filter',
-            'editor'        => $CFG->dirroot.'/lib/editor',
-            'format'        => $CFG->dirroot.'/course/format',
-            'dataformat'    => $CFG->dirroot.'/dataformat',
-            'profilefield'  => $CFG->dirroot.'/user/profile/field',
-            'report'        => $CFG->dirroot.'/report',
-            'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
-            'gradeexport'   => $CFG->dirroot.'/grade/export',
-            'gradeimport'   => $CFG->dirroot.'/grade/import',
-            'gradereport'   => $CFG->dirroot.'/grade/report',
-            'gradingform'   => $CFG->dirroot.'/grade/grading/form',
-            'mlbackend'     => $CFG->dirroot.'/lib/mlbackend',
-            'mnetservice'   => $CFG->dirroot.'/mnet/service',
-            'webservice'    => $CFG->dirroot.'/webservice',
-            'repository'    => $CFG->dirroot.'/repository',
-            'portfolio'     => $CFG->dirroot.'/portfolio',
-            'search'        => $CFG->dirroot.'/search/engine',
-            'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
-            'qformat'       => $CFG->dirroot.'/question/format',
-            'plagiarism'    => $CFG->dirroot.'/plagiarism',
-            'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
-            'cachestore'    => $CFG->dirroot.'/cache/stores',
-            'cachelock'     => $CFG->dirroot.'/cache/locks',
-            'fileconverter' => $CFG->dirroot.'/files/converter',
-        );
+        $types = [];
+        foreach (self::fetch_component_source('plugintypes') as $plugintype => $path) {
+            // Replace admin/ with the config setting.
+            if ($CFG->admin !== 'admin' && strpos($path, 'admin/') === 0) {
+                $path = $CFG->admin . substr($path, 0, 5);
+            }
+            $types[$plugintype] = "{$CFG->dirroot}/{$path}";
+        }
+
         $parents = array();
         $subplugins = array();
 
@@ -596,6 +511,19 @@ $cache = '.var_export($cache, true).';
         return array($types, $parents, $subplugins);
     }
 
+    /**
+     * Returns the component source content as loaded from /lib/components.json.
+     *
+     * @return array
+     */
+    protected static function fetch_component_source(string $key) {
+        if (null === self::$componentsource) {
+            self::$componentsource = (array) json_decode(file_get_contents(__DIR__ . '/../components.json'));
+        }
+
+        return (array) self::$componentsource[$key];
+    }
+
     /**
      * Returns list of subtypes.
      * @param string $ownerdir
@@ -605,28 +533,35 @@ $cache = '.var_export($cache, true).';
         global $CFG;
 
         $types = array();
-        if (file_exists("$ownerdir/db/subplugins.php")) {
-            $subplugins = array();
+        $subplugins = array();
+        if (file_exists("$ownerdir/db/subplugins.json")) {
+            $subplugins = (array) json_decode(file_get_contents("$ownerdir/db/subplugins.json"))->plugintypes;
+        } else if (file_exists("$ownerdir/db/subplugins.php")) {
+            debugging('Use of subplugins.php has been deprecated. ' .
+                    'Please update your plugin to provide a subplugins.json file instead.',
+                    DEBUG_DEVELOPER);
             include("$ownerdir/db/subplugins.php");
-            foreach ($subplugins as $subtype => $dir) {
-                if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
-                    error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
-                    continue;
-                }
-                if (isset(self::$subsystems[$subtype])) {
-                    error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
-                    continue;
-                }
-                if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
-                    $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
-                }
-                if (!is_dir("$CFG->dirroot/$dir")) {
-                    error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
-                    continue;
-                }
-                $types[$subtype] = "$CFG->dirroot/$dir";
+        }
+
+        foreach ($subplugins as $subtype => $dir) {
+            if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
+                error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
+                continue;
+            }
+            if (isset(self::$subsystems[$subtype])) {
+                error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
+                continue;
             }
+            if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
+                $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
+            }
+            if (!is_dir("$CFG->dirroot/$dir")) {
+                error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
+                continue;
+            }
+            $types[$subtype] = "$CFG->dirroot/$dir";
         }
+
         return $types;
     }
 
index 4eaa742..ce5442f 100644 (file)
@@ -511,10 +511,10 @@ class core_plugin_manager {
 
     /**
      * Returns list of plugins that define their subplugins and the information
-     * about them from the db/subplugins.php file.
+     * about them from the db/subplugins.json file.
      *
      * @return array with keys like 'mod_quiz', and values the data from the
-     *      corresponding db/subplugins.php file.
+     *      corresponding db/subplugins.json file.
      */
     public function get_subplugins() {
 
diff --git a/lib/components.json b/lib/components.json
new file mode 100644 (file)
index 0000000..8ca41cf
--- /dev/null
@@ -0,0 +1,111 @@
+{
+    "plugintypes": {
+        "antivirus": "lib\/antivirus",
+        "availability": "availability\/condition",
+        "qtype": "question\/type",
+        "mod": "mod",
+        "auth": "auth",
+        "calendartype": "calendar\/type",
+        "customfield": "customfield\/field",
+        "enrol": "enrol",
+        "message": "message\/output",
+        "block": "blocks",
+        "media": "media\/player",
+        "filter": "filter",
+        "editor": "lib\/editor",
+        "format": "course\/format",
+        "dataformat": "dataformat",
+        "profilefield": "user\/profile\/field",
+        "report": "report",
+        "coursereport": "course\/report",
+        "gradeexport": "grade\/export",
+        "gradeimport": "grade\/import",
+        "gradereport": "grade\/report",
+        "gradingform": "grade\/grading\/form",
+        "mlbackend": "lib\/mlbackend",
+        "mnetservice": "mnet\/service",
+        "webservice": "webservice",
+        "repository": "repository",
+        "portfolio": "portfolio",
+        "search": "search\/engine",
+        "qbehaviour": "question\/behaviour",
+        "qformat": "question\/format",
+        "plagiarism": "plagiarism",
+        "tool": "admin\/tool",
+        "cachestore": "cache\/stores",
+        "cachelock": "cache\/locks",
+        "fileconverter": "files\/converter",
+        "theme": "theme",
+        "local": "local"
+    },
+    "subsystems": {
+        "access": null,
+        "admin": "admin",
+        "analytics": "analytics",
+        "antivirus": "lib\/antivirus",
+        "auth": "auth",
+        "availability": "availability",
+        "backup": "backup\/util\/ui",
+        "badges": "badges",
+        "block": "blocks",
+        "blog": "blog",
+        "bulkusers": null,
+        "cache": "cache",
+        "calendar": "calendar",
+        "cohort": "cohort",
+        "comment": "comment",
+        "competency": "competency",
+        "completion": "completion",
+        "countries": null,
+        "course": "course",
+        "currencies": null,
+        "customfield": "customfield",
+        "dbtransfer": null,
+        "debug": null,
+        "editor": "lib\/editor",
+        "edufields": null,
+        "enrol": "enrol",
+        "error": null,
+        "favourites": "favourites",
+        "filepicker": null,
+        "fileconverter": "files\/converter",
+        "files": "files",
+        "filters": "filter",
+        "form": "lib\/form",
+        "grades": "grade",
+        "grading": "grade\/grading",
+        "group": "group",
+        "help": null,
+        "hub": null,
+        "imscc": null,
+        "install": null,
+        "iso6392": null,
+        "langconfig": null,
+        "license": null,
+        "mathslib": null,
+        "media": "media",
+        "message": "message",
+        "mimetypes": null,
+        "mnet": "mnet",
+        "my": "my",
+        "notes": "notes",
+        "pagetype": null,
+        "pix": null,
+        "plagiarism": "plagiarism",
+        "plugin": null,
+        "portfolio": "portfolio",
+        "privacy": "privacy",
+        "question": "question",
+        "rating": "rating",
+        "repository": "repository",
+        "rss": "rss",
+        "role": "admin\/roles",
+        "search": "search",
+        "table": null,
+        "tag": "tag",
+        "timezones": null,
+        "user": "user",
+        "userkey": "lib\/userkey",
+        "webservice": "webservice"
+    }
+}
index ce63f3d..1e17cdd 100644 (file)
@@ -529,15 +529,6 @@ $functions = array(
         'type' => 'write',
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
-    'core_course_get_activities_overview' => array(
-        'classname' => 'core_course_external',
-        'methodname' => 'get_activities_overview',
-        'classpath' => 'course/externallib.php',
-        'description' => '** DEPRECATED ** Please do not call this function any more.
-                          Return activities overview for the given courses.',
-        'type' => 'read',
-        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
-    ),
     'core_course_get_user_navigation_options' => array(
         'classname' => 'core_course_external',
         'methodname' => 'get_user_navigation_options',
diff --git a/lib/editor/atto/db/subplugins.json b/lib/editor/atto/db/subplugins.json
new file mode 100644 (file)
index 0000000..d9b9d8c
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "atto": "lib\/editor\/atto\/plugins"
+    }
+}
index de0b440..7500a5f 100644 (file)
@@ -24,4 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array('atto' => 'lib/editor/atto/plugins');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
diff --git a/lib/editor/tinymce/db/subplugins.json b/lib/editor/tinymce/db/subplugins.json
new file mode 100644 (file)
index 0000000..f6c8601
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "tinymce": "lib\/editor\/tinymce\/plugins"
+    }
+}
index 68ef191..a5baea9 100644 (file)
@@ -24,4 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array('tinymce' => 'lib/editor/tinymce/plugins');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index bc0d581..ca39e81 100644 (file)
@@ -1448,6 +1448,16 @@ abstract class moodleform {
         $oldclass = $this->_form->getAttribute('class');
         $this->_form->updateAttributes(array('class' => $oldclass . ' full-width-labels'));
     }
+
+    /**
+     * Set the initial 'dirty' state of the form.
+     *
+     * @param bool $state
+     * @since Moodle 3.7.1
+     */
+    public function set_initial_dirty_state($state = false) {
+        $this->_form->set_initial_dirty_state($state);
+    }
 }
 
 /**
@@ -1503,6 +1513,13 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
     /** @var bool whether to automatically initialise M.formchangechecker for this form. */
     protected $_use_form_change_checker = true;
 
+    /**
+     * The initial state of the dirty state.
+     *
+     * @var bool
+     */
+    protected $_initial_form_dirty_state = false;
+
     /**
      * The form name is derived from the class name of the wrapper minus the trailing form
      * It is a name with words joined by underscores whereas the id attribute is words joined by underscores.
@@ -1721,6 +1738,26 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
         $this->_disableShortforms = $disable;
     }
 
+    /**
+     * Set the initial 'dirty' state of the form.
+     *
+     * @param bool $state
+     * @since Moodle 3.7.1
+     */
+    public function set_initial_dirty_state($state = false) {
+        $this->_initial_form_dirty_state = $state;
+    }
+
+    /**
+     * Is the form currently set to dirty?
+     *
+     * @return boolean Initial dirty state.
+     * @since Moodle 3.7.1
+     */
+    public function is_dirty() {
+        return $this->_initial_form_dirty_state;
+    }
+
     /**
      * Call this method if you don't want the formchangechecker JavaScript to be
      * automatically initialised for this form.
@@ -2964,7 +3001,8 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $PAGE->requires->yui_module('moodle-core-formchangechecker',
                     'M.core_formchangechecker.init',
                     array(array(
-                        'formid' => $formid
+                        'formid' => $formid,
+                        'initialdirtystate' => $form->is_dirty(),
                     ))
             );
             $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
index 5001b2f..f279879 100644 (file)
@@ -112,6 +112,7 @@ class Html2Text
         '/&#151;/i',                                     // m-dash in win-1252
         '/&(amp|#38);/i',                                // Ampersand: see converter()
         '/[ ]{2,}/',                                     // Runs of spaces, post-handling
+        '/&#39;/i',                                      // The apostrophe symbol
     );
 
     /**
@@ -125,6 +126,7 @@ class Html2Text
         '—',         // m-dash
         '|+|amp|+|', // Ampersand: see converter()
         ' ',         // Runs of spaces, post-handling
+        '\'',        // Apostrophe
     );
 
     /**
index 617729c..d6ffcde 100644 (file)
@@ -8618,7 +8618,7 @@ function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=fal
     $result = number_format($float, $decimalpoints, $separator, '');
     if ($stripzeros) {
         // Remove zeros and final dot if not needed.
-        $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
+        $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
     }
     return $result;
 }
index b568f28..d9c47f0 100644 (file)
@@ -1549,7 +1549,7 @@ class theme_config {
      * @return string Return compiled css.
      */
     public function get_precompiled_css_content() {
-        $configs = [$this] + $this->parent_configs;
+        $configs = array_reverse($this->parent_configs) + [$this];
         $css = '';
 
         foreach ($configs as $config) {
index f27a6f5..a3083ff 100644 (file)
@@ -36,18 +36,24 @@ class core_moodlelib_testcase extends advanced_testcase {
      * It is not possible to directly change the result of get_string in
      * a unit test. Instead, we create a language pack for language 'xx' in
      * dataroot and make langconfig.php with the string we need to change.
-     * The example separator used here is 'X'; on PHP 5.3 and before this
+     * The default example separator used here is 'X'; on PHP 5.3 and before this
      * must be a single byte character due to PHP bug/limitation in
      * number_format, so you can't use UTF-8 characters.
+     *
+     * @param string $decsep Separator character. Defaults to `'X'`.
      */
-    protected function define_local_decimal_separator() {
+    protected function define_local_decimal_separator(string $decsep = 'X') {
         global $SESSION, $CFG;
 
         $SESSION->lang = 'xx';
-        $langconfig = "<?php\n\$string['decsep'] = 'X';";
+        $langconfig = "<?php\n\$string['decsep'] = '$decsep';";
         $langfolder = $CFG->dataroot . '/lang/xx';
         check_dir_exists($langfolder);
         file_put_contents($langfolder . '/langconfig.php', $langconfig);
+
+        // Ensure the new value is picked up and not taken from the cache.
+        $stringmanager = get_string_manager();
+        $stringmanager->reset_caches(true);
     }
 
     public function test_cleanremoteaddr() {
@@ -2257,6 +2263,14 @@ class core_moodlelib_testcase extends advanced_testcase {
         // Localisation off.
         $this->assertEquals('5.43000', format_float(5.43, 5, false));
         $this->assertEquals('5.43', format_float(5.43, 5, false, true));
+
+        // Tests with tilde as localised decimal separator.
+        $this->define_local_decimal_separator('~');
+
+        // Must also work for '~' as decimal separator.
+        $this->assertEquals('5', format_float(5.0001, 3, true, true));
+        $this->assertEquals('5~43000', format_float(5.43, 5));
+        $this->assertEquals('5~43', format_float(5.43, 5, true, true));
     }
 
     /**
index f59e9c7..505ae73 100644 (file)
     <location>html2text</location>
     <name>HTML2Text</name>
     <license>GPL</license>
-    <version>4.1.0</version>
+    <version>4.2.1</version>
     <licenseversion>2.0+</licenseversion>
   </library>
   <library>
index 1f59d6f..edf885b 100644 (file)
@@ -2,11 +2,15 @@ This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
 === 3.8 ===
+
 * The yui checknet module is removed. Call \core\session\manager::keepalive instead.
 * The generate_uuid() function has been deprecated. Please use \core\uuid::generate() instead.
 * Remove lib/pear/auth/RADIUS.php (MDL-65746)
+* Core components are now defined in /lib/components.json instead of coded into /lib/classes/component.php
+* Subplugins should now be defined using /db/subplugins.json instead of /db/subplugins.php (which will still work in order to maintain backwards compatibility).
 
 === 3.7 ===
+
 * Nodes in the navigation api can have labels for each group. See set/get_collectionlabel().
 * The method core_user::is_real_user() now returns false for userid = 0 parameter
 * 'mform1' dependencies (in themes, js...) will stop working because a randomly generated string has been added to the id
index b584c62..cd39407 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js differ
index 81f74b4..9eb3b20 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js differ
index b584c62..cd39407 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js differ
index f4545f4..59d3283 100644 (file)
@@ -36,6 +36,10 @@ Y.extend(FORMCHANGECHECKER, Y.Base, {
                 return;
             }
 
+            if (!M.core_formchangechecker.stateinformation.formchanged) {
+                M.core_formchangechecker.stateinformation.formchanged = this.get('initialdirtystate');
+            }
+
             // Add a listener here for an editor restore event.
             Y.on(M.core.event.EDITOR_CONTENT_RESTORED, M.core_formchangechecker.reset_form_dirty_state, this);
 
@@ -96,6 +100,9 @@ Y.extend(FORMCHANGECHECKER, Y.Base, {
         ATTRS: {
             formid: {
                 'value': ''
+            },
+            initialdirtystate: {
+                'value': false
             }
         }
     }
diff --git a/mod/assign/db/subplugins.json b/mod/assign/db/subplugins.json
new file mode 100644 (file)
index 0000000..202a240
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "plugintypes": {
+        "assignsubmission": "mod\/assign\/submission",
+        "assignfeedback": "mod\/assign\/feedback"
+    }
+}
index be0d707..ce8b075 100644 (file)
@@ -22,4 +22,5 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$subplugins = array('assignsubmission'=>'mod/assign/submission', 'assignfeedback'=>'mod/assign/feedback');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index e5033dc..7097679 100644 (file)
@@ -131,6 +131,7 @@ $string['couldnotcreatecoursemodule'] = 'Could not create course module.';
 $string['couldnotcreatenewassignmentinstance'] = 'Could not create new assignment instance.';
 $string['couldnotfindassignmenttoupgrade'] = 'Could not find old assignment instance to upgrade.';
 $string['crontask'] = 'Background processing for assignment module';
+$string['currentassigngrade'] = 'Current grade in assignment';
 $string['currentgrade'] = 'Current grade in gradebook';
 $string['currentattempt'] = 'This is attempt {$a}.';
 $string['currentattemptof'] = 'This is attempt {$a->attemptnumber} ( {$a->maxattempts} attempts allowed ).';
@@ -154,7 +155,6 @@ $string['downloadselectedsubmissions'] = 'Download selected submissions';
 $string['duedate'] = 'Due date';
 $string['duedatecolon'] = 'Due date: {$a}';
 $string['duedate_help'] = 'This is when the assignment is due. Submissions will still be allowed after this date, but any assignments submitted after this date will be marked as late. Set an assignment cut-off date to prevent submissions after a certain date.';
-$string['duedateno'] = 'No due date';
 $string['duplicateoverride'] = 'Duplicate override';
 $string['submissionempty'] = 'Nothing was submitted';
 $string['submissionmodified'] = 'You have existing submission data. Please leave this page and try again.';
@@ -350,14 +350,12 @@ $string['moreusers'] = '{$a} more...';
 $string['multipleteams'] = 'Member of more than one group';
 $string['multipleteams_desc'] = 'The assignment requires submission in groups. You are a member of more than one group. To be able to submit you must be a member of only one group. Please contact your teacher to change your group membership.';
 $string['multipleteamsgrader'] = 'Member of more than one group, so unable to make submissions.';
-$string['mysubmission'] = 'My submission: ';
 $string['newsubmissions'] = 'Assignments submitted';
 $string['noattempt'] = 'No attempt';
 $string['noclose'] = 'No close date';
 $string['nofilters'] = 'No filters';
 $string['nofiles'] = 'No files. ';
 $string['nograde'] = 'No grade. ';
-$string['nolatesubmissions'] = 'No late submissions accepted. ';
 $string['nomoresubmissionsaccepted'] = 'Only allowed for participants who have been granted an extension';
 $string['none'] = 'None';
 $string['noonlinesubmissions'] = 'This assignment does not require you to submit anything online';
@@ -365,13 +363,11 @@ $string['noopen'] = 'No open date';
 $string['nooverridedata'] = 'You must override at least one of the assignment settings.';
 $string['nosavebutnext'] = 'Next';
 $string['nosubmission'] = 'Nothing has been submitted for this assignment';
-$string['nosubmissionsacceptedafter'] = 'No submissions accepted after ';
 $string['noteam'] = 'Not a member of any group';
 $string['noteam_desc'] = 'This assignment requires submission in groups. You are not a member of any group, so you cannot create a submission. Please contact your teacher to be added to a group.';
 $string['noteamgrader'] = 'Not a member of any group, so unable to make submissions.';
 $string['notgraded'] = 'Not graded';
 $string['notgradedyet'] = 'Not graded yet';
-$string['notsubmittedyet'] = 'Not submitted yet';
 $string['notifications'] = 'Notifications';
 $string['nousersselected'] = 'No users selected';
 $string['nousers'] = 'No users';
@@ -526,7 +522,6 @@ $string['submissionreceiptsmall'] = 'You have submitted your assignment submissi
 $string['submissionslocked'] = 'This assignment is not accepting submissions';
 $string['submissionslockedshort'] = 'Submission changes not allowed';
 $string['submissions'] = 'Submissions';
-$string['submissionsnotgraded'] = 'Submissions not graded: {$a}';
 $string['submissionsclosed'] = 'Submissions closed';
 $string['submissionsettings'] = 'Submission settings';
 $string['submissionstatement'] = 'Submission statement';
@@ -613,3 +608,11 @@ $string['viewsubmissiongradingtable'] = 'View submission grading table.';
 $string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.';
 $string['workflowfilter'] = 'Workflow filter';
 $string['xofy'] = '{$a->x} of {$a->y}';
+
+// Deprecated since Moodle 3.8.
+$string['duedateno'] = 'No due date';
+$string['mysubmission'] = 'My submission: ';
+$string['nolatesubmissions'] = 'No late submissions accepted. ';
+$string['nosubmissionsacceptedafter'] = 'No submissions accepted after ';
+$string['notsubmittedyet'] = 'Not submitted yet';
+$string['submissionsnotgraded'] = 'Submissions not graded: {$a}';
index e69de29..41f5cf7 100644 (file)
@@ -0,0 +1,6 @@
+duedateno,mod_assign
+mysubmission,mod_assign
+nolatesubmissions,mod_assign
+nosubmissionsacceptedafter,mod_assign
+notsubmittedyet,mod_assign
+submissionsnotgraded,mod_assign
\ No newline at end of file
index 6f0d59b..e494d63 100644 (file)
@@ -560,315 +560,24 @@ function assign_page_type_list($pagetype, $parentcontext, $currentcontext) {
 }
 
 /**
- * Print an overview of all assignments
- * for the courses.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @param mixed $courses The list of courses to print the overview for
- * @param array $htmlarray The array of html to return
- * @return true
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function assign_print_overview($courses, &$htmlarray) {
-    global $CFG, $DB;
-
-    debugging('The function assign_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return true;
-    }
-
-    if (!$assignments = get_all_instances_in_courses('assign', $courses)) {
-        return true;
-    }
-
-    $assignmentids = array();
-
-    // Do assignment_base::isopen() here without loading the whole thing for speed.
-    foreach ($assignments as $key => $assignment) {
-        $time = time();
-        $isopen = false;
-        if ($assignment->duedate) {
-            $duedate = false;
-            if ($assignment->cutoffdate) {
-                $duedate = $assignment->cutoffdate;
-            }
-            if ($duedate) {
-                $isopen = ($assignment->allowsubmissionsfromdate <= $time && $time <= $duedate);
-            } else {
-                $isopen = ($assignment->allowsubmissionsfromdate <= $time);
-            }
-        }
-        if ($isopen) {
-            $assignmentids[] = $assignment->id;
-        }
-    }
-
-    if (empty($assignmentids)) {
-        // No assignments to look at - we're done.
-        return true;
-    }
-
-    // Definitely something to print, now include the constants we need.
-    require_once($CFG->dirroot . '/mod/assign/locallib.php');
-
-    $strduedate = get_string('duedate', 'assign');
-    $strcutoffdate = get_string('nosubmissionsacceptedafter', 'assign');
-    $strnolatesubmissions = get_string('nolatesubmissions', 'assign');
-    $strduedateno = get_string('duedateno', 'assign');
-    $strassignment = get_string('modulename', 'assign');
-
-    // We do all possible database work here *outside* of the loop to ensure this scales.
-    list($sqlassignmentids, $assignmentidparams) = $DB->get_in_or_equal($assignmentids);
-
-    $mysubmissions = null;
-    $unmarkedsubmissions = null;
-
-    foreach ($assignments as $assignment) {
-
-        // Do not show assignments that are not open.
-        if (!in_array($assignment->id, $assignmentids)) {
-            continue;
-        }
-
-        $context = context_module::instance($assignment->coursemodule);
-
-        // Does the submission status of the assignment require notification?
-        if (has_capability('mod/assign:submit', $context, null, false)) {
-            // Does the submission status of the assignment require notification?
-            $submitdetails = assign_get_mysubmission_details_for_print_overview($mysubmissions, $sqlassignmentids,
-                    $assignmentidparams, $assignment);
-        } else {
-            $submitdetails = false;
-        }
-
-        if (has_capability('mod/assign:grade', $context, null, false)) {
-            // Does the grading status of the assignment require notification ?
-            $gradedetails = assign_get_grade_details_for_print_overview($unmarkedsubmissions, $sqlassignmentids,
-                    $assignmentidparams, $assignment, $context);
-        } else {
-            $gradedetails = false;
-        }
-
-        if (empty($submitdetails) && empty($gradedetails)) {
-            // There is no need to display this assignment as there is nothing to notify.
-            continue;
-        }
-
-        $dimmedclass = '';
-        if (!$assignment->visible) {
-            $dimmedclass = ' class="dimmed"';
-        }
-        $href = $CFG->wwwroot . '/mod/assign/view.php?id=' . $assignment->coursemodule;
-        $basestr = '<div class="assign overview">' .
-               '<div class="name">' .
-               $strassignment . ': '.
-               '<a ' . $dimmedclass .
-                   'title="' . $strassignment . '" ' .
-                   'href="' . $href . '">' .
-               format_string($assignment->name) .
-               '</a></div>';
-        if ($assignment->duedate) {
-            $userdate = userdate($assignment->duedate);
-            $basestr .= '<div class="info">' . $strduedate . ': ' . $userdate . '</div>';
-        } else {
-            $basestr .= '<div class="info">' . $strduedateno . '</div>';
-        }
-        if ($assignment->cutoffdate) {
-            if ($assignment->cutoffdate == $assignment->duedate) {
-                $basestr .= '<div class="info">' . $strnolatesubmissions . '</div>';
-            } else {
-                $userdate = userdate($assignment->cutoffdate);
-                $basestr .= '<div class="info">' . $strcutoffdate . ': ' . $userdate . '</div>';
-            }
-        }
-
-        // Show only relevant information.
-        if (!empty($submitdetails)) {
-            $basestr .= $submitdetails;
-        }
-
-        if (!empty($gradedetails)) {
-            $basestr .= $gradedetails;
-        }
-        $basestr .= '</div>';
-
-        if (empty($htmlarray[$assignment->course]['assign'])) {
-            $htmlarray[$assignment->course]['assign'] = $basestr;
-        } else {
-            $htmlarray[$assignment->course]['assign'] .= $basestr;
-        }
-    }
-    return true;
+function assign_print_overview() {
+    throw new coding_exception('assign_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
- * This api generates html to be displayed to students in print overview section, related to their submission status of the given
- * assignment.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @param array $mysubmissions list of submissions of current user indexed by assignment id.
- * @param string $sqlassignmentids sql clause used to filter open assignments.
- * @param array $assignmentidparams sql params used to filter open assignments.
- * @param stdClass $assignment current assignment
- *
- * @return bool|string html to display , false if nothing needs to be displayed.
- * @throws coding_exception
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function assign_get_mysubmission_details_for_print_overview(&$mysubmissions, $sqlassignmentids, $assignmentidparams,
-                                                            $assignment) {
-    global $USER, $DB;
-
-    debugging('The function assign_get_mysubmission_details_for_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if ($assignment->nosubmissions) {
-        // Offline assignment. No need to display alerts for offline assignments.
-        return false;
-    }
-
-    $strnotsubmittedyet = get_string('notsubmittedyet', 'assign');
-
-    if (!isset($mysubmissions)) {
-
-        // Get all user submissions, indexed by assignment id.
-        $dbparams = array_merge(array($USER->id), $assignmentidparams, array($USER->id));
-        $mysubmissions = $DB->get_records_sql('SELECT a.id AS assignment,
-                                                      a.nosubmissions AS nosubmissions,
-                                                      g.timemodified AS timemarked,
-                                                      g.grader AS grader,
-                                                      g.grade AS grade,
-                                                      s.status AS status
-                                                 FROM {assign} a, {assign_submission} s
-                                            LEFT JOIN {assign_grades} g ON
-                                                      g.assignment = s.assignment AND
-                                                      g.userid = ? AND
-                                                      g.attemptnumber = s.attemptnumber
-                                                WHERE a.id ' . $sqlassignmentids . ' AND
-                                                      s.latest = 1 AND
-                                                      s.assignment = a.id AND
-                                                      s.userid = ?', $dbparams);
-    }
-
-    $submitdetails = '';
-    $submitdetails .= '<div class="details">';
-    $submitdetails .= get_string('mysubmission', 'assign');
-    $submission = false;
-
-    if (isset($mysubmissions[$assignment->id])) {
-        $submission = $mysubmissions[$assignment->id];
-    }
-
-    if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
-        // A valid submission already exists, no need to notify students about this.
-        return false;
-    }
-
-    // We need to show details only if a valid submission doesn't exist.
-    if (!$submission ||
-        !$submission->status ||
-        $submission->status == ASSIGN_SUBMISSION_STATUS_DRAFT ||
-        $submission->status == ASSIGN_SUBMISSION_STATUS_NEW
-    ) {
-        $submitdetails .= $strnotsubmittedyet;
-    } else {
-        $submitdetails .= get_string('submissionstatus_' . $submission->status, 'assign');
-    }
-    if ($assignment->markingworkflow) {
-        $workflowstate = $DB->get_field('assign_user_flags', 'workflowstate', array('assignment' =>
-                $assignment->id, 'userid' => $USER->id));
-        if ($workflowstate) {
-            $gradingstatus = 'markingworkflowstate' . $workflowstate;
-        } else {
-            $gradingstatus = 'markingworkflowstate' . ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
-        }
-    } else if (!empty($submission->grade) && $submission->grade !== null && $submission->grade >= 0) {
-        $gradingstatus = ASSIGN_GRADING_STATUS_GRADED;
-    } else {
-        $gradingstatus = ASSIGN_GRADING_STATUS_NOT_GRADED;
-    }
-    $submitdetails .= ', ' . get_string($gradingstatus, 'assign');
-    $submitdetails .= '</div>';
-    return $submitdetails;
+function assign_get_mysubmission_details_for_print_overview() {
+    throw new coding_exception('assign_get_mysubmission_details_for_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
- * This api generates html to be displayed to teachers in print overview section, related to the grading status of the given
- * assignment's submissions.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @param array $unmarkedsubmissions list of submissions of that are currently unmarked indexed by assignment id.
- * @param string $sqlassignmentids sql clause used to filter open assignments.
- * @param array $assignmentidparams sql params used to filter open assignments.
- * @param stdClass $assignment current assignment
- * @param context $context context of the assignment.
- *
- * @return bool|string html to display , false if nothing needs to be displayed.
- * @throws coding_exception
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function assign_get_grade_details_for_print_overview(&$unmarkedsubmissions, $sqlassignmentids, $assignmentidparams,
-                                                     $assignment, $context) {
-    global $DB;
-
-    debugging('The function assign_get_grade_details_for_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (!isset($unmarkedsubmissions)) {
-        // Build up and array of unmarked submissions indexed by assignment id/ userid
-        // for use where the user has grading rights on assignment.
-        $dbparams = array_merge(array(ASSIGN_SUBMISSION_STATUS_SUBMITTED), $assignmentidparams);
-        $rs = $DB->get_recordset_sql('SELECT s.assignment as assignment,
-                                             s.userid as userid,
-                                             s.id as id,
-                                             s.status as status,
-                                             g.timemodified as timegraded
-                                        FROM {assign_submission} s
-                                   LEFT JOIN {assign_grades} g ON
-                                             s.userid = g.userid AND
-                                             s.assignment = g.assignment AND
-                                             g.attemptnumber = s.attemptnumber
-                                   LEFT JOIN {assign} a ON
-                                             a.id = s.assignment
-                                       WHERE
-                                             ( g.timemodified is NULL OR
-                                             s.timemodified >= g.timemodified OR
-                                             g.grade IS NULL OR
-                                             (g.grade = -1 AND
-                                             a.grade < 0)) AND
-                                             s.timemodified IS NOT NULL AND
-                                             s.status = ? AND
-                                             s.latest = 1 AND
-                                             s.assignment ' . $sqlassignmentids, $dbparams);
-
-        $unmarkedsubmissions = array();
-        foreach ($rs as $rd) {
-            $unmarkedsubmissions[$rd->assignment][$rd->userid] = $rd->id;
-        }
-        $rs->close();
-    }
-
-    // Count how many people can submit.
-    $submissions = 0;
-    if ($students = get_enrolled_users($context, 'mod/assign:view', 0, 'u.id')) {
-        foreach ($students as $student) {
-            if (isset($unmarkedsubmissions[$assignment->id][$student->id])) {
-                $submissions++;
-            }
-        }
-    }
-
-    if ($submissions) {
-        $urlparams = array('id' => $assignment->coursemodule, 'action' => 'grading');
-        $url = new moodle_url('/mod/assign/view.php', $urlparams);
-        $gradedetails = '<div class="details">' .
-                '<a href="' . $url . '">' .
-                get_string('submissionsnotgraded', 'assign', $submissions) .
-                '</a></div>';
-        return $gradedetails;
-    } else {
-        return false;
-    }
-
+function assign_get_grade_details_for_print_overview() {
+    throw new coding_exception('assign_get_grade_details_for_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
index bef787c..bb0843d 100644 (file)
@@ -7586,6 +7586,14 @@ class assign {
             $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
             $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
             $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
+            $gradingstatus = $this->get_grading_status($userid);
+            if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
+                if ($grade->grade && $grade->grade != -1) {
+                    $assigngradestring = html_writer::span(grade_floatval($grade->grade), 'currentgrade');
+                    $label = get_string('currentassigngrade', 'assign');
+                    $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
+                }
+            }
         }
 
         if ($this->get_instance()->markingworkflow &&
@@ -7607,6 +7615,7 @@ class assign {
             $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
             $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
         }
+
         $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
         $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
 
index 0d34150..f4cbcef 100644 (file)
@@ -128,8 +128,6 @@ trait mod_assign_test_generator {
             ]);
 
         // Bump all timecreated and timemodified for this user back.
-        // The old assign_print_overview function includes submissions which have been graded where the grade modified
-        // date matches the submission modified date.
         $DB->execute('UPDATE {assign_submission} SET timecreated = timecreated - 1, timemodified = timemodified - 1 WHERE userid = :userid',
             ['userid' => $student->id]);
 
index 759f534..e002e4f 100644 (file)
@@ -46,139 +46,6 @@ class mod_assign_lib_testcase extends advanced_testcase {
     // Use the generator helper.
     use mod_assign_test_generator;
 
-    public function test_assign_print_overview() {
-        global $DB;
-
-        $this->resetAfterTest();
-
-        $course = $this->getDataGenerator()->create_course();
-        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
-        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
-
-        $this->setAdminUser();
-
-        // Assignment with default values.
-        $firstassign = $this->create_instance($course, ['name' => 'First Assignment']);
-
-        // Assignment with submissions.
-        $secondassign = $this->create_instance($course, [
-                'name' => 'Assignment with submissions',
-                'duedate' => time(),
-                'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
-                'maxattempts' => 3,
-                'submissiondrafts' => 1,
-                'assignsubmission_onlinetext_enabled' => 1,
-            ]);
-        $this->add_submission($student, $secondassign);
-        $this->submit_for_grading($student, $secondassign);
-        $this->mark_submission($teacher, $secondassign, $student, 50.0);
-
-        // Past assignments should not show up.
-        $pastassign = $this->create_instance($course, [
-                'name' => 'Past Assignment',
-                'duedate' => time() - DAYSECS - 1,
-                'cutoffdate' => time() - DAYSECS,
-                'nosubmissions' => 0,
-                'assignsubmission_onlinetext_enabled' => 1,
-            ]);
-
-        // Open assignments should show up only if relevant.
-        $openassign = $this->create_instance($course, [
-                'name' => 'Open Assignment',
-                'duedate' => time(),
-                'cutoffdate' => time() + DAYSECS,
-                'nosubmissions' => 0,
-                'assignsubmission_onlinetext_enabled' => 1,
-            ]);
-        $pastsubmission = $pastassign->get_user_submission($student->id, true);
-        $opensubmission = $openassign->get_user_submission($student->id, true);
-
-        // Check the overview as the different users.
-        // For students , open assignments should show only when there are no valid submissions.
-        $this->setUser($student);
-        $overview = array();
-        $courses = $DB->get_records('course', array('id' => $course->id));
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(1, count($overview));
-        $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']); // No valid submission.
-        $this->assertNotRegExp('/.*First Assignment.*/', $overview[$course->id]['assign']); // Has valid submission.
-
-        // And now submit the submission.
-        $opensubmission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
-        $openassign->testable_update_submission($opensubmission, $student->id, true, false);
-
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(0, count($overview));
-
-        $this->setUser($teacher);
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(1, count($overview));
-        // Submissions without a grade.
-        $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
-        $this->assertNotRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
-
-        $this->setUser($teacher);
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(1, count($overview));
-        // Submissions without a grade.
-        $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
-        $this->assertNotRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
-
-        // Let us grade a submission.
-        $this->setUser($teacher);
-        $data = new stdClass();
-        $data->grade = '50.0';
-        $openassign->testable_apply_grade_to_user($data, $student->id, 0);
-
-        // The assign_print_overview expects the grade date to be after the submission date.
-        $graderecord = $DB->get_record('assign_grades', array('assignment' => $openassign->get_instance()->id,
-            'userid' => $student->id, 'attemptnumber' => 0));
-        $graderecord->timemodified += 1;
-        $DB->update_record('assign_grades', $graderecord);
-
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        // Now assignment 4 should not show up.
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEmpty($overview);
-
-        $this->setUser($teacher);
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        // Now assignment 4 should not show up.
-        $this->assertEmpty($overview);
-    }
-
-    /**
-     * Test that assign_print_overview does not return any assignments which are Open Offline.
-     */
-    public function test_assign_print_overview_open_offline() {
-        $this->resetAfterTest();
-        $course = $this->getDataGenerator()->create_course();
-        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
-
-        $this->setAdminUser();
-        $openassign = $this->create_instance($course, [
-                'duedate' => time() + DAYSECS,
-                'cutoffdate' => time() + (DAYSECS * 2),
-            ]);
-
-        $this->setUser($student);
-        $overview = [];
-        assign_print_overview([$course], $overview);
-
-        $this->assertDebuggingCalledCount(1);
-        $this->assertEquals(0, count($overview));
-    }
-
     /**
      * Test that assign_print_recent_activity shows ungraded submitted assignments.
      */
diff --git a/mod/assignment/db/subplugins.json b/mod/assignment/db/subplugins.json
new file mode 100644 (file)
index 0000000..7c6b9c0
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "assignment": "mod\/assignment\/type"
+    }
+}
index 52e3b41..5ea3af4 100644 (file)
@@ -1,3 +1,26 @@
 <?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-$subplugins = array('assignment'=>'mod/assignment/type');
+/**
+ * Definition of sub-plugins
+ *
+ * @package     mod_assignment
+ * @copyright   1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
diff --git a/mod/book/db/subplugins.json b/mod/book/db/subplugins.json
new file mode 100644 (file)
index 0000000..97e2827
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "booktool": "mod\/book\/tool"
+    }
+}
index e3e2e90..5348581 100644 (file)
@@ -24,6 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array(
-    'booktool'       => 'mod/book/tool',
-);
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index bd78708..4441233 100644 (file)
@@ -1136,44 +1136,10 @@ function chat_get_post_actions() {
 }
 
 /**
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @global object
- * @global object
- * @param array $courses
- * @param array $htmlarray Passed by reference
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function chat_print_overview($courses, &$htmlarray) {
-    global $USER, $CFG;
-
-    debugging('The function chat_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return array();
-    }
-
-    if (!$chats = get_all_instances_in_courses('chat', $courses)) {
-        return;
-    }
-
-    $strchat = get_string('modulename', 'chat');
-    $strnextsession  = get_string('nextsession', 'chat');
-
-    foreach ($chats as $chat) {
-        if ($chat->chattime and $chat->schedule) {  // A chat is scheduled.
-            $str = '<div class="chat overview"><div class="name">'.
-                   $strchat.': <a '.($chat->visible ? '' : ' class="dimmed"').
-                   ' href="'.$CFG->wwwroot.'/mod/chat/view.php?id='.$chat->coursemodule.'">'.
-                   $chat->name.'</a></div>';
-            $str .= '<div class="info">'.$strnextsession.': '.userdate($chat->chattime).'</div></div>';
-
-            if (empty($htmlarray[$chat->course]['chat'])) {
-                $htmlarray[$chat->course]['chat'] = $str;
-            } else {
-                $htmlarray[$chat->course]['chat'] .= $str;
-            }
-        }
-    }
+function chat_print_overview() {
+    throw new coding_exception('chat_print_overview() can not be used any more and is obsolete.');
 }
 
 
index 746233c..f9ec368 100644 (file)
@@ -46,14 +46,12 @@ $string['atleastoneoption'] = 'You need to provide at least one possible answer.
 $string['full'] = '(Full)';
 $string['havetologin'] = 'You have to log in before you can submit your choice';
 $string['choice'] = 'Choice';
-$string['choiceactivityname'] = 'Choice: {$a}';
 $string['choice:addinstance'] = 'Add a new choice';
 $string['choiceclose'] = 'Allow responses until';
 $string['choice:deleteresponses'] = 'Modify and delete responses';
 $string['choice:downloadresponses'] = 'Download responses';
 $string['choicefull'] = 'One or more of the options you have selected have already been filled. Your response has not been saved. Please make another selection.';
 $string['choice:choose'] = 'Record a choice';
-$string['choicecloseson'] = 'Choice closes on {$a}';
 $string['choicename'] = 'Choice name';
 $string['choiceopen'] = 'Allow responses from';
 $string['choiceoptions'] = 'Choice options';
@@ -147,4 +145,7 @@ $string['viewchoices'] = 'View choices';
 $string['withselected'] = 'With selected';
 $string['userchoosethisoption'] = 'Users who chose this option';
 $string['yourselection'] = 'Your selection';
-$string['skipresultgraph'] = 'Skip result graph';
+
+// Deprecated since Moodle 3.8.
+$string['choiceactivityname'] = 'Choice: {$a}';
+$string['choicecloseson'] = 'Choice closes on {$a}';
index 4177da3..3e9f384 100644 (file)
@@ -1 +1,2 @@
-skipresultgraph,mod_choice
+choiceactivityname,mod_choice
+choicecloseson,mod_choice
\ No newline at end of file
index c6704c7..ac0da08 100644 (file)
@@ -922,83 +922,10 @@ function choice_page_type_list($pagetype, $parentcontext, $currentcontext) {
 }
 
 /**
- * Prints choice summaries on MyMoodle Page
- *
- * Prints choice name, due date and attempt information on
- * choice activities that have a deadline that has not already passed
- * and it is available for completing.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @uses CONTEXT_MODULE
- * @param array $courses An array of course objects to get choice instances from.
- * @param array $htmlarray Store overview output array( course ID => 'choice' => HTML output )
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function choice_print_overview($courses, &$htmlarray) {
-    global $USER, $DB, $OUTPUT;
-
-    debugging('The function choice_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return;
-    }
-    if (!$choices = get_all_instances_in_courses('choice', $courses)) {
-        return;
-    }
-
-    $now = time();
-    foreach ($choices as $choice) {
-        if ($choice->timeclose != 0                                      // If this choice is scheduled.
-            and $choice->timeclose >= $now                               // And the deadline has not passed.
-            and ($choice->timeopen == 0 or $choice->timeopen <= $now)) { // And the choice is available.
-
-            // Visibility.
-            $class = (!$choice->visible) ? 'dimmed' : '';
-
-            // Link to activity.
-            $url = new moodle_url('/mod/choice/view.php', array('id' => $choice->coursemodule));
-            $url = html_writer::link($url, format_string($choice->name), array('class' => $class));
-            $str = $OUTPUT->box(get_string('choiceactivityname', 'choice', $url), 'name');
-
-             // Deadline.
-            $str .= $OUTPUT->box(get_string('choicecloseson', 'choice', userdate($choice->timeclose)), 'info');
-
-            // Display relevant info based on permissions.
-            if (has_capability('mod/choice:readresponses', context_module::instance($choice->coursemodule))) {
-                $attempts = $DB->count_records_sql('SELECT COUNT(DISTINCT userid) FROM {choice_answers} WHERE choiceid = ?',
-                    [$choice->id]);
-                $url = new moodle_url('/mod/choice/report.php', ['id' => $choice->coursemodule]);
-                $str .= $OUTPUT->box(html_writer::link($url, get_string('viewallresponses', 'choice', $attempts)), 'info');
-
-            } else if (has_capability('mod/choice:choose', context_module::instance($choice->coursemodule))) {
-                // See if the user has submitted anything.
-                $answers = $DB->count_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id));
-                if ($answers > 0) {
-                    // User has already selected an answer, nothing to show.
-                    $str = '';
-                } else {
-                    // User has not made a selection yet.
-                    $str .= $OUTPUT->box(get_string('notanswered', 'choice'), 'info');
-                }
-            } else {
-                // Does not have permission to do anything on this choice activity.
-                $str = '';
-            }
-
-            // Make sure we have something to display.
-            if (!empty($str)) {
-                // Generate the containing div.
-                $str = $OUTPUT->box($str, 'choice overview');
-
-                if (empty($htmlarray[$choice->course]['choice'])) {
-                    $htmlarray[$choice->course]['choice'] = $str;
-                } else {
-                    $htmlarray[$choice->course]['choice'] .= $str;
-                }
-            }
-        }
-    }
-    return;
+function choice_print_overview() {
+    throw new coding_exception('choice_print_overview() can not be used any more and is obsolete.');
 }
 
 
diff --git a/mod/data/db/subplugins.json b/mod/data/db/subplugins.json
new file mode 100644 (file)
index 0000000..67639a6
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "plugintypes": {
+        "datafield": "mod\/data\/field",
+        "datapreset": "mod\/data\/preset"
+    }
+}
index 2c1a7e9..d0dd67e 100644 (file)
@@ -1,4 +1,26 @@
 <?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-$subplugins = array('datafield'  => 'mod/data/field',
-                    'datapreset' => 'mod/data/preset');
+/**
+ * Definition of sub-plugins
+ *
+ * @package   mod_data
+ * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index 74181b6..75583df 100644 (file)
@@ -272,7 +272,7 @@ class subscriptions {
         $sql = "SELECT $fields
                 FROM {user} u
                 JOIN ($esql) je ON je.id = u.id
-               WHERE u.auth <> 'nologin' AND u.suspended = 0
+               WHERE u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1
             ORDER BY $sort";
 
         return $DB->get_records_sql($sql, $params);
@@ -443,7 +443,7 @@ class subscriptions {
                         ) subscriptions
                         JOIN {user} u ON u.id = subscriptions.userid
                         JOIN ($esql) je ON je.id = u.id
-                        WHERE u.auth <> 'nologin' AND u.suspended = 0
+                        WHERE u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1
                         ORDER BY u.email ASC";
 
             } else {
@@ -452,7 +452,7 @@ class subscriptions {
                         JOIN ($esql) je ON je.id = u.id
                         JOIN {forum_subscriptions} s ON s.userid = u.id
                         WHERE
-                          s.forum = :forumid AND u.auth <> 'nologin' AND u.suspended = 0
+                          s.forum = :forumid AND u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1
                         ORDER BY u.email ASC";
             }
             $results = $DB->get_records_sql($sql, $params);
index 37f09d7..5a6fb60 100644 (file)
@@ -1257,9 +1257,13 @@ class mod_forum_external extends external_api {
         $context = context_module::instance($cm->id);
         self::validate_context($context);
 
+        $coursecontext = \context_course::instance($forum->get_course_id());
+        $discussionsubscribe = \mod_forum\subscriptions::get_user_default_subscription($forumrecord, $coursecontext,
+            $cm, null);
+
         // Validate options.
         $options = array(
-            'discussionsubscribe' => true,
+            'discussionsubscribe' => $discussionsubscribe,
             'private'             => false,
             'inlineattachmentsid' => 0,
             'attachmentsid' => null,
@@ -1350,9 +1354,11 @@ class mod_forum_external extends external_api {
                 $completion->update_state($cm, COMPLETION_COMPLETE);
             }
 
-            $settings = new stdClass();
-            $settings->discussionsubscribe = $options['discussionsubscribe'];
-            forum_post_subscription($settings, $forumrecord, $discussionrecord);
+            if ($options['discussionsubscribe']) {
+                $settings = new stdClass();
+                $settings->discussionsubscribe = $options['discussionsubscribe'];
+                forum_post_subscription($settings, $forumrecord, $discussionrecord);
+            }
         } else {
             throw new moodle_exception('couldnotadd', 'forum');
         }
index dd59859..cddc5b1 100644 (file)
@@ -1 +1,3 @@
 inpagereplysubject,mod_forum
+overviewnumpostssince,mod_forum
+overviewnumunread,mod_forum
index 89a544f..5c20a6b 100644 (file)
@@ -438,8 +438,6 @@ $string['numberofreplies'] = 'Number of replies: {$a}';
 $string['olderdiscussions'] = 'Older discussions';
 $string['oldertopics'] = 'Older topics';
 $string['oldpostdays'] = 'Read after days';
-$string['overviewnumpostssince'] = '{$a} posts since last login';
-$string['overviewnumunread'] = '{$a} total unread';
 $string['page-mod-forum-x'] = 'Any forum module page';
 $string['page-mod-forum-view'] = 'Forum module main page';
 $string['page-mod-forum-discuss'] = 'Forum module discussion thread page';
@@ -675,3 +673,5 @@ $string['forumbodydeleted'] = 'The content of this forum post has been removed a
 
 // Deprecated since Moodle 3.8.
 $string['inpagereplysubject'] = 'Re: {$a}';
+$string['overviewnumpostssince'] = '{$a} posts since last login';
+$string['overviewnumunread'] = '{$a} total unread';
index 7763148..db8a3b6 100644 (file)
@@ -540,44 +540,10 @@ function forum_user_complete($course, $user, $mod, $forum) {
 }
 
 /**
- * Filters the forum discussions according to groups membership and config.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @since  Moodle 2.8, 2.7.1, 2.6.4
- * @param  array $discussions Discussions with new posts array
- * @return array Forums with the number of new posts
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function forum_filter_user_groups_discussions($discussions) {
-
-    debugging('The function forum_filter_user_groups_discussions() is now deprecated.', DEBUG_DEVELOPER);
-
-    // Group the remaining discussions posts by their forumid.
-    $filteredforums = array();
-
-    // Discard not visible groups.
-    foreach ($discussions as $discussion) {
-
-        // Course data is already cached.
-        $instances = get_fast_modinfo($discussion->course)->get_instances();
-        $forum = $instances['forum'][$discussion->forum];
-
-        // Continue if the user should not see this discussion.
-        if (!forum_is_user_group_discussion($forum, $discussion->groupid)) {
-            continue;
-        }
-
-        // Grouping results by forum.
-        if (empty($filteredforums[$forum->instance])) {
-            $filteredforums[$forum->instance] = new stdClass();
-            $filteredforums[$forum->instance]->id = $forum->id;
-            $filteredforums[$forum->instance]->count = 0;
-        }
-        $filteredforums[$forum->instance]->count += $discussion->count;
-
-    }
-
-    return $filteredforums;
+function forum_filter_user_groups_discussions() {
+    throw new coding_exception('forum_filter_user_groups_discussions() can not be used any more and is obsolete.');
 }
 
 /**
@@ -607,155 +573,10 @@ function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) {
 }
 
 /**
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @global object
- * @global object
- * @global object
- * @param array $courses
- * @param array $htmlarray
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function forum_print_overview($courses,&$htmlarray) {
-    global $USER, $CFG, $DB, $SESSION;
-
-    debugging('The function forum_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return array();
-    }
-
-    if (!$forums = get_all_instances_in_courses('forum',$courses)) {
-        return;
-    }
-
-    // Courses to search for new posts
-    $coursessqls = array();
-    $params = array();
-    foreach ($courses as $course) {
-
-        // If the user has never entered into the course all posts are pending
-        if ($course->lastaccess == 0) {
-            $coursessqls[] = '(d.course = ?)';
-            $params[] = $course->id;
-
-        // Only posts created after the course last access
-        } else {
-            $coursessqls[] = '(d.course = ? AND p.created > ?)';
-            $params[] = $course->id;
-            $params[] = $course->lastaccess;
-        }
-    }
-    $params[] = $USER->id;
-    $coursessql = implode(' OR ', $coursessqls);
-
-    $sql = "SELECT d.id, d.forum, d.course, d.groupid, COUNT(*) as count "
-                .'FROM {forum_discussions} d '
-                .'JOIN {forum_posts} p ON p.discussion = d.id '
-                ."WHERE ($coursessql) "
-                .'AND p.deleted <> 1 '
-                .'AND p.userid != ? '
-                .'AND (d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?)) '
-                .'GROUP BY d.id, d.forum, d.course, d.groupid '
-                .'ORDER BY d.course, d.forum';
-    $params[] = time();
-    $params[] = time();
-
-    // Avoid warnings.
-    if (!$discussions = $DB->get_records_sql($sql, $params)) {
-        $discussions = array();
-    }
-
-    $forumsnewposts = forum_filter_user_groups_discussions($discussions);
-
-    // also get all forum tracking stuff ONCE.
-    $trackingforums = array();
-    foreach ($forums as $forum) {
-        if (forum_tp_can_track_forums($forum)) {
-            $trackingforums[$forum->id] = $forum;
-        }
-    }
-
-    if (count($trackingforums) > 0) {
-        $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
-        $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
-            ' FROM {forum_posts} p '.
-            ' JOIN {forum_discussions} d ON p.discussion = d.id '.
-            ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE p.deleted <> 1 AND (';
-        $params = array($USER->id);
-
-        foreach ($trackingforums as $track) {
-            $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
-            $params[] = $track->id;
-            if (isset($SESSION->currentgroup[$track->course])) {
-                $groupid =  $SESSION->currentgroup[$track->course];
-            } else {
-                // get first groupid
-                $groupids = groups_get_all_groups($track->course, $USER->id);
-                if ($groupids) {
-                    reset($groupids);
-                    $groupid = key($groupids);
-                    $SESSION->currentgroup[$track->course] = $groupid;
-                } else {
-                    $groupid = 0;
-                }
-                unset($groupids);
-            }
-            $params[] = $groupid;
-        }
-        $sql = substr($sql,0,-3); // take off the last OR
-        $sql .= ') AND p.modified >= ? AND r.id is NULL ';
-        $sql .= 'AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)) ';
-        $sql .= 'GROUP BY d.forum,d.course';
-        $params[] = $cutoffdate;
-        $params[] = time();
-        $params[] = time();
-
-        if (!$unread = $DB->get_records_sql($sql, $params)) {
-            $unread = array();
-        }
-    } else {
-        $unread = array();
-    }
-
-    if (empty($unread) and empty($forumsnewposts)) {
-        return;
-    }
-
-    $strforum = get_string('modulename','forum');
-
-    foreach ($forums as $forum) {
-        $str = '';
-        $count = 0;
-        $thisunread = 0;
-        $showunread = false;
-        // either we have something from logs, or trackposts, or nothing.
-        if (array_key_exists($forum->id, $forumsnewposts) && !empty($forumsnewposts[$forum->id])) {
-            $count = $forumsnewposts[$forum->id]->count;
-        }
-        if (array_key_exists($forum->id,$unread)) {
-            $thisunread = $unread[$forum->id]->count;
-            $showunread = true;
-        }
-        if ($count > 0 || $thisunread > 0) {
-            $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
-                $forum->name.'</a></div>';
-            $str .= '<div class="info"><span class="postsincelogin">';
-            $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
-            if (!empty($showunread)) {
-                $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
-            }
-            $str .= '</div></div>';
-        }
-        if (!empty($str)) {
-            if (!array_key_exists($forum->course,$htmlarray)) {
-                $htmlarray[$forum->course] = array();
-            }
-            if (!array_key_exists('forum',$htmlarray[$forum->course])) {
-                $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
-            }
-            $htmlarray[$forum->course]['forum'] .= $str;
-        }
-    }
+function forum_print_overview() {
+    throw new coding_exception('forum_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
index ec2fa05..49af7e2 100644 (file)
@@ -35,6 +35,8 @@ $name    = optional_param('name', '', PARAM_CLEAN);
 $confirm = optional_param('confirm', 0, PARAM_INT);
 $groupid = optional_param('groupid', null, PARAM_INT);
 $subject = optional_param('subject', '', PARAM_TEXT);
+
+// Values posted via the inpage reply form.
 $prefilledpost = optional_param('post', '', PARAM_TEXT);
 $prefilledpostformat = optional_param('postformat', FORMAT_MOODLE, PARAM_INT);
 $prefilledprivatereply = optional_param('privatereply', false, PARAM_BOOL);
@@ -778,6 +780,25 @@ $mformpost->set_data(
     (isset($discussion->id) ? array('discussion' => $discussion->id) : array())
 );
 
+// If we are being redirected via a no_submit_button press OR if the message is being prefilled.
+// then set the initial 'dirty' state.
+// - A prefilled post will exist when being redirected from the inpage reply form.
+// - A no_submit_button press occurs when being redirected from the inpage add new discussion post form.
+$dirty = $prefilledpost ? true : false;
+if ($mformpost->no_submit_button_pressed()) {
+    $data = $mformpost->get_submitted_data();
+
+    // If a no submit button has been pressed but the default values haven't been then reset the form change.
+    if (!$dirty && isset($data->message['text']) && !empty(trim($data->message['text']))) {
+        $dirty = true;
+    }
+
+    if (!$dirty && isset($data->message['message']) && !empty(trim($data->message['message']))) {
+        $dirty = true;
+    }
+}
+$mformpost->set_initial_dirty_state($dirty);
+
 if ($mformpost->is_cancelled()) {
     if (!isset($discussion->id) || $forum->type === 'single') {
         // Single forums don't have a discussion page.
index 93632b4..3db6787 100644 (file)
                                     <td scope="col" class="p-0 text-center align-middle">
                                         {{#unread}}
                                             {{! TODO Rewrite as AJAX}}
-                                            <div class="p-3 w-100 h-100 d-block">
+                                            <span class="p-1 w-100 h-100 d-block unread">
                                                 <a href="{{{discussion.urls.viewfirstunread}}}">{{unread}}</a>
                                                 <a href="{{{discussion.urls.markasread}}}">{{#pix}}t/markasread, core, {{#str}}markalldread, mod_forum{{/str}}{{/pix}}</a>
-                                            </div>
+                                            </span>
                                         {{/unread}}
                                         {{^unread}}
                                             <span class="p-3 w-100 h-100 d-block">
diff --git a/mod/forum/tests/behat/app_basic_usage.feature b/mod/forum/tests/behat/app_basic_usage.feature
deleted file mode 100644 (file)
index 1cd4db6..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-@mod @mod_forum @app @javascript
-Feature: Test basic usage in app
-  In order to participate in the forum while using the mobile app
-  As a student
-  I need basic forum functionality to work
-
-  Background:
-    Given the following "courses" exist:
-      | fullname | shortname |
-      | Course 1 | C1        |
-    And the following "users" exist:
-      | username |
-      | student1 |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student1 | C1     | student |
-    And the following "activities" exist:
-      | activity   | name            | intro       | course | idnumber | groupmode |
-      | forum      | Test forum name | Test forum  | C1     | forum    | 0         |
-
-  Scenario: Student starts a discussion
-    When I enter the app
-    And I log in as "student1"
-    And I press "Course 1" near "Course overview" in the app
-    And I press "Test forum name" in the app
-    And I press "Add a new discussion topic" in the app
-    And I set the field "Subject" to "My happy subject" in the app
-    And I set the field "Message" to "An awesome message" in the app
-    And I press "Post to forum" in the app
-    Then I should see "My happy subject"
-    And I should see "An awesome message"
-
-  Scenario: Student posts a reply
-    When I enter the app
-    And I log in as "student1"
-    And I press "Course 1" near "Course overview" in the app
-    And I press "Test forum name" in the app
-    And I press "Add a new discussion topic" in the app
-    And I set the field "Subject" to "DiscussionSubject" in the app
-    And I set the field "Message" to "DiscussionMessage" in the app
-    And I press "Post to forum" in the app
-    And I press "DiscussionSubject" in the app
-    And I press "Reply" in the app
-    And I set the field "Message" to "ReplyMessage" in the app
-    And I press "Post to forum" in the app
-    Then I should see "DiscussionMessage"
-    And I should see "ReplyMessage"
-
-  Scenario: Test that 'open in browser' works for forum
-    When I enter the app
-    And I change viewport size to "360x640"
-    And I log in as "student1"
-    And I press "Course 1" near "Course overview" in the app
-    And I press "Test forum name" in the app
-    And I press the page menu button in the app
-    And I press "Open in browser" in the app
-    And I switch to the browser tab opened by the app
-    And I log in as "student1"
-    Then I should see "Test forum name"
-    And I should see "Add a new discussion topic"
-    And I close the browser tab opened by the app
-    And I press the back button in the app
index 8b6fd97..97db997 100644 (file)
@@ -1691,6 +1691,92 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test add_discussion_post and auto subscription to a discussion.
+     */
+    public function test_add_discussion_post_subscribe_discussion() {
+        global $USER;
+
+        $this->resetAfterTest(true);
+
+        self::setAdminUser();
+
+        $user = self::getDataGenerator()->create_user();
+        $admin = get_admin();
+        // Create course to add the module.
+        $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
+
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        // Forum with tracking off.
+        $record = new stdClass();
+        $record->course = $course->id;
+        $forum = self::getDataGenerator()->create_module('forum', $record);
+        $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
+
+        // Add discussions to the forums.
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->userid = $admin->id;
+        $record->forum = $forum->id;
+        $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+        $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Try to post as user.
+        self::setUser($user);
+        // Enable auto subscribe discussion.
+        $USER->autosubscribe = true;
+        // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference enabled).
+        mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject', 'some text here...');
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(2, count($posts['posts']));
+        // The user should be subscribed to the discussion after adding a discussion post.
+        $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
+
+        // Disable auto subscribe discussion.
+        $USER->autosubscribe = false;
+        $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
+        // Add a discussion post in a forum discussion where the user is subscribed (auto-subscribe preference disabled).
+        mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject 1', 'some text here 1...');
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(3, count($posts['posts']));
+        // The user should still be subscribed to the discussion after adding a discussion post.
+        $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
+
+        $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
+        // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled).
+        mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...');
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(2, count($posts['posts']));
+        // The user should still not be subscribed to the discussion after adding a discussion post.
+        $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
+
+        // Passing a value for the discussionsubscribe option parameter.
+        $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
+        // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled),
+        // and the option parameter 'discussionsubscribe' => true in the webservice.
+        $option = array('name' => 'discussionsubscribe', 'value' => true);
+        $options[] = $option;
+        mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...',
+            $options);
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(3, count($posts['posts']));
+        // The user should now be subscribed to the discussion after adding a discussion post.
+        $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
+    }
+
     /*
      * Test add_discussion. A basic test since all the API functions are already covered by unit tests.
      */
index 80e2773..8c696fe 100644 (file)
@@ -2524,273 +2524,6 @@ class mod_forum_lib_testcase extends advanced_testcase {
         $this->assertArrayHasKey('forumdiscussions', $nodes->getValue($tree));
     }
 
-    public function test_print_overview() {
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-        $course2 = self::getDataGenerator()->create_course();
-
-        // Create an author user.
-        $author = self::getDataGenerator()->create_user();
-        $this->getDataGenerator()->enrol_user($author->id, $course1->id);
-        $this->getDataGenerator()->enrol_user($author->id, $course2->id);
-
-        // Create a viewer user.
-        $viewer = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
-        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
-
-        // Create two forums - one in each course.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course1->id));
-        $forum2 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course2->id));
-
-        // A standard post in the forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $record->userid = $author->id;
-        $record->forum = $forum1->id;
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $this->setUser($viewer->id);
-        $courses = array(
-            $course1->id => clone $course1,
-            $course2->id => clone $course2,
-        );
-
-        foreach ($courses as $courseid => $course) {
-            $courses[$courseid]->lastaccess = 0;
-        }
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        // There should be one entry for course1, and no others.
-        $this->assertCount(1, $results);
-
-        // There should be one entry for a forum in course1.
-        $this->assertCount(1, $results[$course1->id]);
-        $this->assertArrayHasKey('forum', $results[$course1->id]);
-    }
-
-    public function test_print_overview_groups() {
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
-        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
-
-        // Create an author user.
-        $author = self::getDataGenerator()->create_user();
-        $this->getDataGenerator()->enrol_user($author->id, $course1->id);
-
-        // Create two viewer users - one in each group.
-        $viewer1 = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer1->id, $course1->id);
-        $this->getDataGenerator()->create_group_member(array('userid' => $viewer1->id, 'groupid' => $group1->id));
-
-        $viewer2 = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer2->id, $course1->id);
-        $this->getDataGenerator()->create_group_member(array('userid' => $viewer2->id, 'groupid' => $group2->id));
-
-        // Create a forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array(
-            'course'        => $course1->id,
-            'groupmode'     => SEPARATEGROUPS,
-        ));
-
-        // A post in the forum for group1.
-        $record = new stdClass();
-        $record->course     = $course1->id;
-        $record->userid     = $author->id;
-        $record->forum      = $forum1->id;
-        $record->groupid    = $group1->id;
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $course1->lastaccess = 0;
-        $courses = array($course1->id => $course1);
-
-        // As viewer1 (same group as post).
-        $this->setUser($viewer1->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        // There should be one entry for course1.
-        $this->assertCount(1, $results);
-
-        // There should be one entry for a forum in course1.
-        $this->assertCount(1, $results[$course1->id]);
-        $this->assertArrayHasKey('forum', $results[$course1->id]);
-
-        $this->setUser($viewer2->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        // There should be one entry for course1.
-        $this->assertCount(0, $results);
-    }
-
-    /**
-     * @dataProvider print_overview_timed_provider
-     */
-    public function test_print_overview_timed($config, $hasresult) {
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-
-        // Create an author user.
-        $author = self::getDataGenerator()->create_user();
-        $this->getDataGenerator()->enrol_user($author->id, $course1->id);
-
-        // Create a viewer user.
-        $viewer = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
-
-        // Create a forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course1->id));
-
-        // A timed post with a timestart in the past (24 hours ago).
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $record->userid = $author->id;
-        $record->forum = $forum1->id;
-        if (isset($config['timestartmodifier'])) {
-            $record->timestart = time() + $config['timestartmodifier'];
-        }
-        if (isset($config['timeendmodifier'])) {
-            $record->timeend = time() + $config['timeendmodifier'];
-        }
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $course1->lastaccess = 0;
-        $courses = array($course1->id => $course1);
-
-        // As viewer, check the forum_print_overview result.
-        $this->setUser($viewer->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        if ($hasresult) {
-            // There should be one entry for course1.
-            $this->assertCount(1, $results);
-
-            // There should be one entry for a forum in course1.
-            $this->assertCount(1, $results[$course1->id]);
-            $this->assertArrayHasKey('forum', $results[$course1->id]);
-        } else {
-            // There should be no entries for any course.
-            $this->assertCount(0, $results);
-        }
-    }
-
-    /**
-     * @dataProvider print_overview_timed_provider
-     */
-    public function test_print_overview_timed_groups($config, $hasresult) {
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
-        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
-
-        // Create an author user.
-        $author = self::getDataGenerator()->create_user();
-        $this->getDataGenerator()->enrol_user($author->id, $course1->id);
-
-        // Create two viewer users - one in each group.
-        $viewer1 = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer1->id, $course1->id);
-        $this->getDataGenerator()->create_group_member(array('userid' => $viewer1->id, 'groupid' => $group1->id));
-
-        $viewer2 = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer2->id, $course1->id);
-        $this->getDataGenerator()->create_group_member(array('userid' => $viewer2->id, 'groupid' => $group2->id));
-
-        // Create a forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array(
-            'course'        => $course1->id,
-            'groupmode'     => SEPARATEGROUPS,
-        ));
-
-        // A post in the forum for group1.
-        $record = new stdClass();
-        $record->course     = $course1->id;
-        $record->userid     = $author->id;
-        $record->forum      = $forum1->id;
-        $record->groupid    = $group1->id;
-        if (isset($config['timestartmodifier'])) {
-            $record->timestart = time() + $config['timestartmodifier'];
-        }
-        if (isset($config['timeendmodifier'])) {
-            $record->timeend = time() + $config['timeendmodifier'];
-        }
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $course1->lastaccess = 0;
-        $courses = array($course1->id => $course1);
-
-        // As viewer1 (same group as post).
-        $this->setUser($viewer1->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        if ($hasresult) {
-            // There should be one entry for course1.
-            $this->assertCount(1, $results);
-
-            // There should be one entry for a forum in course1.
-            $this->assertCount(1, $results[$course1->id]);
-            $this->assertArrayHasKey('forum', $results[$course1->id]);
-        } else {
-            // There should be no entries for any course.
-            $this->assertCount(0, $results);
-        }
-
-        $this->setUser($viewer2->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        // There should be one entry for course1.
-        $this->assertCount(0, $results);
-    }
-
-    public function print_overview_timed_provider() {
-        return array(
-            'timestart_past' => array(
-                'discussionconfig' => array(
-                    'timestartmodifier' => -86000,
-                ),
-                'hasresult'         => true,
-            ),
-            'timestart_future' => array(
-                'discussionconfig' => array(
-                    'timestartmodifier' => 86000,
-                ),
-                'hasresult'         => false,
-            ),
-            'timeend_past' => array(
-                'discussionconfig' => array(
-                    'timeendmodifier'   => -86000,
-                ),
-                'hasresult'         => false,
-            ),
-            'timeend_future' => array(
-                'discussionconfig' => array(
-                    'timeendmodifier'   => 86000,
-                ),
-                'hasresult'         => true,
-            ),
-        );
-    }
-
     /**
      * Test test_pinned_discussion_with_group.
      */
index 4b1654c..7688acb 100644 (file)
@@ -137,6 +137,7 @@ class mod_forum_mail_testcase extends advanced_testcase {
     }
 
     public function test_forced_subscription() {
+        global $DB;
         $this->resetAfterTest(true);
 
         // Create a course, with a forum.
@@ -145,8 +146,14 @@ class mod_forum_mail_testcase extends advanced_testcase {
         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE);
         $forum = $this->getDataGenerator()->create_module('forum', $options);
 
-        // Create two users enrolled in the course as students.
-        list($author, $recipient) = $this->helper_create_users($course, 2);
+        // Create users enrolled in the course as students.
+        list($author, $recipient, $unconfirmed, $deleted) = $this->helper_create_users($course, 4);
+
+        // Make the third user unconfirmed (thence inactive) to make sure it does not break the notifications.
+        $DB->set_field('user', 'confirmed', 0, ['id' => $unconfirmed->id]);
+
+        // Mark the fourth user as deleted to make sure it does not break the notifications.
+        $DB->set_field('user', 'deleted', 1, ['id' => $deleted->id]);
 
         // Post a discussion to the forum.
         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
@@ -160,11 +167,21 @@ class mod_forum_mail_testcase extends advanced_testcase {
                 'userid' => $recipient->id,
                 'messages' => 1,
             ],
+            (object) [
+                'userid' => $unconfirmed->id,
+                'messages' => 0,
+            ],
+            (object) [
+                'userid' => $deleted->id,
+                'messages' => 0,
+            ],
         ];
         $this->queue_tasks_and_assert($expect);
 
         $this->send_notifications_and_assert($author, [$post]);
         $this->send_notifications_and_assert($recipient, [$post]);
+        $this->send_notifications_and_assert($unconfirmed, []);
+        $this->send_notifications_and_assert($deleted, []);
     }
 
     /**
diff --git a/mod/lesson/lang/en/deprecated.txt b/mod/lesson/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..fa96fa1
--- /dev/null
@@ -0,0 +1,4 @@
+additionalattemptsremaining,mod_lesson
+lessoncloseson,mod_lesson
+lessonname,mod_lesson
+xattempts,mod_lesson
\ No newline at end of file
index 62eb108..0e0b753 100644 (file)
@@ -55,7 +55,6 @@ $string['addmultichoice'] = 'Create a Multichoice question page';
 $string['addnewgroupoverride'] = 'Add group override';
 $string['addnewuseroverride'] = 'Add user override';
 $string['addnumerical'] = 'Create a Numerical question page';
-$string['additionalattemptsremaining'] = 'Completed, You can re-attempt this lesson';
 $string['addpage'] = 'Add a page';
 $string['addshortanswer'] = 'Create a Short answer question page';
 $string['addtruefalse'] = 'Create a True/false question page';
@@ -284,14 +283,12 @@ $string['lesson:grade'] = 'Grade lesson essay questions';
 $string['lessonclosed'] = 'This lesson closed on {$a}.';
 $string['lessoncloses'] = 'Lesson closes';
 $string['lessoneventcloses'] = '{$a} closes';
-$string['lessoncloseson'] = 'Lesson closes on {$a}';
 $string['lesson:edit'] = 'Edit a lesson activity';
 $string['lessonformating'] = 'Lesson formatting';
 $string['lesson:manage'] = 'Manage a lesson activity';
 $string['lesson:manageoverrides'] = 'Manage lesson overrides';
 $string['lesson:view'] = 'View lesson activity';
 $string['lesson:viewreports'] = 'View lesson reports';
-$string['lessonname'] = 'Lesson: {$a}';
 $string['lessonmenu'] = 'Lesson menu';
 $string['lessonnotready'] = 'This lesson is not ready to be taken.  Please contact your {$a}.';
 $string['lessonnotready2'] = 'This lesson is not ready to be taken.';
@@ -584,10 +581,14 @@ $string['whatdofirst'] = 'What would you like to do first?';
 $string['wronganswerjump'] = 'Wrong answer jump';
 $string['wronganswerscore'] = 'Wrong answer score';
 $string['wrongresponse'] = 'Wrong response';
-$string['xattempts'] = '{$a} attempts';
 $string['youhaveseen'] = 'You have seen more than one page of this lesson already.<br />Do you want to start at the last page you saw?';
 $string['youranswer'] = 'Your answer';
 $string['yourcurrentgradeis'] = 'Your current grade is {$a}';
 $string['yourcurrentgradeisoutof'] = 'Your current grade is {$a->grade} out of {$a->total}';
 $string['youshouldview'] = 'You should answer at least: {$a}';
 
+// Deprecated since Moodle 3.8.
+$string['additionalattemptsremaining'] = 'Completed, You can re-attempt this lesson';
+$string['lessoncloseson'] = 'Lesson closes on {$a}';
+$string['lessonname'] = 'Lesson: {$a}';
+$string['xattempts'] = '{$a} attempts';
index 2adefdb..6c509b5 100644 (file)
@@ -553,185 +553,10 @@ function lesson_user_complete($course, $user, $mod, $lesson) {
 }
 
 /**
- * Prints lesson summaries on MyMoodle Page
- *
- * Prints lesson name, due date and attempt information on
- * lessons that have a deadline that has not already passed
- * and it is available for taking.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @global object
- * @global stdClass
- * @global object
- * @uses CONTEXT_MODULE
- * @param array $courses An array of course objects to get lesson instances from
- * @param array $htmlarray Store overview output array( course ID => 'lesson' => HTML output )
- * @return void
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function lesson_print_overview($courses, &$htmlarray) {
-    global $USER, $CFG, $DB, $OUTPUT;
-
-    debugging('The function lesson_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (!$lessons = get_all_instances_in_courses('lesson', $courses)) {
-        return;
-    }
-
-    // Get all of the current users attempts on all lessons.
-    $params = array($USER->id);
-    $sql = 'SELECT lessonid, userid, count(userid) as attempts
-              FROM {lesson_grades}
-             WHERE userid = ?
-          GROUP BY lessonid, userid';
-    $allattempts = $DB->get_records_sql($sql, $params);
-    $completedattempts = array();
-    foreach ($allattempts as $myattempt) {
-        $completedattempts[$myattempt->lessonid] = $myattempt->attempts;
-    }
-
-    // Get the current course ID.
-    $listoflessons = array();
-    foreach ($lessons as $lesson) {
-        $listoflessons[] = $lesson->id;
-    }
-    // Get the last page viewed by the current user for every lesson in this course.
-    list($insql, $inparams) = $DB->get_in_or_equal($listoflessons, SQL_PARAMS_NAMED);
-    $dbparams = array_merge($inparams, array('userid' => $USER->id));
-
-    // Get the lesson attempts for the user that have the maximum 'timeseen' value.
-    $select = "SELECT l.id, l.timeseen, l.lessonid, l.userid, l.retry, l.pageid, l.answerid as nextpageid, p.qtype ";
-    $from = "FROM {lesson_attempts} l
-             JOIN (
-                   SELECT idselect.lessonid, idselect.userid, MAX(idselect.id) AS id
-                     FROM {lesson_attempts} idselect
-                     JOIN (
-                           SELECT lessonid, userid, MAX(timeseen) AS timeseen
-                             FROM {lesson_attempts}
-                            WHERE userid = :userid
-                              AND lessonid $insql
-                         GROUP BY userid, lessonid
-                           ) timeselect
-                       ON timeselect.timeseen = idselect.timeseen
-                      AND timeselect.userid = idselect.userid
-                      AND timeselect.lessonid = idselect.lessonid
-                 GROUP BY idselect.userid, idselect.lessonid
-                   ) aid
-               ON l.id = aid.id
-             JOIN {lesson_pages} p
-               ON l.pageid = p.id ";
-    $lastattempts = $DB->get_records_sql($select . $from, $dbparams);
-
-    // Now, get the lesson branches for the user that have the maximum 'timeseen' value.
-    $select = "SELECT l.id, l.timeseen, l.lessonid, l.userid, l.retry, l.pageid, l.nextpageid, p.qtype ";
-    $from = str_replace('{lesson_attempts}', '{lesson_branch}', $from);
-    $lastbranches = $DB->get_records_sql($select . $from, $dbparams);
-
-    $lastviewed = array();
-    foreach ($lastattempts as $lastattempt) {
-        $lastviewed[$lastattempt->lessonid] = $lastattempt;
-    }
-
-    // Go through the branch times and record the 'timeseen' value if it doesn't exist
-    // for the lesson, or replace it if it exceeds the current recorded time.
-    foreach ($lastbranches as $lastbranch) {
-        if (!isset($lastviewed[$lastbranch->lessonid])) {
-            $lastviewed[$lastbranch->lessonid] = $lastbranch;
-        } else if ($lastviewed[$lastbranch->lessonid]->timeseen < $lastbranch->timeseen) {
-            $lastviewed[$lastbranch->lessonid] = $lastbranch;
-        }
-    }
-
-    // Since we have lessons in this course, now include the constants we need.
-    require_once($CFG->dirroot . '/mod/lesson/locallib.php');
-
-    $now = time();
-    foreach ($lessons as $lesson) {
-        if ($lesson->deadline != 0                                         // The lesson has a deadline
-            and $lesson->deadline >= $now                                  // And it is before the deadline has been met
-            and ($lesson->available == 0 or $lesson->available <= $now)) { // And the lesson is available
-
-            // Visibility.
-            $class = (!$lesson->visible) ? 'dimmed' : '';
-
-            // Context.
-            $context = context_module::instance($lesson->coursemodule);
-
-            // Link to activity.
-            $url = new moodle_url('/mod/lesson/view.php', array('id' => $lesson->coursemodule));
-            $url = html_writer::link($url, format_string($lesson->name, true, array('context' => $context)), array('class' => $class));
-            $str = $OUTPUT->box(get_string('lessonname', 'lesson', $url), 'name');
-
-            // Deadline.
-            $str .= $OUTPUT->box(get_string('lessoncloseson', 'lesson', userdate($lesson->deadline)), 'info');
-
-            // Attempt information.
-            if (has_capability('mod/lesson:manage', $context)) {
-                // This is a teacher, Get the Number of user attempts.
-                $attempts = $DB->count_records('lesson_grades', array('lessonid' => $lesson->id));
-                $str     .= $OUTPUT->box(get_string('xattempts', 'lesson', $attempts), 'info');
-                $str      = $OUTPUT->box($str, 'lesson overview');
-            } else {
-                // This is a student, See if the user has at least started the lesson.
-                if (isset($lastviewed[$lesson->id]->timeseen)) {
-                    // See if the user has finished this attempt.
-                    if (isset($completedattempts[$lesson->id]) &&
-                             ($completedattempts[$lesson->id] == ($lastviewed[$lesson->id]->retry + 1))) {
-                        // Are additional attempts allowed?
-                        if ($lesson->retake) {
-                            // User can retake the lesson.
-                            $str .= $OUTPUT->box(get_string('additionalattemptsremaining', 'lesson'), 'info');
-                            $str = $OUTPUT->box($str, 'lesson overview');
-                        } else {
-                            // User has completed the lesson and no retakes are allowed.
-                            $str = '';
-                        }
-
-                    } else {
-                        // The last attempt was not finished or the lesson does not contain questions.
-                        // See if the last page viewed was a branchtable.
-                        require_once($CFG->dirroot . '/mod/lesson/pagetypes/branchtable.php');
-                        if ($lastviewed[$lesson->id]->qtype == LESSON_PAGE_BRANCHTABLE) {
-                            // See if the next pageid is the end of lesson.
-                            if ($lastviewed[$lesson->id]->nextpageid == LESSON_EOL) {
-                                // The last page viewed was the End of Lesson.
-                                if ($lesson->retake) {
-                                    // User can retake the lesson.
-                                    $str .= $OUTPUT->box(get_string('additionalattemptsremaining', 'lesson'), 'info');
-                                    $str = $OUTPUT->box($str, 'lesson overview');
-                                } else {
-                                    // User has completed the lesson and no retakes are allowed.
-                                    $str = '';
-                                }
-
-                            } else {
-                                // The last page viewed was NOT the end of lesson.
-                                $str .= $OUTPUT->box(get_string('notyetcompleted', 'lesson'), 'info');
-                                $str = $OUTPUT->box($str, 'lesson overview');
-                            }
-
-                        } else {
-                            // Last page was a question page, so the attempt is not completed yet.
-                            $str .= $OUTPUT->box(get_string('notyetcompleted', 'lesson'), 'info');
-                            $str = $OUTPUT->box($str, 'lesson overview');
-                        }
-                    }
-
-                } else {
-                    // User has not yet started this lesson.
-                    $str .= $OUTPUT->box(get_string('nolessonattempts', 'lesson'), 'info');
-                    $str = $OUTPUT->box($str, 'lesson overview');
-                }
-            }
-            if (!empty($str)) {
-                if (empty($htmlarray[$lesson->course]['lesson'])) {
-                    $htmlarray[$lesson->course]['lesson'] = $str;
-                } else {
-                    $htmlarray[$lesson->course]['lesson'] .= $str;
-                }
-            }
-        }
-    }
+function lesson_print_overview() {
+    throw new coding_exception('lesson_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
diff --git a/mod/lti/db/subplugins.json b/mod/lti/db/subplugins.json
new file mode 100644 (file)
index 0000000..35f0d16
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "plugintypes": {
+        "ltisource": "mod\/lti\/source",
+        "ltiservice": "mod\/lti\/service"
+    }
+}
index d5244d2..13967c3 100644 (file)
@@ -24,7 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array(
-    'ltisource' => 'mod/lti/source',
-    'ltiservice' => 'mod/lti/service'
-);
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index 87d2943..2df8276 100644 (file)
@@ -2121,7 +2121,8 @@ class quiz_attempt {
                 // First question on page, go to top.
                 $fragment = '#';
             } else {
-                $fragment = '#q' . $slot;
+                $qa = $this->get_question_attempt($slot);
+                $fragment = '#' . $qa->get_outer_question_div_unique_id();
             }
         }
 
diff --git a/mod/quiz/db/subplugins.json b/mod/quiz/db/subplugins.json
new file mode 100644 (file)
index 0000000..d0d0c2c
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "plugintypes": {
+        "quiz": "mod\/quiz\/report",
+        "quizaccess": "mod\/quiz\/accessrule"
+    }
+}
index efcc67d..951d92d 100644 (file)
@@ -24,7 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array(
-    'quiz' => 'mod/quiz/report',
-    'quizaccess' => 'mod/quiz/accessrule',
-);
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index e69de29..483774c 100644 (file)
@@ -0,0 +1 @@
+numattemptsmade,mod_quiz
\ No newline at end of file
index bed8995..503021d 100644 (file)
@@ -558,7 +558,6 @@ $string['notyetviewed'] = 'Not yet viewed';
 $string['notyourattempt'] = 'This is not your attempt!';
 $string['noview'] = 'Logged-in user is not allowed to view this quiz';
 $string['numattempts'] = '{$a->studentnum} {$a->studentstring} have made {$a->attemptnum} attempts';
-$string['numattemptsmade'] = '{$a} attempts made on this quiz';
 $string['numberabbr'] = '#';
 $string['numerical'] = 'Numerical';
 $string['numquestionsx'] = 'Questions: {$a}';
@@ -989,3 +988,6 @@ $string['wronguse'] = 'You can not use this page like that';
 $string['xhtml'] = 'XHTML';
 $string['youneedtoenrol'] = 'You need to enrol in this course before you can attempt this quiz';
 $string['yourfinalgradeis'] = 'Your final grade for this quiz is {$a}.';
+
+// Deprecated since Moodle 3.8.
+$string['numattemptsmade'] = '{$a} attempts made on this quiz';
index 9a3baa5..9ece885 100644 (file)
@@ -1576,103 +1576,10 @@ function quiz_reset_userdata($data) {
 }
 
 /**
- * Prints quiz summaries on MyMoodle Page
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @param array $courses
- * @param array $htmlarray
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function quiz_print_overview($courses, &$htmlarray) {
-    global $USER, $CFG;
-
-    debugging('The function quiz_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    // These next 6 Lines are constant in all modules (just change module name).
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return array();
-    }
-
-    if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
-        return;
-    }
-
-    // Get the quizzes attempts.
-    $attemptsinfo = [];
-    $quizids = [];
-    foreach ($quizzes as $quiz) {
-        $quizids[] = $quiz->id;
-        $attemptsinfo[$quiz->id] = ['count' => 0, 'hasfinished' => false];
-    }
-    $attempts = quiz_get_user_attempts($quizids, $USER->id);
-    foreach ($attempts as $attempt) {
-        $attemptsinfo[$attempt->quiz]['count']++;
-        $attemptsinfo[$attempt->quiz]['hasfinished'] = true;
-    }
-    unset($attempts);
-
-    // Fetch some language strings outside the main loop.
-    $strquiz = get_string('modulename', 'quiz');
-    $strnoattempts = get_string('noattempts', 'quiz');
-
-    // We want to list quizzes that are currently available, and which have a close date.
-    // This is the same as what the lesson does, and the dabate is in MDL-10568.
-    $now = time();
-    foreach ($quizzes as $quiz) {
-        if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
-            $str = '';
-
-            // Now provide more information depending on the uers's role.
-            $context = context_module::instance($quiz->coursemodule);
-            if (has_capability('mod/quiz:viewreports', $context)) {
-                // For teacher-like people, show a summary of the number of student attempts.
-                // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
-                // fields set to make the following call work.
-                $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
-
-            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
-                // For student-like people, tell them how many attempts they have made.
-
-                if (isset($USER->id)) {
-                    if ($attemptsinfo[$quiz->id]['hasfinished']) {
-                        // The student's last attempt is finished.
-                        continue;
-                    }
-
-                    if ($attemptsinfo[$quiz->id]['count'] > 0) {
-                        $str .= '<div class="info">' .
-                            get_string('numattemptsmade', 'quiz', $attemptsinfo[$quiz->id]['count']) . '</div>';
-                    } else {
-                        $str .= '<div class="info">' . $strnoattempts . '</div>';
-                    }
-
-                } else {
-                    $str .= '<div class="info">' . $strnoattempts . '</div>';
-                }
-
-            } else {
-                // For ayone else, there is no point listing this quiz, so stop processing.
-                continue;
-            }
-
-            // Give a link to the quiz, and the deadline.
-            $html = '<div class="quiz overview">' .
-                    '<div class="name">' . $strquiz . ': <a ' .
-                    ($quiz->visible ? '' : ' class="dimmed"') .
-                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
-                    $quiz->coursemodule . '">' .
-                    $quiz->name . '</a></div>';
-            $html .= '<div class="info">' . get_string('quizcloseson', 'quiz',
-                    userdate($quiz->timeclose)) . '</div>';
-            $html .= $str;
-            $html .= '</div>';
-            if (empty($htmlarray[$quiz->course]['quiz'])) {
-                $htmlarray[$quiz->course]['quiz'] = $html;
-            } else {
-                $htmlarray[$quiz->course]['quiz'] .= $html;
-            }
-        }
-    }
+function quiz_print_overview() {
+    throw new coding_exception('quiz_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
index b9c2415..ebadab8 100644 (file)
@@ -28,281 +28,192 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 
-
 /**
- * Subclass of quiz_attempt to allow faking of the page layout.
+ * Tests for the quiz_attempt class.
  *
  * @copyright 2014 Tim Hunt
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mod_quiz_attempt_testable extends quiz_attempt {
-    /** @var array list of slots to treat as if they contain descriptions in the fake layout. */
-    protected $infos = array();
+class mod_quiz_attempt_testcase extends advanced_testcase {
 
     /**
-     * Set a fake page layout. Used when we test URL generation.
-     * @param int $id assumed attempt id.
+     * Create quiz and attempt data with layout.
+     *
      * @param string $layout layout to set. Like quiz attempt.layout. E.g. '1,2,0,3,4,0,'.
-     * @param array $infos slot numbers which contain 'descriptions', or other non-questions.
-     * @return quiz_attempt attempt object for use in unit tests.
+     * @return quiz_attempt the new quiz_attempt object
      */
-    public static function setup_fake_attempt_layout($id, $layout, $infos = array()) {
-        $attempt = new stdClass();
-        $attempt->id = $id;
-        $attempt->layout = $layout;
+    protected function create_quiz_and_attempt_with_layout($layout) {
+        $this->resetAfterTest(true);
+
+        // Make a user to do the quiz.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        // Make a quiz.
+        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+        $quiz = $quizgenerator->create_instance(['course' => $course->id,
+            'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout]);
+
+        $quizobj = quiz::create($quiz->id, $user->id);
 
-        $course = new stdClass();
-        $quiz = new stdClass();
-        $cm = new stdClass();
-        $cm->id = 0;
 
-        $attemptobj = new self($attempt, $quiz, $cm, $course, false);
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
 
-        $attemptobj->slots = array();
+        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $questiongenerator->create_question_category();
+
+        $page = 1;
         foreach (explode(',', $layout) as $slot) {
             if ($slot == 0) {
+                $page += 1;
                 continue;
             }
-            $attemptobj->slots[$slot] = new stdClass();
-            $attemptobj->slots[$slot]->slot = $slot;
-            $attemptobj->slots[$slot]->requireprevious = 0;
-            $attemptobj->slots[$slot]->questionid = 0;
-        }
-
-        $attemptobj->sections = array();
-        $attemptobj->sections[0] = new stdClass();
-        $attemptobj->sections[0]->heading = '';
-        $attemptobj->sections[0]->firstslot = 1;
-        $attemptobj->sections[0]->shufflequestions = 0;
 
-        $attemptobj->infos = $infos;
-        $attemptobj->link_sections_and_slots();
-        $attemptobj->determine_layout();
-        $attemptobj->number_questions();
+            $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
+            quiz_add_quiz_question($question->id, $quiz, $page);
+        }
 
-        return $attemptobj;
-    }
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user->id);
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj, $quba, $attempt);
 
-    public function is_real_question($slot) {
-        return !in_array($slot, $this->infos);
+        return quiz_attempt::create($attempt->id);
     }
-}
-
 
-/**
- * Tests for the quiz_attempt class.
- *
- * @copyright 2014 Tim Hunt
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class mod_quiz_attempt_testcase extends advanced_testcase {
     /**
      * Test the functions quiz_update_open_attempts() and get_list_of_overdue_attempts()
      */
     public function test_attempt_url() {
-        $attempt = mod_quiz_attempt_testable::setup_fake_attempt_layout(
-                123, '1,2,0,3,4,0,5,6,0');
+        $attempt = $this->create_quiz_and_attempt_with_layout('1,2,0,3,4,0,5,6,0');
 
-        // Attempt pages.
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/attempt.php?attempt=123&cmid=0'),
-                $attempt->attempt_url());
+        $attemptid = $attempt->get_attempt()->id;
+        $cmid = $attempt->get_cmid();
+        $url = '/mod/quiz/attempt.php';
+        $params = ['attempt' => $attemptid, 'cmid' => $cmid, 'page' => 2];
 
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/attempt.php?attempt=123&page=2&cmid=0'),
-                $attempt->attempt_url(null, 2));
+        $this->assertEquals(new moodle_url($url, $params), $attempt->attempt_url(null, 2));
 
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/attempt.php?attempt=123&page=1&cmid=0#'),
-                $attempt->attempt_url(3));
+        $params['page'] = 1;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->attempt_url(3));
 
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/attempt.php?attempt=123&page=1&cmid=0#q4'),
-                $attempt->attempt_url(4));
+        $questionattempt = $attempt->get_question_attempt(4);
+        $expecteanchor = $questionattempt->get_outer_question_div_unique_id();
+        $this->assertEquals(new moodle_url($url, $params, $expecteanchor), $attempt->attempt_url(4));
 
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->attempt_url(null, 2, 2));
+        $this->assertEquals(new moodle_url('#'), $attempt->attempt_url(null, 2, 2));
+        $this->assertEquals(new moodle_url('#'), $attempt->attempt_url(3, -1, 1));
 
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->attempt_url(3, -1, 1));
-
-        $this->assertEquals(new moodle_url(
-                '#q4'),
-                $attempt->attempt_url(4, -1, 1));
+        $questionattempt = $attempt->get_question_attempt(4);
+        $expecteanchor = $questionattempt->get_outer_question_div_unique_id();
+        $this->assertEquals(new moodle_url(null, null, $expecteanchor, null), $attempt->attempt_url(4, -1, 1));
 
         // Summary page.
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/summary.php?attempt=123&cmid=0'),
-                $attempt->summary_url());
+        $url = '/mod/quiz/summary.php';
+        unset($params['page']);
+        $this->assertEquals(new moodle_url($url, $params), $attempt->summary_url());
 
         // Review page.
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&cmid=0'),
-                $attempt->review_url());
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&page=2&cmid=0'),
-                $attempt->review_url(null, 2));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&page=1&cmid=0'),
-                $attempt->review_url(3, -1, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&page=1&cmid=0#q4'),
-                $attempt->review_url(4, -1, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&cmid=0'),
-                $attempt->review_url(null, 2, true));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&cmid=0'),
-                $attempt->review_url(1, -1, true));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&page=2&cmid=0'),
-                $attempt->review_url(null, 2, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&showall=0&cmid=0'),
-                $attempt->review_url(null, 0, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&showall=0&cmid=0'),
-                $attempt->review_url(1, -1, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&page=1&cmid=0'),
-                $attempt->review_url(3, -1, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&page=2&cmid=0'),
-                $attempt->review_url(null, 2));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(null, -1, null, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#q3'),
-                $attempt->review_url(3, -1, null, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#q4'),
-                $attempt->review_url(4, -1, null, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(null, 2, true, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(1, -1, true, 0));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&page=2&cmid=0'),
-                $attempt->review_url(null, 2, false, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(null, 0, false, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(1, -1, false, 0));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=123&page=1&cmid=0#'),
-                $attempt->review_url(3, -1, false, 0));
-
-        // Review with more than 50 questions in the quiz.
-        $attempt = mod_quiz_attempt_testable::setup_fake_attempt_layout(
-                124, '1,2,3,4,5,6,7,8,9,10,0,11,12,13,14,15,16,17,18,19,20,0,' .
-                '21,22,23,24,25,26,27,28,29,30,0,31,32,33,34,35,36,37,38,39,40,0,' .
-                '41,42,43,44,45,46,47,48,49,50,0,51,52,53,54,55,56,57,58,59,60,0');
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&cmid=0'),
-                $attempt->review_url());
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=2&cmid=0'),
-                $attempt->review_url(null, 2));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=1&cmid=0'),
-                $attempt->review_url(11, -1, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=1&cmid=0#q12'),
-                $attempt->review_url(12, -1, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&showall=1&cmid=0'),
-                $attempt->review_url(null, 2, true));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&showall=1&cmid=0'),
-                $attempt->review_url(1, -1, true));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=2&cmid=0'),
-                $attempt->review_url(null, 2, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&cmid=0'),
-                $attempt->review_url(null, 0, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=1&cmid=0'),
-                $attempt->review_url(11, -1, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=1&cmid=0#q12'),
-                $attempt->review_url(12, -1, false));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=2&cmid=0'),
-                $attempt->review_url(null, 2));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(null, -1, null, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#q3'),
-                $attempt->review_url(3, -1, null, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#q4'),
-                $attempt->review_url(4, -1, null, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(null, 2, true, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(1, -1, true, 0));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=2&cmid=0'),
-                $attempt->review_url(null, 2, false, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(null, 0, false, 0));
-
-        $this->assertEquals(new moodle_url(
-                '#'),
-                $attempt->review_url(1, -1, false, 0));
-
-        $this->assertEquals(new moodle_url(
-                '/mod/quiz/review.php?attempt=124&page=1&cmid=0#'),
-                $attempt->review_url(11, -1, false, 0));
+        $url = '/mod/quiz/review.php';
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url());
+
+        $params['page'] = 1;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(3, -1, false));
+        $this->assertEquals(new moodle_url($url, $params, $expecteanchor), $attempt->review_url(4, -1, false));
+
+        unset($params['page']);
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 2, true));
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(1, -1, true));
+
+        $params['page'] = 2;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 2, false));
+        unset($params['page']);
+
+        $params['showall'] = 0;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 0, false));
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(1, -1, false));
+
+        $params['page'] = 1;
+        unset($params['showall']);
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(3, -1, false));
+
+        $params['page'] = 2;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 2));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(null, -1, null, 0));
+
+        $questionattempt = $attempt->get_question_attempt(3);
+        $expecteanchor = '#' . $questionattempt->get_outer_question_div_unique_id();
+        $this->assertEquals(new moodle_url($expecteanchor), $attempt->review_url(3, -1, null, 0));
+
+        $questionattempt = $attempt->get_question_attempt(4);
+        $expecteanchor = '#' . $questionattempt->get_outer_question_div_unique_id();
+        $this->assertEquals(new moodle_url($expecteanchor), $attempt->review_url(4, -1, null, 0));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(null, 2, true, 0));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(1, -1, true, 0));
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 2, false, 0));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(null, 0, false, 0));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(1, -1, false, 0));
+
+        $params['page'] = 1;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(3, -1, false, 0));
+
+        // Setup another attempt.
+        $attempt = $this->create_quiz_and_attempt_with_layout(
+            '1,2,3,4,5,6,7,8,9,10,0,11,12,13,14,15,16,17,18,19,20,0,' .
+            '21,22,23,24,25,26,27,28,29,30,0,31,32,33,34,35,36,37,38,39,40,0,' .
+            '41,42,43,44,45,46,47,48,49,50,0,51,52,53,54,55,56,57,58,59,60,0');
+
+        $attemptid = $attempt->get_attempt()->id;
+        $cmid = $attempt->get_cmid();
+        $params = ['attempt' => $attemptid, 'cmid' => $cmid];
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url());
+
+        $params['page'] = 2;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 2));
+
+        $params['page'] = 1;
+        unset($params['showall']);
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(11, -1, false));
+
+        $questionattempt = $attempt->get_question_attempt(12);
+        $expecteanchor = $questionattempt->get_outer_question_div_unique_id();
+        $this->assertEquals(new moodle_url($url, $params, $expecteanchor), $attempt->review_url(12, -1, false));
+
+        $params['showall'] = 1;
+        unset($params['page']);
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 2, true));
+
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(1, -1, true));
+        $params['page'] = 2;
+        unset($params['showall']);
+        $this->assertEquals(new moodle_url($url, $params),  $attempt->review_url(null, 2, false));
+        unset($params['page']);
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 0, false));
+        $params['page'] = 1;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(11, -1, false));
+        $this->assertEquals(new moodle_url($url, $params, $expecteanchor), $attempt->review_url(12, -1, false));
+        $params['page'] = 2;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 2));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(null, -1, null, 0));
+
+        $questionattempt = $attempt->get_question_attempt(3);
+        $expecteanchor = $questionattempt->get_outer_question_div_unique_id();
+        $this->assertEquals(new moodle_url(null, null, $expecteanchor), $attempt->review_url(3, -1, null, 0));
+
+        $questionattempt = $attempt->get_question_attempt(4);
+        $expecteanchor = $questionattempt->get_outer_question_div_unique_id();
+        $this->assertEquals(new moodle_url(null, null, $expecteanchor), $attempt->review_url(4, -1, null, 0));
+
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(null, 2, true, 0));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(1, -1, true, 0));
+
+        $params['page'] = 2;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(null, 2, false, 0));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(null, 0, false, 0));
+        $this->assertEquals(new moodle_url('#'), $attempt->review_url(1, -1, false, 0));
+
+        $params['page'] = 1;
+        $this->assertEquals(new moodle_url($url, $params), $attempt->review_url(11, -1, false, 0));
     }
 
     public function test_is_participant() {
@@ -339,4 +250,4 @@ class mod_quiz_attempt_testcase extends advanced_testcase {
         $this->assertEquals(true, $quizobj->is_participant($USER->id),
             'Admin is enrolled, suspended and can participate');
     }
-}
\ No newline at end of file
+}
index ca6bfe7..1771846 100644 (file)
@@ -39,11 +39,11 @@ require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
 class mod_quiz_events_testcase extends advanced_testcase {
 
     /**
-     * Setup some convenience test data with a single attempt.
+     * Setup a quiz.
      *
-     * @param bool $ispreview Make the attempt a preview attempt when true.
+     * @return quiz the generated quiz.
      */
-    protected function prepare_quiz_data($ispreview = false) {
+    protected function prepare_quiz() {
 
         $this->resetAfterTest(true);
 
@@ -53,8 +53,8 @@ class mod_quiz_events_testcase extends advanced_testcase {
         // Make a quiz.
         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 
-        $quiz = $quizgenerator->create_instance(array('course'=>$course->id, 'questionsperpage' => 0,
-            'grade' => 100.0, 'sumgrades' => 2));
+        $quiz = $quizgenerator->create_instance(array('course' => $course->id, 'questionsperpage' => 0,
+                'grade' => 100.0, 'sumgrades' => 2));
 
         $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id);
 
@@ -73,8 +73,17 @@ class mod_quiz_events_testcase extends advanced_testcase {
         $user1 = $this->getDataGenerator()->create_user();
         $this->setUser($user1);
 
-        $quizobj = quiz::create($quiz->id, $user1->id);
+        return quiz::create($quiz->id, $user1->id);
+    }
 
+    /**
+     * Setup a quiz attempt at the quiz created by {@link prepare_quiz()}.
+     *
+     * @param quiz $quizobj the generated quiz.
+     * @param bool $ispreview Make the attempt a preview attempt when true.
+     * @return array with three elements, array($quizobj, $quba, $attempt)
+     */
+    protected function prepare_quiz_attempt($quizobj, $ispreview = false) {
         // Start the attempt.
         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
@@ -87,6 +96,17 @@ class mod_quiz_events_testcase extends advanced_testcase {
         return array($quizobj, $quba, $attempt);
     }
 
+    /**
+     * Setup some convenience test data with a single attempt.
+     *
+     * @param bool $ispreview Make the attempt a preview attempt when true.
+     * @return array with three elements, array($quizobj, $quba, $attempt)
+     */
+    protected function prepare_quiz_data($ispreview = false) {
+        $quizobj = $this->prepare_quiz();
+        return $this->prepare_quiz_attempt($quizobj, $ispreview);
+    }
+
     public function test_attempt_submitted() {
 
         list($quizobj, $quba, $attempt) = $this->prepare_quiz_data();
@@ -194,10 +214,14 @@ class mod_quiz_events_testcase extends advanced_testcase {
     }
 
     public function test_attempt_started() {
-        list($quizobj, $quba, $attempt) = $this->prepare_quiz_data();
+        $quizobj = $this->prepare_quiz();
 
-        // Create another attempt.
-        $attempt = quiz_create_attempt($quizobj, 1, false, time(), false, 2);
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 
         // Trigger and capture the event.
         $sink = $this->redirectEvents();
@@ -666,11 +690,14 @@ class mod_quiz_events_testcase extends advanced_testcase {
      * Test the attempt previewed event.
      */
     public function test_attempt_preview_started() {
-        list($quizobj, $quba, $attempt) = $this->prepare_quiz_data();
+        $quizobj = $this->prepare_quiz();
 
-        // We want to preview this attempt.
-        $attempt = quiz_create_attempt($quizobj, 1, false, time(), false, 2);
-        $attempt->preview = 1;
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, true);
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
 
         // Trigger and capture the event.
         $sink = $this->redirectEvents();
diff --git a/mod/scorm/db/subplugins.json b/mod/scorm/db/subplugins.json
new file mode 100644 (file)
index 0000000..dd48250
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "scormreport": "mod\/scorm\/report"
+    }
+}
index ac3aa87..f39017f 100644 (file)
@@ -14,4 +14,5 @@
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-$subplugins = array('scormreport' => 'mod/scorm/report');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
diff --git a/mod/scorm/lang/en/deprecated.txt b/mod/scorm/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..32ff438
--- /dev/null
@@ -0,0 +1 @@
+duedate,mod_scorm
\ No newline at end of file
index 2ce6445..e844e5b 100644 (file)
@@ -121,7 +121,6 @@ $string['displaydesc'] = 'Whether to display the SCORM package in a new window.'
 $string['displaysettings'] = 'Display settings';
 $string['dnduploadscorm'] = 'Add a SCORM package';
 $string['domxml'] = 'DOMXML external library';
-$string['duedate'] = 'Due date';
 $string['element'] = 'Element';
 $string['enter'] = 'Enter';
 $string['entercourse'] = 'Enter course';
@@ -444,3 +443,6 @@ $string['whatgradedesc'] = 'Whether the highest, average (mean), first or last c
 $string['width'] = 'Width';
 $string['window'] = 'Window';
 $string['youmustselectastatus'] = 'You must select a status to require';
+
+// Deprecated since Moodle 3.8.
+$string['duedate'] = 'Due date';
index ae9672e..e256454 100644 (file)
@@ -1101,59 +1101,10 @@ function scorm_debug_log_remove($type, $scoid) {
 }
 
 /**
- * writes overview info for course_overview block - displays upcoming scorm objects that have a due date
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @param object $type - type of log(aicc,scorm12,scorm13) used as prefix for filename
- * @param array $htmlarray
- * @return mixed
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function scorm_print_overview($courses, &$htmlarray) {
-    global $USER, $CFG;
-
-    debugging('The function scorm_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return array();
-    }
-
-    if (!$scorms = get_all_instances_in_courses('scorm', $courses)) {
-        return;
-    }
-
-    $strscorm   = get_string('modulename', 'scorm');
-    $strduedate = get_string('duedate', 'scorm');
-
-    foreach ($scorms as $scorm) {
-        $time = time();
-        $showattemptstatus = false;
-        if ($scorm->timeopen) {
-            $isopen = ($scorm->timeopen <= $time && $time <= $scorm->timeclose);
-        }
-        if ($scorm->displayattemptstatus == SCORM_DISPLAY_ATTEMPTSTATUS_ALL ||
-                $scorm->displayattemptstatus == SCORM_DISPLAY_ATTEMPTSTATUS_MY) {
-            $showattemptstatus = true;
-        }
-        if ($showattemptstatus || !empty($isopen) || !empty($scorm->timeclose)) {
-            $str = html_writer::start_div('scorm overview').html_writer::div($strscorm. ': '.
-                    html_writer::link($CFG->wwwroot.'/mod/scorm/view.php?id='.$scorm->coursemodule, $scorm->name,
-                                        array('title' => $strscorm, 'class' => $scorm->visible ? '' : 'dimmed')), 'name');
-            if ($scorm->timeclose) {
-                $str .= html_writer::div($strduedate.': '.userdate($scorm->timeclose), 'info');
-            }
-            if ($showattemptstatus) {
-                require_once($CFG->dirroot.'/mod/scorm/locallib.php');
-                $str .= html_writer::div(scorm_get_attempt_status($USER, $scorm), 'details');
-            }
-            $str .= html_writer::end_div();
-            if (empty($htmlarray[$scorm->course]['scorm'])) {
-                $htmlarray[$scorm->course]['scorm'] = $str;
-            } else {
-                $htmlarray[$scorm->course]['scorm'] .= $str;
-            }
-        }
-    }
+function scorm_print_overview() {
+    throw new coding_exception('scorm_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
index b8dddb0..ea4dbb2 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /mod/* - activity modules,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+
+* The final deprecation of xxx_print_overview() callback means that this function will no longer be called.
+
 === 3.6 ===
 
 * The final deprecation of xxx_get_types() callback means that this function will no longer be called.
@@ -44,7 +48,8 @@ information provided here is intended especially for developers.
   - isexternalfile (if is a file reference to a external repository)
   - repositorytype (the repository name in case is a external file)
   Those fields are VALUE_OPTIONAL for backwards compatibility.
-* The block_course_overview has been removed and the related core module *_print_overview functions have been deprecated.
+* The block_course_overview has been removed and the related core module
+*_print_overview functions have been deprecated.
 * The block_myoverview has replaced block_course_overview to provide better information to students. To support this,
   actions can now be attached to calendar events. Documentation for the following new API callbacks introduced in
   MDL-55611 can be found at https://docs.moodle.org/dev/Calendar_API. The 3 new callbacks are:
diff --git a/mod/workshop/db/subplugins.json b/mod/workshop/db/subplugins.json
new file mode 100644 (file)
index 0000000..a6807bf
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "plugintypes": {
+        "workshopform": "mod\/workshop\/form",
+        "workshopallocation": "mod\/workshop\/allocation",
+        "workshopeval": "mod\/workshop\/eval"
+    }
+}
index 66260e8..1c0fd19 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -25,8 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array(
-                    'workshopform'       => 'mod/workshop/form',
-                    'workshopallocation' => 'mod/workshop/allocation',
-                    'workshopeval'       => 'mod/workshop/eval',
-                    );
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index 0ef5816..26af89a 100644 (file)
@@ -104,6 +104,8 @@ class question_engine_data_mapper {
         if ($stepdata) {
             $this->insert_all_step_data($stepdata);
         }
+
+        $quba->set_observer(new question_engine_unit_of_work($quba));
     }
 
     /**
index af8b9d6..42b938e 100644 (file)
@@ -154,4 +154,80 @@ class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_b
                     array($questiondata1->id, $questiondata2->id, $questiondata3->id),
                     new qubaid_list(array($quba->get_id()))));
     }
+
+    public function test_repeated_usage_saving_new_usage() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $initialqurows = $DB->count_records('question_usages');
+        $initialqarows = $DB->count_records('question_attempts');
+        $initialqasrows = $DB->count_records('question_attempt_steps');
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $generator->create_question_category();
+        $questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
+
+        $quba = question_engine::make_questions_usage_by_activity('test', context_system::instance());
+        $quba->set_preferred_behaviour('deferredfeedback');
+        $quba->add_question(question_bank::load_question($questiondata1->id));
+        $quba->start_all_questions();
+        question_engine::save_questions_usage_by_activity($quba);
+
+        // Check one usage, question_attempts and step added.
+        $firstid = $quba->get_id();
+        $this->assertEquals(1, $DB->count_records('question_usages') - $initialqurows);
+        $this->assertEquals(1, $DB->count_records('question_attempts') - $initialqarows);
+        $this->assertEquals(1, $DB->count_records('question_attempt_steps') - $initialqasrows);
+
+        $quba->finish_all_questions();
+        question_engine::save_questions_usage_by_activity($quba);
+
+        // Check usage id not changed.
+        $this->assertEquals($firstid, $quba->get_id());
+
+        // Check still one usage, question_attempts, but now two steps.
+        $this->assertEquals(1, $DB->count_records('question_usages') - $initialqurows);
+        $this->assertEquals(1, $DB->count_records('question_attempts') - $initialqarows);
+        $this->assertEquals(2, $DB->count_records('question_attempt_steps') - $initialqasrows);
+    }
+
+    public function test_repeated_usage_saving_existing_usage() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $generator->create_question_category();
+        $questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
+
+        $initquba = question_engine::make_questions_usage_by_activity('test', context_system::instance());
+        $initquba->set_preferred_behaviour('deferredfeedback');
+        $slot = $initquba->add_question(question_bank::load_question($questiondata1->id));
+        $initquba->start_all_questions();
+        question_engine::save_questions_usage_by_activity($initquba);
+
+        $quba = question_engine::load_questions_usage_by_activity($initquba->get_id());
+
+        $initialqurows = $DB->count_records('question_usages');
+        $initialqarows = $DB->count_records('question_attempts');
+        $initialqasrows = $DB->count_records('question_attempt_steps');
+
+        $quba->process_all_actions(time(), $quba->prepare_simulated_post_data(
+                [$slot => ['answer' => 'Frog']]));
+        question_engine::save_questions_usage_by_activity($quba);
+
+        // Check one usage, question_attempts and step added.
+        $this->assertEquals(0, $DB->count_records('question_usages') - $initialqurows);
+        $this->assertEquals(0, $DB->count_records('question_attempts') - $initialqarows);
+        $this->assertEquals(1, $DB->count_records('question_attempt_steps') - $initialqasrows);
+
+        $quba->finish_all_questions();
+        question_engine::save_questions_usage_by_activity($quba);
+
+        // Check still one usage, question_attempts, but now two steps.
+        $this->assertEquals(0, $DB->count_records('question_usages') - $initialqurows);
+        $this->assertEquals(0, $DB->count_records('question_attempts') - $initialqarows);
+        $this->assertEquals(2, $DB->count_records('question_attempt_steps') - $initialqasrows);
+    }
 }
index 6fee897..93218c4 100644 (file)
Binary files a/question/type/ddmarker/amd/build/shapes.min.js and b/question/type/ddmarker/amd/build/shapes.min.js differ
index 2345a46..dec8c09 100644 (file)
@@ -234,6 +234,8 @@ define(function() {
      * @constructor
      */
     function Circle(label, x, y, radius) {
+        x = x || 15;
+        y = y || 15;
         Shape.call(this, label, x, y);
         this.radius = radius || 15;
     }
index ae71c4f..7309a08 100644 (file)
@@ -130,9 +130,11 @@ $param->table = 'user_'.$param->table;
 $wheres = [
     "userid = :userid",
     "timeend >= :timeend",
+    "stattype = :stattype",
 ];
 $params['userid'] = $user->id;
 $params['timeend'] = $param->timeafter;
+$params['stattype'] = $param->stattype;
 // Add condition for course ID when specified.
 if ($course->id != SITEID) {
     $wheres[] = "courseid = :courseid";
index 74b2e08..f908d50 100644 (file)
@@ -272,7 +272,7 @@ class engine extends \core_search\engine {
 
         $query = new \SolrDisMaxQuery();
 
-        $this->set_query($query, $data->q);
+        $this->set_query($query, self::replace_underlines($data->q));
         $this->add_fields($query);
 
         // Search filters applied, we don't cache these filters as we don't want to pollute the cache with tmp filters
@@ -750,6 +750,23 @@ class engine extends \core_search\engine {
         return true;
     }
 
+    /**
+     * Replaces underlines at edges of words in the content with spaces.
+     *
+     * For example '_frogs_' will become 'frogs', '_frogs and toads_' will become 'frogs and toads',
+     * and 'frogs_and_toads' will be left as 'frogs_and_toads'.
+     *
+     * The reason for this is that for italic content_to_text puts _italic_ underlines at the start
+     * and end of the italicised phrase (not between words). Solr treats underlines as part of the
+     * word, which means that if you search for a word in italic then you can't find it.
+     *
+     * @param string $str String to replace
+     * @return string Replaced string
+     */
+    protected static function replace_underlines(string $str): string {
+        return preg_replace('~\b_|_\b~', '', $str);
+    }
+
     /**
      * Adds a text document to the search engine.
      *
@@ -758,6 +775,14 @@ class engine extends \core_search\engine {
      */
     protected function add_solr_document($doc) {
         $solrdoc = new \SolrInputDocument();
+
+        // Replace underlines in the content with spaces. The reason for this is that for italic
+        // text, content_to_text puts _italic_ underlines. Solr treats underlines as part of the
+        // word, which means that if you search for a word in italic then you can't find it.
+        if (array_key_exists('content', $doc)) {
+            $doc['content'] = self::replace_underlines($doc['content']);
+        }
+
         foreach ($doc as $field => $value) {
             $solrdoc->addField($field, $value);
         }
index 0f43ed8..47f244e 100644 (file)
@@ -1010,6 +1010,68 @@ class search_solr_engine_testcase extends advanced_testcase {
                 ['Post1', 'Post2'], $results);
     }
 
+    /**
+     * Tests searching for results containing words in italic text. (This used to fail.)
+     */
+    public function test_italics() {
+        global $USER;
+
+        // Use real search areas.
+        $this->search->clear_static();
+        $this->search->add_core_search_areas();
+
+        // Create a course and a forum.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $forum = $generator->create_module('forum', ['course' => $course->id]);
+
+        // As admin user, create forum discussions with various words in italics or with underlines.
+        $this->setAdminUser();
+        $forumgen = $generator->get_plugin_generator('mod_forum');
+        $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
+                'userid' => $USER->id, 'name' => 'Post1',
+                'message' => '<p>This is a post about <i>frogs</i>.</p>']);
+        $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
+                'userid' => $USER->id, 'name' => 'Post2',
+                'message' => '<p>This is a post about <i>toads and zombies</i>.</p>']);
+        $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
+                'userid' => $USER->id, 'name' => 'Post3',
+                'message' => '<p>This is a post about toads_and_zombies.</p>']);
+        $forumgen->create_discussion(['course' => $course->id, 'forum' => $forum->id,
+                'userid' => $USER->id, 'name' => 'Post4',
+                'message' => '<p>This is a post about _leading and trailing_ underlines.</p>']);
+
+        // Index the data.
+        $this->search->index();
+
+        // Search for 'frogs' should find the post.
+        $querydata = new stdClass();
+        $querydata->q = 'frogs';
+        $results = $this->search->search($querydata);
+        $this->assert_result_titles(['Post1'], $results);
+
+        // Search for 'toads' or 'zombies' should find post 2 (and not 3)...
+        $querydata->q = 'toads';
+        $results = $this->search->search($querydata);
+        $this->assert_result_titles(['Post2'], $results);
+        $querydata->q = 'zombies';
+        $results = $this->search->search($querydata);
+        $this->assert_result_titles(['Post2'], $results);
+
+        // Search for 'toads_and_zombies' should find post 3.
+        $querydata->q = 'toads_and_zombies';
+        $results = $this->search->search($querydata);
+        $this->assert_result_titles(['Post3'], $results);
+
+        // Search for '_leading' or 'trailing_' should find post 4.
+        $querydata->q = '_leading';
+        $results = $this->search->search($querydata);
+        $this->assert_result_titles(['Post4'], $results);
+        $querydata->q = 'trailing_';
+        $results = $this->search->search($querydata);
+        $this->assert_result_titles(['Post4'], $results);
+    }
+
     /**
      * Asserts that the returned documents have the expected titles (regardless of order).
      *
index 178e38e..95e22b3 100644 (file)
@@ -300,17 +300,17 @@ body.drawer-open-left #region-main.has-blocks {
 .block_navigation .block_tree [aria-expanded="true"]:before {
     content: $fa-var-angle-down;
     margin-right: 0;
+    @include fa-icon();
     font-size: 16px;
-    @extend .fa;
     width: 16px;
 }
 
 .block_settings .block_tree [aria-expanded="false"]:before,
 .block_navigation .block_tree [aria-expanded="false"]:before {
     content: $fa-var-angle-right;
-    font-size: 16px;
     margin-right: 0;
-    @extend .fa;
+    @include fa-icon();
+    font-size: 16px;
     width: 16px;
 }
 .dir-rtl {
index d8b90be..adaa1b6 100644 (file)
@@ -10,9 +10,7 @@
  *  Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
  *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
  */
-.fa, .block_settings .block_tree [aria-expanded="true"]:before,
-.block_navigation .block_tree [aria-expanded="true"]:before, .block_settings .block_tree [aria-expanded="false"]:before,
-.block_navigation .block_tree [aria-expanded="false"]:before {
+.fa {
   display: inline-block;
   font: normal normal normal 14px/1 FontAwesome;
   font-size: inherit;
 .fa-pull-right {
   float: right; }
 
-.fa.fa-pull-left, .block_settings .block_tree .fa-pull-left[aria-expanded="true"]:before,
-.block_navigation .block_tree .fa-pull-left[aria-expanded="true"]:before, .block_settings .block_tree .fa-pull-left[aria-expanded="false"]:before,
-.block_navigation .block_tree .fa-pull-left[aria-expanded="false"]:before {
+.fa.fa-pull-left {
   margin-right: .3em; }
 
-.fa.fa-pull-right, .block_settings .block_tree .fa-pull-right[aria-expanded="true"]:before,
-.block_navigation .block_tree .fa-pull-right[aria-expanded="true"]:before, .block_settings .block_tree .fa-pull-right[aria-expanded="false"]:before,
-.block_navigation .block_tree .fa-pull-right[aria-expanded="false"]:before {
+.fa.fa-pull-right {
   margin-left: .3em; }
 
 /* Deprecated as of 4.4.0 */
 .pull-left {
   float: left; }
 
-.fa.pull-left, .block_settings .block_tree .pull-left[aria-expanded="true"]:before,
-.block_navigation .block_tree .pull-left[aria-expanded="true"]:before, .block_settings .block_tree .pull-left[aria-expanded="false"]:before,
-.block_navigation .block_tree .pull-left[aria-expanded="false"]:before {
+.fa.pull-left {
   margin-right: .3em; }
 
-.fa.pull-right, .block_settings .block_tree .pull-right[aria-expanded="true"]:before,
-.block_navigation .block_tree .pull-right[aria-expanded="true"]:before, .block_settings .block_tree .pull-right[aria-expanded="false"]:before,
-.block_navigation .block_tree .pull-right[aria-expanded="false"]:before {
+.fa.pull-right {
   margin-left: .3em; }
 
 .fa-spin {
@@ -11398,14 +11388,26 @@ div.editor_atto_toolbar button .icon {
 .block_navigation .block_tree [aria-expanded="true"]:before {
   content: "";
   margin-right: 0;
+  display: inline-block;
+  font: normal normal normal 14px/1 FontAwesome;
+  font-size: inherit;
+  text-rendering: auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
   font-size: 16px;
   width: 16px; }
 
 .block_settings .block_tree [aria-expanded="false"]:before,
 .block_navigation .block_tree [aria-expanded="false"]:before {
   content: "";
-  font-size: 16px;
   margin-right: 0;
+  display: inline-block;
+  font: normal normal normal 14px/1 FontAwesome;
+  font-size: inherit;
+  text-rendering: auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-size: 16px;
   width: 16px; }
 
 .dir-rtl .block_settings .block_tree [aria-expanded="false"]:before,
index 710a78d..9915eb3 100644 (file)
@@ -10,9 +10,7 @@
  *  Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
  *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
  */
-.fa, .block_settings .block_tree [aria-expanded="true"]:before,
-.block_navigation .block_tree [aria-expanded="true"]:before, .block_settings .block_tree [aria-expanded="false"]:before,
-.block_navigation .block_tree [aria-expanded="false"]:before {
+.fa {
   display: inline-block;
   font: normal normal normal 14px/1 FontAwesome;
   font-size: inherit;
 .fa-pull-right {
   float: right; }
 
-.fa.fa-pull-left, .block_settings .block_tree .fa-pull-left[aria-expanded="true"]:before,
-.block_navigation .block_tree .fa-pull-left[aria-expanded="true"]:before, .block_settings .block_tree .fa-pull-left[aria-expanded="false"]:before,
-.block_navigation .block_tree .fa-pull-left[aria-expanded="false"]:before {
+.fa.fa-pull-left {
   margin-right: .3em; }
 
-.fa.fa-pull-right, .block_settings .block_tree .fa-pull-right[aria-expanded="true"]:before,
-.block_navigation .block_tree .fa-pull-right[aria-expanded="true"]:before, .block_settings .block_tree .fa-pull-right[aria-expanded="false"]:before,
-.block_navigation .block_tree .fa-pull-right[aria-expanded="false"]:before {
+.fa.fa-pull-right {
   margin-left: .3em; }
 
 /* Deprecated as of 4.4.0 */
 .pull-left {
   float: left; }
 
-.fa.pull-left, .block_settings .block_tree .pull-left[aria-expanded="true"]:before,
-.block_navigation .block_tree .pull-left[aria-expanded="true"]:before, .block_settings .block_tree .pull-left[aria-expanded="false"]:before,
-.block_navigation .block_tree .pull-left[aria-expanded="false"]:before {
+.fa.pull-left {
   margin-right: .3em; }
 
-.fa.pull-right, .block_settings .block_tree .pull-right[aria-expanded="true"]:before,
-.block_navigation .block_tree .pull-right[aria-expanded="true"]:before, .block_settings .block_tree .pull-right[aria-expanded="false"]:before,
-.block_navigation .block_tree .pull-right[aria-expanded="false"]:before {
+.fa.pull-right {
   margin-left: .3em; }
 
 .fa-spin {
@@ -11640,14 +11630,26 @@ div.editor_atto_toolbar button .icon {
 .block_navigation .block_tree [aria-expanded="true"]:before {
   content: "";
   margin-right: 0;
+  display: inline-block;
+  font: normal normal normal 14px/1 FontAwesome;
+  font-size: inherit;
+  text-rendering: auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
   font-size: 16px;
   width: 16px; }
 
 .block_settings .block_tree [aria-expanded="false"]:before,
 .block_navigation .block_tree [aria-expanded="false"]:before {
   content: "";
-  font-size: 16px;
   margin-right: 0;
+  display: inline-block;
+  font: normal normal normal 14px/1 FontAwesome;
+  font-size: inherit;
+  text-rendering: auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-size: 16px;
   width: 16px; }
 
 .dir-rtl .block_settings .block_tree [aria-expanded="false"]:before,
index 6506249..59f64cd 100644 (file)
@@ -99,6 +99,7 @@ class user_editadvanced_form extends moodleform {
 
         $purpose = user_edit_map_field_purpose($userid, 'username');
         $mform->addElement('text', 'username', get_string('username'), 'size="20"' . $purpose);
+        $mform->addRule('username', get_string('required'), 'required', null, 'client');
         $mform->addHelpButton('username', 'username', 'auth');
         $mform->setType('username', PARAM_RAW);
 
index 9a957fb..531426a 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2019061400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2019061400.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.