Merge branch 'MDL-54606' of https://github.com/mr-russ/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 9 Aug 2016 08:32:46 +0000 (09:32 +0100)
committerDan Poltawski <dan@moodle.com>
Tue, 9 Aug 2016 08:32:46 +0000 (09:32 +0100)
72 files changed:
.stylelintignore [new file with mode: 0644]
Gruntfile.js
admin/settings/development.php
admin/tool/lp/templates/user_competency_summary_in_course.mustache
admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php
admin/tool/profiling/settings.php
filter/mathjaxloader/db/upgrade.php
filter/mathjaxloader/filter.php
filter/mathjaxloader/lang/en/filter_mathjaxloader.php
filter/mathjaxloader/settings.php
filter/mathjaxloader/version.php
grade/export/ods/classes/event/grade_exported.php [new file with mode: 0644]
grade/export/ods/export.php
grade/export/ods/lang/en/gradeexport_ods.php
grade/export/ods/tests/logging_test.php [new file with mode: 0644]
grade/export/txt/classes/event/grade_exported.php [new file with mode: 0644]
grade/export/txt/export.php
grade/export/txt/lang/en/gradeexport_txt.php
grade/export/txt/tests/logging_test.php [new file with mode: 0644]
grade/export/xls/classes/event/grade_exported.php [new file with mode: 0644]
grade/export/xls/export.php
grade/export/xls/lang/en/gradeexport_xls.php
grade/export/xls/tests/logging_test.php [new file with mode: 0644]
grade/export/xml/classes/event/grade_exported.php [new file with mode: 0644]
grade/export/xml/export.php
grade/export/xml/lang/en/gradeexport_xml.php
grade/export/xml/tests/logging_test.php [new file with mode: 0644]
lib/classes/component.php
lib/classes/event/grade_exported.php [new file with mode: 0644]
lib/dml/pgsql_native_moodle_database.php
lib/javascript.php
lib/moodlelib.php
lib/requirejs.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_forms.php
lib/tests/component_test.php
lib/tests/fixtures/component/overlap/subnamespace/example.php [new file with mode: 0644]
lib/tests/fixtures/component/overlap/subnamespace/example2.php [new file with mode: 0644]
lib/tests/fixtures/component/psr0/main.php [new file with mode: 0644]
lib/tests/fixtures/component/psr0/subnamespace/example.php [new file with mode: 0644]
lib/tests/fixtures/component/psr0/subnamespace/slashes.php [new file with mode: 0644]
lib/tests/fixtures/component/psr4/main.php [new file with mode: 0644]
lib/tests/fixtures/component/psr4/subnamespace/example.php [new file with mode: 0644]
lib/tests/fixtures/component/psr4/subnamespace/underscore_example.php [new file with mode: 0644]
lib/upgrade.txt
lib/xhprof/readme_moodle.txt
lib/xhprof/xhprof_moodle.php
mod/assign/externallib.php
mod/assign/tests/externallib_test.php
mod/chat/lib.php
mod/chat/tests/format_message_test.php [new file with mode: 0644]
mod/data/tests/behat/required_entries.feature
mod/data/tests/generator/lib.php
mod/data/tests/generator_test.php
mod/feedback/lib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/classes/external.php
mod/quiz/tests/external_test.php
mod/quiz/upgrade.txt
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/lang/en/qtype_multianswer.php
question/type/multianswer/questiontype.php
question/type/multianswer/renderer.php
question/type/multianswer/tests/helper.php
question/type/multianswer/tests/walkthrough_test.php
report/log/classes/renderable.php
report/log/classes/table_log.php
report/log/tests/behat/filter_log_actions.feature [new file with mode: 0644]
search/classes/manager.php
search/tests/fixtures/testable_core_search.php
search/tests/manager_test.php

diff --git a/.stylelintignore b/.stylelintignore
new file mode 100644 (file)
index 0000000..c73d41a
--- /dev/null
@@ -0,0 +1,55 @@
+# Generated by "grunt ignorefiles"
+theme/bootstrapbase/style/
+node_modules/
+vendor/
+auth/cas/CAS/
+auth/fc/fcFPP.php
+enrol/lti/ims-blti/
+filter/algebra/AlgParser.pm
+filter/tex/mimetex.*
+lib/editor/atto/yui/src/rangy/js/*.*
+lib/editor/tinymce/plugins/pdw/tinymce/
+lib/editor/tinymce/plugins/spellchecker/rpc.php
+lib/editor/tinymce/tiny_mce/
+lib/adodb/
+lib/bennu/
+lib/evalmath/
+lib/lessphp/
+lib/phpexcel/
+lib/pear/Net/
+lib/google/
+lib/htmlpurifier/
+lib/jabber/
+lib/minify/
+lib/flowplayer/
+lib/pear/Auth/RADIUS.php
+lib/pear/Crypt/CHAP.php
+lib/pear/HTML/Common.php
+lib/pear/HTML/QuickForm.php
+lib/pear/HTML/QuickForm/
+lib/pear/PEAR.php
+lib/phpmailer/
+lib/simplepie/
+lib/tcpdf/
+lib/typo3/
+lib/yuilib/
+lib/yuilib/gallery/
+lib/jquery/
+lib/html2text/
+lib/markdown/
+lib/recaptchalib.php
+lib/xhprof/
+lib/xmlize.php
+lib/horde/
+lib/requirejs/
+lib/amd/src/loglevel.js
+lib/mustache/
+lib/amd/src/mustache.js
+lib/graphlib.php
+lib/spout/
+lib/amd/src/chartjs-lazy.js
+mod/assign/feedback/editpdf/fpdi/
+repository/s3/S3.php
+theme/bootstrapbase/less/bootstrap/
+theme/bootstrapbase/javascript/html5shiv.js
+theme/bootstrapbase/amd/src/bootstrap.js
\ No newline at end of file
index 4d9a81d..5e547eb 100644 (file)
@@ -188,7 +188,7 @@ module.exports = function(grunt) {
                         }
                     }
                 },
-                src: ['theme/**/*.less', '!theme/bootstrapbase/less/bootstrap/*'],
+                src: ['theme/**/*.less']
             }
         }
     });
@@ -202,6 +202,9 @@ module.exports = function(grunt) {
       // Generate .eslintignore.
       var eslintIgnores = ['# Generated by "grunt ignorefiles"', '*/**/yui/src/*/meta/', '*/**/build/'].concat(thirdPartyPaths);
       grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
+      // Generate .stylelintignore.
+      var stylelintIgnores = ['# Generated by "grunt ignorefiles"', 'theme/bootstrapbase/style/'].concat(thirdPartyPaths);
+      grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
     };
 
     /**
@@ -314,7 +317,7 @@ module.exports = function(grunt) {
           var files = Object.keys(changedFiles);
           grunt.config('eslint.amd.src', files);
           grunt.config('eslint.yui.src', files);
-          grunt.config('uglify.amd.files', [{ expand: true, src: files, rename: uglifyRename }]);
+          grunt.config('uglify.amd.files', [{expand: true, src: files, rename: uglifyRename}]);
           grunt.config('shifter.options.paths', files);
           grunt.config('stylelint.less.src', files);
           changedFiles = Object.create(null);
index 41427f0..d0a6531 100644 (file)
@@ -32,7 +32,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $ADMIN->add('development', $temp);
 
     // "Profiling" settingpage (conditionally if the 'xhprof' extension is available only).
-    $xhprofenabled = extension_loaded('xhprof') && function_exists('xhprof_enable');
+    $xhprofenabled = extension_loaded('xhprof') || extension_loaded('tideways');
     $temp = new admin_settingpage('profiling', new lang_string('profiling', 'admin'), 'moodle/site:config', !$xhprofenabled);
     // Main profiling switch.
     $temp->add(new admin_setting_configcheckbox('profilingenabled', new lang_string('profilingenabled', 'admin'), new lang_string('profilingenabled_help', 'admin'), false));
index 632864c..8eea3dc 100644 (file)
@@ -26,7 +26,7 @@
 require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
 
 use Behat\Gherkin\Node\TableNode as TableNode;
-use Behat\Behat\Exception\PendingException as PendingException;
+use Behat\Behat\Tester\Exception\PendingException as PendingException;
 use core_competency\competency;
 use core_competency\competency_framework;
 use core_competency\plan;
index 70bd8d7..c04d32b 100644 (file)
@@ -26,6 +26,7 @@
 defined('MOODLE_INTERNAL') || die;
 
 // profiling tool, added to development
-if (extension_loaded('xhprof') && function_exists('xhprof_enable') && (!empty($CFG->profilingenabled) || !empty($CFG->earlyprofilingenabled))) {
-    $ADMIN->add('development', new admin_externalpage('toolprofiling', get_string('pluginname', 'tool_profiling'), "$CFG->wwwroot/$CFG->admin/tool/profiling/index.php", 'moodle/site:config'));
+if ((extension_loaded('xhprof') || extension_loaded('tideways')) && (!empty($CFG->profilingenabled) || !empty($CFG->earlyprofilingenabled))) {
+    $ADMIN->add('development', new admin_externalpage('toolprofiling', get_string('pluginname', 'tool_profiling'),
+            "$CFG->wwwroot/$CFG->admin/tool/profiling/index.php", 'moodle/site:config'));
 }
index 336208c..77fc2d9 100644 (file)
@@ -111,9 +111,6 @@ MathJax.Hub.Config({
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2016032200) {
 
         $httpurl = get_config('filter_mathjaxloader', 'httpurl');
@@ -131,5 +128,25 @@ MathJax.Hub.Config({
         upgrade_plugin_savepoint(true, 2016032200, 'filter', 'mathjaxloader');
     }
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
+    if ($oldversion < 2016080200) {
+        // We are consolodating the two settings for http and https url into only the https
+        // setting. Since it is preferably to always load the secure resource.
+
+        $httpurl = get_config('filter_mathjaxloader', 'httpurl');
+        if ($httpurl !== 'http://cdn.mathjax.org/mathjax/2.6-latest/MathJax.js') {
+            // If the http setting has been changed, we make the admin choose the https setting because
+            // it indicates some sort of custom setup. This will be supported by the release notes.
+            unset_config('httpsurl', 'filter_mathjaxloader');
+        }
+
+        // The seperate http setting has been removed. We always use the secure resource.
+        unset_config('httpurl', 'filter_mathjaxloader');
+
+        upgrade_plugin_savepoint(true, 2016080200, 'filter', 'mathjaxloader');
+    }
+
     return true;
 }
index c5428c8..40d49dd 100644 (file)
@@ -99,11 +99,7 @@ class filter_mathjaxloader extends moodle_text_filter {
         static $jsinitialised = false;
 
         if (empty($jsinitialised)) {
-            if (is_https()) {
-                $url = get_config('filter_mathjaxloader', 'httpsurl');
-            } else {
-                $url = get_config('filter_mathjaxloader', 'httpurl');
-            }
+            $url = get_config('filter_mathjaxloader', 'httpsurl');
             $lang = $this->map_language_code(current_language());
             $url = new moodle_url($url, array('delayStartupUntil' => 'configured'));
 
index 175b023..5337865 100644 (file)
@@ -27,10 +27,8 @@ $string['additionaldelimiters'] = 'Additional equation delimiters';
 $string['additionaldelimiters_help'] = 'MathJax filter parses text for equations contained within delimiter characters.
 
 The list of recognised delimiter characters can be added to here (e.g. AsciiMath uses `). Delimiters can contain multiple characters and multiple delimiters can be separated with commas.';
-$string['httpurl'] = 'HTTP MathJax URL';
-$string['httpurl_help'] = 'Full URL to MathJax library. Used when the page is loaded via http.';
-$string['httpsurl'] = 'HTTPS MathJax URL';
-$string['httpsurl_help'] = 'Full URL to MathJax library. Used when the page is loaded via https (secure). ';
+$string['httpsurl'] = 'MathJax URL';
+$string['httpsurl_help'] = 'Full URL to MathJax library.';
 $string['texfiltercompatibility'] = 'TeX filter compatibility';
 $string['texfiltercompatibility_help'] = 'The MathJax filter can be used as a replacement for the TeX notation filter.
 
index 481e902..0453a47 100644 (file)
@@ -30,13 +30,6 @@ if ($ADMIN->fulltree) {
                                       new lang_string('localinstall_help', 'filter_mathjaxloader'));
     $settings->add($item);
 
-    $item = new admin_setting_configtext('filter_mathjaxloader/httpurl',
-                                         new lang_string('httpurl', 'filter_mathjaxloader'),
-                                         new lang_string('httpurl_help', 'filter_mathjaxloader'),
-                                         'http://cdn.mathjax.org/mathjax/2.6-latest/MathJax.js',
-                                         PARAM_RAW);
-    $settings->add($item);
-
     $item = new admin_setting_configtext('filter_mathjaxloader/httpsurl',
                                          new lang_string('httpsurl', 'filter_mathjaxloader'),
                                          new lang_string('httpsurl_help', 'filter_mathjaxloader'),
index 8e64d3a..60bf517 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version  = 2016052300;
+$plugin->version  = 2016080200;
 $plugin->requires = 2016051900;  // Requires this Moodle version.
 $plugin->component= 'filter_mathjaxloader';
diff --git a/grade/export/ods/classes/event/grade_exported.php b/grade/export/ods/classes/event/grade_exported.php
new file mode 100644 (file)
index 0000000..56834f7
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * Grade export event.
+ *
+ * @package    gradeexport_ods
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradeexport_ods\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade export event class.
+ *
+ * @package    gradeexport_ods
+ * @since      Moodle 3.2
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class grade_exported extends \core\event\grade_exported {
+}
\ No newline at end of file
index 031f568..7ab7660 100644 (file)
@@ -55,5 +55,7 @@ if (!empty($CFG->gradepublishing) && !empty($key)) {
     echo $export->get_grade_publishing_url();
     echo $OUTPUT->footer();
 } else {
+    $event = \gradeexport_ods\event\grade_exported::create(array('context' => $context));
+    $event->trigger();
     $export->print_grades();
 }
index dd65582..18b7ce3 100644 (file)
@@ -23,6 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['eventgradeexported'] = 'OpenDocument grade exported';
 $string['pluginname'] = 'OpenDocument spreadsheet';
 $string['ods:publish'] = 'Publish ODS grade export';
 $string['ods:view'] = 'Use OpenDocument grade export';
diff --git a/grade/export/ods/tests/logging_test.php b/grade/export/ods/tests/logging_test.php
new file mode 100644 (file)
index 0000000..67ce658
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Events test.
+ *
+ * @package    gradeexport_ods
+ * @copyright  2016 Zane Karl zkarl@oid.ucla.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Resource events test cases.
+ *
+ * @package    gradeexport_ods
+ * @copyright  2016 Zane Karl zkarl@oid.ucla.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class ods_logging_events_testcase extends advanced_testcase {
+
+    /**
+     * Setup is called before calling test case.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    /**
+     * Test course_module_instance_list_viewed event.
+     */
+    public function test_logging() {
+        // There is no proper API to call to trigger this event, so what we are
+        // doing here is simply making sure that the events returns the right information.
+        $course = $this->getDataGenerator()->create_course();
+        $params = array(
+            'context' => context_course::instance($course->id)
+        );
+        $event = \gradeexport_ods\event\grade_exported::create($params);
+        // Triggering and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\gradeexport_ods\event\grade_exported', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('ods', $event->get_export_type());
+    }
+}
\ No newline at end of file
diff --git a/grade/export/txt/classes/event/grade_exported.php b/grade/export/txt/classes/event/grade_exported.php
new file mode 100644 (file)
index 0000000..046d856
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * Grade export event.
+ *
+ * @package    gradeexport_txt
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradeexport_txt\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade export event class.
+ *
+ * @package    gradeexport_txt
+ * @since      Moodle 3.2
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class grade_exported extends \core\event\grade_exported {
+}
\ No newline at end of file
index d150e1a..903cfbf 100644 (file)
@@ -62,5 +62,7 @@ if (!empty($CFG->gradepublishing) && !empty($key)) {
     echo $export->get_grade_publishing_url();
     echo $OUTPUT->footer();
 } else {
+    $event = \gradeexport_txt\event\grade_exported::create(array('context' => $context));
+    $event->trigger();
     $export->print_grades();
 }
