Merge branch 'MDL-55513-master' of https://github.com/lucisgit/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 15 Aug 2016 06:07:41 +0000 (14:07 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 15 Aug 2016 06:07:41 +0000 (14:07 +0800)
113 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
config-dist.php
course/externallib.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]
install/lang/da/error.php
install/lang/da/install.php
install/lang/zh_cn/error.php
lib/classes/component.php
lib/classes/event/grade_exported.php [new file with mode: 0644]
lib/classes/session/redis.php [new file with mode: 0644]
lib/deprecatedlib.php
lib/dml/pgsql_native_moodle_database.php
lib/filestorage/file_storage.php
lib/form/modgrade.php
lib/javascript.php
lib/moodlelib.php
lib/outputrenderers.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/tests/session_redis_test.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/behat/rescale_grades.feature
mod/assign/tests/externallib_test.php
mod/chat/lib.php
mod/chat/tests/format_message_test.php [new file with mode: 0644]
mod/data/field.php
mod/data/field/checkbox/lang/en/datafield_checkbox.php
mod/data/field/date/lang/en/datafield_date.php
mod/data/field/file/lang/en/datafield_file.php
mod/data/field/latlong/lang/en/datafield_latlong.php
mod/data/field/menu/field.class.php
mod/data/field/menu/lang/en/datafield_menu.php
mod/data/field/multimenu/lang/en/datafield_multimenu.php
mod/data/field/number/lang/en/datafield_number.php
mod/data/field/picture/lang/en/datafield_picture.php
mod/data/field/radiobutton/field.class.php
mod/data/field/radiobutton/lang/en/datafield_radiobutton.php
mod/data/field/text/lang/en/datafield_text.php
mod/data/field/textarea/lang/en/datafield_textarea.php
mod/data/field/url/lang/en/datafield_url.php
mod/data/lang/en/data.php
mod/data/lang/en/deprecated.txt [new file with mode: 0644]
mod/data/lib.php
mod/data/tests/behat/required_entries.feature
mod/data/tests/generator/lib.php
mod/data/tests/generator_test.php
mod/feedback/classes/responses_table.php
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/label/db/access.php
mod/label/lang/en/label.php
mod/label/version.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/classes/external.php
mod/quiz/tests/external_test.php
mod/quiz/upgrade.txt
mod/resource/locallib.php
mod/upgrade.txt
mod/wiki/pagelib.php
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
user/editor.php
user/editor_form.php
version.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 faa77de..ac9297e 100644 (file)
@@ -249,6 +249,15 @@ $CFG->admin = 'admin';
 //      $CFG->session_memcached_acquire_lock_timeout = 120;
 //      $CFG->session_memcached_lock_expire = 7200;       // Ignored if PECL memcached is below version 2.2.0
 //
+//   Redis session handler (requires redis server and redis extension):
+//      $CFG->session_handler_class = '\core\session\redis';
+//      $CFG->session_redis_host = '127.0.0.1';
+//      $CFG->session_redis_port = 6379;  // Optional.
+//      $CFG->session_redis_database = 0;  // Optional, default is db 0.
+//      $CFG->session_redis_prefix = ''; // Optional, default is don't set one.
+//      $CFG->session_redis_acquire_lock_timeout = 120;
+//      $CFG->session_redis_lock_expire = 7200;
+//
 //   Memcache session handler (requires memcached server and memcache extension):
 //      $CFG->session_handler_class = '\core\session\memcache';
 //      $CFG->session_memcache_save_path = '127.0.0.1:11211';