index db10755..fc0f415 100644 (file)
@@ -23,6 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['eventgradeexported'] = 'TXT grade exported';
 $string['pluginname'] = 'Plain text file';
 $string['timeexported'] = 'Last downloaded from this course';
 $string['txt:publish'] = 'Publish TXT grade export';
diff --git a/grade/export/txt/tests/logging_test.php b/grade/export/txt/tests/logging_test.php
new file mode 100644 (file)
index 0000000..e2e7d7b
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Events test.
+ *
+ * @package    gradeexport_txt
+ * @copyright  2016 Zane Karl zkarl@oid.ucla.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Resource events test cases.
+ *
+ * @package    gradeexport_txt
+ * @copyright  2016 Zane Karl zkarl@oid.ucla.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class txt_logging_events_testcase extends advanced_testcase {
+
+    /**
+     * Setup is called before calling test case.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    /**
+     * Test course_module_instance_list_viewed event.
+     */
+    public function test_logging() {
+        // There is no proper API to call to trigger this event, so what we are
+        // doing here is simply making sure that the events returns the right information.
+        $course = $this->getDataGenerator()->create_course();
+        $params = array(
+            'context' => context_course::instance($course->id)
+        );
+        $event = \gradeexport_txt\event\grade_exported::create($params);
+        // Triggering and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\gradeexport_txt\event\grade_exported', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('txt', $event->get_export_type());
+    }
+}
\ No newline at end of file
diff --git a/grade/export/xls/classes/event/grade_exported.php b/grade/export/xls/classes/event/grade_exported.php
new file mode 100644 (file)
index 0000000..ccea485
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * Grade export event.
+ *
+ * @package    gradeexport_xls
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradeexport_xls\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade export event class.
+ *
+ * @package    gradeexport_xls
+ * @since      Moodle 3.2
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class grade_exported extends \core\event\grade_exported {
+}
\ No newline at end of file
index a844172..0855517 100644 (file)
@@ -55,6 +55,8 @@ if (!empty($CFG->gradepublishing) && !empty($key)) {
     echo $export->get_grade_publishing_url();
     echo $OUTPUT->footer();
 } else {
+    $event = \gradeexport_xls\event\grade_exported::create(array('context' => $context));
+    $event->trigger();
     $export->print_grades();
 }
 
index 32b96c5..706fe83 100644 (file)
@@ -23,6 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['eventgradeexported'] = 'XLS grade exported';
 $string['pluginname'] = 'Excel spreadsheet';
 $string['timeexported'] = 'Last downloaded from this course';
 $string['xls:publish'] = 'Publish XLS grade export';
diff --git a/grade/export/xls/tests/logging_test.php b/grade/export/xls/tests/logging_test.php
new file mode 100644 (file)
index 0000000..fcada0d
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Events test.
+ *
+ * @package    gradeexport_xls
+ * @copyright  2016 Zane Karl zkarl@oid.ucla.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Resource events test cases.
+ *
+ * @package    gradeexport_xls
+ * @copyright  2016 Zane Karl zkarl@oid.ucla.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class xls_logging_events_testcase extends advanced_testcase {
+
+    /**
+     * Setup is called before calling test case.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    /**
+     * Test course_module_instance_list_viewed event.
+     */
+    public function test_logging() {
+        // There is no proper API to call to trigger this event, so what we are
+        // doing here is simply making sure that the events returns the right information.
+        $course = $this->getDataGenerator()->create_course();
+        $params = array(
+            'context' => context_course::instance($course->id)
+        );
+        $event = \gradeexport_xls\event\grade_exported::create($params);
+        // Triggering and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\gradeexport_xls\event\grade_exported', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('xls', $event->get_export_type());
+    }
+}
\ No newline at end of file
diff --git a/grade/export/xml/classes/event/grade_exported.php b/grade/export/xml/classes/event/grade_exported.php
new file mode 100644 (file)
index 0000000..d9943ec
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * Grade export event.
+ *
+ * @package    gradeexport_xml
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradeexport_xml\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade export event class.
+ *
+ * @package    gradeexport_xml
+ * @since      Moodle 3.2
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class grade_exported extends \core\event\grade_exported {
+}
\ No newline at end of file
index a2f1bb3..d7ae8e7 100644 (file)
@@ -56,6 +56,8 @@ if (!empty($CFG->gradepublishing) && !empty($key)) {
     echo $export->get_grade_publishing_url();
     echo $OUTPUT->footer();
 } else {
+    $event = \gradeexport_xml\event\grade_exported::create(array('context' => $context));
+    $event->trigger();
     $export->print_grades();
 }
 
index 7cb8dbc..babd5f3 100644 (file)
@@ -23,6 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['eventgradeexported'] = 'XML grade exported';
 $string['pluginname'] = 'XML file';
 $string['xml:publish'] = 'Publish XML grade export';
 $string['xml:view'] = 'Use XML grade export';
diff --git a/grade/export/xml/tests/logging_test.php b/grade/export/xml/tests/logging_test.php
new file mode 100644 (file)
index 0000000..b88d953
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Events test.
+ *
+ * @package    gradeexport_xml
+ * @copyright  2016 Zane Karl zkarl@oid.ucla.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Resource events test cases.
+ *
+ * @package    gradeexport_xml
+ * @copyright  2016 Zane Karl zkarl@oid.ucla.edu
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class xml_logging_events_testcase extends advanced_testcase {
+
+    /**
+     * Setup is called before calling test case.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    /**
+     * Test course_module_instance_list_viewed event.
+     */
+    public function test_logging() {
+        // There is no proper API to call to trigger this event, so what we are
+        // doing here is simply making sure that the events returns the right information.
+        $course = $this->getDataGenerator()->create_course();
+        $params = array(
+            'context' => context_course::instance($course->id)
+        );
+        $event = \gradeexport_xml\event\grade_exported::create($params);
+        // Triggering and capturing the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\gradeexport_xml\event\grade_exported', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('xml', $event->get_export_type());
+    }
+}
\ No newline at end of file
index 754e44a..519606c 100644 (file)
@@ -67,8 +67,13 @@ class core_component {
     protected static $version = null;
     /** @var array list of the files to map. */
     protected static $filestomap = array('lib.php', 'settings.php');
-    /** @var array cache of PSR loadable systems */
-    protected static $psrclassmap = null;
+    /** @var array associative array of PSR-0 namespaces and corresponding paths. */
+    protected static $psr0namespaces = array(
+        'Horde' => 'lib/horde/framework/Horde'
+    );
+    /** @var array associative array of PRS-4 namespaces and corresponding paths. */
+    protected static $psr4namespaces = array(
+    );
 
     /**
      * Class loader for Frankenstyle named classes in standard locations.
@@ -107,15 +112,78 @@ class core_component {
             return;
         }
 
-        // Attempt to normalize the classname.
-        $normalizedclassname = str_replace(array('/', '\\'), '_', $classname);
-        if (isset(self::$psrclassmap[$normalizedclassname])) {
-            // Function include would be faster, but for BC it is better to include only once.
-            include_once(self::$psrclassmap[$normalizedclassname]);
+        $file = self::psr_classloader($classname);
+        // If the file is found, require it.
+        if (!empty($file)) {
+            require($file);
             return;
         }
     }
 
+    /**
+     * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
+     * demand. Only returns paths to files that exist.
+     *
+     * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
+     * compatible.
+     *
+     * @param string $class the name of the class.
+     * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
+     */
+    protected static function psr_classloader($class) {
+        // Iterate through each PSR-4 namespace prefix.
+        foreach (self::$psr4namespaces as $prefix => $path) {
+            $file = self::get_class_file($class, $prefix, $path, array('\\'));
+            if (!empty($file) && file_exists($file)) {
+                return $file;
+            }
+        }
+
+        // Iterate through each PSR-0 namespace prefix.
+        foreach (self::$psr0namespaces as $prefix => $path) {
+            $file = self::get_class_file($class, $prefix, $path, array('\\', '_'));
+            if (!empty($file) && file_exists($file)) {
+                return $file;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Return the path to the class based on the given namespace prefix and path it corresponds to.
+     *
+     * Will return the path even if the file does not exist. Check the file esists before requiring.
+     *
+     * @param string $class the name of the class.
+     * @param string $prefix The namespace prefix used to identify the base directory of the source files.
+     * @param string $path The relative path to the base directory of the source files.
+     * @param string[] $separators The characters that should be used for separating.
+     * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
+     */
+    protected static function get_class_file($class, $prefix, $path, $separators) {
+        global $CFG;
+
+        // Does the class use the namespace prefix?
+        $len = strlen($prefix);
+        if (strncmp($prefix, $class, $len) !== 0) {
+            // No, move to the next prefix.
+            return false;
+        }
+        $path = $CFG->dirroot . '/' . $path;
+
+        // Get the relative class name.
+        $relativeclass = substr($class, $len);
+
+        // Replace the namespace prefix with the base directory, replace namespace
+        // separators with directory separators in the relative class name, append
+        // with .php.
+        $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
+
+        return $file;
+    }
+
+
     /**
      * Initialise caches, always call before accessing self:: caches.
      */
@@ -155,7 +223,6 @@ class core_component {
                 self::$classmap         = $cache['classmap'];
                 self::$classmaprenames  = $cache['classmaprenames'];
                 self::$filemap          = $cache['filemap'];
-                self::$psrclassmap      = $cache['psrclassmap'];
                 return;
             }
 
@@ -196,7 +263,6 @@ class core_component {
                     self::$classmap         = $cache['classmap'];
                     self::$classmaprenames  = $cache['classmaprenames'];
                     self::$filemap          = $cache['filemap'];
-                    self::$psrclassmap      = $cache['psrclassmap'];
                     return;
                 }
                 // Note: we do not verify $CFG->admin here intentionally,
@@ -284,7 +350,6 @@ class core_component {
             'classmaprenames'   => self::$classmaprenames,
             'filemap'           => self::$filemap,
             'version'           => self::$version,
-            'psrclassmap'       => self::$psrclassmap,
         );
 
         return '<?php
@@ -308,7 +373,6 @@ $cache = '.var_export($cache, true).';
         self::fill_classmap_cache();
         self::fill_classmap_renames_cache();
         self::fill_filemap_cache();
-        self::fill_psr_cache();
         self::fetch_core_version();
     }
 
@@ -692,77 +756,6 @@ $cache = '.var_export($cache, true).';
         unset($items);
     }
 
-    /**
-     * Fill caches for classes following the PSR-0 standard for the
-     * specified Vendors.
-     *
-     * PSR Autoloading is detailed at http://www.php-fig.org/psr/psr-0/.
-     */
-    protected static function fill_psr_cache() {
-        global $CFG;
-
-        $psrsystems = array(
-            'Horde' => 'horde/framework',
-        );
-        self::$psrclassmap = array();
-
-        foreach ($psrsystems as $system => $fulldir) {
-            if (!$fulldir) {
-                continue;
-            }
-            self::load_psr_classes($CFG->libdir . DIRECTORY_SEPARATOR . $fulldir);
-        }
-    }
-
-    /**
-     * Find all PSR-0 style classes in within the base directory.
-     *
-     * @param string $basedir The base directory that the PSR-type library can be found in.
-     * @param string $subdir The directory within the basedir to search for classes within.
-     */
-    protected static function load_psr_classes($basedir, $subdir = null) {
-        if ($subdir) {
-            $fulldir = realpath($basedir . DIRECTORY_SEPARATOR . $subdir);
-            $classnameprefix = preg_replace('#' . preg_quote(DIRECTORY_SEPARATOR) . '#', '_', $subdir);
-        } else {
-            $fulldir = $basedir;
-        }
-        if (!$fulldir || !is_dir($fulldir)) {
-            return;
-        }
-
-        $items = new \DirectoryIterator($fulldir);
-        foreach ($items as $item) {
-            if ($item->isDot()) {
-                continue;
-            }
-            if ($item->isDir()) {
-                $dirname = $item->getFilename();
-                $newsubdir = $dirname;
-                if ($subdir) {
-                    $newsubdir = implode(DIRECTORY_SEPARATOR, array($subdir, $dirname));
-                }
-                self::load_psr_classes($basedir, $newsubdir);
-                continue;
-            }
-
-            $filename = $item->getFilename();
-            $classname = preg_replace('/\.php$/', '', $filename);
-
-            if ($filename === $classname) {
-                // Not a php file.
-                continue;
-            }
-
-            if ($classnameprefix) {
-                $classname = $classnameprefix . '_' . $classname;
-            }
-
-            self::$psrclassmap[$classname] = $fulldir . DIRECTORY_SEPARATOR . $filename;
-        }
-        unset($item);
-        unset($items);
-    }
 
     /**
      * List all core subsystems and their location
diff --git a/lib/classes/event/grade_exported.php b/lib/classes/event/grade_exported.php
new file mode 100644 (file)
index 0000000..88e1e31
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Grade report viewed event.
+ *
+ * @package    core
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade report viewed event class.
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Zane Karl <zkarl@oid.ucla.edu>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class grade_exported extends base {
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        if (!($this instanceof grade_exported)) {
+            throw new Exception('grade_exported abstract is NOT extended by a valid component.');
+        }
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised export type.
+     *
+     * @return string
+     */
+    public static function get_export_type() {
+        $classname = explode('\\', get_called_class());
+        $exporttype = explode('_', $classname[0]);
+        return $exporttype[1];
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventgradeexported', 'gradeexport_'. self::get_export_type());
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid'"
+                . " exported grades using the ".
+                $this->get_export_type() ." export in the gradebook.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $url = '/grade/export/' . $this->get_export_type() . '/export.php';
+        return new \moodle_url($url, array('id' => $this->courseid));
+    }
+}
\ No newline at end of file
index 381e889..97ba197 100644 (file)
@@ -706,14 +706,11 @@ class pgsql_native_moodle_database extends moodle_database {
 
         list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
 
-        if ($limitfrom or $limitnum) {
-            if ($limitnum < 1) {
-                $limitnum = "ALL";
-            } else if (PHP_INT_MAX - $limitnum < $limitfrom) {
-                // this is a workaround for weird max int problem
-                $limitnum = "ALL";
-            }
-            $sql .= " LIMIT $limitnum OFFSET $limitfrom";
+        if ($limitnum) {
+            $sql .= " LIMIT $limitnum";
+        }
+        if ($limitfrom) {
+            $sql .= " OFFSET $limitfrom";
         }
 
         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
@@ -748,14 +745,11 @@ class pgsql_native_moodle_database extends moodle_database {
 
         list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
 
-        if ($limitfrom or $limitnum) {
-            if ($limitnum < 1) {
-                $limitnum = "ALL";
-            } else if (PHP_INT_MAX - $limitnum < $limitfrom) {
-                // this is a workaround for weird max int problem
-                $limitnum = "ALL";
-            }
-            $sql .= " LIMIT $limitnum OFFSET $limitfrom";
+        if ($limitnum) {
+            $sql .= " LIMIT $limitnum";
+        }
+        if ($limitfrom) {
+            $sql .= " OFFSET $limitfrom";
         }
 
         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
index 0cb4067..f8ad74c 100644 (file)
@@ -106,4 +106,4 @@ $content = '';
 foreach ($jsfiles as $jsfile) {
     $content .= file_get_contents($jsfile)."\n";
 }
-js_send_uncached($content, $etag);
+js_send_uncached($content);
index 411499f..9e57b24 100644 (file)
@@ -2053,6 +2053,11 @@ function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0,
 
     $time = $date->getTimestamp();
 
+    if ($time === false) {
+        throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
+            ' This can fail if year is more than 2038 and OS is 32 bit windows');
+    }
+
     // Moodle BC DST stuff.
     if (!$applydst) {
         $time += dst_offset_on($time, $timezone);
index 7cc8d0f..607cf93 100644 (file)
@@ -141,4 +141,4 @@ foreach ($jsfiles as $modulename => $jsfile) {
     $js = implode($replace, explode($search, $js, 2));
     $content .= $js;
 }
-js_send_uncached($content, $etag, 'requirejs.php');
+js_send_uncached($content, 'requirejs.php');
index 657b351..4d7263a 100644 (file)
@@ -28,7 +28,7 @@
 require_once(__DIR__ . '/../../behat/behat_base.php');
 
 use Behat\Gherkin\Node\TableNode as TableNode;
-use Behat\Behat\Exception\PendingException as PendingException;
+use Behat\Behat\Tester\Exception\PendingException as PendingException;
 
 /**
  * Class to set up quickly a Given environment.
index 26774b3..b257482 100644 (file)
@@ -28,7 +28,8 @@
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
 use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
-    Behat\Gherkin\Node\TableNode as TableNode;
+    Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Gherkin\Node\PyStringNode as PyStringNode;
 
 /**
  * Deprecated behat step definitions.
@@ -269,6 +270,22 @@ class behat_deprecated extends behat_base {
         $this->deprecated_message($alternative, true);
     }
 
+    /**
+     * Sets the specified value to the field.
+     *
+     * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to multiline$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $field
+     * @param PyStringNode $value
+     * @deprecated since Moodle 3.2 MDL-55406 - please do not use this step any more.
+     */
+    public function i_set_the_field_to_multiline($field, PyStringNode $value) {
+
+        $alternative = 'I set the field "' . $this->escape($field) . '"  to multiline:';
+        $this->deprecated_message($alternative);
+
+        $this->execute('behat_forms::i_set_the_field_to_multiline', array($field, $value));
+    }
 
     /**
      * Throws an exception if $CFG->behat_usedeprecated is not allowed.
index d681d2c..2b362df 100644 (file)
@@ -221,7 +221,7 @@ class behat_forms extends behat_base {
     /**
      * Sets the specified value to the field.
      *
-     * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to multiline$/
+     * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to multiline:$/
      * @throws ElementNotFoundException Thrown by behat_base::find
      * @param string $field
      * @param PyStringNode $value
index b329411..21750ec 100644 (file)
@@ -36,6 +36,25 @@ class core_component_testcase extends advanced_testcase {
     // always verify that it does not collide with any existing add-on modules and subplugins!!!
     const SUBSYSTEMCOUNT = 65;
 
+    public function setUp() {
+        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
+        $psr0namespaces->setAccessible(true);
+        $this->oldpsr0namespaces = $psr0namespaces->getValue(null);
+
+        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
+        $psr4namespaces->setAccessible(true);
+        $this->oldpsr4namespaces = $psr4namespaces->getValue(null);
+    }
+    public function tearDown() {
+        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
+        $psr0namespaces->setAccessible(true);
+        $psr0namespaces->setValue(null, $this->oldpsr0namespaces);
+
+        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
+        $psr4namespaces->setAccessible(true);
+        $psr4namespaces->setValue(null, $this->oldpsr4namespaces);
+    }
+
     public function test_get_core_subsystems() {
         global $CFG;
 
@@ -469,4 +488,275 @@ class core_component_testcase extends advanced_testcase {
         $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile\\'));
         $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile'));
     }
+
+    /**
+     * Data provider for classloader test
+     */
+    public function classloader_provider() {
+        global $CFG;
+
+        // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
+        // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
+        // If problems arise we can remove this test, but will need to add a warning.
+        // Normalise to forward slash for testing purposes.
+        $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/";
+
+        $psr0 = [
+          'psr0'      => 'lib/tests/fixtures/component/psr0',
+          'overlap'   => 'lib/tests/fixtures/component/overlap'
+        ];
+        $psr4 = [
+          'psr4'      => 'lib/tests/fixtures/component/psr4',
+          'overlap'   => 'lib/tests/fixtures/component/overlap'
+        ];
+        return [
+          'PSR-0 Classloading - Root' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr0_main',
+              'includedfiles' => "{$directory}psr0/main.php",
+          ],
+          'PSR-0 Classloading - Sub namespace - underscores' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr0_subnamespace_example',
+              'includedfiles' => "{$directory}psr0/subnamespace/example.php",
+          ],
+          'PSR-0 Classloading - Sub namespace - slashes' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr0\\subnamespace\\slashes',
+              'includedfiles' => "{$directory}psr0/subnamespace/slashes.php",
+          ],
+          'PSR-4 Classloading - Root' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr4\\main',
+              'includedfiles' => "{$directory}psr4/main.php",
+          ],
+          'PSR-4 Classloading - Sub namespace' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr4\\subnamespace\\example',
+              'includedfiles' => "{$directory}psr4/subnamespace/example.php",
+          ],
+          'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr4\\subnamespace\\underscore_example',
+              'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php",
+          ],
+          'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'overlap\\subnamespace\\example',
+              'includedfiles' => "{$directory}overlap/subnamespace/example.php",
+          ],
+          'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'overlap_subnamespace_example2',
+              'includedfiles' => "{$directory}overlap/subnamespace/example2.php",
+          ],
+        ];
+    }
+
+    /**
+     * Test the classloader.
+     *
+     * @dataProvider classloader_provider
+     * @param array $psr0 The PSR-0 namespaces to be used in the test.
+     * @param array $psr4 The PSR-4 namespaces to be used in the test.
+     * @param string $classname The name of the class to attempt to load.
+     * @param string $includedfiles The file expected to be loaded.
+     */
+    public function test_classloader($psr0, $psr4, $classname, $includedfiles) {
+        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
+        $psr0namespaces->setAccessible(true);
+        $psr0namespaces->setValue(null, $psr0);
+
+        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
+        $psr4namespaces->setAccessible(true);
+        $psr4namespaces->setValue(null, $psr4);
+
+        core_component::classloader($classname);
+        if (DIRECTORY_SEPARATOR != '/') {
+            // Denormalise the expected path so that we can quickly compare with get_included_files.
+            $includedfiles = str_replace('/', DIRECTORY_SEPARATOR, $includedfiles);
+        }
+        $this->assertContains($includedfiles, get_included_files());
+        $this->assertTrue(class_exists($classname, false));
+    }
+
+    /**
+     * Data provider for psr_classloader test
+     */
+    public function psr_classloader_provider() {
+        global $CFG;
+
+        // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces.
+        // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names.
+        // If problems arise we can remove this test, but will need to add a warning.
+        // Normalise to forward slash for testing purposes.
+        $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/";
+
+        $psr0 = [
+          'psr0'      => 'lib/tests/fixtures/component/psr0',
+          'overlap'   => 'lib/tests/fixtures/component/overlap'
+        ];
+        $psr4 = [
+          'psr4'      => 'lib/tests/fixtures/component/psr4',
+          'overlap'   => 'lib/tests/fixtures/component/overlap'
+        ];
+        return [
+          'PSR-0 Classloading - Root' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr0_main',
+              'file' => "{$directory}psr0/main.php",
+          ],
+          'PSR-0 Classloading - Sub namespace - underscores' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr0_subnamespace_example',
+              'file' => "{$directory}psr0/subnamespace/example.php",
+          ],
+          'PSR-0 Classloading - Sub namespace - slashes' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr0\\subnamespace\\slashes',
+              'file' => "{$directory}psr0/subnamespace/slashes.php",
+          ],
+          'PSR-0 Classloading - non-existant file' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr0_subnamespace_nonexistant_file',
+              'file' => false,
+          ],
+          'PSR-4 Classloading - Root' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr4\\main',
+              'file' => "{$directory}psr4/main.php",
+          ],
+          'PSR-4 Classloading - Sub namespace' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr4\\subnamespace\\example',
+              'file' => "{$directory}psr4/subnamespace/example.php",
+          ],
+          'PSR-4 Classloading - Ensure underscores are not converted to paths' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr4\\subnamespace\\underscore_example',
+              'file' => "{$directory}psr4/subnamespace/underscore_example.php",
+          ],
+          'PSR-4 Classloading - non-existant file' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'psr4\\subnamespace\\nonexistant',
+              'file' => false,
+          ],
+          'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'overlap\\subnamespace\\example',
+              'file' => "{$directory}overlap/subnamespace/example.php",
+          ],
+          'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [
+              'psr0' => $psr0,
+              'psr4' => $psr4,
+              'classname' => 'overlap_subnamespace_example2',
+              'file' => "{$directory}overlap/subnamespace/example2.php",
+          ],
+        ];
+    }
+
+    /**
+     * Test the PSR classloader.
+     *
+     * @dataProvider psr_classloader_provider
+     * @param array $psr0 The PSR-0 namespaces to be used in the test.
+     * @param array $psr4 The PSR-4 namespaces to be used in the test.
+     * @param string $classname The name of the class to attempt to load.
+     * @param string|bool $file The expected file corresponding to the class or false for nonexistant.
+     */
+    public function test_psr_classloader($psr0, $psr4, $classname, $file) {
+        $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
+        $psr0namespaces->setAccessible(true);
+        $psr0namespaces->setValue(null, $psr0);
+
+        $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces');
+        $psr4namespaces->setAccessible(true);
+        $oldpsr4namespaces = $psr4namespaces->getValue(null);
+        $psr4namespaces->setValue(null, $psr4);
+
+        $component = new ReflectionClass('core_component');
+        $psrclassloader = $component->getMethod('psr_classloader');
+        $psrclassloader->setAccessible(true);
+
+        $returnvalue = $psrclassloader->invokeArgs(null, array($classname));
+        // Normalise to forward slashes for testing comparison.
+        if ($returnvalue) {
+            $returnvalue = str_replace('\\', '/', $returnvalue);
+        }
+        $this->assertEquals($file, $returnvalue);
+    }
+
+    /**
+     * Data provider for get_class_file test
+     */
+    public function get_class_file_provider() {
+        global $CFG;
+
+        return [
+          'Getting a file with underscores' => [
+              'classname' => 'Test_With_Underscores',
+              'prefix' => "Test",
+              'path' => 'test/src',
+              'separators' => ['_'],
+              'result' => $CFG->dirroot . "/test/src/With/Underscores.php",
+          ],
+          'Getting a file with slashes' => [
+              'classname' => 'Test\\With\\Slashes',
+              'prefix' => "Test",
+              'path' => 'test/src',
+              'separators' => ['\\'],
+              'result' => $CFG->dirroot . "/test/src/With/Slashes.php",
+          ],
+          'Getting a file with multiple namespaces' => [
+              'classname' => 'Test\\With\\Multiple\\Namespaces',
+              'prefix' => "Test\\With",
+              'path' => 'test/src',
+              'separators' => ['\\'],
+              'result' => $CFG->dirroot . "/test/src/Multiple/Namespaces.php",
+          ],
+          'Getting a file with multiple namespaces' => [
+              'classname' => 'Nonexistant\\Namespace\\Test',
+              'prefix' => "Test",
+              'path' => 'test/src',
+              'separators' => ['\\'],
+              'result' => false,
+          ],
+        ];
+    }
+
+    /**
+     * Test the PSR classloader.
+     *
+     * @dataProvider get_class_file_provider
+     * @param string $classname the name of the class.
+     * @param string $prefix The namespace prefix used to identify the base directory of the source files.
+     * @param string $path The relative path to the base directory of the source files.
+     * @param string[] $separators The characters that should be used for separating.
+     * @param string|bool $result The expected result to be returned from get_class_file.
+     */
+    public function test_get_class_file($classname, $prefix, $path, $separators, $result) {
+        $component = new ReflectionClass('core_component');
+        $psrclassloader = $component->getMethod('get_class_file');
+        $psrclassloader->setAccessible(true);
+
+        $file = $psrclassloader->invokeArgs(null, array($classname, $prefix, $path, $separators));
+        $this->assertEquals($result, $file);
+    }
 }