index 7a2367d..ec6f7c9 100644 (file)
@@ -2253,8 +2253,8 @@ class core_course_external extends external_api {
             $coursecontext = context_course::instance($course->id);
 
             // Category information.
-            if (!isset($categoriescache[$course->category])) {
-                $categoriescache[$course->category] = coursecat::get($course->category);
+            if (!array_key_exists($course->category, $categoriescache)) {
+                $categoriescache[$course->category] = coursecat::get($course->category, IGNORE_MISSING);
             }
             $category = $categoriescache[$course->category];
 
@@ -2302,7 +2302,7 @@ class core_course_external extends external_api {
             $coursereturns['displayname']       = external_format_string($displayname, $coursecontext->id);
             $coursereturns['shortname']         = external_format_string($course->shortname, $coursecontext->id);
             $coursereturns['categoryid']        = $course->category;
-            $coursereturns['categoryname']      = $category->name;
+            $coursereturns['categoryname']      = $category == null ? '' : $category->name;
             $coursereturns['summary']           = $summary;
             $coursereturns['summaryformat']     = $summaryformat;
             $coursereturns['overviewfiles']     = $files;
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 ec7a094..e40905c 100644 (file)
@@ -40,9 +40,12 @@ $string['cannotsavemd5file'] = 'Kan ikke gemme md5-fil';
 $string['cannotsavezipfile'] = 'Kan ikke gemme zip-fil';
 $string['cannotunzipfile'] = 'Kan ikke pakke filen ud';
 $string['componentisuptodate'] = 'Komponenten er ajour';
+$string['dmlexceptiononinstall'] = '<p>En database fejl er opstået [{$a->errorcode}].<br />{$a->debuginfo}</p>';
 $string['downloadedfilecheckfailed'] = 'Downloadet fil-tjek fejlede';
 $string['invalidmd5'] = 'Tjekvariablen var forkert - prøv igen';
 $string['missingrequiredfield'] = 'Der mangler nogle obligatoriske felter';
+$string['remotedownloaderror'] = '<p> Download af komponent til din server fejlede. Venligst verificer proxy indstillilnger; PHP cURL filtypen anbefales kraftigt. </p>
+<p>Du må downloade <a href="{$a->url}">{$a->url}</a> filen manuelt, kopier den til "{$a->dest}" på din server og udpak den her </p>';
 $string['wrongdestpath'] = 'Forkert destinationssti';
 $string['wrongsourcebase'] = 'Forkert kilde-URL';
 $string['wrongzipfilename'] = 'Forkert zip-filnavn';
index 9fe5de6..cb886c2 100644 (file)
@@ -41,6 +41,7 @@ $string['databasehost'] = 'Databasevært';
 $string['databasename'] = 'Databasenavn';
 $string['databasetypehead'] = 'Vælg databasedriver';
 $string['dataroot'] = 'Datamappe';
+$string['datarootpermission'] = 'Rettighed til data mapper';
 $string['dbprefix'] = 'Præfix for tabeller';
 $string['dirroot'] = 'Moodle-mappe';
 $string['environmenthead'] = 'Kontrollerer din serveropsætning...';
index 1663ed5..d9123a6 100644 (file)
@@ -30,6 +30,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['cannotcreatedboninstall'] = '<p>无法建立数据库</p>
+<p>指定数据库不存在。使用者没有权限建立数据库</p>
+<p>网站管理员需查明数据库状态.</p>';
 $string['cannotcreatelangdir'] = '无法创建 lang 目录。';
 $string['cannotcreatetempdir'] = '无法创建 temp 目录。';
 $string['cannotdownloadcomponents'] = '无法下载组件';
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
diff --git a/lib/classes/session/redis.php b/lib/classes/session/redis.php
new file mode 100644 (file)
index 0000000..d8b0610
--- /dev/null
@@ -0,0 +1,382 @@
+<?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/>.
+
+/**
+ * Redis based session handler.
+ *
+ * @package    core
+ * @copyright  2015 Russell Smith <mr-russ@smith2001.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\session;
+
+use RedisException;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Redis based session handler.
+ *
+ * The default Redis session handler does not handle locking in 2.2.7, so we have written a php session handler
+ * that uses locking.  The places where locking is used was modeled from the memcached code that is used in Moodle
+ * https://github.com/php-memcached-dev/php-memcached/blob/master/php_memcached_session.c
+ *
+ * @package    core
+ * @copyright  2016 Russell Smith
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class redis extends handler {
+    /** @var string $host save_path string  */
+    protected $host = '';
+    /** @var int $port The port to connect to */
+    protected $port = 6379;
+    /** @var int $database the Redis database to store sesions in */
+    protected $database = 0;
+    /** @var array $servers list of servers parsed from save_path */
+    protected $prefix = '';
+    /** @var int $acquiretimeout how long to wait for session lock in seconds */
+    protected $acquiretimeout = 120;
+    /**
+     * @var int $lockexpire how long to wait in seconds before expiring the lock automatically
+     * so that other requests may continue execution, ignored if PECL redis is below version 2.2.0.
+     */
+    protected $lockexpire = 7200;
+
+    /** @var Redis Connection */
+    protected $connection = null;
+
+    /** @var array $locks List of currently held locks by this page. */
+    protected $locks = array();
+
+    /**
+     * Create new instance of handler.
+     */
+    public function __construct() {
+        global $CFG;
+
+        if (isset($CFG->session_redis_host)) {
+            $this->host = $CFG->session_redis_host;
+        }
+
+        if (isset($CFG->session_redis_port)) {
+            $this->port = (int)$CFG->session_redis_port;
+        }
+
+        if (isset($CFG->session_redis_database)) {
+            $this->database = (int)$CFG->session_redis_database;
+        }
+
+        if (isset($CFG->session_redis_prefix)) {
+            $this->prefix = $CFG->session_redis_prefix;
+        }
+
+        if (isset($CFG->session_redis_acquire_lock_timeout)) {
+            $this->acquiretimeout = (int)$CFG->session_redis_acquire_lock_timeout;
+        }
+
+        if (isset($CFG->session_redis_lock_expire)) {
+            $this->lockexpire = (int)$CFG->session_redis_lock_expire;
+        }
+    }
+
+    /**
+     * Start the session.
+     *
+     * @return bool success
+     */
+    public function start() {
+        $result = parent::start();
+
+        return $result;
+    }
+
+    /**
+     * Init session handler.
+     */
+    public function init() {
+        if (!extension_loaded('redis')) {
+            throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension is not loaded');
+        }
+
+        if (empty($this->host)) {
+            throw new exception('sessionhandlerproblem', 'error', '', null,
+                    '$CFG->session_redis_host must be specified in config.php');
+        }
+
+        // The session handler requires a version of Redis with the SETEX command (at least 2.0).
+        $version = phpversion('Redis');
+        if (!$version or version_compare($version, '2.0') <= 0) {
+            throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension version must be at least 2.0');
+        }
+
+        $this->connection = new \Redis();
+
+        $result = session_set_save_handler(array($this, 'handler_open'),
+            array($this, 'handler_close'),
+            array($this, 'handler_read'),
+            array($this, 'handler_write'),
+            array($this, 'handler_destroy'),
+            array($this, 'handler_gc'));
+        if (!$result) {
+            throw new exception('redissessionhandlerproblem', 'error');
+        }
+
+        try {
+            // One second timeout was chosen as it is long for connection, but short enough for a user to be patient.
+            if (!$this->connection->connect($this->host, $this->port, 1)) {
+                throw new RedisException('Unable to connect to host.');
+            }
+            if (!$this->connection->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_PHP)) {
+                throw new RedisException('Unable to set Redis PHP Serializer option.');
+            }
+
+            if ($this->prefix !== '') {
+                // Use custom prefix on sessions.
+                if (!$this->connection->setOption(\Redis::OPT_PREFIX, $this->prefix)) {
+                    throw new RedisException('Unable to set Redis Prefix option.');
+                }
+            }
+            if ($this->database !== 0) {
+                if (!$this->connection->select($this->database)) {
+                    throw new RedisException('Unable to select Redis database '.$this->database.'.');
+                }
+            }
+            $this->connection->ping();
+            return true;
+        } catch (RedisException $e) {
+            error_log('Failed to connect to redis at '.$this->host.':'.$this->port.', error returned was: '.$e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Update our session search path to include session name when opened.
+     *
+     * @param string $savepath  unused session save path. (ignored)
+     * @param string $sessionname Session name for this session. (ignored)
+     * @return bool true always as we will succeed.
+     */
+    public function handler_open($savepath, $sessionname) {
+        return true;
+    }
+
+    /**
+     * Close the session completely. We also remove all locks we may have obtained that aren't expired.
+     *
+     * @return bool true on success.  false on unable to unlock sessions.
+     */
+    public function handler_close() {
+        try {
+            foreach ($this->locks as $id => $expirytime) {
+                if ($expirytime > $this->time()) {
+                    $this->unlock_session($id);
+                }
+                unset($this->locks[$id]);
+            }
+        } catch (RedisException $e) {
+            error_log('Failed talking to redis: '.$e->getMessage());
+            return false;
+        }
+
+        return true;
+    }
+    /**
+     * Read the session data from storage
+     *
+     * @param string $id The session id to read from storage.
+     * @return string The session data for PHP to process.
+     *
+     * @throws RedisException when we are unable to talk to the Redis server.
+     */
+    public function handler_read($id) {
+        try {
+            $this->lock_session($id);
+            $sessiondata = $this->connection->get($id);
+            if ($sessiondata === false) {
+                $this->unlock_session($id);
+                return '';
+            }
+            $this->connection->expire($id, $this->lockexpire);
+        } catch (RedisException $e) {
+            error_log('Failed talking to redis: '.$e->getMessage());
+            throw $e;
+        }
+        return $sessiondata;
+    }
+
+    /**
+     * Write the serialized session data to our session store.
+     *
+     * @param string $id session id to write.
+     * @param string $data session data
+     * @return bool true on write success, false on failure
+     */
+    public function handler_write($id, $data) {
+        if (is_null($this->connection)) {
+            // The session has already been closed, don't attempt another write.
+            error_log('Tried to write session: '.$id.' before open or after close.');
+            return false;
+        }
+
+        // We do not do locking here because memcached doesn't.  Also
+        // PHP does open, read, destroy, write, close. When a session doesn't exist.
+        // There can be race conditions on new sessions racing each other but we can
+        // address that in the future.
+        try {
+            $this->connection->setex($id, $this->lockexpire, $data);
+        } catch (RedisException $e) {
+            error_log('Failed talking to redis: '.$e->getMessage());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Handle destroying a session.
+     *
+     * @param string $id the session id to destroy.
+     * @return bool true if the session was deleted, false otherwise.
+     */
+    public function handler_destroy($id) {
+        try {
+            $this->connection->del($id);
+            $this->unlock_session($id);
+        } catch (RedisException $e) {
+            error_log('Failed talking to redis: '.$e->getMessage());
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Garbage collect sessions.  We don't we any as Redis does it for us.
+     *
+     * @param integer $maxlifetime All sessions older than this should be removed.
+     * @return bool true, as Redis handles expiry for us.
+     */
+    public function handler_gc($maxlifetime) {
+        return true;
+    }
+
+    /**
+     * Unlock a session.
+     *
+     * @param string $id Session id to be unlocked.
+     */
+    protected function unlock_session($id) {
+        if (isset($this->locks[$id])) {
+            $this->connection->del($id.".lock");
+            unset($this->locks[$id]);
+        }
+    }
+
+    /**
+     * Obtain a session lock so we are the only one using it at the moent.
+     *
+     * @param string $id The session id to lock.
+     * @return bool true when session was locked, exception otherwise.
+     * @throws exception When we are unable to obtain a session lock.
+     */
+    protected function lock_session($id) {
+        $lockkey = $id.".lock";
+
+        $haslock = isset($this->locks[$id]) && $this->time() < $this->locks[$id];
+        $startlocktime = $this->time();
+
+        /* To be able to ensure sessions don't write out of order we must obtain an exclusive lock
+         * on the session for the entire time it is open.  If another AJAX call, or page is using
+         * the session then we just wait until it finishes before we can open the session.
+         */
+        while (!$haslock) {
+            $haslock = $this->connection->setnx($lockkey, '1');
+            if (!$haslock) {
+                usleep(rand(100000, 1000000));
+                if ($this->time() > $startlocktime + $this->acquiretimeout) {
+                    // This is a fatal error, better inform users.
+                    // It should not happen very often - all pages that need long time to execute
+                    // should close session immediately after access control checks.
+                    error_log('Cannot obtain session lock for sid: '.$id.' within '.$this->acquiretimeout.
+                            '. It is likely another page has a long session lock, or the session lock was never released.');
+                    throw new exception("Unable to obtain session lock");
+                }
+            } else {
+                $this->locks[$id] = $this->time() + $this->lockexpire;
+                $this->connection->expire($lockkey, $this->lockexpire);
+                return true;
+            }
+        }
+    }
+
+    /**
+     * Return the current time.
+     *
+     * @return int the current time as a unixtimestamp.
+     */
+    protected function time() {
+        return time();
+    }
+
+    /**
+     * Check the backend contains data for this session id.
+     *
+     * Note: this is intended to be called from manager::session_exists() only.
+     *
+     * @param string $sid
+     * @return bool true if session found.
+     */
+    public function session_exists($sid) {
+        if (!$this->connection) {
+            return false;
+        }
+
+        try {
+            return $this->connection->exists($sid);
+        } catch (RedisException $e) {
+            return false;
+        }
+    }
+
+    /**
+     * Kill all active sessions, the core sessions table is purged afterwards.
+     */
+    public function kill_all_sessions() {
+        global $DB;
+        if (!$this->connection) {
+            return;
+        }
+
+        $rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid');
+        foreach ($rs as $record) {
+            $this->handler_destroy($record->sid);
+        }
+        $rs->close();
+    }
+
+    /**
+     * Kill one session, the session record is removed afterwards.
+     *
+     * @param string $sid
+     */
+    public function kill_session($sid) {
+        if (!$this->connection) {
+            return;
+        }
+
+        $this->handler_destroy($sid);
+    }
+}
\ No newline at end of file
index fcc6cfb..aed6165 100644 (file)
@@ -1114,7 +1114,7 @@ function print_checkbox($name, $value, $checked = true, $label = '', $alt = '',
 /**
  * Prints the 'update this xxx' button that appears on module pages.
  *
- * @deprecated since Moodle 2.0
+ * @deprecated since Moodle 3.2
  *
  * @param string $cmid the course_module id.
  * @param string $ignored not used any more. (Used to be courseid.)
@@ -1124,9 +1124,9 @@ function print_checkbox($name, $value, $checked = true, $label = '', $alt = '',
 function update_module_button($cmid, $ignored, $string) {
     global $CFG, $OUTPUT;
 
-    // debugging('update_module_button() has been deprecated. Please change your code to use $OUTPUT->update_module_button().');
-
-    //NOTE: DO NOT call new output method because it needs the module name we do not have here!
+    debugging('update_module_button() has been deprecated and should not be used anymore. Activity modules should not add the ' .
+        'edit module button, the link is already available in the Administration block. Themes can choose to display the link ' .
+        'in the buttons row consistently for all module types.', DEBUG_DEVELOPER);
 
     if (has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
         $string = get_string('updatethis', '', $string);
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 38a20a1..0c105e2 100644 (file)
@@ -354,9 +354,9 @@ class file_storage {
         // Copy the file to the tmp dir.
         $uniqdir = "core_file/conversions/" . uniqid($file->get_id() . "-", true);
         $tmp = make_temp_directory($uniqdir);
-        $localfilename = $file->get_filename();
+        $ext = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
         // Safety.
-        $localfilename = clean_param($localfilename, PARAM_FILE);
+        $localfilename = $file->get_id() . '.' . $ext;
 
         $filename = $tmp . '/' . $localfilename;
         try {
index b33f437..9fe7806 100644 (file)
@@ -266,7 +266,8 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group {
         $point = (isset($vals['modgrade_point'])) ? $vals['modgrade_point'] : null;
         $scale = (isset($vals['modgrade_scale'])) ? $vals['modgrade_scale'] : null;
         $rescalegrades = (isset($vals['modgrade_rescalegrades'])) ? $vals['modgrade_rescalegrades'] : null;
-        $return = $this->process_value($type, $scale, $point);
+
+        $return = $this->process_value($type, $scale, $point, $rescalegrades);
         return array($this->getName() => $return, $this->getName() . '_rescalegrades' => $rescalegrades);
     }
 
@@ -276,11 +277,17 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group {
      * @param  string $type The value of the grade type select box. Can be 'none', 'scale', or 'point'
      * @param  string|int $scale The value of the scale select box.
      * @param  string|int $point The value of the point grade textbox.
+     * @param  string $rescalegrades The value of the rescalegrades select.
      * @return int The resulting value
      */
-    protected function process_value($type='none', $scale=null, $point=null) {
+    protected function process_value($type='none', $scale=null, $point=null, $rescalegrades=null) {
         global $COURSE;
         $val = 0;
+        if ($this->isupdate && $this->hasgrades && $this->canrescale && $this->currentgradetype == 'point' && empty($rescalegrades)) {
+            // If the maxgrade field is disabled with javascript, no value is sent with the form and mform assumes the default.
+            // If the user was forced to choose a rescale option - and they haven't - prevent any changes to the max grade.
+            return $this->currentgrade;
+        }
         switch ($type) {
             case 'point':
                 if ($this->validate_point($point) === true) {
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 4f89e27..63be693 100644 (file)
@@ -2685,12 +2685,19 @@ EOD;
     /**
      * Returns HTML to display the 'Update this Modulename' button that appears on module pages.
      *
+     * @deprecated since Moodle 3.2
+     *
      * @param string $cmid the course_module id.
      * @param string $modulename the module name, eg. "forum", "quiz" or "workshop"
      * @return string the HTML for the button, if this user has permission to edit it, else an empty string.
      */
     public function update_module_button($cmid, $modulename) {
         global $CFG;
+
+        debugging('core_renderer::update_module_button() has been deprecated and should not be used anymore. Activity modules ' .
+            'should not add the edit module button, the link is already available in the Administration block. Themes can choose ' .
+            'to display the link in the buttons row consistently for all module types.', DEBUG_DEVELOPER);
+
         if (has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
             $modulename = get_string('modulename', $modulename);
             $string = get_string('updatethis', '', $modulename);
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 {
+}
diff --git a/lib/tests/session_redis_test.php b/lib/tests/session_redis_test.php
new file mode 100644 (file)
index 0000000..8bc1202
--- /dev/null
@@ -0,0 +1,272 @@
+<?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/>.
+
+/**
+ * Redis session tests.
+ *
+ * NOTE: in order to execute this test you need to set up
+ *       Redis server and add configuration a constant
+ *       to config.php or phpunit.xml configuration file:
+ *
+ * define('TEST_SESSION_REDIS_HOST', '127.0.0.1');
+ *
+ * @package   core
+ * @author    Russell Smith <mr-russ@smith2001.net>
+ * @copyright 2016 Russell Smith
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit tests for classes/session/redis.php.
+ *
+ * @package   core
+ * @author    Russell Smith <mr-russ@smith2001.net>
+ * @copyright 2016 Russell Smith
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_session_redis_testcase extends advanced_testcase {
+
+    /** @var $keyprefix This key prefix used when testing Redis */
+    protected $keyprefix = null;
+    /** @var $redis The current testing redis connection */
+    protected $redis = null;
+
+    public function setUp() {
+        global $CFG;
+
+        if (!extension_loaded('redis')) {
+            $this->markTestSkipped('Redis extension not loaded.');
+        }
+        if (!defined('TEST_SESSION_REDIS_HOST')) {
+            $this->markTestSkipped('Session test server not set. define: TEST_SESSION_REDIS_HOST');
+        }
+
+        $this->resetAfterTest();
+
+        $this->keyprefix = 'phpunit'.rand(1, 100000);
+
+        $CFG->session_redis_host = TEST_SESSION_REDIS_HOST;
+        $CFG->session_redis_prefix = $this->keyprefix;
+
+        // Set a very short lock timeout to ensure tests run quickly.  We are running single threaded,
+        // so unless we lock and expect it to be there, we will always see a lock.
+        $CFG->session_redis_acquire_lock_timeout = 1;
+        $CFG->session_redis_lock_expire = 70;
+
+        $this->redis = new Redis();
+        $this->redis->connect(TEST_SESSION_REDIS_HOST);
+    }
+
+    public function tearDown() {
+        if (!extension_loaded('redis') || !defined('TEST_SESSION_REDIS_HOST')) {
+            return;
+        }
+
+        $list = $this->redis->keys($this->keyprefix.'*');
+        foreach ($list as $keyname) {
+            $this->redis->del($keyname);
+        }
+        $this->redis->close();
+    }
+
+    public function test_normal_session_start_stop_works() {
+        $sess = new \core\session\redis();
+        $sess->init();
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertSame('', $sess->handler_read('sess1'));
+        $this->assertTrue($sess->handler_write('sess1', 'DATA'));
+        $this->assertTrue($sess->handler_close());
+
+        // Read the session again to ensure locking did what it should.
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertSame('DATA', $sess->handler_read('sess1'));
+        $this->assertTrue($sess->handler_write('sess1', 'DATA-new'));
+        $this->assertTrue($sess->handler_close());
+        $this->assertSessionNoLocks();
+    }
+
+    public function test_session_blocks_with_existing_session() {
+        $sess = new \core\session\redis();
+        $sess->init();
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertSame('', $sess->handler_read('sess1'));
+        $this->assertTrue($sess->handler_write('sess1', 'DATA'));
+        $this->assertTrue($sess->handler_close());
+
+        // Sessions are not locked until they have been saved once.
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertSame('DATA', $sess->handler_read('sess1'));
+
+        $sessblocked = new \core\session\redis();
+        $sessblocked->init();
+        $this->assertTrue($sessblocked->handler_open('Not used', 'Not used'));
+
+        // Trap the error log and send it to stdOut so we can expect output at the right times.
+        $errorlog = tempnam(sys_get_temp_dir(), "rediserrorlog");
+        $this->iniSet('error_log', $errorlog);
+        try {
+            $sessblocked->handler_read('sess1');
+            $this->fail('Session lock must fail to be obtained.');
+        } catch (\core\session\exception $e) {
+            $this->assertContains("Unable to obtain session lock", $e->getMessage());
+            $this->assertContains('Cannot obtain session lock for sid: sess1', file_get_contents($errorlog));
+        }
+
+        $this->assertTrue($sessblocked->handler_close());
+        $this->assertTrue($sess->handler_write('sess1', 'DATA-new'));
+        $this->assertTrue($sess->handler_close());
+        $this->assertSessionNoLocks();
+    }
+
+    public function test_session_is_destroyed_when_it_does_not_exist() {
+        $sess = new \core\session\redis();
+        $sess->init();
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertTrue($sess->handler_destroy('sess-destroy'));
+        $this->assertSessionNoLocks();
+    }
+
+    public function test_session_is_destroyed_when_we_have_it_open() {
+        $sess = new \core\session\redis();
+        $sess->init();
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertSame('', $sess->handler_read('sess-destroy'));
+        $this->assertTrue($sess->handler_destroy('sess-destroy'));
+        $this->assertTrue($sess->handler_close());
+        $this->assertSessionNoLocks();
+    }
+
+    public function test_multiple_sessions_do_not_interfere_with_each_other() {
+        $sess1 = new \core\session\redis();
+        $sess1->init();
+        $sess2 = new \core\session\redis();
+        $sess2->init();
+
+        // Initialize session 1.
+        $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
+        $this->assertSame('', $sess1->handler_read('sess1'));
+        $this->assertTrue($sess1->handler_write('sess1', 'DATA'));
+        $this->assertTrue($sess1->handler_close());
+
+        // Initialize session 2.
+        $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
+        $this->assertSame('', $sess2->handler_read('sess2'));
+        $this->assertTrue($sess2->handler_write('sess2', 'DATA2'));
+        $this->assertTrue($sess2->handler_close());
+
+        // Open and read session 1 and 2.
+        $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
+        $this->assertSame('DATA', $sess1->handler_read('sess1'));
+        $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
+        $this->assertSame('DATA2', $sess2->handler_read('sess2'));
+
+        // Write both sessions.
+        $this->assertTrue($sess1->handler_write('sess1', 'DATAX'));
+        $this->assertTrue($sess2->handler_write('sess2', 'DATA2X'));
+
+        // Read both sessions.
+        $this->assertTrue($sess1->handler_open('Not used', 'Not used'));
+        $this->assertTrue($sess2->handler_open('Not used', 'Not used'));
+        $this->assertEquals('DATAX', $sess1->handler_read('sess1'));
+        $this->assertEquals('DATA2X', $sess2->handler_read('sess2'));
+
+        // Close both sessions
+        $this->assertTrue($sess1->handler_close());
+        $this->assertTrue($sess2->handler_close());
+
+        // Read the session again to ensure locking did what it should.
+        $this->assertSessionNoLocks();
+    }
+
+    public function test_multiple_sessions_work_with_a_single_instance() {
+        $sess = new \core\session\redis();
+        $sess->init();
+
+        // Initialize session 1.
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertSame('', $sess->handler_read('sess1'));
+        $this->assertTrue($sess->handler_write('sess1', 'DATA'));
+        $this->assertSame('', $sess->handler_read('sess2'));
+        $this->assertTrue($sess->handler_write('sess2', 'DATA2'));
+        $this->assertSame('DATA', $sess->handler_read('sess1'));
+        $this->assertSame('DATA2', $sess->handler_read('sess2'));
+        $this->assertTrue($sess->handler_destroy('sess2'));
+
+        $this->assertTrue($sess->handler_close());
+        $this->assertSessionNoLocks();
+
+        $this->assertTrue($sess->handler_close());
+    }
+
+    public function test_session_exists_returns_valid_values() {
+        $sess = new \core\session\redis();
+        $sess->init();
+
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertSame('', $sess->handler_read('sess1'));
+
+        $this->assertFalse($sess->session_exists('sess1'), 'Session must not exist yet, it has not been saved');
+        $this->assertTrue($sess->handler_write('sess1', 'DATA'));
+        $this->assertTrue($sess->session_exists('sess1'), 'Session must exist now.');
+        $this->assertTrue($sess->handler_destroy('sess1'));
+        $this->assertFalse($sess->session_exists('sess1'), 'Session should be destroyed.');
+    }
+
+    public function test_kill_sessions_removes_the_session_from_redis() {
+        global $DB;
+
+        $sess = new \core\session\redis();
+        $sess->init();
+
+        $this->assertTrue($sess->handler_open('Not used', 'Not used'));
+        $this->assertTrue($sess->handler_write('sess1', 'DATA'));
+        $this->assertTrue($sess->handler_write('sess2', 'DATA'));
+        $this->assertTrue($sess->handler_write('sess3', 'DATA'));
+
+        $sessiondata = new \stdClass();
+        $sessiondata->userid = 2;
+        $sessiondata->timecreated = time();
+        $sessiondata->timemodified = time();
+
+        $sessiondata->sid = 'sess1';
+        $DB->insert_record('sessions', $sessiondata);
+        $sessiondata->sid = 'sess2';
+        $DB->insert_record('sessions', $sessiondata);
+        $sessiondata->sid = 'sess3';
+        $DB->insert_record('sessions', $sessiondata);
+
+        $this->assertNotEquals('', $sess->handler_read('sess1'));
+        $sess->kill_session('sess1');
+        $this->assertEquals('', $sess->handler_read('sess1'));
+
+        $this->assertEmpty($this->redis->keys($this->keyprefix.'sess1.lock'));
+
+        $sess->kill_all_sessions();
+
+        $this->assertEquals(3, $DB->count_records('sessions'), 'Moodle handles session database, plugin must not change it.');
+        $this->assertSessionNoLocks();
+        $this->assertEmpty($this->redis->keys($this->keyprefix.'*'), 'There should be no session data left.');
+    }
+
+    /**
+     * Assert that we don't have any session locks in Redis.
+     */
+    protected function assertSessionNoLocks() {
+        $this->assertEmpty($this->redis->keys($this->keyprefix.'*.lock'));
+    }
+}
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 8b589ac..4d861e5 100644 (file)
@@ -45,6 +45,22 @@ Feature: Check that the assignment grade can be rescaled when the max grade is c
     And I follow "View all submissions"
     Then "Student 1" row "Grade" column of "generaltable" table should contain "40.00"
 
+  Scenario: Update an assignment without touching the max grades
+    Given I follow "Edit settings"
+    And I expand all fieldsets
+    And I set the field "Rescale existing grades" to "No"
+    And I set the field "Maximum grade" to "80"
+    And I press "Save and display"
+    And I follow "Edit settings"
+    And I press "Save and display"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    And I set the field "Rescale existing grades" to "Yes"
+    And I set the field "Maximum grade" to "80"
+    When I press "Save and display"
+    And I follow "View all submissions"
+    Then "Student 1" row "Grade" column of "generaltable" table should contain "40.00"
+
   Scenario: Update the max grade for an assignment rescaling existing grades
     Given I follow "Edit settings"
     And I expand all fieldsets
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 fdf2e87..9ffab95 100644 (file)
@@ -296,7 +296,7 @@ if (($mode == 'new') && (!empty($newtype)) && confirm_sesskey()) {          ///
 
                 $table->data[] = array(
                     html_writer::link($displayurl, $field->field->name),
-                    $field->image() . '&nbsp;' . get_string($field->type, 'data'),
+                    $field->image() . '&nbsp;' . $field->name(),
                     $field->field->required ? get_string('yes') : get_string('no'),
                     shorten_text($field->field->description, 30),
                     html_writer::link($displayurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))) .
index c845038..6638b33 100644 (file)
@@ -25,3 +25,4 @@
  */
 
 $string['pluginname'] = 'Date';
+$string['fieldtypelabel'] = 'Date field';
index 690ee8e..2e97c99 100644 (file)
@@ -25,3 +25,4 @@
  */
 
 $string['pluginname'] = 'File';
+$string['fieldtypelabel'] = 'File field';
index a4b3cf2..b13c381 100644 (file)
@@ -25,3 +25,4 @@
  */
 
 $string['pluginname'] = 'Latlong';
+$string['fieldtypelabel'] = 'Latitude/longitude field';
index 3411492..53e2d4f 100644 (file)
@@ -97,7 +97,8 @@ class data_field_menu extends data_field_base {
             return '';
         }
 
-        $return = html_writer::label(get_string('namemenu', 'data'), 'menuf_'. $this->field->id, false, array('class' => 'accesshide'));
+        $return = html_writer::label(get_string('fieldtypelabel', "datafield_" . $this->type),
+            'menuf_' . $this->field->id, false, array('class' => 'accesshide'));
         $return .= html_writer::select($options, 'f_'.$this->field->id, $content);
         return $return;
     }
index 73cec9f..49da27d 100644 (file)
@@ -25,3 +25,4 @@
  */
 
 $string['pluginname'] = 'Menu';
+$string['fieldtypelabel'] = 'Menu field';
index 4822dbb..765519c 100644 (file)
@@ -25,3 +25,4 @@
  */
 
 $string['pluginname'] = 'Multimenu';
+$string['fieldtypelabel'] = 'Multiple-selection menu field';
index 63ecf92..4ed6ce7 100644 (file)
@@ -25,3 +25,4 @@
  */
 
 $string['pluginname'] = 'Number';
+$string['fieldtypelabel'] = 'Number field';
index 053d503..6dd1d3e 100644 (file)
@@ -96,7 +96,8 @@ class data_field_radiobutton extends data_field_base {
                 $options[$rec->content] = $rec->content;  //Build following indicies from the sql.
             }
         }
-        $return = html_writer::label(get_string('nameradiobutton', 'data'), 'menuf_'. $this->field->id, false, array('class' => 'accesshide'));
+        $return = html_writer::label(get_string('fieldtypelabel', "datafield_" . $this->type),
+            'menuf_' . $this->field->id, false, array('class' => 'accesshide'));
         $return .= html_writer::select($options, 'f_'.$this->field->id, $value);
         return $return;
     }
index 2e53113..5b87802 100644 (file)
@@ -25,3 +25,4 @@
  */
 
 $string['pluginname'] = 'Text input';
+$string['fieldtypelabel'] = 'Text field';
index 892aea4..c5359b0 100644 (file)
@@ -27,3 +27,4 @@
 $string['maxbytes'] = 'Maximum embedded file size (bytes)';
 $string['maxbytes_desc'] = 'If set to zero will be unlimited by default';
 $string['pluginname'] = 'Text area';
+$string['fieldtypelabel'] = 'Textarea field';
index 65ddcae..4df275d 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'URL';
 $string['openlinkinnewwindow'] = 'Open link in new window';
+$string['fieldtypelabel'] = 'URL field';
\ No newline at end of file
index 484f9e2..a8127e0 100644 (file)
@@ -253,18 +253,6 @@ $string['movezipfailed'] = 'Can\'t move zip';
 $string['multientry'] = 'Repeated entry';
 $string['multimenu'] = 'Menu (Multi-select)';
 $string['multipletags'] = 'Multiple tags found! Template not saved';
-$string['namedate'] = 'Date field';
-$string['namefile'] = 'File field';
-$string['namecheckbox'] = 'Checkbox field';
-$string['namelatlong'] = 'Latitude/longitude field';
-$string['namemenu'] = 'Menu field';
-$string['namemultimenu'] = 'Multiple-selection menu field';
-$string['namenumber'] = 'Number field';
-$string['namepicture'] = 'Picture field';
-$string['nameradiobutton'] = 'Radio button field';
-$string['nametext'] = 'Text field';
-$string['nametextarea'] = 'Textarea field';
-$string['nameurl'] = 'URL field';
 $string['newentry'] = 'New entry';
 $string['newfield'] = 'Create a new field';
 $string['newfield_help'] = 'A field allows the input of data. Each entry in a database activity can have multiple fields of multiple types such as a date field, which allows participants to select a day, month and year from a dropdown list, a picture field, which allows participants to upload an image file, or a checkbox field, which allows participants to select one or more options.
@@ -374,3 +362,17 @@ $string['viewfromdate'] = 'Read only from';
 $string['viewtodate'] = 'Read only to';
 $string['viewtodatevalidation'] = 'The read only to date cannot be before the read only from date.';
 $string['wrongdataid'] = 'Wrong data id provided';
+
+// Deprecated since Moodle 3.2.
+$string['namedate'] = 'Date field';
+$string['namefile'] = 'File field';
+$string['namecheckbox'] = 'Checkbox field';
+$string['namelatlong'] = 'Latitude/longitude field';
+$string['namemenu'] = 'Menu field';
+$string['namemultimenu'] = 'Multiple-selection menu field';
+$string['namenumber'] = 'Number field';
+$string['namepicture'] = 'Picture field';
+$string['nameradiobutton'] = 'Radio button field';
+$string['nametext'] = 'Text field';
+$string['nametextarea'] = 'Textarea field';
+$string['nameurl'] = 'URL field';
\ No newline at end of file
diff --git a/mod/data/lang/en/deprecated.txt b/mod/data/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..cf44132
--- /dev/null
@@ -0,0 +1,12 @@
+namedate,mod_data
+namefile,mod_data
+namecheckbox,mod_data
+namelatlong,mod_data
+namemenu,mod_data
+namemultimenu,mod_data
+namenumber,mod_data
+namepicture,mod_data
+nameradiobutton,mod_data
+nametext,mod_data
+nametextarea,mod_data
+nameurl,mod_data
\ No newline at end of file
index 0eeb878..6c03edf 100644 (file)
@@ -477,7 +477,7 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
      * @return string
      */
     function name() {
-        return get_string('name'.$this->type, 'data');
+        return get_string('fieldtypelabel', "datafield_$this->type");
     }
 
     /**
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..86480c9 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,224 @@ 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 = true;
+
+            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'] = (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.');
+                }
+
+                $contents[$fieldid] = $values;
+
+                foreach ($values as $fieldname => $value) {
+                    if (!$field->notemptyfield($value, $fieldname)) {
+                        $fieldhascontent = false;
+                    }
+                }
+            } else if ($field->type === 'textarea') {
+                $values = array();
+
+                $values['field_' . $fieldid] = $contents[$fieldid];
+                $values['field_' . $fieldid . '_content1'] = 1;
+
+                $contents[$fieldid] = $values;
+
+                foreach ($values as $fieldname => $value) {
+                    if (!$field->notemptyfield($value, $fieldname)) {
+                        $fieldhascontent = false;
+                    }
+                }
+            } 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];
+                }
+
+                $contents[$fieldid] = $values;
+
+                foreach ($values as $fieldname => $value) {
+                    if (!$field->notemptyfield($value, $fieldname)) {
+                        $fieldhascontent = false;
+                    }
+                }
+            } else {
+                if ($field->notemptyfield($contents[$fieldid], 'field_' . $fieldid . '_0')) {
+                    continue;
+                }
+            }
+
+            if ($field->field->required && !$fieldhascontent) {
+                return false;
+            }
+        }
+
+        foreach ($contents as $fieldid => $content) {
+            $field = data_get_field_from_id($fieldid, $data);
+
+            if (is_array($content)) {
+
+                foreach ($content as $fieldname => $value) {
+                    $field->update_content($recordid, $value, $fieldname);
+                }
+
+            } else {
+                $field->update_content($recordid, $content);
+            }
+        }
+
+        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 cd8291e..5644183 100644 (file)
@@ -36,6 +36,23 @@ require_once($CFG->libdir . '/tablelib.php');
  */
 class mod_feedback_responses_table extends table_sql {
 
+    /**
+     * Maximum number of feedback questions to display in the "Show responses" table
+     */
+    const PREVIEWCOLUMNSLIMIT = 10;
+
+    /**
+     * Maximum number of feedback questions answers to retrieve in one SQL query.
+     * Mysql has a limit of 60, we leave 1 for joining with users table.
+     */
+    const TABLEJOINLIMIT = 59;
+
+    /**
+     * When additional queries are needed to retrieve more than TABLEJOINLIMIT questions answers, do it in chunks every x rows.
+     * Value too small will mean too many DB queries, value too big may cause memory overflow.
+     */
+    const ROWCHUNKSIZE = 100;
+
     /** @var mod_feedback_structure */
     protected $feedbackstructure;
 
@@ -51,6 +68,10 @@ class mod_feedback_responses_table extends table_sql {
     /** @var string */
     protected $downloadparamname = 'download';
 
+    /** @var int number of columns that were not retrieved in the main SQL query
+     * (no more than TABLEJOINLIMIT tables with values can be joined). */
+    protected $hasmorecolumns = 0;
+
     /**
      * Constructor
      *
@@ -237,13 +258,27 @@ class mod_feedback_responses_table extends table_sql {
         $tablecolumns = array_keys($this->columns);
         $tableheaders = $this->headers;
 
-        // Add all feedback response values.
         $items = $this->feedbackstructure->get_items(true);
+        if (!$this->is_downloading()) {
+            // In preview mode do not show all columns or the page becomes unreadable.
+            // The information message will be displayed to the teacher that the rest of the data can be viewed when downloading.
+            $items = array_slice($items, 0, self::PREVIEWCOLUMNSLIMIT, true);
+        }
+
+        $columnscount = 0;
+        $this->hasmorecolumns = max(0, count($items) - self::TABLEJOINLIMIT);
+
+        // Add feedback response values.
         foreach ($items as $nr => $item) {
-            $this->sql->fields .= ", v{$nr}.value AS val{$nr}";
-            $this->sql->from .= " LEFT OUTER JOIN {feedback_value} v{$nr} " .
-                "ON v{$nr}.completed = c.id AND v{$nr}.item = :itemid{$nr}";
-            $this->sql->params["itemid{$nr}"] = $item->id;
+            if ($columnscount++ < self::TABLEJOINLIMIT) {
+                // Mysql has a limit on the number of tables in the join, so we only add limited number of columns here,
+                // the rest will be added in {@link self::build_table()} and {@link self::build_table_chunk()} functions.
+                $this->sql->fields .= ", v{$nr}.value AS val{$nr}";
+                $this->sql->from .= " LEFT OUTER JOIN {feedback_value} v{$nr} " .
+                    "ON v{$nr}.completed = c.id AND v{$nr}.item = :itemid{$nr}";
+                $this->sql->params["itemid{$nr}"] = $item->id;
+            }
+
             $tablecolumns[] = "val{$nr}";
             $itemobj = feedback_get_item_class($item->typ);
             $tableheaders[] = $itemobj->get_display_name($item);
@@ -352,6 +387,11 @@ class mod_feedback_responses_table extends table_sql {
             echo $OUTPUT->box(get_string('nothingtodisplay'), 'generalbox nothingtodisplay');
             return;
         }
+
+        if (count($this->feedbackstructure->get_items(true)) > self::PREVIEWCOLUMNSLIMIT) {
+            echo $OUTPUT->notification(get_string('questionslimited', 'feedback', self::PREVIEWCOLUMNSLIMIT), 'info');
+        }
+
         $this->out($this->showall ? $grandtotal : FEEDBACK_DEFAULT_PAGE_COUNT,
                 $grandtotal > FEEDBACK_DEFAULT_PAGE_COUNT);
 
@@ -412,6 +452,89 @@ class mod_feedback_responses_table extends table_sql {
         exit;
     }
 
+    /**
+     * Take the data returned from the db_query and go through all the rows
+     * processing each col using either col_{columnname} method or other_cols
+     * method or if other_cols returns NULL then put the data straight into the
+     * table.
+     *
+     * This overwrites the parent method because full SQL query may fail on Mysql
+     * because of the limit in the number of tables in the join. Therefore we only
+     * join 59 tables in the main query and add the rest here.
+     *
+     * @return void
+     */
+    public function build_table() {
+        if ($this->rawdata instanceof \Traversable && !$this->rawdata->valid()) {
+            return;
+        }
+        if (!$this->rawdata) {
+            return;
+        }
+
+        $columnsgroups = [];
+        if ($this->hasmorecolumns) {
+            $items = $this->feedbackstructure->get_items(true);
+            $notretrieveditems = array_slice($items, self::TABLEJOINLIMIT, $this->hasmorecolumns, true);
+            $columnsgroups = array_chunk($notretrieveditems, self::TABLEJOINLIMIT, true);
+        }
+
+        $chunk = [];
+        foreach ($this->rawdata as $row) {
+            if ($this->hasmorecolumns) {
+                $chunk[$row->id] = $row;
+                if (count($chunk) >= self::ROWCHUNKSIZE) {
+                    $this->build_table_chunk($chunk, $columnsgroups);
+                    $chunk = [];
+                }
+            } else {
+                $this->add_data_keyed($this->format_row($row), $this->get_row_class($row));
+            }
+        }
+        $this->build_table_chunk($chunk, $columnsgroups);
+
+        $this->rawdata->close();
+    }
+
+    /**
+     * Retrieve additional columns. Database engine may have a limit on number of joins.
+     *
+     * @param array $rows Array of rows with already retrieved data, new values will be added to this array
+     * @param array $columnsgroups array of arrays of columns. Each element has up to self::TABLEJOINLIMIT items. This
+     *     is easy to calculate but because we can call this method many times we calculate it once and pass by
+     *     reference for performance reasons
+     */
+    protected function build_table_chunk(&$rows, &$columnsgroups) {
+        global $DB;
+        if (!$rows) {
+            return;
+        }
+
+        foreach ($columnsgroups as $columnsgroup) {
+            $fields = 'c.id';
+            $from = '{feedback_completed} c';
+            $params = [];
+            foreach ($columnsgroup as $nr => $item) {
+                $fields .= ", v{$nr}.value AS val{$nr}";
+                $from .= " LEFT OUTER JOIN {feedback_value} v{$nr} " .
+                    "ON v{$nr}.completed = c.id AND v{$nr}.item = :itemid{$nr}";
+                $params["itemid{$nr}"] = $item->id;
+            }
+            list($idsql, $idparams) = $DB->get_in_or_equal(array_keys($rows), SQL_PARAMS_NAMED);
+            $sql = "SELECT $fields FROM $from WHERE c.id ".$idsql;
+            $results = $DB->get_records_sql($sql, $params + $idparams);
+            foreach ($results as $result) {
+                foreach ($result as $key => $value) {
+                    $rows[$result->id]->{$key} = $value;
+                }
+            }
+        }
+
+        foreach ($rows as $row) {
+            $this->add_data_keyed($this->format_row($row), $this->get_row_class($row));
+        }
+    }
+
     /**
      * Returns html code for displaying "Download" button if applicable.
      */
index c5291cb..36a4ce7 100644 (file)
@@ -212,6 +212,7 @@ $string['public'] = 'Public';
 $string['question'] = 'Question';
 $string['questionandsubmission'] = 'Question and submission settings';
 $string['questions'] = 'Questions';
+$string['questionslimited'] = 'Showing only {$a} first questions, view individual answers or download table data to view all.';
 $string['radio'] = 'Multiple choice - single answer';
 $string['radio_values'] = 'Responses';
 $string['ready_feedbacks'] = 'Ready feedbacks';
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 57677e6..571004f 100644 (file)
@@ -38,4 +38,12 @@ $capabilities = array(
         'clonepermissionsfrom' => 'moodle/course:manageactivities'
     ),
 
+    'mod/label:view' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'user' => CAP_ALLOW,
+            'guest' => CAP_ALLOW
+        )
+    ),
 );
index 65c12a0..1f6e972 100644 (file)
@@ -32,6 +32,7 @@ $string['dndresizewidth'] = 'Resize drag and drop width';
 $string['dnduploadlabel'] = 'Add image to course page';
 $string['dnduploadlabeltext'] = 'Add a label to the course page';
 $string['label:addinstance'] = 'Add a new label';
+$string['label:view'] = 'View labels';
 $string['labeltext'] = 'Label text';
 $string['modulename'] = 'Label';
 $string['modulename_help'] = 'The label module enables text and multimedia to be inserted into the course page in between links to other resources and activities. Labels are very versatile and can help to improve the appearance of a course if used thoughtfully.
index a688937..dd2859d 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016052300;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016080400;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016051900;    // Requires this Moodle version
 $plugin->component = 'mod_label'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
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 09eec96..83d731a 100644 (file)
@@ -253,7 +253,6 @@ function resource_print_header($resource, $cm, $course) {
     $PAGE->set_title($course->shortname.': '.$resource->name);
     $PAGE->set_heading($course->fullname);
     $PAGE->set_activity_record($resource);
-    $PAGE->set_button(update_module_button($cm->id, '', get_string('modulename', 'resource')));
     echo $OUTPUT->header();
 }
 
index 74e8d00..09f1298 100644 (file)
@@ -4,6 +4,9 @@ information provided here is intended especially for developers.
 === 3.2 ===
 
 * Callback delete_course is deprecated and should be replaced with observer for event \core\event\course_content_deleted
+* update_module_button() and core_renderer::update_module_button() have been deprecated and should not be used anymore.
+  Activity modules should not add the edit module button, the link is already available in the Administration block.
+  Themes can choose to display the link in the buttons row consistently for all module types.
 
 === 3.1 ===
 
index 76c5973..660f92e 100644 (file)
@@ -978,14 +978,6 @@ class page_wiki_preview extends page_wiki_edit {
 
     private $newcontent;
 
-    function __construct($wiki, $subwiki, $cm) {
-        global $PAGE, $CFG, $OUTPUT;
-        parent::__construct($wiki, $subwiki, $cm);
-        $buttons = $OUTPUT->update_module_button($cm->id, 'wiki');
-        $PAGE->set_button($buttons);
-
-    }
-
     function print_header() {
         global $PAGE, $CFG;
 
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'));
+    }
 }
index 3abcc04..febb4c3 100644 (file)
@@ -36,7 +36,7 @@ $PAGE->set_url('/user/editor.php', array('id' => $userid, 'course' => $courseid)
 list($user, $course) = useredit_setup_preference_page($userid, $courseid);
 
 // Create form.
-$editorform = new user_edit_editor_form(null, array('userid' => $user->id));
+$editorform = new user_edit_editor_form();
 
 $user->preference_htmleditor = get_user_preferences( 'htmleditor', '', $user->id);
 $editorform->set_data($user);
index 248499c..c1f5ba5 100644 (file)
@@ -45,6 +45,10 @@ class user_edit_editor_form extends moodleform {
         $mform = $this->_form;
 
         $editors = editors_get_enabled();
+
+        $mform->addElement('hidden', 'id');
+        $mform->setType('id', PARAM_INT);
+
         if (count($editors) > 1) {
             $choices = array('' => get_string('defaulteditor'));
             $firsteditor = '';
index 3dbec31..90b4002 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016080400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016081100.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.2dev (Build: 20160804)'; // Human-friendly version name
+$release  = '3.2dev (Build: 20160811)'; // Human-friendly version name
 
 $branch   = '32';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.