diff --git a/lib/tests/fixtures/component/overlap/subnamespace/example.php b/lib/tests/fixtures/component/overlap/subnamespace/example.php
new file mode 100644 (file)
index 0000000..9ac41a4
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Example file declaring a class
+ *
+ * @package    core
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace overlap\subnamespace;
+
+/**
+ * Example Test Class
+ *
+ * @package   core
+ * @copyright 2016 John Okely <john@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class example {
+}
diff --git a/lib/tests/fixtures/component/overlap/subnamespace/example2.php b/lib/tests/fixtures/component/overlap/subnamespace/example2.php
new file mode 100644 (file)
index 0000000..dada85f
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Example file declaring a class
+ *
+ * @package    core
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Example Test Class
+ *
+ * @package   core
+ * @copyright 2016 John Okely <john@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class overlap_subnamespace_example2 {
+}
diff --git a/lib/tests/fixtures/component/psr0/main.php b/lib/tests/fixtures/component/psr0/main.php
new file mode 100644 (file)
index 0000000..7e70a2e
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Example file declaring a class
+ *
+ * @package    core
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Example Test Class
+ *
+ * @package   core
+ * @copyright 2016 John Okely <john@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class psr0_main {
+}
diff --git a/lib/tests/fixtures/component/psr0/subnamespace/example.php b/lib/tests/fixtures/component/psr0/subnamespace/example.php
new file mode 100644 (file)
index 0000000..f0a89c9
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Example file declaring a class
+ *
+ * @package    core
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Example Test Class
+ *
+ * @package   core
+ * @copyright 2016 John Okely <john@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class psr0_subnamespace_example {
+}
diff --git a/lib/tests/fixtures/component/psr0/subnamespace/slashes.php b/lib/tests/fixtures/component/psr0/subnamespace/slashes.php
new file mode 100644 (file)
index 0000000..a63c0b8
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Example file declaring a class
+ *
+ * @package    core
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace psr0\subnamespace;
+
+/**
+ * Example Test Class
+ *
+ * @package   core
+ * @copyright 2016 John Okely <john@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class slashes {
+}
diff --git a/lib/tests/fixtures/component/psr4/main.php b/lib/tests/fixtures/component/psr4/main.php
new file mode 100644 (file)
index 0000000..4c46bde
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Example file declaring a class
+ *
+ * @package    core
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+ namespace PSR4;
+
+/**
+ * Example Test Class
+ *
+ * @package   core
+ * @copyright 2016 John Okely <john@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class main {
+}
diff --git a/lib/tests/fixtures/component/psr4/subnamespace/example.php b/lib/tests/fixtures/component/psr4/subnamespace/example.php
new file mode 100644 (file)
index 0000000..f2f93da
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Example file declaring a class
+ *
+ * @package    core
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace psr4\subnamespace;
+
+/**
+ * Example Test Class
+ *
+ * @package   core
+ * @copyright 2016 John Okely <john@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class example {
+}
diff --git a/lib/tests/fixtures/component/psr4/subnamespace/underscore_example.php b/lib/tests/fixtures/component/psr4/subnamespace/underscore_example.php
new file mode 100644 (file)
index 0000000..5723f10
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Example file declaring a class
+ *
+ * @package    core
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace psr4\subnamespace;
+
+/**
+ * Example Test Class
+ *
+ * @package   core
+ * @copyright 2016 John Okely <john@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class underscore_example {
+}
index 926ad3d..4f72273 100644 (file)
@@ -51,6 +51,7 @@ information provided here is intended especially for developers.
 * Added down arrow: $OUTPUT->darrow.
 * All file_packer implementations now accept an additional parameter to allow a simple boolean return value instead of
   an array of individual file statuses.
+* "I set the field "field_string" to multiline:" now end with colon (:), as PyStrings is supposed to end with ":"
 
 === 3.1 ===
 
index b227f05..c9b85f0 100644 (file)
@@ -36,3 +36,4 @@ TODO:
 20101122 - MDL-24600 - Eloy Lafuente (stronk7): Original import of 0.9.2 release
 20110318 - MDL-26891 - Eloy Lafuente (stronk7): Implemented earlier profiling runs
 20130621 - MDL-39733 - Eloy Lafuente (stronk7): Export & import of profiling runs
+20160721 - MDL-55292 - Russell Smith (mr-russ): Add support for tideways profiler collection for PHP7
index b2f16fc..fb0794a 100644 (file)
@@ -69,7 +69,7 @@ function profiling_start() {
     global $CFG, $SESSION, $SCRIPT;
 
     // If profiling isn't available, nothing to start
-    if (!extension_loaded('xhprof') || !function_exists('xhprof_enable')) {
+    if (!extension_loaded('xhprof') && !extension_loaded('tideways')) {
         return false;
     }
 
@@ -146,7 +146,11 @@ function profiling_start() {
 
     // Arrived here, the script is going to be profiled, let's do it
     $ignore = array('call_user_func', 'call_user_func_array');
-    xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' =>  $ignore));
+    if (extension_loaded('tideways')) {
+        tideways_enable(TIDEWAYS_FLAGS_CPU + TIDEWAYS_FLAGS_MEMORY, array('ignored_functions' =>  $ignore));
+    } else {
+        xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));
+    }
     profiling_is_running(true);
 
     // Started, return true
@@ -160,7 +164,7 @@ function profiling_stop() {
     global $CFG, $DB, $SCRIPT;
 
     // If profiling isn't available, nothing to stop
-    if (!extension_loaded('xhprof') || !function_exists('xhprof_enable')) {
+    if (!extension_loaded('xhprof') && !extension_loaded('tideways')) {
         return false;
     }
 
@@ -179,7 +183,11 @@ function profiling_stop() {
 
     // Arrived here, profiling is running, stop and save everything
     profiling_is_running(false);
-    $data = xhprof_disable();
+    if (extension_loaded('tideways')) {
+        $data = tideways_disable();
+    } else {
+        $data = xhprof_disable();
+    }
 
     // We only save the run after ensuring the DB table exists
     // (this prevents problems with profiling runs enabled in
index 846f467..ce4451e 100644 (file)
@@ -634,6 +634,14 @@ class mod_assign_external extends external_api {
                     'text' => $assignplugin->get_editor_text($name, $item->id),
                     'format' => $assignplugin->get_editor_format($name, $item->id)
                 );
+
+                // Now format the text.
+                foreach ($fileareas as $filearea => $name) {
+                    list($editorfieldinfo['text'], $editorfieldinfo['format']) = external_format_text(
+                        $editorfieldinfo['text'], $editorfieldinfo['format'], $assign->get_context()->id,
+                        $component, $filearea, $item->id);
+                }
+
                 $plugin['editorfields'][] = $editorfieldinfo;
             }
             $plugins[] = $plugin;
index d24714a..fb87d8d 100644 (file)
@@ -1868,7 +1868,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
 
         $data = new stdClass();
         $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
-                                         'text' => 'Submission text',
+                                         'text' => 'Submission text with a <a href="@@PLUGINFILE@@/intro.txt">link</a>',
                                          'format' => FORMAT_MOODLE);
 
         $draftidfile = file_get_unused_draft_itemid();
@@ -1908,6 +1908,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $this->resetAfterTest(true);
 
         list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status();
+        $studentsubmission = $assign->get_user_submission($student1->id, true);
 
         $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
         // We expect debugging because of the $PAGE object, this won't happen in a normal WS request.
@@ -1944,7 +1945,14 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         foreach ($result['lastattempt']['submission']['plugins'] as $plugin) {
             $submissionplugins[$plugin['type']] = $plugin;
         }
-        $this->assertEquals('Submission text', $submissionplugins['onlinetext']['editorfields'][0]['text']);
+
+        // Format expected online text.
+        $onlinetext = 'Submission text with a <a href="@@PLUGINFILE@@/intro.txt">link</a>';
+        list($expectedtext, $expectedformat) = external_format_text($onlinetext, FORMAT_HTML, $assign->get_context()->id,
+                'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $studentsubmission->id);
+
+        $this->assertEquals($expectedtext, $submissionplugins['onlinetext']['editorfields'][0]['text']);
+        $this->assertEquals($expectedformat, $submissionplugins['onlinetext']['editorfields'][0]['format']);
         $this->assertEquals('/', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']);
         $this->assertEquals('t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filename']);
     }
@@ -2016,6 +2024,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $this->resetAfterTest(true);
 
         list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status(true);
+        $studentsubmission = $assign->get_user_submission($student1->id, true);
 
         $this->setUser($teacher);
         // Grade and reopen.
@@ -2087,9 +2096,16 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         foreach ($result['previousattempts'][0]['submission']['plugins'] as $plugin) {
             $submissionplugins[$plugin['type']] = $plugin;
         }
-        $this->assertEquals('Submission text', $submissionplugins['onlinetext']['editorfields'][0]['text']);
+        // Format expected online text.
+        $onlinetext = 'Submission text with a <a href="@@PLUGINFILE@@/intro.txt">link</a>';
+        list($expectedtext, $expectedformat) = external_format_text($onlinetext, FORMAT_HTML, $assign->get_context()->id,
+                'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $studentsubmission->id);
+
+        $this->assertEquals($expectedtext, $submissionplugins['onlinetext']['editorfields'][0]['text']);
+        $this->assertEquals($expectedformat, $submissionplugins['onlinetext']['editorfields'][0]['format']);
         $this->assertEquals('/', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']);
         $this->assertEquals('t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filename']);
+
     }
 
     /**
index 5fde3dc..e36a70a 100644 (file)
@@ -746,22 +746,25 @@ function chat_format_message_manually($message, $courseid, $sender, $currentuser
     }
 
     // It's not a system event.
-    $text = trim($message->message);
+    $rawtext = trim($message->message);
 
-    // Parse the text to clean and filter it.
+    // Options for format_text, when we get to it...
+    // format_text call will parse the text to clean and filter it.
+    // It cannot be called here as HTML-isation interferes with special case
+    // recognition, but *must* be called on any user-sourced text to be inserted
+    // into $outmain.
     $options = new stdClass();
     $options->para = false;
     $options->blanktarget = true;
-    $text = format_text($text, FORMAT_MOODLE, $options, $courseid);
 
     // And now check for special cases.
     $patternto = '#^\s*To\s([^:]+):(.*)#';
     $special = false;
 
-    if (substr($text, 0, 5) == 'beep ') {
+    if (substr($rawtext, 0, 5) == 'beep ') {
         // It's a beep!
         $special = true;
-        $beepwho = trim(substr($text, 5));
+        $beepwho = trim(substr($rawtext, 5));
 
         if ($beepwho == 'all') {   // Everyone.
             $outinfobasic = get_string('messagebeepseveryone', 'chat', fullname($sender));
@@ -779,29 +782,31 @@ function chat_format_message_manually($message, $courseid, $sender, $currentuser
         } else {  // Something is not caught?
             return false;
         }
-    } else if (substr($text, 0, 1) == '/') {     // It's a user command.
+    } else if (substr($rawtext, 0, 1) == '/') {     // It's a user command.
         $special = true;
         $pattern = '#(^\/)(\w+).*#';
-        preg_match($pattern, $text, $matches);
+        preg_match($pattern, $rawtext, $matches);
         $command = isset($matches[2]) ? $matches[2] : false;
         // Support some IRC commands.
         switch ($command) {
             case 'me':
                 $outinfo = $message->strtime;
-                $outmain = '*** <b>'.$sender->firstname.' '.substr($text, 4).'</b>';
+                $text = '*** <b>'.$sender->firstname.' '.substr($rawtext, 4).'</b>';
+                $outmain = format_text($text, FORMAT_MOODLE, $options, $courseid);
                 break;
             default:
                 // Error, we set special back to false to use the classic message output.
                 $special = false;
                 break;
         }
-    } else if (preg_match($patternto, $text)) {
+    } else if (preg_match($patternto, $rawtext)) {
         $special = true;
         $matches = array();
-        preg_match($patternto, $text, $matches);
+        preg_match($patternto, $rawtext, $matches);
         if (isset($matches[1]) && isset($matches[2])) {
+            $text = format_text($matches[2], FORMAT_MOODLE, $options, $courseid);
             $outinfo = $message->strtime;
-            $outmain = $sender->firstname.' '.get_string('saidto', 'chat').' <i>'.$matches[1].'</i>: '.$matches[2];
+            $outmain = $sender->firstname.' '.get_string('saidto', 'chat').' <i>'.$matches[1].'</i>: '.$text;
         } else {
             // Error, we set special back to false to use the classic message output.
             $special = false;
@@ -809,6 +814,7 @@ function chat_format_message_manually($message, $courseid, $sender, $currentuser
     }
 
     if (!$special) {
+        $text = format_text($rawtext, FORMAT_MOODLE, $options, $courseid);
         $outinfo = $message->strtime.' '.$sender->firstname;
         $outmain = $text;
     }
@@ -920,13 +926,16 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     }
 
     // It's not a system event.
-    $text = trim($message->message);
+    $rawtext = trim($message->message);
 
-    // Parse the text to clean and filter it.
+    // Options for format_text, when we get to it...
+    // format_text call will parse the text to clean and filter it.
+    // It cannot be called here as HTML-isation interferes with special case
+    // recognition, but *must* be called on any user-sourced text to be inserted
+    // into $outmain.
     $options = new stdClass();
     $options->para = false;
     $options->blanktarget = true;
-    $text = format_text($text, FORMAT_MOODLE, $options, $courseid);
 
     // And now check for special cases.
     $special = false;
@@ -936,11 +945,11 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     $outmain = '';
     $patternto = '#^\s*To\s([^:]+):(.*)#';
 
-    if (substr($text, 0, 5) == 'beep ') {
+    if (substr($rawtext, 0, 5) == 'beep ') {
         $special = true;
         // It's a beep!
         $result->type = 'beep';
-        $beepwho = trim(substr($text, 5));
+        $beepwho = trim(substr($rawtext, 5));
 
         if ($beepwho == 'all') {   // Everyone.
             $outmain = get_string('messagebeepseveryone', 'chat', fullname($sender));
@@ -959,29 +968,31 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
                 $outmain = get_string('messageyoubeep', 'chat', $beepwho);
             }
         }
-    } else if (substr($text, 0, 1) == '/') {     // It's a user command.
+    } else if (substr($rawtext, 0, 1) == '/') {     // It's a user command.
         $special = true;
         $result->type = 'command';
         $pattern = '#(^\/)(\w+).*#';
-        preg_match($pattern, $text, $matches);
+        preg_match($pattern, $rawtext, $matches);
         $command = isset($matches[2]) ? $matches[2] : false;
         // Support some IRC commands.
         switch ($command) {
             case 'me':
-                $outmain = '*** <b>'.$sender->firstname.' '.substr($text, 4).'</b>';
+                $text = '*** <b>'.$sender->firstname.' '.substr($rawtext, 4).'</b>';
+                $outmain = format_text($text, FORMAT_MOODLE, $options, $courseid);
                 break;
             default:
                 // Error, we set special back to false to use the classic message output.
                 $special = false;
                 break;
         }
-    } else if (preg_match($patternto, $text)) {
+    } else if (preg_match($patternto, $rawtext)) {
         $special = true;
         $result->type = 'dialogue';
         $matches = array();
-        preg_match($patternto, $text, $matches);
+        preg_match($patternto, $rawtext, $matches);
         if (isset($matches[1]) && isset($matches[2])) {
-            $outmain = $sender->firstname.' <b>'.get_string('saidto', 'chat').'</b> <i>'.$matches[1].'</i>: '.$matches[2];
+            $text = format_text($matches[2], FORMAT_MOODLE, $options, $courseid);
+            $outmain = $sender->firstname.' <b>'.get_string('saidto', 'chat').'</b> <i>'.$matches[1].'</i>: '.$text;
         } else {
             // Error, we set special back to false to use the classic message output.
             $special = false;
@@ -989,6 +1000,7 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     }
 
     if (!$special) {
+        $text = format_text($rawtext, FORMAT_MOODLE, $options, $courseid);
         $outmain = $text;
     }
 
diff --git a/mod/chat/tests/format_message_test.php b/mod/chat/tests/format_message_test.php
new file mode 100644 (file)
index 0000000..10dd8c7
--- /dev/null
@@ -0,0 +1,169 @@
+<?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/>.
+
+/**
+ * Tests for format_message.
+ *
+ * @package    mod_chat
+ * @copyright  2016 Andrew NIcols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/chat/lib.php');
+
+/**
+ * Tests for format_message.
+ *
+ * @package    mod_chat
+ * @copyright  2016 Andrew NIcols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_chat_format_message_testcase extends advanced_testcase {
+
+    const USER_CURRENT = 1;
+    const USER_OTHER = 2;
+
+    public function chat_format_message_manually_provider() {
+        $dateregexp = '\d{2}:\d{2}';
+        return [
+            'Beep everyone' => [
+                'message'       => 'beep all',
+                'system'        => false,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp}: " . get_string('messagebeepseveryone', 'chat', '__CURRENTUSER__') . ': /',
+                'refreshusers'  => false,
+                'beep'          => true,
+            ],
+            'Beep the current user' => [
+                'message'       => 'beep __CURRENTUSER__',
+                'system'        => false,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp}: " . get_string('messagebeepsyou', 'chat', '__CURRENTUSER__') . ': /',
+                'refreshusers'  => false,
+                'beep'          => true,
+            ],
+            'Beep another user' => [
+                'message'       => 'beep __OTHERUSER__',
+                'system'        => false,
+                'willreturn'    => false,
+                'expecttext'    => null,
+                'refreshusers'  => null,
+                'beep'          => null,
+            ],
+            'Malformed beep' => [
+                'message'       => 'beep',
+                'system'        => false,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp} __CURRENTUSER_FIRST__: beep$/",
+                'refreshusers'  => false,
+                'beep'          => false,
+            ],
+            '/me says' => [
+                'message'       => '/me writes a test',
+                'system'        => false,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp}: \*\*\* __CURRENTUSER_FIRST__ writes a test$/",
+                'refreshusers'  => false,
+                'beep'          => false,
+            ],
+            'Invalid command' => [
+                'message'       => '/help',
+                'system'        => false,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp} __CURRENTUSER_FIRST__: \/help$/",
+                'refreshusers'  => false,
+                'beep'          => false,
+            ],
+            'To user' => [
+                'message'       => 'To Bernard:I love tests',
+                'system'        => false,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp}: __CURRENTUSER_FIRST__ " . get_string('saidto', 'chat') . " Bernard: I love tests$/",
+                'refreshusers'  => false,
+                'beep'          => false,
+            ],
+            'To user trimmed' => [
+                'message'       => 'To Bernard: I love tests',
+                'system'        => false,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp}: __CURRENTUSER_FIRST__ " . get_string('saidto', 'chat') . " Bernard: I love tests$/",
+                'refreshusers'  => false,
+                'beep'          => false,
+            ],
+            'System: enter' => [
+                'message'       => 'enter',
+                'system'        => true,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp}: " . get_string('messageenter', 'chat', '__CURRENTUSER__') . "$/",
+                'refreshusers'  => true,
+                'beep'          => false,
+            ],
+            'System: exit' => [
+                'message'       => 'exit',
+                'system'        => true,
+                'willreturn'    => true,
+                'expecttext'    => "/^{$dateregexp}: " . get_string('messageexit', 'chat', '__CURRENTUSER__') . "$/",
+                'refreshusers'  => true,
+                'beep'          => false,
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider chat_format_message_manually_provider
+     */
+    public function test_chat_format_message_manually($messagetext, $system, $willreturn,
+            $expecttext, $refreshusers, $expectbeep) {
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $currentuser = $this->getDataGenerator()->create_user();
+        $this->setUser($currentuser);
+        $otheruser = $this->getDataGenerator()->create_user();
+
+        // Replace the message texts.
+        // These can't be done in the provider because it runs before the
+        // test starts.
+        $messagetext = str_replace('__CURRENTUSER__', $currentuser->id, $messagetext);
+        $messagetext = str_replace('__OTHERUSER__', $otheruser->id, $messagetext);
+
+        $message = (object) [
+            'message'   => $messagetext,
+            'timestamp' => time(),
+            'system'    => $system,
+        ];
+
+        $result = chat_format_message_manually($message, $course->id, $currentuser, $currentuser);
+
+        if (!$willreturn) {
+            $this->assertFalse($result);
+        } else {
+            $this->assertNotFalse($result);
+            if (!empty($expecttext)) {
+                $expecttext = str_replace('__CURRENTUSER__', fullname($currentuser), $expecttext);
+                $expecttext = str_replace('__CURRENTUSER_FIRST__', $currentuser->firstname, $expecttext);
+                $this->assertRegexp($expecttext, $result->text);
+            }
+
+            $this->assertEquals($refreshusers, $result->refreshusers);
+            $this->assertEquals($expectbeep, $result->beep);
+        }
+    }
+}
index 0909f42..39d3423 100644 (file)
@@ -36,7 +36,7 @@ Feature: Users can be required to specify certain fields when adding entries to
       | Field name | Required Two-Option Checkbox |
       | Field description | Required Two-Option Checkbox |
       | Required | yes |
-    And I set the field "Options" to multiline
+    And I set the field "Options" to multiline:
     """
     RTOC Option 1
     RTOC Option 2
@@ -83,7 +83,7 @@ Feature: Users can be required to specify certain fields when adding entries to
       | Field name | Required Two-Option Multimenu |
       | Field description | Required Two-Option Multimenu |
       | Required | yes |
-    And I set the field "Options" to multiline
+    And I set the field "Options" to multiline:
     """
     Option 1
     Option 2
index 9098904..75c714d 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * mod_data data generator
+ * Data generator class for mod_data.
  *
  * @package    mod_data
  * @category   test
@@ -27,7 +27,9 @@ defined('MOODLE_INTERNAL') || die();
 
 
 /**
- * Database module data generator class
+ * Data generator class for mod_data.
+ *
+ * Currently, the field types in the ignoredfieldtypes array aren't supported.
  *
  * @package    mod_data
  * @category   test
@@ -36,8 +38,44 @@ defined('MOODLE_INTERNAL') || die();
  */
 class mod_data_generator extends testing_module_generator {
 
+    /**
+     * @var int keep track of how many database fields have been created.
+     */
+    protected $databasefieldcount = 0;
+
+    /**
+     * @var int keep track of how many database records have been created.
+     */
+    protected $databaserecordcount = 0;
+
+    /**
+     * @var The field types which not handled by the generator as of now.
+     */
+    protected $ignoredfieldtypes = array('latlong', 'file', 'picture');
+
+
+    /**
+     * To be called from data reset code only,
+     * do not use in tests.
+     * @return void
+     */
+    public function reset() {
+        $this->databasefieldcount = 0;
+        $this->databaserecordcount = 0;
+
+        parent::reset();
+    }
+
+    /**
+     * Creates a mod_data instance
+     *
+     * @param array $record
+     * @param array $options
+     * @return StdClass
+     */
     public function create_instance($record = null, array $options = null) {
-        $record = (object)(array)$record;
+        // Note, the parent class does not type $record to cast to array and then to object.
+        $record = (object) (array) $record;
 
         if (!isset($record->assessed)) {
             $record->assessed = 0;
@@ -46,6 +84,264 @@ class mod_data_generator extends testing_module_generator {
             $record->scale = 0;
         }
 
-        return parent::create_instance($record, (array)$options);
+        return parent::create_instance((array) $record, $options);
+    }
+
+    /**
+     * Creates a field for a mod_data instance.
+     * Currently, the field types in the ignoredfieldtypes array aren't supported.
+     *
+     * @param StdClass $record
+     * @param mod_data $data
+     * @return data_field_{type}
+     */
+    public function create_field(stdClass $record = null, $data = null) {
+        $record = (array) $record;
+
+        if (in_array($record['type'], $this->ignoredfieldtypes)) {
+            throw new coding_exception('$record\'s type value must not be same as values in ignoredfieldtypes
+                    in phpunit_util::create_field()');
+            return false;
+        }
+
+        $this->databasefieldcount++;
+
+        if (!isset($data->course)) {
+            throw new coding_exception('course must be present in phpunit_util::create_field() $data');
+        }
+
+        if (!isset($data->id)) {
+            throw new coding_exception('dataid must be present in phpunit_util::create_field() $data');
+        } else {
+            $record['dataid'] = $data->id;
+        }
+
+        if (!isset($record['type'])) {
+            throw new coding_exception('type must be present in phpunit_util::create_field() $record');
+        }
+
+        if (!isset($record['required'])) {
+            $record['required'] = 0;
+        }
+
+        if (!isset($record['name'])) {
+            $record['name'] = "testField - " . $this->databasefieldcount;
+        }
+
+        if (!isset($record['description'])) {
+            $record['description'] = " This is testField - " . $this->databasefieldcount;
+        }
+
+        if (!isset($record['param1'])) {
+            if (in_array($record['type'], array('checkbox', 'menu', 'multimenu', 'radiobutton'))) {
+                $record['param1'] = implode("\n", array('one', 'two', 'three', 'four'));
+            } else if (($record['type'] === 'text') || ($record['type'] === 'url')) {
+                $record['param1'] = 1;
+            } else {
+                $record['param1'] = '';
+            }
+        }
+
+        if (!isset($record['param2'])) {
+
+            if ($record['type'] === 'textarea') {
+                $record['param2'] = 60;
+            } else {
+                $record['param2'] = '';
+            }
+        }
+
+        if (!isset($record['param3'])) {
+
+            if (($record['type'] === 'textarea')) {
+                $record['param3'] = 35;
+            } else {
+                $record['param3'] = '';
+            }
+        }
+
+        if (!isset($record['param4'])) {
+
+            if (($record['type'] === 'textarea')) {
+                $record['param4'] = 1;
+            }
+        }
+
+        if (!isset($record['param5'])) {
+            if (($record['type'] === 'textarea')) {
+                $record['param5'] = 0;
+            }
+        }
+
+        $record = (object) $record;
+
+        $field = data_get_field($record, $data);
+        $field->insert_field();
+
+        data_generate_default_template($data, 'addtemplate', 0, false, true);
+
+        return $field;
+    }
+
+    /**
+     * Creates a field for a mod_data instance.
+     * Currently, the field types in the ignoredfieldtypes array aren't supported.
+     * The developers using the generator must adhere to the following format :
+     *
+     *   Syntax : $contents[ fieldid ] = fieldvalue
+     *   $contents['checkbox'] = array('val1', 'val2', 'val3' .....)
+     *   $contents['data'] = 'dd-mm-yyyy'
+     *   $contents['menu'] = 'value';
+     *   $contents['multimenu'] =  array('val1', 'val2', 'val3' .....)
+     *   $contents['number'] = 'numeric value'
+     *   $contents['radiobuton'] = 'value'
+     *   $contents['text'] = 'text'
+     *   $contents['textarea'] = 'text'
+     *   $contents['url'] = 'example.url' or array('example.url', 'urlname')
+     *
+     * @param mod_data $data
+     * @param array $contents
+     * @return data_field_{type}
+     */
+    public function create_entry($data, array $contents) {
+        global $DB;
+
+        $this->databaserecordcount++;
+
+        $recordid = data_add_record($data);
+
+        $fields = $DB->get_records('data_fields', array('dataid' => $data->id));
+
+        // Validating whether required field are filled.
+        foreach ($fields as $field) {
+            $fieldhascontent = false;
+
+            if (in_array($field->type, $this->ignoredfieldtypes)) {
+                continue;
+            }
+
+            $field = data_get_field($field, $data);
+
+            $fieldid = $field->field->id;
+
+            if ($field->type === 'date') {
+                $values = array();
+
+                $temp = explode('-', $contents[$fieldid], 3);
+
+                $values['field_' . $fieldid . '_day'] = $temp[0];
+                $values['field_' . $fieldid . '_month'] = $temp[1];
+                $values['field_' . $fieldid . '_year'] = $temp[2];
+
+                foreach ($values as $fieldname => $value) {
+                    if ($field->notemptyfield($value, $fieldname)) {
+                        continue 2;
+                    }
+                }
+            } else if ($field->type === 'textarea') {
+                $values = array();
+
+                $values['field_' . $fieldid] = $contents[$fieldid];
+                $values['field_' . $fieldid . '_content1'] = 1;
+
+                foreach ($values as $fieldname => $value) {
+                    if ($field->notemptyfield($value, $fieldname)) {
+                        continue 2;
+                    }
+                }
+            } else if ($field->type === 'url') {
+                $values = array();
+
+                if (is_array($contents[$fieldid])) {
+                    foreach ($contents[$fieldid] as $key => $value) {
+                        $values['field_' . $fieldid . '_' . $key] = $value;
+                    }
+                } else {
+                    $values['field_' . $fieldid . '_0'] = $contents[$fieldid];
+                }
+
+                foreach ($values as $fieldname => $value) {
+                    if ($field->notemptyfield($value, $fieldname)) {
+                        continue 2;
+                    }
+                }
+
+            } else {
+                if ($field->notemptyfield($contents[$fieldid], 'field_' . $fieldid . '_0')) {
+                    continue;
+                }
+            }
+
+            if ($field->field->required && !$fieldhascontent) {
+                return false;
+            }
+        }
+
+        foreach ($contents as $fieldid => $content) {
+
+            $field = $DB->get_record('data_fields', array('id' => $fieldid));
+            $field = data_get_field($field, $data);
+
+            if (in_array($field->field->type, $this->ignoredfieldtypes)) {
+                continue;
+            }
+
+            if ($field->type === 'date') {
+                $values = array();
+
+                $temp = explode('-', $content, 3);
+
+                $values['field_' . $fieldid . '_day'] = (int)trim($temp[0]);
+                $values['field_' . $fieldid . '_month'] = (int)trim($temp[1]);
+                $values['field_' . $fieldid . '_year'] = (int)trim($temp[2]);
+
+                // Year should be less than 2038, so it can be handled by 32 bit windows.
+                if ($values['field_' . $fieldid . '_year'] > 2038) {
+                    throw new coding_exception('DateTime::getTimestamp resturns false on 32 bit win for year beyond ' .
+                        '2038. Please use year less than 2038.');
+                }
+
+                foreach ($values as $fieldname => $value) {
+                    $field->update_content($recordid, $value, $fieldname);
+                }
+
+                continue;
+            }
+
+            if ($field->type === 'textarea') {
+                $values = array();
+
+                $values['field_' . $fieldid] = $content;
+                $values['field_' . $fieldid . '_content1'] = 1;
+
+                foreach ($values as $fieldname => $value) {
+                    $field->update_content($recordid, $value, $fieldname);
+                }
+
+                continue;
+            }
+
+            if ($field->type === 'url') {
+                $values = array();
+
+                if (is_array($content)) {
+                    foreach ($content as $key => $value) {
+                        $values['field_' . $fieldid . '_' . $key] = $value;
+                    }
+                } else {
+                    $values['field_' . $fieldid . '_0'] = $content;
+                }
+
+                foreach ($values as $fieldname => $value) {
+                    $field->update_content($recordid, $value, $fieldname);
+                }
+
+                continue;
+            }
+
+            $field->update_content($recordid, $contents[$fieldid]);
+        }
+
+        return $recordid;
     }
 }
index a10d11a..c554423 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * PHPUnit data generator tests
+ * PHPUnit data generator tests.
  *
  * @package    mod_data
  * @category   phpunit
@@ -27,7 +27,7 @@ defined('MOODLE_INTERNAL') || die();
 
 
 /**
- * PHPUnit data generator testcase
+ * PHPUnit data generator testcase.
  *
  * @package    mod_data
  * @category   phpunit
@@ -49,9 +49,9 @@ class mod_data_generator_testcase extends advanced_testcase {
         $this->assertInstanceOf('mod_data_generator', $generator);
         $this->assertEquals('data', $generator->get_modulename());
 
-        $generator->create_instance(array('course'=>$course->id));
-        $generator->create_instance(array('course'=>$course->id));
-        $data = $generator->create_instance(array('course'=>$course->id));
+        $generator->create_instance(array('course' => $course->id));
+        $generator->create_instance(array('course' => $course->id));
+        $data = $generator->create_instance(array('course' => $course->id));
         $this->assertEquals(3, $DB->count_records('data'));
 
         $cm = get_coursemodule_from_instance('data', $data->id);
@@ -63,12 +63,138 @@ class mod_data_generator_testcase extends advanced_testcase {
         $this->assertEquals($data->cmid, $context->instanceid);
 
         // test gradebook integration using low level DB access - DO NOT USE IN PLUGIN CODE!
-        $data = $generator->create_instance(array('course'=>$course->id, 'assessed'=>1, 'scale'=>100));
-        $gitem = $DB->get_record('grade_items', array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule'=>'data', 'iteminstance'=>$data->id));
+        $data = $generator->create_instance(array('course' => $course->id, 'assessed' => 1, 'scale' => 100));
+        $gitem = $DB->get_record('grade_items', array('courseid' => $course->id, 'itemtype' => 'mod',
+                'itemmodule' => 'data', 'iteminstance' => $data->id));
         $this->assertNotEmpty($gitem);
         $this->assertEquals(100, $gitem->grademax);
         $this->assertEquals(0, $gitem->grademin);
         $this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype);
+    }
+
+    public function test_create_field() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $this->setAdminUser();
+        $this->assertEquals(0, $DB->count_records('data'));
+
+        $course = $this->getDataGenerator()->create_course();
+
+        /** @var mod_data_generator $generator */
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+        $this->assertInstanceOf('mod_data_generator', $generator);
+        $this->assertEquals('data', $generator->get_modulename());
+
+        $data = $generator->create_instance(array('course' => $course->id));
+        $this->assertEquals(1, $DB->count_records('data'));
+
+        $cm = get_coursemodule_from_instance('data', $data->id);
+        $this->assertEquals($data->id, $cm->instance);
+        $this->assertEquals('data', $cm->modname);
+        $this->assertEquals($course->id, $cm->course);
+
+        $context = context_module::instance($cm->id);
+        $this->assertEquals($data->cmid, $context->instanceid);
+
+        $fieldtypes = array('checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url');
+
+        $count = 1;
+
+        // Creating test Fields with default parameter values.
+        foreach ($fieldtypes as $fieldtype) {
+
+            // Creating variables dynamically.
+            $fieldname = 'field-' . $count;
+            $record = new StdClass();
+            $record->name = $fieldname;
+            $record->type = $fieldtype;
+
+            ${$fieldname} = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data);
+
+            $this->assertInstanceOf('data_field_' . $fieldtype, ${$fieldname});
+            $count++;
+        }
+
+        $this->assertEquals(count($fieldtypes), $DB->count_records('data_fields', array('dataid' => $data->id)));
+
+        $addtemplate = $DB->get_record('data', array('id' => $data->id), 'addtemplate');
+        $addtemplate = $addtemplate->addtemplate;
+
+        for ($i = 1; $i < $count; $i++) {
+            $fieldname = 'field-' . $i;
+            $this->assertTrue(strpos($addtemplate, '[[' . $fieldname . ']]') >= 0);
+        }
+    }
+
+    public function test_create_entry() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $this->setAdminUser();
+        $this->assertEquals(0, $DB->count_records('data'));
+
+        $course = $this->getDataGenerator()->create_course();
+
+        /** @var mod_data_generator $generator */
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+        $this->assertInstanceOf('mod_data_generator', $generator);
+        $this->assertEquals('data', $generator->get_modulename());
+
+        $data = $generator->create_instance(array('course' => $course->id));
+        $this->assertEquals(1, $DB->count_records('data'));
+
+        $cm = get_coursemodule_from_instance('data', $data->id);
+        $this->assertEquals($data->id, $cm->instance);
+        $this->assertEquals('data', $cm->modname);
+        $this->assertEquals($course->id, $cm->course);
+
+        $context = context_module::instance($cm->id);
+        $this->assertEquals($data->cmid, $context->instanceid);
+
+        $fieldtypes = array('checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url');
+
+        $count = 1;
+
+        // Creating test Fields with default parameter values.
+        foreach ($fieldtypes as $fieldtype) {
+
+            // Creating variables dynamically.
+            $fieldname = 'field-' . $count;
+            $record = new StdClass();
+            $record->name = $fieldname;
+            $record->type = $fieldtype;
+
+            ${$fieldname} = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data);
+            $this->assertInstanceOf('data_field_' . $fieldtype, ${$fieldname});
+            $count++;
+        }
+
+        $this->assertEquals(count($fieldtypes), $DB->count_records('data_fields', array('dataid' => $data->id)));
+
+        $fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id');
+
+        $contents = array();
+        $contents[] = array('one', 'two', 'three', 'four');
+        $contents[] = '01-01-2037'; // It should be lower than 2038, to avoid failing on 32-bit windows.
+        $contents[] = 'one';
+        $contents[] = array('one', 'two', 'three', 'four');
+        $contents[] = '12345';
+        $contents[] = 'one';
+        $contents[] = 'text for testing';
+        $contents[] = '<p>text area testing<br /></p>';
+        $contents[] = array('example.url', 'sampleurl');
+        $count = 0;
+        $fieldcontents = array();
+        foreach ($fields as $fieldrecord) {
+            $fieldcontents[$fieldrecord->id] = $contents[$count++];
+        }
+
+        $datarecordid = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($data, $fieldcontents);
 
+        $this->assertEquals(1, $DB->count_records('data_records', array('dataid' => $data->id)));
+        $this->assertEquals(count($contents), $DB->count_records('data_content', array('recordid' => $datarecordid)));
     }
 }
index a2a0b2e..7df4a3a 100644 (file)
@@ -23,6 +23,8 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+defined('MOODLE_INTERNAL') || die();
+
 /** Include eventslib.php */
 require_once($CFG->libdir.'/eventslib.php');
 // Include forms lib.
index ed4f87e..c8367e3 100644 (file)
@@ -239,6 +239,10 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
             $data->overduehandling = get_config('quiz', 'overduehandling');
         }
 
+        // Old shufflequestions setting is now stored in quiz sections,
+        // so save it here if necessary so it is available when we need it.
+        $this->legacyshufflequestionsoption = !empty($data->shufflequestions);
+
         // Insert the quiz record.
         $newitemid = $DB->insert_record('quiz', $data);
         // Immediately after inserting "activity" record, call this.
index 2359578..2b71155 100644 (file)
@@ -863,6 +863,7 @@ class mod_quiz_external extends external_api {
      *
      * @return external_single_structure the question structure
      * @since  Moodle 3.1
+     * @since Moodle 3.2 blockedbyprevious parameter added.
      */
     private static function question_structure() {
         return new external_single_structure(
@@ -875,6 +876,8 @@ class mod_quiz_external extends external_api {
                 'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL),
                 'state' => new external_value(PARAM_ALPHA, 'the state where the question is in', VALUE_OPTIONAL),
                 'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
+                'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
+                        VALUE_OPTIONAL),
                 'mark' => new external_value(PARAM_RAW, 'the mark awarded', VALUE_OPTIONAL),
                 'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt', VALUE_OPTIONAL),
             )
@@ -911,6 +914,7 @@ class mod_quiz_external extends external_api {
                 $question['number'] = $attemptobj->get_question_number($slot);
                 $question['state'] = (string) $attemptobj->get_question_state($slot);
                 $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
+                $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
             }
             if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
                 $question['maxmark'] = $attemptobj->get_question_attempt($slot)->get_max_mark();
index c106aca..9aa209e 100644 (file)
@@ -104,14 +104,16 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
      *
      * @param  boolean $startattempt whether to start a new attempt
      * @param  boolean $finishattempt whether to finish the new attempt
+     * @param  string $behaviour the quiz preferredbehaviour, defaults to 'deferredfeedback'.
      * @return array array containing the quiz, context and the attempt
      */
-    private function create_quiz_with_questions($startattempt = false, $finishattempt = false) {
+    private function create_quiz_with_questions($startattempt = false, $finishattempt = false, $behaviour = 'deferredfeedback') {
 
         // Create a new quiz with attempts.
         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
         $data = array('course' => $this->course->id,
-                      'sumgrades' => 2);
+                      'sumgrades' => 2,
+                      'preferredbehaviour' => $behaviour);
         $quiz = $quizgenerator->create_instance($data);
         $context = context_module::instance($quiz->cmid);
 
@@ -929,6 +931,51 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
 
     }
 
+    /**
+     * Test get_attempt_data with blocked questions.
+     * @since 3.2
+     */
+    public function test_get_attempt_data_with_blocked_questions() {
+        global $DB;
+
+        // Create a new quiz with one attempt started and using immediatefeedback.
+        list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(
+                true, false, 'immediatefeedback');
+
+        $quizobj = $attemptobj->get_quizobj();
+
+        // Make second question blocked by the first one.
+        $structure = $quizobj->get_structure();
+        $slots = $structure->get_slots();
+        $structure->update_question_dependency(end($slots)->id, true);
+
+        $quizobj->preload_questions();
+        $quizobj->load_questions();
+        $questions = $quizobj->get_questions();
+
+        $this->setUser($this->student);
+
+        // We receive one question per page.
+        $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
+
+        $this->assertEquals($attempt, (object) $result['attempt']);
+        $this->assertCount(1, $result['questions']);
+        $this->assertEquals(1, $result['questions'][0]['slot']);
+        $this->assertEquals(1, $result['questions'][0]['number']);
+        $this->assertEquals(false, $result['questions'][0]['blockedbyprevious']);
+
+        // Now try the last page.
+        $result = mod_quiz_external::get_attempt_data($attempt->id, 1);
+        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
+
+        $this->assertEquals($attempt, (object) $result['attempt']);
+        $this->assertCount(1, $result['questions']);
+        $this->assertEquals(2, $result['questions'][0]['slot']);
+        $this->assertEquals(2, $result['questions'][0]['number']);
+        $this->assertEquals(true, $result['questions'][0]['blockedbyprevious']);
+    }
+
     /**
      * Test get_attempt_summary
      */
index 299f877..3568c52 100644 (file)
@@ -1,5 +1,11 @@
 This files describes API changes in the quiz code.
 
+=== 3.2 ===
+
+* External functions mod_quiz_external::get_attempt_data, mod_quiz_external::get_attempt_summary
+  and mod_quiz_external::get_attempt_review now return additional optional fields:
+   - blockedbyprevious: Whether a question is blocked by the previous question.
+
 === 3.1 ===
 
 * quiz_attempt::question_print_comment_fields() has been removed. It was broken
index 91b86a9..80e12ed 100644 (file)
@@ -347,22 +347,38 @@ class qtype_multianswer_edit_form extends question_edit_form {
 
                         if ($subquestion->qtype == 'multichoice') {
                             $defaultvalues[$prefix.'layout'] = $subquestion->layout;
-                            switch ($subquestion->layout) {
-                                case '0':
-                                    $defaultvalues[$prefix.'layout'] =
+                            if ($subquestion->single == 1) {
+                                switch ($subquestion->layout) {
+                                    case '0':
+                                        $defaultvalues[$prefix.'layout'] =
                                             get_string('layoutselectinline', 'qtype_multianswer');
-                                    break;
-                                case '1':
-                                    $defaultvalues[$prefix.'layout'] =
+                                        break;
+                                    case '1':
+                                        $defaultvalues[$prefix.'layout'] =
                                             get_string('layoutvertical', 'qtype_multianswer');
-                                    break;
-                                case '2':
-                                    $defaultvalues[$prefix.'layout'] =
+                                        break;
+                                    case '2':
+                                        $defaultvalues[$prefix.'layout'] =
                                             get_string('layouthorizontal', 'qtype_multianswer');
-                                    break;
-                                default:
-                                    $defaultvalues[$prefix.'layout'] =
+                                        break;
+                                    default:
+                                        $defaultvalues[$prefix.'layout'] =
                                             get_string('layoutundefined', 'qtype_multianswer');
+                                }
+                            } else {
+                                switch ($subquestion->layout) {
+                                    case '1':
+                                        $defaultvalues[$prefix.'layout'] =
+                                            get_string('layoutmultiple_vertical', 'qtype_multianswer');
+                                        break;
+                                    case '2':
+                                        $defaultvalues[$prefix.'layout'] =
+                                            get_string('layoutmultiple_horizontal', 'qtype_multianswer');
+                                        break;
+                                    default:
+                                        $defaultvalues[$prefix.'layout'] =
+                                            get_string('layoutundefined', 'qtype_multianswer');
+                                }
                             }
                             if ($subquestion->shuffleanswers ) {
                                 $defaultvalues[$prefix.'shuffleanswers'] = get_string('yes', 'moodle');
@@ -393,6 +409,11 @@ class qtype_multianswer_edit_form extends question_edit_form {
                                 if ($subquestion->fraction[$key] > $maxfraction) {
                                     $maxfraction = $subquestion->fraction[$key];
                                 }
+                                // For 'multiresponse' we are OK if there is at least one fraction > 0.
+                                if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 &&
+                                    $subquestion->fraction[$key] > 0) {
+                                    $maxgrade = true;
+                                }
                             }
 
                             $defaultvalues[$prefix.'answer['.$key.']'] =
@@ -484,6 +505,11 @@ class qtype_multianswer_edit_form extends question_edit_form {
                             if ($subquestion->fraction[$key] > $maxfraction) {
                                 $maxfraction = $subquestion->fraction[$key];
                             }
+                            // For 'multiresponse' we are OK if there is at least one fraction > 0.
+                            if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 &&
+                                $subquestion->fraction[$key] > 0) {
+                                $maxgrade = true;
+                            }
                         }
                     }
                     if ($answercount == 0) {
index 5c42080..ddbe942 100644 (file)
@@ -30,6 +30,8 @@ $string['correctanswerandfeedback'] = 'Correct answer and feedback';
 $string['decodeverifyquestiontext'] = 'Decode and verify the question text';
 $string['layout'] = 'Layout';
 $string['layouthorizontal'] = 'Horizontal row of radio-buttons';
+$string['layoutmultiple_horizontal'] = 'Horizontal row of checkboxes';
+$string['layoutmultiple_vertical'] = 'Vertical column of checkboxes';
 $string['layoutselectinline'] = 'Dropdown menu in-line in the text';
 $string['layoutundefined'] = 'Undefined layout';
 $string['layoutvertical'] = 'Vertical column of radio buttons';
index e1acd61..17274b4 100644 (file)
@@ -279,7 +279,8 @@ define('NUMERICAL_ABS_ERROR_MARGIN', 6);
 define('ANSWER_TYPE_DEF_REGEX',
         '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
         '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)|' .
-        '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)');
+        '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)|'.
+        '(MULTIRESPONSE|MR)|(MULTIRESPONSE_H|MRH)|(MULTIRESPONSE_S|MRS)|(MULTIRESPONSE_HS|MRHS)');
 define('ANSWER_START_REGEX',
        '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
 
@@ -301,7 +302,11 @@ define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED', 9);
 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED', 10);
 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED', 11);
-define('ANSWER_REGEX_ALTERNATIVES', 12);
+define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE', 12);
+define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL', 13);
+define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED', 14);
+define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED', 15);
+define('ANSWER_REGEX_ALTERNATIVES', 16);
 
 /**
  * Initialise subquestion fields that are constant across all MULTICHOICE
@@ -387,6 +392,26 @@ function qtype_multianswer_extract_question($text) {
             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
             $wrapped->shuffleanswers = 1;
             $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
+        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE])) {
+            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
+            $wrapped->single = 0;
+            $wrapped->shuffleanswers = 0;
+            $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
+        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL])) {
+            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
+            $wrapped->single = 0;
+            $wrapped->shuffleanswers = 0;
+            $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
+        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED])) {
+            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
+            $wrapped->single = 0;
+            $wrapped->shuffleanswers = 1;
+            $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
+        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED])) {
+            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
+            $wrapped->single = 0;
+            $wrapped->shuffleanswers = 1;
+            $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
         } else {
             print_error('unknownquestiontype', 'question', '', $answerregs[2]);
             return false;
@@ -403,12 +428,14 @@ function qtype_multianswer_extract_question($text) {
         $wrapped->questiontext['itemid'] = '';
         $answerindex = 0;
 
+        $hasspecificfraction = false;
         $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
         while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) {
             if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
                 $wrapped->fraction["{$answerindex}"] = '1';
             } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
                 $wrapped->fraction["{$answerindex}"] = .01 * $percentile;
+                $hasspecificfraction = true;
             } else {
                 $wrapped->fraction["{$answerindex}"] = '0';
             }
@@ -453,6 +480,26 @@ function qtype_multianswer_extract_question($text) {
             $answerindex++;
         }
 
+        // Fix the score for multichoice_multi questions (as positive scores should add up to 1, not have a maximum of 1).
+        if (isset($wrapped->single) && $wrapped->single == 0) {
+            $total = 0;
+            foreach ($wrapped->fraction as $idx => $fraction) {
+                if ($fraction > 0) {
+                    $total += $fraction;
+                }
+            }
+            if ($total) {
+                foreach ($wrapped->fraction as $idx => $fraction) {
+                    if ($fraction > 0) {
+                        $wrapped->fraction[$idx] = $fraction / $total;
+                    } else if (!$hasspecificfraction) {
+                        // If no specific fractions are given, set incorrect answers to each cancel out one correct answer.
+                        $wrapped->fraction[$idx] = -(1.0 / $total);
+                    }
+                }
+            }
+        }
+
         $question->defaultmark += $wrapped->defaultmark;
         $question->options->questions[$positionkey] = clone($wrapped);
         $question->questiontext['text'] = implode("{#$positionkey}",
index baa76ce..7d54c58 100644 (file)
@@ -84,12 +84,20 @@ class qtype_multianswer_renderer extends qtype_renderer {
         if ($subtype == 'numerical' || $subtype == 'shortanswer') {
             $subrenderer = 'textfield';
         } else if ($subtype == 'multichoice') {
-            if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
-                $subrenderer = 'multichoice_inline';
-            } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
-                $subrenderer = 'multichoice_horizontal';
+            if ($subq instanceof qtype_multichoice_multi_question) {
+                if ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL) {
+                    $subrenderer = 'multiresponse_vertical';
+                } else {
+                    $subrenderer = 'multiresponse_horizontal';
+                }
             } else {
-                $subrenderer = 'multichoice_vertical';
+                if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
+                    $subrenderer = 'multichoice_inline';
+                } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
+                    $subrenderer = 'multichoice_horizontal';
+                } else {
+                    $subrenderer = 'multichoice_vertical';
+                }
             }
         } else {
             throw new coding_exception('Unexpected subquestion type.', $subq);
@@ -470,3 +478,187 @@ class qtype_multianswer_multichoice_horizontal_renderer
                 html_writer::end_tag('table');
     }
 }
+
+/**
+ * Class qtype_multianswer_multiresponse_renderer
+ *
+ * @copyright  2016 Davo Smith, Synergy Learning
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_multianswer_multiresponse_vertical_renderer extends qtype_multianswer_subq_renderer_base {
+
+    /**
+     * Output the content of the subquestion.
+     *
+     * @param question_attempt $qa
+     * @param question_display_options $options
+     * @param int $index
+     * @param question_graded_automatically $subq
+     * @return string
+     */
+    public function subquestion(question_attempt $qa, question_display_options $options,
+                                $index, question_graded_automatically $subq) {
+
+        if (!$subq instanceof qtype_multichoice_multi_question) {
+            throw new coding_exception('Expecting subquestion of type qtype_multichoice_multi_question');
+        }
+
+        $fieldprefix = 'sub' . $index . '_';
+        $fieldname = $fieldprefix . 'choice';
+
+        // Extract the responses that related to this question + strip off the prefix.
+        $fieldprefixlen = strlen($fieldprefix);
+        $response = [];
+        foreach ($qa->get_last_qt_data() as $name => $val) {
+            if (substr($name, 0, $fieldprefixlen) == $fieldprefix) {
+                $name = substr($name, $fieldprefixlen);
+                $response[$name] = $val;
+            }
+        }
+
+        $basename = $qa->get_qt_field_name($fieldname);
+        $inputattributes = array(
+            'type' => 'checkbox',
+            'value' => 1,
+        );
+        if ($options->readonly) {
+            $inputattributes['disabled'] = 'disabled';
+        }
+
+        $result = $this->all_choices_wrapper_start();
+
+        // Calculate the total score (as we need to know if choices should be marked as 'correct' or 'partial').
+        $fraction = 0;
+        foreach ($subq->get_order($qa) as $value => $ansid) {
+            $ans = $subq->answers[$ansid];
+            if ($subq->is_choice_selected($response, $value)) {
+                $fraction += $ans->fraction;
+            }
+        }
+        // Display 'correct' answers as correct, if we are at 100%, otherwise mark them as 'partial'.
+        $answerfraction = ($fraction > 0.999) ? 1.0 : 0.5;
+
+        foreach ($subq->get_order($qa) as $value => $ansid) {
+            $ans = $subq->answers[$ansid];
+
+            $name = $basename.$value;
+            $inputattributes['name'] = $name;
+            $inputattributes['id'] = $name;
+
+            $isselected = $subq->is_choice_selected($response, $value);
+            if ($isselected) {
+                $inputattributes['checked'] = 'checked';
+            } else {
+                unset($inputattributes['checked']);
+            }
+
+            $class = 'r' . ($value % 2);
+            if ($options->correctness && $isselected) {
+                $thisfrac = ($ans->fraction > 0) ? $answerfraction : 0;
+                $feedbackimg = $this->feedback_image($thisfrac);
+                $class .= ' ' . $this->feedback_class($thisfrac);
+            } else {
+                $feedbackimg = '';
+            }
+
+            $result .= $this->choice_wrapper_start($class);
+            $result .= html_writer::empty_tag('input', $inputattributes);
+            $result .= html_writer::tag('label', $subq->format_text($ans->answer,
+                                                                    $ans->answerformat, $qa, 'question', 'answer', $ansid),
+                                        array('for' => $inputattributes['id']));
+            $result .= $feedbackimg;
+
+            if ($options->feedback && $isselected && trim($ans->feedback)) {
+                $result .= html_writer::tag('div',
+                                            $subq->format_text($ans->feedback, $ans->feedbackformat,
+                                                               $qa, 'question', 'answerfeedback', $ansid),
+                                            array('class' => 'specificfeedback'));
+            }
+
+            $result .= $this->choice_wrapper_end();
+        }
+
+        $result .= $this->all_choices_wrapper_end();
+
+        $feedback = array();
+        if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
+            $subq->maxmark > 0) {
+            $a = new stdClass();
+            $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
+            $a->max = format_float($subq->maxmark, $options->markdp);
+
+            $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
+        }
+
+        if ($options->rightanswer) {
+            $correct = [];
+            foreach ($subq->answers as $ans) {
+                if (question_state::graded_state_for_fraction($ans->fraction) == question_state::$gradedpartial) {
+                    $correct[] = $subq->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ans->id);
+                }
+            }
+            $correct = '<ul><li>'.implode('</li><li>', $correct).'</li></ul>';
+            $feedback[] = get_string('correctansweris', 'qtype_multichoice', $correct);
+        }
+
+        $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
+
+        return $result;
+    }
+
+    /**
+     * @param string $class class attribute value.
+     * @return string HTML to go before each choice.
+     */
+    protected function choice_wrapper_start($class) {
+        return html_writer::start_tag('div', array('class' => $class));
+    }
+
+    /**
+     * @return string HTML to go after each choice.
+     */
+    protected function choice_wrapper_end() {
+        return html_writer::end_tag('div');
+    }
+
+    /**
+     * @return string HTML to go before all the choices.
+     */
+    protected function all_choices_wrapper_start() {
+        return html_writer::start_tag('div', array('class' => 'answer'));
+    }
+
+    /**
+     * @return string HTML to go after all the choices.
+     */
+    protected function all_choices_wrapper_end() {
+        return html_writer::end_tag('div');
+    }
+}
+
+/**
+ * Render an embedded multiple-response question horizontally.
+ *
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_multianswer_multiresponse_horizontal_renderer
+    extends qtype_multianswer_multiresponse_vertical_renderer {
+
+    protected function choice_wrapper_start($class) {
+        return html_writer::start_tag('td', array('class' => $class));
+    }
+
+    protected function choice_wrapper_end() {
+        return html_writer::end_tag('td');
+    }
+
+    protected function all_choices_wrapper_start() {
+        return html_writer::start_tag('table', array('class' => 'answer')) .
+        html_writer::start_tag('tbody') . html_writer::start_tag('tr');
+    }
+
+    protected function all_choices_wrapper_end() {
+        return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
+        html_writer::end_tag('table');
+    }
+}
index 1ba3408..cc19a48 100644 (file)
@@ -37,7 +37,7 @@ require_once($CFG->dirroot . '/question/type/multianswer/question.php');
  */
 class qtype_multianswer_test_helper extends question_test_helper {
     public function get_test_questions() {
-        return array('twosubq', 'fourmc', 'numericalzero', 'dollarsigns');
+        return array('twosubq', 'fourmc', 'numericalzero', 'dollarsigns', 'multiple');
     }
 
     /**
@@ -387,4 +387,96 @@ class qtype_multianswer_test_helper extends question_test_helper {
         return $q;
     }
 
+    /**
+     * Makes a multianswer question with multichoice_multiple questions in it.
+     * @return qtype_multianswer_question
+     */
+    public function make_multianswer_question_multiple() {
+        question_bank::load_question_definition_classes('multianswer');
+        $q = new qtype_multianswer_question();
+        test_question_maker::initialise_a_question($q);
+        $q->name = 'Multichoice multiple';
+        $q->questiontext = 'Please select the fruits {#1} and vegetables {#2}';
+        $q->generalfeedback = 'You should know which foods are fruits or vegetables.';
+        $q->qtype = question_bank::get_qtype('multianswer');
+
+        $q->textfragments = array(
+            'Please select the fruits ',
+            ' and vegetables ',
+            ''
+        );
+        $q->places = array('1' => '1', '2' => '2');
+
+        // Multiple-choice subquestion.
+        question_bank::load_question_definition_classes('multichoice');
+        $mc = new qtype_multichoice_multi_question();
+        test_question_maker::initialise_a_question($mc);
+        $mc->name = 'Multianswer 1';
+        $mc->questiontext = '{1:MULTIRESPONSE:=Apple#Good~%-50%Burger~%-50%Hot dog#Not a fruit~%-50%Pizza' .
+            '~=Orange#Correct~=Banana}';
+        $mc->questiontextformat = FORMAT_HTML;
+        $mc->generalfeedback = '';
+        $mc->generalfeedbackformat = FORMAT_HTML;
+
+        $mc->shuffleanswers = 0;
+        $mc->answernumbering = 'none';
+        $mc->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
+        $mc->single = 0;
+
+        $mc->answers = array(
+            16 => new question_answer(16, 'Apple', 0.3333333,
+                                      'Good', FORMAT_HTML),
+            17 => new question_answer(17, 'Burger', -0.5,
+                                      '', FORMAT_HTML),
+            18 => new question_answer(18, 'Hot dog', -0.5,
+                                      'Not a fruit', FORMAT_HTML),
+            19 => new question_answer(19, 'Pizza', -0.5,
+                                      '', FORMAT_HTML),
+            20 => new question_answer(20, 'Orange', 0.3333333,
+                                      'Correct', FORMAT_HTML),
+            21 => new question_answer(21, 'Banana', 0.3333333,
+                                      '', FORMAT_HTML),
+        );
+        $mc->qtype = question_bank::get_qtype('multichoice');
+        $mc->maxmark = 1;
+
+        // Multiple-choice subquestion.
+        question_bank::load_question_definition_classes('multichoice');
+        $mc2 = new qtype_multichoice_multi_question();
+        test_question_maker::initialise_a_question($mc2);
+        $mc2->name = 'Multichoice 2';
+        $mc2->questiontext = '{1:MULTIRESPONSE:=Raddish#Good~%-50%Chocolate~%-50%Biscuit#Not a vegetable~%-50%Cheese' .
+            '~=Carrot#Correct}';
+        $mc2->questiontextformat = FORMAT_HTML;
+        $mc2->generalfeedback = '';
+        $mc2->generalfeedbackformat = FORMAT_HTML;
+
+        $mc2->shuffleanswers = 0;
+        $mc2->answernumbering = 'none';
+        $mc2->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
+        $mc2->single = 0;
+
+        $mc2->answers = array(
+            22 => new question_answer(22, 'Raddish', 0.5,
+                                      'Good', FORMAT_HTML),
+            23 => new question_answer(23, 'Chocolate', -0.5,
+                                      '', FORMAT_HTML),
+            24 => new question_answer(24, 'Biscuit', -0.5,
+                                      'Not a vegetable', FORMAT_HTML),
+            25 => new question_answer(25, 'Cheese', -0.5,
+                                      '', FORMAT_HTML),
+            26 => new question_answer(26, 'Carrot', 0.5,
+                                      'Correct', FORMAT_HTML),
+        );
+        $mc2->qtype = question_bank::get_qtype('multichoice');
+        $mc2->maxmark = 1;
+
+        $q->subquestions = array(
+            1 => $mc,
+            2 => $mc2,
+        );
+
+        return $q;
+    }
+
 }
index a7b7e84..e29f6a2 100644 (file)
@@ -465,4 +465,61 @@ class qtype_multianswer_walkthrough_test extends qbehaviour_walkthrough_test_bas
                 $this->get_contains_correct_expectation(),
                 new question_no_pattern_expectation('/class="control\b[^"]*\bpartiallycorrect"/'));
     }
+
+    public function test_deferred_feedback_multiple() {
+
+        // Create a multianswer question.
+        $q = test_question_maker::make_question('multianswer', 'multiple');
+        $this->start_attempt_at_question($q, 'deferredfeedback', 2);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+            $this->get_contains_marked_out_of_summary(),
+            $this->get_does_not_contain_feedback_expectation(),
+            $this->get_does_not_contain_validation_error_expectation());
+
+        // Save in incomplete answer.
+        $this->process_submission(array('sub1_choice0' => '1', 'sub1_choice1' => '1',
+                                        'sub1_choice2' => '', 'sub1_choice3' => '',
+                                        'sub1_choice4' => '', 'sub1_choice5' => '1',
+                                        ));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+            $this->get_contains_marked_out_of_summary(),
+            $this->get_does_not_contain_feedback_expectation(),
+            $this->get_contains_validation_error_expectation());
+
+        // Save a partially correct answer.
+        $this->process_submission(array('sub1_choice0' => '1', 'sub1_choice1' => '',
+                                        'sub1_choice2' => '', 'sub1_choice3' => '',
+                                        'sub1_choice4' => '1', 'sub1_choice5' => '1',
+                                        'sub2_choice0' => '', 'sub2_choice1' => '',
+                                        'sub2_choice2' => '', 'sub2_choice3' => '',
+                                        'sub2_choice4' => '1',
+                                  ));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+            $this->get_contains_marked_out_of_summary(),
+            $this->get_does_not_contain_feedback_expectation(),
+            $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit all and finish.
+        $this->finish();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedpartial);
+        $this->check_current_mark(1.5);
+        $this->check_current_output(
+            $this->get_contains_mark_summary(1.5),
+            $this->get_contains_partcorrect_expectation(),
+            $this->get_does_not_contain_validation_error_expectation());
+    }
 }
index 8bdb8b4..f5f12fa 100644 (file)
@@ -286,7 +286,7 @@ class report_log_renderable implements renderable {
                 'r' => get_string('view'),
                 'u' => get_string('update'),
                 'd' => get_string('delete'),
-                '' => get_string('allchanges')
+                'cud' => get_string('allchanges')
                 );
         return $actions;
     }
index 58860ce..c1f3ff9 100644 (file)
@@ -380,8 +380,9 @@ class report_log_table_log extends table_sql {
                 $params['action'] = '%'.$action.'%';
             }
         } else if (!empty($this->filterparams->action)) {
-            $sql = "crud = :crud";
-            $params['crud'] = $this->filterparams->action;
+             list($sql, $params) = $DB->get_in_or_equal(str_split($this->filterparams->action),
+                    SQL_PARAMS_NAMED, 'crud');
+            $sql = "crud " . $sql;
         } else {
             // Add condition for all possible values of crud (to use db index).
             list($sql, $params) = $DB->get_in_or_equal(array('c', 'r', 'u', 'd'),
diff --git a/report/log/tests/behat/filter_log_actions.feature b/report/log/tests/behat/filter_log_actions.feature
new file mode 100644 (file)
index 0000000..ac602ed
--- /dev/null
@@ -0,0 +1,88 @@
+@report @report_log
+Feature: In a report, admin can filter log data by action
+  In order to filter log data by action
+  As an admin
+  I need to view the logs and apply a filter
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    # Create Action.
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment 1 |
+      | Description | Offline text |
+      | assignsubmission_file_enabled | 0 |
+    And I follow "Course 1"
+    # View Action.
+    And I follow "Test assignment 1"
+    # Update Action.
+    And I navigate to "Edit settings" node in "Assignment administration"
+    And I press "Save and return to course"
+    # Delete Action.
+    And I delete "Test assignment 1" activity
+    And I log out
+
+  Scenario: View only create actions.
+    Given I log in as "admin"
+    When I navigate to "Logs" node in "Site administration > Reports"
+    And I set the field "menumodaction" to "Create"
+    And I press "Get these logs"
+    Then I should see "Course module created"
+    And I should not see "Course module updated"
+    And I should not see "The status of the submission has been viewed."
+    And I should not see "Course module deleted"
+
+  Scenario: View only update actions.
+    Given I log in as "admin"
+    When I navigate to "Logs" node in "Site administration > Reports"
+    And I set the field "menumodaction" to "Update"
+    And I press "Get these logs"
+    Then I should see "Course module updated"
+    And I should not see "Course module created"
+    And I should not see "The status of the submission has been viewed."
+    And I should not see "Course module deleted"
+
+  Scenario: View only view actions.
+    Given I log in as "admin"
+    When I navigate to "Logs" node in "Site administration > Reports"
+    And I set the field "menumodaction" to "View"
+    And I press "Get these logs"
+    Then I should see "The status of the submission has been viewed."
+    And I should not see "Course module created"
+    And I should not see "Course module updated"
+    And I should not see "Course module deleted"
+
+  Scenario: View only delete actions.
+    Given I log in as "admin"
+    When I navigate to "Logs" node in "Site administration > Reports"
+    And I set the field "menumodaction" to "Delete"
+    And I press "Get these logs"
+    Then I should see "Course module deleted"
+    And I should not see "Course module created"
+    And I should not see "Course module updated"
+    And I should not see "The status of the submission has been viewed."
+
+  Scenario: View only changes.
+    Given I log in as "admin"
+    When I navigate to "Logs" node in "Site administration > Reports"
+    And I set the field "menumodaction" to "All changes"
+    And I press "Get these logs"
+    Then I should see "Course module deleted"
+    And I should see "Course module created"
+    And I should see "Course module updated"
+    And I should not see "The status of the submission has been viewed."
+
+  Scenario: View all actions.
+    Given I log in as "admin"
+    When I navigate to "Logs" node in "Site administration > Reports"
+    And I set the field "menumodaction" to "All actions"
+    And I press "Get these logs"
+    Then I should see "Course module deleted"
+    And I should see "Course module created"
+    And I should see "Course module updated"
+    And I should see "The status of the submission has been viewed."
index 7aa5b46..662a264 100644 (file)
@@ -207,7 +207,8 @@ class manager {
         }
 
         $classname = static::get_area_classname($areaid);
-        if (class_exists($classname)) {
+
+        if (class_exists($classname) && static::is_search_area($classname)) {
             return new $classname();
         }
 
@@ -240,6 +241,11 @@ class manager {
                 $searchclasses = \core_component::get_component_classes_in_namespace($componentname, 'search');
                 foreach ($searchclasses as $classname => $classpath) {
                     $areaname = substr(strrchr($classname, '\\'), 1);
+
+                    if (!static::is_search_area($classname)) {
+                        continue;
+                    }
+
                     $areaid = static::generate_areaid($componentname, $areaname);
                     $searchclass = new $classname();
                     if (!$enabled || ($enabled && $searchclass->is_enabled())) {
@@ -256,6 +262,11 @@ class manager {
 
             foreach ($searchclasses as $classname => $classpath) {
                 $areaname = substr(strrchr($classname, '\\'), 1);
+
+                if (!static::is_search_area($classname)) {
+                    continue;
+                }
+
                 $areaid = static::generate_areaid($componentname, $areaname);
                 $searchclass = new $classname();
                 if (!$enabled || ($enabled && $searchclass->is_enabled())) {
@@ -716,4 +727,18 @@ class manager {
         }
         return $configsettings;
     }
+
+    /**
+     * Checks whether a classname is of an actual search area.
+     *
+     * @param string $classname
+     * @return bool
+     */
+    protected static function is_search_area($classname) {
+        if (is_subclass_of($classname, 'core_search\base')) {
+            return (new \ReflectionClass($classname))->isInstantiable();
+        }
+
+        return false;
+    }
 }
index 9b3b5ea..9414531 100644 (file)
@@ -97,4 +97,15 @@ class testable_core_search extends \core_search\manager {
         self::get_search_areas_list(false);
         self::get_search_areas_list(true);
     }
+
+    /**
+     * Changes visibility.
+     *
+     * @param string $classname
+     * @return bool
+     */
+    public static function is_search_area($classname) {
+        return parent::is_search_area($classname);
+    }
+
 }
index 879d838..a0b60b6 100644 (file)
@@ -253,4 +253,19 @@ class search_manager_testcase extends advanced_testcase {
         $this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
         $this->assertEquals(array($course1ctx->id => $course1ctx->id), $contexts[$this->mycoursesareaid]);
     }
+
+    /**
+     * test_is_search_area
+     *
+     * @return void
+     */
+    public function test_is_search_area() {
+
+        $this->assertFalse(testable_core_search::is_search_area('\asd\asd'));
+        $this->assertFalse(testable_core_search::is_search_area('\mod_forum\search\posta'));
+        $this->assertFalse(testable_core_search::is_search_area('\core_search\base_mod'));
+        $this->assertTrue(testable_core_search::is_search_area('\mod_forum\search\post'));
+        $this->assertTrue(testable_core_search::is_search_area('\\mod_forum\\search\\post'));
+        $this->assertTrue(testable_core_search::is_search_area('mod_forum\\search\\post'));
+    }
 }