Merge branch 'MDL-55239_m32v1' of https://github.com/sbourget/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 5 Sep 2016 17:23:13 +0000 (19:23 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 5 Sep 2016 17:23:13 +0000 (19:23 +0200)
273 files changed:
.csslintrc [deleted file]
.stylelintrc
admin/lib.php
admin/settings/appearance.php
admin/settings/security.php
admin/settings/server.php
admin/tool/behat/cli/init.php
admin/tool/behat/cli/run.php
admin/tool/behat/cli/util.php
admin/tool/behat/cli/util_single_run.php
admin/tool/behat/index.php
admin/tool/behat/tests/manager_test.php
admin/tool/behat/tests/manager_util_test.php [new file with mode: 0644]
admin/tool/generator/classes/course_backend.php
admin/tool/generator/lang/en/tool_generator.php
admin/tool/monitor/classes/eventlist.php
admin/tool/monitor/classes/rule.php
admin/tool/monitor/classes/subscription.php
admin/tool/monitor/edit.php
admin/tool/monitor/lang/en/tool_monitor.php
backup/util/dbops/restore_dbops.class.php
blocks/navigation/tests/behat/participants_link.feature [new file with mode: 0644]
blocks/rss_client/templates/feed.mustache
blocks/site_main_menu/styles.css
blog/edit.php
blog/index.php
blog/preferences.php
blog/rsslib.php
cache/classes/definition.php
cache/classes/loaders.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
composer.json
composer.lock
course/externallib.php
course/lib.php
enrol/imsenterprise/lang/en/enrol_imsenterprise.php
enrol/lti/cartridge.php [new file with mode: 0644]
enrol/lti/classes/helper.php
enrol/lti/classes/manage_table.php
enrol/lti/lang/en/enrol_lti.php
enrol/lti/styles.css [new file with mode: 0644]
enrol/lti/tests/fixtures/input.xml [new file with mode: 0644]
enrol/lti/tests/fixtures/test_ambiguous_nodes-expected.xml [new file with mode: 0644]
enrol/lti/tests/fixtures/test_correct_xpath-expected.xml [new file with mode: 0644]
enrol/lti/tests/fixtures/test_missing_node-expected.xml [new file with mode: 0644]
enrol/lti/tests/fixtures/test_nodes_removed-expected.xml [new file with mode: 0644]
enrol/lti/tests/helper_test.php
enrol/lti/version.php
enrol/lti/xml/imslticc.xml [new file with mode: 0644]
enrol/self/db/access.php
enrol/self/db/upgrade.php
enrol/self/tests/behat/key_holder.feature [new file with mode: 0644]
enrol/self/version.php
filter/mathjaxloader/filter.php
grade/grading/form/rubric/edit.php
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/lib.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/rubriceditor.php
grade/grading/form/rubric/tests/behat/edit_rubric.feature
grade/grading/form/rubric/tests/behat/grade_calculation.feature [new file with mode: 0644]
grade/grading/form/rubric/tests/behat/negative_points.feature [new file with mode: 0644]
install/lang/fa/admin.php
install/lang/fa/error.php
install/lang/fa/install.php
install/lang/fa/moodle.php
install/lang/uk/admin.php
install/lang/uk/moodle.php
lang/en/admin.php
lang/en/grades.php
lang/en/moodle.php
lang/en/search.php
lib/accesslib.php
lib/amd/build/chart_bar.min.js
lib/amd/build/chart_output_chartjs.min.js
lib/amd/build/chart_output_htmltable.min.js
lib/amd/build/chart_pie.min.js
lib/amd/build/inplace_editable.min.js
lib/amd/build/templates.min.js
lib/amd/src/chart_bar.js
lib/amd/src/chart_output_chartjs.js
lib/amd/src/chart_output_htmltable.js
lib/amd/src/chart_pie.js
lib/amd/src/inplace_editable.js
lib/amd/src/templates.js
lib/behat/behat_base.php
lib/behat/classes/behat_config_manager.php
lib/behat/classes/behat_config_util.php [new file with mode: 0644]
lib/behat/classes/behat_context_helper.php
lib/behat/classes/behat_selectors.php
lib/behat/classes/util.php
lib/behat/lib.php
lib/classes/chart_bar.php
lib/classes/chart_pie.php
lib/classes/component.php
lib/classes/event/base.php
lib/classes/event/content_viewed.php
lib/classes/event/course_module_instances_list_viewed.php
lib/classes/event/mnet_access_control_updated.php
lib/classes/event/role_assigned.php
lib/classes/plugininfo/filter.php
lib/classes/progress/display.php
lib/classes/session/manager.php
lib/classes/task/stats_cron_task.php
lib/completionlib.php
lib/db/caches.php
lib/db/install.xml
lib/db/tasks.php
lib/db/upgrade.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/form/modgrade.php
lib/gdlib.php
lib/grade/grade_item.php
lib/gradelib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/phpunit/classes/util.php
lib/requirejs/moodle-config.js
lib/sessionlib.php
lib/setup.php
lib/statslib.php
lib/tablelib.php
lib/templates/copy_box.mustache [new file with mode: 0644]
lib/testing/classes/tests_finder.php
lib/testing/classes/util.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/completionlib_test.php
lib/tests/component_test.php
lib/tests/gdlib_test.php
lib/tests/htmlpurifier_test.php
lib/tests/moodlelib_test.php
lib/tests/progress_display_test.php [new file with mode: 0644]
lib/tests/sessionlib_test.php
lib/tests/statslib_test.php
lib/tests/unoconv_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/weblib.php
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/notification/js/ajaxexception.js
lib/yui/src/notification/js/exception.js
message/classes/search/base_message.php [new file with mode: 0644]
message/classes/search/message_received.php [new file with mode: 0644]
message/classes/search/message_sent.php [new file with mode: 0644]
message/tests/search_test_received.php [new file with mode: 0644]
message/tests/search_test_sent.php [new file with mode: 0644]
mod/assign/gradingtable.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/renderable.php
mod/assign/templates/grading_navigation_user_summary.mustache
mod/assign/tests/behat/rescale_grades.feature
mod/choice/classes/event/answer_created.php [new file with mode: 0644]
mod/choice/classes/event/answer_deleted.php
mod/choice/classes/event/answer_submitted.php
mod/choice/classes/event/answer_updated.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/mod_form.php
mod/choice/renderer.php
mod/choice/report.php
mod/choice/styles.css
mod/choice/tests/behat/modify_choice.feature [new file with mode: 0644]
mod/choice/tests/events_test.php
mod/choice/tests/generator/lib.php
mod/choice/upgrade.txt
mod/choice/view.php
mod/data/classes/search/entry.php [new file with mode: 0644]
mod/data/classes/search/sortedcontentqueue.php [new file with mode: 0644]
mod/data/field/checkbox/field.class.php
mod/data/field/latlong/field.class.php
mod/data/field/menu/field.class.php
mod/data/field/multimenu/field.class.php
mod/data/field/radiobutton/field.class.php
mod/data/field/text/field.class.php
mod/data/field/textarea/field.class.php
mod/data/field/url/field.class.php
mod/data/import.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/tests/generator/lib.php
mod/data/tests/generator_test.php
mod/data/tests/search_test.php
mod/data/upgrade.txt [new file with mode: 0644]
mod/forum/classes/output/email/renderer.php
mod/forum/classes/subscriptions.php
mod/forum/index.php
mod/forum/lang/en/deprecated.txt
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/renderer.php
mod/forum/tests/subscriptions_test.php
mod/glossary/import.php
mod/label/lang/en/label.php
mod/lesson/backup/moodle2/restore_lesson_activity_task.class.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/lang/en/lesson.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/truefalse.php
mod/lti/OAuth.php
mod/lti/lang/en/lti.php
mod/lti/service/toolproxy/classes/local/resource/toolproxy.php
mod/quiz/edit_rest.php
mod/quiz/locallib.php
mod/quiz/report/attemptsreport.php
mod/quiz/tests/locallib_test.php
mod/scorm/backup/moodle1/lib.php
mod/scorm/backup/moodle2/backup_scorm_stepslib.php
mod/scorm/backup/moodle2/restore_scorm_stepslib.php
mod/scorm/classes/external.php
mod/scorm/db/install.xml
mod/scorm/db/upgrade.php
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/scorm/mod_form.php
mod/scorm/tests/behat/completion_condition_require_status.feature [new file with mode: 0644]
mod/scorm/tests/externallib_test.php
mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip [new file with mode: 0644]
mod/scorm/tests/packages/readme_moodle.txt
mod/scorm/version.php
mod/wiki/classes/external.php
mod/wiki/create_form.php
mod/wiki/lib.php
mod/wiki/locallib.php
mod/wiki/pagelib.php
mod/wiki/tests/externallib_test.php
mod/workshop/form/accumulative/assessment_form.php
mod/workshop/form/accumulative/lang/en/deprecated.txt [new file with mode: 0644]
mod/workshop/form/accumulative/lang/en/workshopform_accumulative.php
mod/workshop/form/comments/assessment_form.php
mod/workshop/form/comments/lang/en/deprecated.txt [new file with mode: 0644]
mod/workshop/form/comments/lang/en/workshopform_comments.php
mod/workshop/form/numerrors/assessment_form.php
mod/workshop/form/numerrors/lang/en/deprecated.txt [new file with mode: 0644]
mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php
mod/workshop/lang/en/deprecated.txt [new file with mode: 0644]
mod/workshop/lang/en/workshop.php
question/category_class.php
question/format.php
question/tests/generator/lib.php
question/type/multianswer/renderer.php
report/eventlist/classes/list_generator.php
report/eventlist/eventdetail.php
report/log/styles.css
report/search/version.php [deleted file]
report/security/lang/en/report_security.php
report/security/locallib.php
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/dock.less
theme/bootstrapbase/less/moodle/expendable.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/less/moodle/undo.less
theme/bootstrapbase/style/moodle.css
theme/clean/classes/core_renderer.php
theme/clean/lang/en/theme_clean.php
theme/clean/lib.php
theme/more/lang/en/theme_more.php
theme/more/lib.php
theme/upgrade.txt
version.php

diff --git a/.csslintrc b/.csslintrc
deleted file mode 100644 (file)
index 4203daa..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
---errors=errors,duplicate-properties
---warnings=known-properties,display-property-grouping,empty-rules,important
---exclude-list=vendor/,node_modules/,lib/editor/tinymce/,lib/yuilib/,theme/bootstrapbase/style/,lib/jquery/,lib/htmlpurifier,theme/clean/style/custom.css,theme/more/style/custom.css,
index e5e61a2..2b4b9f8 100644 (file)
@@ -31,6 +31,7 @@
         "declaration-colon-newline-after": "always-multi-line",
         "declaration-colon-space-after": "always-single-line",
         "declaration-colon-space-before": "never",
+        "declaration-no-important": true,
         "function-calc-no-unspaced-operator": true,
         "function-comma-newline-after": "always-multi-line",
         "function-comma-space-after": "always-single-line",
@@ -50,6 +51,7 @@
         "media-feature-colon-space-after": "always",
         "media-feature-colon-space-before": "never",
         "media-feature-no-missing-punctuation": true,
+        "media-feature-parentheses-space-inside": "never",
         "media-feature-range-operator-space-after": "always",
         "media-feature-range-operator-space-before": "always",
         "media-query-list-comma-newline-after": "always-multi-line",
@@ -62,6 +64,7 @@
         "no-invalid-double-slash-comments": true,
         "no-unknown-animations": true,
         "property-case": "lower",
+        "property-no-unknown": true,
         "selector-attribute-brackets-space-inside": "never",
         "selector-attribute-operator-space-after": "never",
         "selector-attribute-operator-space-before": "never",
index c9ddefa..3955084 100644 (file)
@@ -24,6 +24,8 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+defined('MOODLE_INTERNAL') || die();
+
 /**
  * Return a list of page types
  * @param string $pagetype current page type
@@ -37,3 +39,86 @@ function admin_page_type_list($pagetype, $parentcontext, $currentcontext) {
     );
     return $array;
 }
+
+/**
+ * File serving.
+ *
+ * @param stdClass $course The course object.
+ * @param stdClass $cm The cm object.
+ * @param context $context The context object.
+ * @param string $filearea The file area.
+ * @param array $args List of arguments.
+ * @param bool $forcedownload Whether or not to force the download of the file.
+ * @param array $options Array of options.
+ * @return void|false
+ */
+function core_admin_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) {
+    global $CFG;
+
+    if (in_array($filearea, ['logo', 'logocompact'])) {
+        $size = array_shift($args); // The path hides the size.
+        $itemid = clean_param(array_shift($args), PARAM_INT);
+        $filename = clean_param(array_shift($args), PARAM_FILE);
+        $themerev = theme_get_revision();
+        if ($themerev <= 0) {
+            // Normalise to 0 as -1 doesn't place well with paths.
+             $themerev = 0;
+        }
+
+        // Extract the requested width and height.
+        $maxwidth = 0;
+        $maxheight = 0;
+        if (preg_match('/^\d+x\d+$/', $size)) {
+            list($maxwidth, $maxheight) = explode('x', $size);
+            $maxwidth = clean_param($maxwidth, PARAM_INT);
+            $maxheight = clean_param($maxheight, PARAM_INT);
+        }
+
+        $lifetime = 0;
+        if ($itemid > 0 && $themerev == $itemid) {
+            // The itemid is $CFG->themerev, when 0 or less no caching. Also no caching when they don't match.
+            $lifetime = DAYSECS * 60;
+        }
+
+        // Anyone, including guests and non-logged in users, can view the logos.
+        $options = ['cacheability' => 'public'];
+
+        // Check if we've got a cached file to return. When lifetime is 0 then we don't want to cached one.
+        $candidate = $CFG->localcachedir . "/core_admin/$themerev/$filearea/{$maxwidth}x{$maxheight}/$filename";
+        if (file_exists($candidate) && $lifetime > 0) {
+            send_file($candidate, $filename, $lifetime, 0, false, false, '', false, $options);
+        }
+
+        // Find the original file.
+        $fs = get_file_storage();
+        $filepath = "/{$context->id}/core_admin/{$filearea}/0/{$filename}";
+        if (!$file = $fs->get_file_by_hash(sha1($filepath))) {
+            send_file_not_found();
+        }
+
+        // No need for resizing, but if the file should be cached we save it so we can serve it fast next time.
+        if (empty($maxwidth) && empty($maxheight)) {
+            if ($lifetime) {
+                file_safe_save_content($file->get_content(), $candidate);
+            }
+            send_stored_file($file, $lifetime, 0, false, $options);
+        }
+
+        // Proceed with the resizing.
+        $filedata = $file->resize_image($maxwidth, $maxheight);
+        if (!$filedata) {
+            send_file_not_found();
+        }
+
+        // If we don't want to cached the file, serve now and quit.
+        if (!$lifetime) {
+            send_content_uncached($filedata, $filename);
+        }
+
+        // Save, serve and quit.
+        file_safe_save_content($filedata, $candidate);
+        send_file($candidate, $filename, $lifetime, 0, false, false, '', false, $options);
+    }
+
+    send_file_not_found();
+}
index a8b6c6a..d8697d4 100644 (file)
@@ -51,6 +51,26 @@ preferences,moodle|/user/preferences.php|preferences',
         }
     }
 
+    // Logos section.
+    $temp = new admin_settingpage('logos', new lang_string('logossettings', 'admin'));
+
+    // Logo file setting.
+    $title = get_string('logo', 'admin');
+    $description = get_string('logo_desc', 'admin');
+    $setting = new admin_setting_configstoredfile('core_admin/logo', $title, $description, 'logo', 0,
+        ['maxfiles' => 1, 'accepted_types' => ['.jpg', '.png']]);
+    $setting->set_updatedcallback('theme_reset_all_caches');
+    $temp->add($setting);
+
+    // Small logo file setting.
+    $title = get_string('logocompact', 'admin');
+    $description = get_string('logocompact_desc', 'admin');
+    $setting = new admin_setting_configstoredfile('core_admin/logocompact', $title, $description, 'logocompact', 0,
+        ['maxfiles' => 1, 'accepted_types' => ['.jpg', '.png']]);
+    $setting->set_updatedcallback('theme_reset_all_caches');
+    $temp->add($setting);
+
+    $ADMIN->add('appearance', $temp);
 
     // Calendar settings.
     $temp = new admin_settingpage('calendar', new lang_string('calendarsettings','admin'));
index 00c9429..946abbd 100644 (file)
@@ -109,7 +109,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     // "httpsecurity" settingpage
     $temp = new admin_settingpage('httpsecurity', new lang_string('httpsecurity', 'admin'));
     $temp->add(new admin_setting_configcheckbox('loginhttps', new lang_string('loginhttps', 'admin'), new lang_string('configloginhttps', 'admin'), 0));
-    $temp->add(new admin_setting_configcheckbox('cookiesecure', new lang_string('cookiesecure', 'admin'), new lang_string('configcookiesecure', 'admin'), 0));
+    $temp->add(new admin_setting_configcheckbox('cookiesecure', new lang_string('cookiesecure', 'admin'), new lang_string('configcookiesecure', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('cookiehttponly', new lang_string('cookiehttponly', 'admin'), new lang_string('configcookiehttponly', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowframembedding', new lang_string('allowframembedding', 'admin'), new lang_string('allowframembedding_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('loginpasswordautocomplete', new lang_string('loginpasswordautocomplete', 'admin'), new lang_string('loginpasswordautocomplete_help', 'admin'), 0));
index 548031b..8ef94a7 100644 (file)
@@ -79,7 +79,6 @@ $temp->add(new admin_setting_configselect('statsmaxruntime', new lang_string('st
                                                                                                                                                             60*60*7 => '7 '.new lang_string('hours'),
                                                                                                                                                             60*60*8 => '8 '.new lang_string('hours') )));
 $temp->add(new admin_setting_configtext('statsruntimedays', new lang_string('statsruntimedays', 'admin'), new lang_string('configstatsruntimedays', 'admin'), 31, PARAM_INT));
-$temp->add(new admin_setting_configtime('statsruntimestarthour', 'statsruntimestartminute', new lang_string('statsruntimestart', 'admin'), new lang_string('configstatsruntimestart', 'admin'), array('h' => 0, 'm' => 0)));
 $temp->add(new admin_setting_configtext('statsuserthreshold', new lang_string('statsuserthreshold', 'admin'), new lang_string('configstatsuserthreshold', 'admin'), 0, PARAM_INT));
 $ADMIN->add('server', $temp);
 
index 4517a7e..f58a1bc 100644 (file)
@@ -47,6 +47,7 @@ list($options, $unrecognized) = cli_get_params(
         'help'     => false,
         'fromrun'  => 1,
         'torun'    => 0,
+        'run-with-theme' => false,
     ),
     array(
         'j' => 'parallel',
@@ -63,10 +64,11 @@ Usage:
   php init.php [--parallel=value [--maxruns=value] [--fromrun=value --torun=value]] [--help]
 
 Options:
--j, --parallel Number of parallel behat run to initialise
--m, --maxruns  Max parallel processes to be executed at one time.
---fromrun      Execute run starting from (Used for parallel runs on different vms)
---torun        Execute run till (Used for parallel runs on different vms)
+-j, --parallel   Number of parallel behat run to initialise
+-m, --maxruns    Max parallel processes to be executed at one time.
+--fromrun        Execute run starting from (Used for parallel runs on different vms)
+--torun          Execute run till (Used for parallel runs on different vms)
+--run-with-theme Run all core features with specified theme.
 
 -h, --help     Print out this help
 
@@ -95,6 +97,11 @@ if ($options['parallel'] && $options['parallel'] > 1) {
     }
 }
 
+$themesuitewithallfeatures = '';
+if ($options['run-with-theme']) {
+    $themesuitewithallfeatures = '--run-with-theme="true"';
+}
+
 // Changing the cwd to admin/tool/behat/cli.
 $cwd = getcwd();
 $output = null;
@@ -104,7 +111,7 @@ testing_update_composer_dependencies();
 
 // Check whether the behat test environment needs to be updated.
 chdir(__DIR__);
-exec("php $utilfile --diag $paralleloption", $output, $code);
+exec("php $utilfile --diag $paralleloption $themesuitewithallfeatures", $output, $code);
 
 if ($code == 0) {
     echo "Behat test environment already installed\n";
@@ -112,7 +119,7 @@ if ($code == 0) {
 } else if ($code == BEHAT_EXITCODE_INSTALL) {
     // Behat and dependencies are installed and we need to install the test site.
     chdir(__DIR__);
-    passthru("php $utilfile --install $paralleloption", $code);
+    passthru("php $utilfile --install $paralleloption $themesuitewithallfeatures", $code);
     if ($code != 0) {
         chdir($cwd);
         exit($code);
@@ -121,14 +128,14 @@ if ($code == 0) {
 } else if ($code == BEHAT_EXITCODE_REINSTALL) {
     // Test site data is outdated.
     chdir(__DIR__);
-    passthru("php $utilfile --drop $paralleloption", $code);
+    passthru("php $utilfile --drop $paralleloption $themesuitewithallfeatures", $code);
     if ($code != 0) {
         chdir($cwd);
         exit($code);
     }
 
     chdir(__DIR__);
-    passthru("php $utilfile --install $paralleloption", $code);
+    passthru("php $utilfile --install $paralleloption $themesuitewithallfeatures", $code);
     if ($code != 0) {
         chdir($cwd);
         exit($code);
@@ -143,7 +150,7 @@ if ($code == 0) {
 
 // Enable editing mode according to config.php vars.
 chdir(__DIR__);
-passthru("php $utilfile --enable $paralleloption", $code);
+passthru("php $utilfile --enable $paralleloption $themesuitewithallfeatures", $code);
 if ($code != 0) {
     echo "Error enabling site" . PHP_EOL;
     chdir($cwd);
index f8a37e7..c6dece8 100644 (file)
@@ -51,9 +51,11 @@ list($options, $unrecognised) = cli_get_params(
         'tags'     => '',
         'profile'  => '',
         'feature'  => '',
+        'suite'    => '',
         'fromrun'  => 1,
         'torun'    => 0,
         'single-run' => false,
+        'run-with-theme' => false,
     ),
     array(
         'h' => 'help',
@@ -73,9 +75,11 @@ Usage:
 Options:
 --BEHAT_OPTION     Any combination of behat option specified in http://behat.readthedocs.org/en/v2.5/guides/6.cli.html
 --feature          Only execute specified feature file (Absolute path of feature file).
+--suite            Specified theme scenarios will be executed.
 --replace          Replace args string with run process number, useful for output.
 --fromrun          Execute run starting from (Used for parallel runs on different vms)
 --torun            Execute run till (Used for parallel runs on different vms)
+--run-with-theme   Run all core features with specified theme.
 
 -h, --help         Print out this help
 
@@ -101,9 +105,6 @@ if (empty($options['torun'])) {
 if (extension_loaded('pcntl')) {
     $disabled = explode(',', ini_get('disable_functions'));
     if (!in_array('pcntl_signal', $disabled)) {
-        // Handle interrupts on PHP7.
-        declare(ticks = 1);
-
         pcntl_signal(SIGTERM, "signal_handler");
         pcntl_signal(SIGINT, "signal_handler");
     }
@@ -144,6 +145,11 @@ if ($options['tags']) {
     $extraopts[] = '--tags="' . $tags . '"';
 }
 
+// Add suite option if specified.
+if ($options['suite']) {
+    $extraopts[] = '--suite="' . $options['suite'] . '"';
+}
+
 // Feature should be added to last, for behat command.
 if ($options['feature']) {
     $extraopts[] = $options['feature'];
@@ -170,6 +176,8 @@ if (empty($parallelrun)) {
 
 // Update config file if tags defined.
 if ($tags) {
+    define('ABORT_AFTER_CONFIG_CANCEL', true);
+    require("$CFG->dirroot/lib/setup.php");
     // Hack to set proper dataroot and wwwroot.
     $behatdataroot = $CFG->behat_dataroot;
     $behatwwwroot  = $CFG->behat_wwwroot;
@@ -186,7 +194,7 @@ if ($tags) {
         } else {
             $CFG->behat_dataroot = $behatdataroot . $i;
         }
-        behat_config_manager::update_config_file('', true, $tags);
+        behat_config_manager::update_config_file('', true, $tags, $options['run-with-theme'], $parallelrun);
     }
     $CFG->behat_dataroot = $behatdataroot;
     $CFG->behat_wwwroot = $behatwwwroot;
index a952efc..a30e30a 100644 (file)
@@ -56,11 +56,12 @@ list($options, $unrecognized) = cli_get_params(
         'updatesteps' => false,
         'fromrun'     => 1,
         'torun'       => 0,
+        'run-with-theme' => false,
     ),
     array(
         'h' => 'help',
         'j' => 'parallel',
-        'm' => 'maxruns'
+        'm' => 'maxruns',
     )
 );
 
@@ -78,8 +79,10 @@ Options:
 --disable      Disables test environment
 --diag         Get behat test environment status code
 --updatesteps  Update feature step file.
+
 -j, --parallel Number of parallel behat run operation
 -m, --maxruns  Max parallel processes to be executed at one time.
+--run-with-theme Run all core features with specified theme.
 
 -h, --help     Print out this help
 
@@ -175,12 +178,15 @@ if ($options['diag'] || $options['enable'] || $options['disable']) {
 } else if ($options['updatesteps']) {
     // Rewrite config file to ensure we have all the features covered.
     if (empty($options['parallel'])) {
-        behat_config_manager::update_config_file();
+        behat_config_manager::update_config_file('', true, '', $options['run-with-theme'], false, false);
     } else {
         // Update config file, ensuring we have up-to-date behat.yml.
         for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
             $CFG->behatrunprocess = $i;
-            behat_config_manager::update_config_file();
+
+            // Update config file for each run.
+            behat_config_manager::update_config_file('', true, '', $options['run-with-theme'],
+                $options['parallel'], $i);
         }
         unset($CFG->behatrunprocess);
     }
index 1991707..70aca44 100644 (file)
@@ -40,16 +40,17 @@ list($options, $unrecognized) = cli_get_params(
         'help'        => false,
         'install'     => false,
         'parallel'    => 0,
-        'run'         => '',
+        'run'         => 0,
         'drop'        => false,
         'enable'      => false,
         'disable'     => false,
         'diag'        => false,
         'tags'        => '',
         'updatesteps' => false,
+        'run-with-theme' => false,
     ),
     array(
-        'h' => 'help'
+        'h' => 'help',
     )
 );
 
@@ -65,12 +66,13 @@ Usage:
   php util_single_run.php [--install|--drop|--enable|--disable|--diag|--updatesteps|--help]
 
 Options:
---install     Installs the test environment for acceptance tests
---drop        Drops the database tables and the dataroot contents
---enable      Enables test environment and updates tests list
---disable     Disables test environment
---diag        Get behat test environment status code
---updatesteps Update feature step file.
+--install        Installs the test environment for acceptance tests
+--drop           Drops the database tables and the dataroot contents
+--enable         Enables test environment and updates tests list
+--disable        Disables test environment
+--diag           Get behat test environment status code
+--updatesteps    Update feature step file.
+--run-with-theme Run all core features with specified theme.
 
 -h, --help Print out this help
 
@@ -130,15 +132,22 @@ require_once($CFG->libdir . '/behat/classes/behat_command.php');
 require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
 
 // Ensure run option is <= parallel run installed.
+$run = 0;
+$parallel = 0;
 if ($options['run']) {
+    $run = $options['run'];
+    // If parallel option is not passed, then try get it form config.
     if (!$options['parallel']) {
-        $options['parallel'] = behat_config_manager::get_parallel_test_runs();
+        $parallel = behat_config_manager::get_parallel_test_runs();
+    } else {
+        $parallel = $options['parallel'];
     }
-    if (empty($options['parallel']) || $options['run'] > $options['parallel']) {
-        echo "Parallel runs can't be more then ".$options['parallel'].PHP_EOL;
+
+    if (empty($parallel) || $run > $parallel) {
+        echo "Parallel runs can't be more then ".$parallel.PHP_EOL;
         exit(1);
     }
-    $CFG->behatrunprocess = $options['run'];
+    $CFG->behatrunprocess = $run;
 }
 
 // Run command (only one per time).
@@ -146,7 +155,7 @@ if ($options['install']) {
     behat_util::install_site();
 
     // This is only displayed once for parallel install.
-    if (empty($options['run'])) {
+    if (empty($run)) {
         mtrace("Acceptance tests site installed");
     }
 
@@ -155,30 +164,30 @@ if ($options['install']) {
     test_lock::acquire('behat');
     behat_util::drop_site();
     // This is only displayed once for parallel install.
-    if (empty($options['run'])) {
+    if (empty($run)) {
         mtrace("Acceptance tests site dropped");
     }
 
 } else if ($options['enable']) {
-    if (!empty($options['parallel'])) {
+    if (!empty($parallel)) {
         // Save parallel site info for enable and install options.
         $filepath = behat_config_manager::get_parallel_test_file_path();
-        if (!file_put_contents($filepath, $options['parallel'])) {
+        if (!file_put_contents($filepath, $parallel)) {
             behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
         }
     }
 
     // Enable test mode.
-    behat_util::start_test_mode();
+    behat_util::start_test_mode($options['run-with-theme'], $parallel, $run);
 
     // This is only displayed once for parallel install.
-    if (empty($options['run'])) {
+    if (empty($run)) {
         // Notify user that 2.5 profile has been converted to 3.5.
         if (behat_config_manager::$autoprofileconversion) {
             mtrace("2.5 behat profile detected, automatically converted to current 3.x format");
         }
 
-        $runtestscommand = behat_command::get_behat_command(true, !empty($options['run']));
+        $runtestscommand = behat_command::get_behat_command(true, !empty($run));
 
         $runtestscommand .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
         mtrace("Acceptance tests environment enabled on $CFG->behat_wwwroot, to run the tests use: " . PHP_EOL .
@@ -188,7 +197,7 @@ if ($options['install']) {
 } else if ($options['disable']) {
     behat_util::stop_test_mode();
     // This is only displayed once for parallel install.
-    if (empty($options['run'])) {
+    if (empty($run)) {
         mtrace("Acceptance tests environment disabled");
     }
 
index 5e71bdf..f3b1ac0 100644 (file)
@@ -45,7 +45,8 @@ $steps = tool_behat::stepsdefinitions($type, $component, $filter);
 $componentswithsteps = array('' => get_string('allavailablesteps', 'tool_behat'));
 
 // Complete the components list with the moodle steps definitions.
-$components = behat_config_manager::get_components_steps_definitions();
+$behatconfig = new behat_config_util();
+$components = $behatconfig->get_components_contexts();
 if ($components) {
     foreach ($components as $component => $filepath) {
         // TODO Use a class static attribute instead of the class name.
index 0f24f38..ae052e5 100644 (file)
@@ -70,6 +70,7 @@ class tool_behat_manager_testcase extends advanced_testcase {
         );
 
         $array = testable_behat_config_manager::merge_config($array1, $array2);
+        $this->assertDebuggingCalled("Use of merge_config is deprecated, please see behat_config_util");
 
         // Overrides are applied.
         $this->assertEquals('OVERRIDDEN1', $array['simple']);
@@ -95,6 +96,7 @@ class tool_behat_manager_testcase extends advanced_testcase {
         );
 
         $array = testable_behat_config_manager::merge_config($array1, $array2);
+        $this->assertDebuggingCalled("Use of merge_config is deprecated, please see behat_config_util");
 
         // Overrides applied.
         $this->assertNotEmpty($array['simple']);
@@ -138,6 +140,7 @@ class tool_behat_manager_testcase extends advanced_testcase {
         );
 
         $contents = testable_behat_config_manager::get_config_file_contents($features, $stepsdefinitions);
+        $this->assertDebuggingCalled("Use of get_config_file_contents is deprecated, please see behat_config_util");
 
         // YAML decides when is is necessary to wrap strings between single quotes, so not controlled
         // values like paths should not be asserted including the key name as they would depend on the
diff --git a/admin/tool/behat/tests/manager_util_test.php b/admin/tool/behat/tests/manager_util_test.php
new file mode 100644 (file)
index 0000000..2f59806
--- /dev/null
@@ -0,0 +1,398 @@
+<?php
+// @codingStandardsIgnoreFile
+// @codeCoverageIgnoreStart
+
+// 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/>.
+
+/**
+ * Unit tests for behat manager.
+ *
+ * @package   tool_behat
+ * @copyright  2016 Rajesh Taneja
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/' . $CFG->admin .'/tool/behat/locallib.php');
+require_once($CFG->libdir . '/behat/classes/util.php');
+require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
+
+/**
+ * Behat manager tests.
+ *
+ * @package    tool_behat
+ * @copyright  2016 Rajesh Taneja
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_behat_manager_util_testcase extends advanced_testcase {
+
+    /**
+     * @var array core features.
+     */
+    private $corefeatures = array(
+        'feedback_editpdf_behat_test1' => '/test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature',
+        'feedback_file_behat_test2' => "C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature",
+        'moodle_login_behat_test3' => "C:\\test\\moodle/login/tests/behat/behat_test3.feature",
+        );
+
+
+    /**
+     * @var array theme features.
+     */
+    private $themefeatures = array(
+            'behat_themetest1_core_behat_tests_testtheme_theme' => '/test/moodle/theme/testtheme/tests/behat/core/behat_themetest1.feature',
+            'behat_themetest2_mod_assign_behat_tests_testtheme_theme' => "C:\\test\\moodle\\theme\\testtheme\\tests\\behat\\mod_assign\\behat_themetest2.feature",
+            'behat_themetest3_behat_tests_testtheme_theme_moodle' => "C:\\test\\moodle/theme/testtheme/tests/behat/behat_themetest3.feature",
+        );
+
+    /**
+     * @var array core contexts.
+     */
+    private $corecontexts = array(
+            'behat_context1' => '/test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_context1.php',
+            'behat_context2' => "C:\\test\\moodle\\blocks\\comments\\tests\\behat\\behat_context2.php",
+            'behat_context3' => "C:\\test\\moodle/lib/editor/atto/tests/behat/behat_context3.php",
+        );
+
+    /**
+     * @var array Theme contexts for test.
+     */
+    private $themecontexts = array(
+            'behat_theme_testtheme_behat_context1' =>
+                '/test/moodle/theme/testtheme/tests/behat/mod_assign/behat_theme_testtheme_behat_context1.php',
+            'behat_theme_testtheme_behat_context2' =>
+                "C:\\test\\moodle\\theme\\testtheme\\tests\\behat\\block_comments\\behat_theme_testtheme_behat_context2.php",
+            'behat_theme_testtheme_behat_context3' =>
+                "C:\\test\\moodle/theme/testtheme/tests/behat/editor_atto/behat_theme_testtheme_behat_context3.php"
+        );
+
+    /**
+     * Keep instance of behat_config_util mock object.
+     *
+     * @var null
+     */
+    private $behatconfigutil = null;
+
+    /**
+     * Test setup.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+        $mockbuilder = $this->getMockBuilder('behat_config_util');
+        $mockbuilder->setMethods(array('get_behat_features_for_theme', 'get_behat_contexts_for_theme',
+            'get_list_of_themes', 'get_overridden_theme_contexts'));
+
+        $this->behatconfigutil = $mockbuilder->getMock();
+
+        // List of themes is const for test.
+        $this->behatconfigutil->expects($this->any())
+            ->method('get_list_of_themes')
+            ->will($this->returnValue(array('testtheme')));
+
+        $this->behatconfigutil->expects($this->any())
+            ->method('get_behat_contexts_for_theme')
+            ->with($this->equalTo('testtheme'))
+            ->will($this->returnValue(array(array_keys($this->themecontexts), $this->themecontexts)));
+
+    }
+
+    /**
+     * Behat config for single run.
+     *
+     */
+    public function test_get_config_file_contents_with_single_run() {
+        global $CFG;
+
+        $CFG->behat_wwwroot = 'http://example.com/behat';
+
+        $behatconfigutil = $this->behatconfigutil;
+
+        // No theme feature exists.
+        $behatconfigutil->expects($this->once())
+            ->method('get_behat_features_for_theme')
+            ->with($this->anything())
+            ->will($this->returnValue(array(array(), array())));
+
+        $config = $behatconfigutil->get_config_file_contents($this->corefeatures, $this->corecontexts);
+
+        $expectedconfigwithfeatures = "default:
+  formatters:
+    moodle_progress:
+      output_styles:
+        comment:
+          - magenta
+  suites:
+    default:
+      paths:
+        - /test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature
+        - 'C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature'
+        - 'C:\\test\\moodle/login/tests/behat/behat_test3.feature'
+      contexts:
+        - behat_context1
+        - behat_context2
+        - behat_context3
+    testtheme:
+      paths: {  }
+      contexts:
+        - behat_theme_testtheme_behat_context1
+        - behat_theme_testtheme_behat_context2
+        - behat_theme_testtheme_behat_context3
+  extensions:
+    Behat\\MinkExtension:
+      base_url: 'http://example.com/behat'
+      goutte: null
+      selenium2:
+        wd_host: 'http://localhost:4444/wd/hub'
+";
+
+        $this->assertContains($expectedconfigwithfeatures, $config);
+
+        $expectedstepdefinitions = "steps_definitions:
+        behat_context1: /test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_context1.php
+        behat_context2: 'C:\\test\\moodle\\blocks\\comments\\tests\\behat\\behat_context2.php'
+        behat_context3: 'C:\\test\\moodle/lib/editor/atto/tests/behat/behat_context3.php'
+";
+        $this->assertContains($expectedstepdefinitions, $config);
+    }
+
+    /**
+     * Behat config for parallel run.
+     */
+    public function test_get_config_file_contents_with_parallel_run() {
+        global $CFG;
+
+        $CFG->behat_wwwroot = 'http://example.com/behat';
+        $behatconfigutil = $this->behatconfigutil;
+
+        // No theme feature exists.
+        $behatconfigutil->expects($this->any())
+            ->method('get_behat_features_for_theme')
+            ->with($this->anything())
+            ->will($this->returnValue(array(array(), array())));
+
+        $config = $behatconfigutil->get_config_file_contents($this->corefeatures, $this->corecontexts, '', 3, 1);
+
+        // First run.
+        $this->assertContains('/test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle/login/tests/behat/behat_test3.feature',
+            $config);
+
+        // Second run.
+        $config = $behatconfigutil->get_config_file_contents($this->corefeatures, $this->corecontexts, '', 3, 2);
+
+        $this->assertNotContains('/test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature',
+            $config);
+        $this->assertContains('C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle/login/tests/behat/behat_test3.feature',
+            $config);
+
+        $config = $behatconfigutil->get_config_file_contents($this->corefeatures, $this->corecontexts, '', 3, 3);
+
+        $this->assertNotContains('/test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature',
+            $config);
+        $this->assertContains('C:\\test\\moodle/login/tests/behat/behat_test3.feature',
+            $config);
+    }
+
+    /**
+     * Behat config with theme features.
+     */
+    public function test_get_config_file_contents_with_theme_features() {
+        global $CFG;
+
+        $behatconfigutil = $this->behatconfigutil;
+
+        $suitefeatures = array_merge($this->corefeatures, $this->themefeatures);
+        $themefeatures = $this->themefeatures;
+        $behatconfigutil->expects($this->once())
+            ->method('get_behat_features_for_theme')
+            ->with($this->equalTo('testtheme'))
+            ->will($this->returnValue(array(array(), $themefeatures)));
+
+        $behatconfigutil->expects($this->once())
+            ->method('get_behat_contexts_for_theme')
+            ->with($this->equalTo('testtheme'))
+            ->will($this->returnValue(array(array_keys($this->themecontexts), $this->themecontexts)));
+
+        $behatconfigutil->expects($this->once())
+            ->method('get_overridden_theme_contexts')
+            ->will($this->returnValue($this->themecontexts));
+        $behatconfigutil->set_theme_suite_to_include_core_features(true);
+
+        $CFG->behat_wwwroot = 'http://example.com/behat';
+        $config = $behatconfigutil->get_config_file_contents($suitefeatures, $this->corecontexts);
+
+        $expectedconfigwithfeatures = "default:
+  formatters:
+    moodle_progress:
+      output_styles:
+        comment:
+          - magenta
+  suites:
+    default:
+      paths:
+        - /test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature
+        - 'C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature'
+        - 'C:\\test\\moodle/login/tests/behat/behat_test3.feature'
+      contexts:
+        - behat_context1
+        - behat_context2
+        - behat_context3
+    testtheme:
+      paths:
+        - /test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature
+        - 'C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature'
+        - 'C:\\test\\moodle/login/tests/behat/behat_test3.feature'
+        - /test/moodle/theme/testtheme/tests/behat/core/behat_themetest1.feature
+        - 'C:\\test\\moodle\\theme\\testtheme\\tests\\behat\\mod_assign\\behat_themetest2.feature'
+        - 'C:\\test\\moodle/theme/testtheme/tests/behat/behat_themetest3.feature'
+      contexts:
+        - behat_theme_testtheme_behat_context1
+        - behat_theme_testtheme_behat_context2
+        - behat_theme_testtheme_behat_context3
+  extensions:
+    Behat\\MinkExtension:
+      base_url: 'http://example.com/behat'
+      goutte: null
+      selenium2:
+        wd_host: 'http://localhost:4444/wd/hub'
+";
+        $this->assertContains($expectedconfigwithfeatures, $config);
+
+        $expectedstepdefinitions = "steps_definitions:
+        behat_context1: /test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_context1.php
+        behat_context2: 'C:\\test\\moodle\\blocks\\comments\\tests\\behat\\behat_context2.php'
+        behat_context3: 'C:\\test\\moodle/lib/editor/atto/tests/behat/behat_context3.php'
+        behat_theme_testtheme_behat_context1: /test/moodle/theme/testtheme/tests/behat/mod_assign/behat_theme_testtheme_behat_context1.php
+        behat_theme_testtheme_behat_context2: 'C:\\test\\moodle\\theme\\testtheme\\tests\\behat\\block_comments\\behat_theme_testtheme_behat_context2.php'
+        behat_theme_testtheme_behat_context3: 'C:\\test\\moodle/theme/testtheme/tests/behat/editor_atto/behat_theme_testtheme_behat_context3.php'";
+
+        $this->assertContains($expectedstepdefinitions, $config);
+    }
+
+    /**
+     * Behat config for parallel run.
+     */
+    public function test_get_config_file_contents_with_theme_and_parallel_run() {
+        global $CFG;
+
+        $CFG->behat_wwwroot = 'http://example.com/behat';
+
+        $behatconfigutil = $this->behatconfigutil;
+
+        $features = array_merge($this->corefeatures, $this->themefeatures);
+        $themefeatures = $this->themefeatures;
+        $behatconfigutil->expects($this->atLeastOnce())
+            ->method('get_behat_features_for_theme')
+            ->with($this->equalTo('testtheme'))
+            ->will($this->returnValue(array(array(), $themefeatures)));
+
+        $behatconfigutil->expects($this->atLeastOnce())
+            ->method('get_behat_contexts_for_theme')
+            ->with($this->equalTo('testtheme'))
+            ->will($this->returnValue(array(array_keys($this->themecontexts), $this->themecontexts)));
+
+        $CFG->behat_wwwroot = 'http://example.com/behat';
+
+        $behatconfigutil->set_theme_suite_to_include_core_features(false);
+
+        $config = $behatconfigutil->get_config_file_contents($features, $this->themecontexts, '', 3, 1);
+
+        // First run.
+        $this->assertContains('/test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle/login/tests/behat/behat_test3.feature',
+            $config);
+        // Theme suite features.
+        $this->assertContains('/test/moodle/theme/testtheme/tests/behat/core/behat_themetest1.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle\\theme\\testtheme\\tests\\behat\\mod_assign\\behat_themetest2.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle/theme/testtheme/tests/behat/behat_themetest3.feature',
+            $config);
+
+        // Second run.
+        $config = $behatconfigutil->get_config_file_contents($features, $this->themecontexts, '', 3, 2);
+        $this->assertNotContains('/test/moodle/mod/assign/feedback/editpdf/tests/behat/behat_test1.feature',
+            $config);
+        $this->assertContains('C:\\test\\moodle\\mod\\assign\\feedback\\file\\tests\\behat\\behat_test2.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle/login/tests/behat/behat_test3.feature',
+            $config);
+        // Theme suite features.
+        $this->assertNotContains('/test/moodle/theme/testtheme/tests/behat/core/behat_themetest1.feature',
+            $config);
+        $this->assertContains('C:\\test\\moodle\\theme\\testtheme\\tests\\behat\\mod_assign\\behat_themetest2.feature',
+            $config);
+        $this->assertNotContains('C:\\test\\moodle/theme/testtheme/tests/behat/behat_themetest3.feature',
+            $config);
+    }
+
+    /**
+     * Test if clean features key and path is returned.
+     * @dataProvider clean_features_path_list
+     */
+    public function test_get_clean_feature_key_and_path($featurepath, $key, $cleanfeaturepath) {
+        global $CFG;
+
+        // This is a hack so directory name is correctly detected in tests.
+        //FIXME: MDL-55722 work out why this is necessary..
+        $oldroot = $CFG->dirroot;
+        $CFG->dirroot = 'C:';
+
+        $behatconfigutil = $this->behatconfigutil;
+        // Fix expected directory path for OS.
+        $cleanfeaturepath = str_replace('\\', DIRECTORY_SEPARATOR, $cleanfeaturepath);
+        $cleanfeaturepath = str_replace('/', DIRECTORY_SEPARATOR, $cleanfeaturepath);
+
+        if (testing_is_cygwin()) {
+            $featurepath = str_replace('\\', '/', $cleanfeaturepath);
+        }
+
+        list($retkey, $retcleanfeaturepath) = $behatconfigutil->get_clean_feature_key_and_path($featurepath);
+
+        $this->assertEquals($key, $retkey);
+        $this->assertEquals($cleanfeaturepath, $retcleanfeaturepath);
+        //FIXME: MDL-55722 work out why this is necessary..
+        $CFG->dirroot = $oldroot;
+    }
+
+    public function clean_features_path_list() {
+        return array(
+            ['/home/test/this/that/test/behat/mod_assign.feature', 'mod_assign_behat_test_that_this_test', '/home/test/this/that/test/behat/mod_assign.feature'],
+            ['/home/this/that/test/behat/mod_assign.feature', 'mod_assign_behat_test_that_this_home', '/home/this/that/test/behat/mod_assign.feature'],
+            ['/home/that/test/behat/mod_assign.feature', 'mod_assign_behat_test_that_home', '/home/that/test/behat/mod_assign.feature'],
+            ['/home/test/behat/mod_assign.feature', 'mod_assign_behat_test_home', '/home/test/behat/mod_assign.feature'],
+            ['mod_assign.feature', 'mod_assign', 'mod_assign.feature'],
+            ['C:\test\this\that\test\behat\mod_assign.feature', 'mod_assign_behat_test_that_this_test', 'C:\test\this\that\test\behat\mod_assign.feature'],
+            ['C:\this\that\test\behat\mod_assign.feature', 'mod_assign_behat_test_that_this', 'C:\this\that\test\behat\mod_assign.feature'],
+            ['C:\that\test\behat\mod_assign.feature', 'mod_assign_behat_test_that', 'C:\that\test\behat\mod_assign.feature'],
+            ['C:\test\behat\mod_assign.feature', 'mod_assign_behat_test', 'C:\test\behat\mod_assign.feature'],
+            ['C:\mod_assign.feature', 'mod_assign', 'C:\mod_assign.feature'],
+        );
+    }
+}
+// @codeCoverageIgnoreEnd
index 0219ebe..e5e7ec4 100644 (file)
@@ -216,19 +216,19 @@ class tool_generator_course_backend extends tool_generator_backend {
 
         $entirestart = microtime(true);
 
-        // Start transaction.
-        $transaction = $DB->start_delegated_transaction();
-
         // Get generator.
         $this->generator = phpunit_util::get_data_generator();
 
         // Make course.
         $this->course = $this->create_course();
-        $this->create_users();
+
         $this->create_assignments();
         $this->create_pages();
         $this->create_small_files();
         $this->create_big_files();
+
+        // Create users as late as possible to reduce regarding in the gradebook.
+        $this->create_users();
         $this->create_forum();
 
         // Log total time.
@@ -238,8 +238,6 @@ class tool_generator_course_backend extends tool_generator_backend {
             echo html_writer::end_tag('ul');
         }
 
-        // Commit transaction and finish.
-        $transaction->allow_commit();
         return $this->course->id;
     }
 
@@ -435,7 +433,7 @@ class tool_generator_course_backend extends tool_generator_backend {
 
             // Generate random binary data (different for each file so it
             // doesn't compress unrealistically).
-            $data = self::get_random_binary($this->limit_filesize(self::$paramsmallfilesize[$this->size]));
+            $data = random_bytes_emulate($this->limit_filesize(self::$paramsmallfilesize[$this->size]));
 
             $fs->create_file_from_string($filerecord, $data);
             $this->dot($i, $count);
@@ -444,33 +442,10 @@ class tool_generator_course_backend extends tool_generator_backend {
         $this->end_log();
     }
 
-    /**
-     * Creates a string of random binary data. The start of the string includes
-     * the current time, in an attempt to avoid large-scale repetition.
-     *
-     * @param int $length Number of bytes
-     * @return Random data
-     */
-    private static function get_random_binary($length) {
-
-        $data = microtime(true);
-        if (strlen($data) > $length) {
-            // Use last digits of data.
-            return substr($data, -$length);
-        }
-        $length -= strlen($data);
-        for ($j = 0; $j < $length; $j++) {
-            $data .= chr(rand(1, 255));
-        }
-        return $data;
-    }
-
     /**
      * Creates a number of resource activities with one big file each.
      */
     private function create_big_files() {
-        global $CFG;
-
         // Work out how many files and how many blocks to use (up to 64KB).
         $count = self::$parambigfilecount[$this->size];
         $filesize = $this->limit_filesize(self::$parambigfilesize[$this->size]);
@@ -499,7 +474,7 @@ class tool_generator_course_backend extends tool_generator_backend {
                 throw new coding_exception('Failed to open temporary file');
             }
             for ($j = 0; $j < $blocks; $j++) {
-                $data = self::get_random_binary($blocksize);
+                $data = random_bytes_emulate($blocksize);
                 fwrite($handle, $data);
                 $this->dot($i * $blocks + $j, $count * $blocks);
             }
index 86247ec..8711269 100644 (file)
@@ -43,10 +43,10 @@ DEVELOPER debugging level.)';
 
 $string['coursesize_0'] = 'XS (~10KB; create in ~1 second)';
 $string['coursesize_1'] = 'S (~10MB; create in ~30 seconds)';
-$string['coursesize_2'] = 'M (~100MB; create in ~5 minutes)';
-$string['coursesize_3'] = 'L (~1GB; create in ~1 hour)';
-$string['coursesize_4'] = 'XL (~10GB; create in ~4 hours)';
-$string['coursesize_5'] = 'XXL (~20GB; create in ~8 hours)';
+$string['coursesize_2'] = 'M (~100MB; create in ~2 minutes)';
+$string['coursesize_3'] = 'L (~1GB; create in ~30 minutes)';
+$string['coursesize_4'] = 'XL (~10GB; create in ~2 hours)';
+$string['coursesize_5'] = 'XXL (~20GB; create in ~4 hours)';
 $string['coursewithoutusers'] = 'The selected course has no users';
 $string['createcourse'] = 'Create course';
 $string['createtestplan'] = 'Create test plan';
index 989bc3c..ccce626 100644 (file)
@@ -67,7 +67,7 @@ class eventlist {
                 $ref = new \ReflectionClass($classname);
                 // Ignore abstracts.
                 if (!$ref->isAbstract() && $file != 'manager') {
-                    $eventinformation[$classname] = $classname::get_name();
+                    $eventinformation[$classname] = $classname::get_name_with_info();
                 }
             }
         }
@@ -103,15 +103,16 @@ class eventlist {
             foreach ($pluginlist as $plugin => $directory) {
                 $plugindirectory = $directory . '/classes/event';
                 foreach (self::get_file_list($plugindirectory) as $eventname => $notused) {
-                    $plugineventname = '\\' . $plugintype . '_' . $plugin . '\\event\\' . $eventname;
+                    $fullpluginname = $plugintype . '_' . $plugin;
+                    $plugineventname = '\\' . $fullpluginname . '\\event\\' . $eventname;
                     // Check that this is actually an event.
-                    if (method_exists($plugineventname, 'get_static_info')  && $plugin != 'monitor') { // No selfie here.
+                    if (method_exists($plugineventname, 'get_static_info')  && $fullpluginname !== 'tool_monitor') { // No selfie here.
                         $ref = new \ReflectionClass($plugineventname);
-                        if (!$ref->isAbstract() && $plugin != 'legacy') {
+                        if (!$ref->isAbstract() && $fullpluginname !== 'logstore_legacy') {
                             if ($withoutcomponent) {
-                                $noncorepluginlist[$plugineventname] = $plugineventname::get_name();
+                                $noncorepluginlist[$plugineventname] = $plugineventname::get_name_with_info();
                             } else {
-                                $noncorepluginlist[$plugintype . '_' . $plugin][$plugineventname] = $plugineventname::get_name();
+                                $noncorepluginlist[$fullpluginname][$plugineventname] = $plugineventname::get_name_with_info();
                             }
                         }
                     }
index 2a23eb1..3c5e6d2 100644 (file)
@@ -202,7 +202,7 @@ class rule {
     public function get_event_name() {
         $eventclass = $this->eventname;
         if (class_exists($eventclass)) {
-            return $eventclass::get_name();
+            return $eventclass::get_name_with_info();
         }
         return get_string('eventnotfound', 'tool_monitor');
     }
index 98518e9..5beddbb 100644 (file)
@@ -111,7 +111,7 @@ class subscription {
     public function get_event_name() {
         $eventclass = $this->eventname;
         if (class_exists($eventclass)) {
-            return $eventclass::get_name();
+            return $eventclass::get_name_with_info();
         }
         return get_string('eventnotfound', 'tool_monitor');
     }
index 20bd3c4..1e01a32 100644 (file)
@@ -54,12 +54,6 @@ $PAGE->set_heading($coursename);
 // Get data ready for mform.
 $eventlist = tool_monitor\eventlist::get_all_eventlist(true);
 $pluginlist = tool_monitor\eventlist::get_plugin_list();
-$eventlist = array_merge(array('' => get_string('choosedots')), $eventlist);
-$pluginlist = array_merge(array('' => get_string('choosedots')), $pluginlist);
-
-// Set up the yui module.
-$PAGE->requires->yui_module('moodle-tool_monitor-dropdown', 'Y.M.tool_monitor.DropDown.init',
-        array(array('eventlist' => $eventlist)));
 
 // Site level report.
 if (empty($courseid)) {
@@ -74,11 +68,25 @@ if (!empty($ruleid)) {
     $rule = \tool_monitor\rule_manager::get_rule($ruleid)->get_mform_set_data();
     $rule->minutes = $rule->timewindow / MINSECS;
     $subscriptioncount = \tool_monitor\subscription_manager::count_rule_subscriptions($ruleid);
+
+    // Filter out events which cannot be triggered for some reason.
+    $eventlist = array_filter($eventlist, function($classname) use ($rule) {
+        // Filter out all deprecated events, except for the current one.
+        return $classname === $rule->eventname || !$classname::is_deprecated();
+    }, ARRAY_FILTER_USE_KEY);
 } else {
     $rule = new stdClass();
     $subscriptioncount = 0;
+
+    // Filter out events which cannot be triggered for some reason.
+    $eventlist = array_filter($eventlist, function($classname) {
+        return !$classname::is_deprecated();
+    }, ARRAY_FILTER_USE_KEY);
 }
 
+// Modify the lists to add the choosers.
+$eventlist = array_merge(array('' => get_string('choosedots')), $eventlist);
+$pluginlist = array_merge(array('' => get_string('choosedots')), $pluginlist);
 $mform = new tool_monitor\rule_form(null, array('eventlist' => $eventlist, 'pluginlist' => $pluginlist, 'rule' => $rule,
         'courseid' => $courseid, 'subscriptioncount' => $subscriptioncount));
 
@@ -98,6 +106,10 @@ if ($mformdata = $mform->get_data()) {
 
     redirect($manageurl);
 } else {
+    // Set up the yui module.
+    $PAGE->requires->yui_module('moodle-tool_monitor-dropdown', 'Y.M.tool_monitor.DropDown.init',
+            array(array('eventlist' => $eventlist)));
+
     echo $OUTPUT->header();
     $mform->set_data($rule);
     // If there's any subscription for this rule, display an information message.
index fb47717..ed41095 100644 (file)
@@ -98,6 +98,6 @@ $string['subdeletesuccess'] = 'Subscription successfully removed';
 $string['subhelp'] = 'Subscription details';
 $string['subhelp_help'] = 'This subscription listens for when the event \'{$a->eventname}\' has been triggered in \'{$a->moduleinstance}\' {$a->frequency} time(s) in {$a->minutes} minute(s).';
 $string['subscribeto'] = 'Subscribe to rule "{$a}"';
-$string['taskcleanevents'] = 'Removes any unnecessary event monitor events';
+$string['taskcleanevents'] = 'Cleanup event monitor events';
 $string['taskchecksubscriptions'] = 'Activate/deactivate invalid rule subscriptions';
 $string['unsubscribe'] = 'Unsubscribe';
index 6d48d36..a4c7075 100644 (file)
@@ -795,7 +795,7 @@ abstract class restore_dbops {
                      // Prepare the query
                      list($stamp_sql, $stamp_params) = $DB->get_in_or_equal($stamps);
                      list($context_sql, $context_params) = $DB->get_in_or_equal($contexts);
-                     $sql = "SELECT contextid
+                     $sql = "SELECT DISTINCT contextid
                                FROM {question_categories}
                               WHERE stamp $stamp_sql
                                 AND contextid $context_sql";
diff --git a/blocks/navigation/tests/behat/participants_link.feature b/blocks/navigation/tests/behat/participants_link.feature
new file mode 100644 (file)
index 0000000..5b2f4d6
--- /dev/null
@@ -0,0 +1,45 @@
+@block @block_navigation
+Feature: Displaying the link to the Participants page
+  In order to see the course / site participants
+  As a student / admin respectively
+  I need a link to the Participants page be displayed (but only if I can access that page)
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | One      | student1@example.com |
+      | student2 | Student   | Two      | student2@example.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course1  | C1        |
+    And the following "course enrolments" exist:
+      | user     | course | role    |
+      | student1 | C1     | student |
+
+  @javascript
+  Scenario: Course participants link is displayed to enrolled students after expanding the course node
+    When I log in as "student1"
+    And I expand "C1" node
+    Then "Participants" "link" should exist in the "Navigation" "block"
+    And I navigate to "Participants" node in "My courses > C1"
+    And I should see "Participants"
+    And "Student One" "link" should exist
+    And "Student Two" "link" should not exist
+
+  Scenario: Site participants link is displayed to admins
+    When I log in as "admin"
+    Then "Participants" "link" should exist in the "Navigation" "block"
+    And I navigate to "Participants" node in "Site pages"
+    And I should see "Participants"
+    And "Student One" "link" should exist
+    And "Student Two" "link" should exist
+
+  @javascript
+  Scenario: Site participants link is not displayed to students (MDL-55667)
+    Given I log in as "admin"
+    And I set the following administration settings values:
+      | defaultfrontpageroleid | Student (student) |
+    And I log out
+    When I log in as "student2"
+    And I expand "Site pages" node
+    Then "Participants" "link" should not exist in the "Navigation" "block"
index ad9ae3e..a69f3e8 100644 (file)
@@ -66,7 +66,7 @@
 
 {{$title}}
     {{#title}}
-        <div class="title">{{feedtitle}}</div>
+        <div class="title">{{title}}</div>
     {{/title}}
 {{/title}}
 
index 6d43c31..9174440 100644 (file)
@@ -4,7 +4,13 @@
     width: 100%;
     display: table;
 }
-.block_site_main_menu li .buttons { float: right; margin: 0; }
+.block_site_main_menu li .buttons {
+    float: right;
+    margin: 0;
+    padding: 0;
+    border: 0;
+    background-color: inherit;
+}
 .dir-rtl .block_site_main_menu li .buttons { float: left; }
 .block_site_main_menu li .buttons a img{ vertical-align: text-bottom;}
 .block_site_main_menu .footer { margin-top: 1em; }
index 36a544c..bf9a166 100644 (file)
@@ -79,7 +79,7 @@ if (empty($CFG->enableblogs)) {
 }
 
 if (isguestuser()) {
-    print_error('noguestentry', 'blog');
+    print_error('noguest');
 }
 
 $returnurl = new moodle_url('/blog/index.php');
index fb1ef89..e639df3 100644 (file)
@@ -92,7 +92,7 @@ if ($CFG->bloglevel == BLOG_GLOBAL_LEVEL) {
     require_login();
     if (isguestuser()) {
         // They must have entered the url manually.
-        print_error('blogdisable', 'blog');
+        print_error('noguest');
     }
 
 } else if ($CFG->bloglevel == BLOG_USER_LEVEL) {
index 9c42bec..1871161 100644 (file)
@@ -63,6 +63,10 @@ if (empty($CFG->enableblogs)) {
     print_error('blogdisable', 'blog');
 }
 
+if (isguestuser()) {
+    print_error('noguest');
+}
+
 // The preference is site wide not blog specific. Hence user should have permissions in site level.
 require_capability('moodle/blog:view', $sitecontext);
 
index 366c5cd..760a0ea 100644 (file)
@@ -22,6 +22,8 @@
  * @copyright  2010 Andrew Davis
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+defined('MOODLE_INTERNAL') || die();
+
 require_once($CFG->dirroot.'/lib/rsslib.php');
 require_once($CFG->dirroot .'/blog/lib.php');
 
index 9b29e91..0e5c62e 100644 (file)
@@ -808,10 +808,8 @@ class cache_definition {
             }
         }
 
-        if ($this->identifiers === null) {
-            // Initialize identifiers if they have not been.
-            $this->identifiers = array();
-        }
+        $this->identifiers = array();
+
         foreach ($identifiers as $name => $value) {
             $this->identifiers[$name] = (string)$value;
         }
index 752c1c2..2612fea 100644 (file)
@@ -310,12 +310,12 @@ class cache implements cache_loader {
                     $result = $result->data;
                 }
             }
-            if ($result instanceof cache_cached_object) {
-                $result = $result->restore_object();
-            }
             if ($usesstaticacceleration) {
                 $this->static_acceleration_set($key, $result);
             }
+            if ($result instanceof cache_cached_object) {
+                $result = $result->restore_object();
+            }
         }
 
         // 4. Load if from the loader/datasource if we don't already have it.
@@ -410,12 +410,12 @@ class cache implements cache_loader {
                         $value = $value->data;
                     }
                 }
-                if ($value instanceof cache_cached_object) {
-                    $value = $value->restore_object();
-                }
                 if ($value !== false && $this->use_static_acceleration()) {
                     $this->static_acceleration_set($keystofind[$key], $value);
                 }
+                if ($value instanceof cache_cached_object) {
+                    $value = $value->restore_object();
+                }
                 $resultstore[$key] = $value;
             }
         }
@@ -836,11 +836,7 @@ class cache implements cache_loader {
      */
     public function purge() {
         // 1. Purge the static acceleration array.
-        $this->staticaccelerationarray = array();
-        if ($this->staticaccelerationsize !== false) {
-            $this->staticaccelerationkeys = array();
-            $this->staticaccelerationcount = 0;
-        }
+        $this->static_acceleration_purge();
         // 2. Purge the store.
         $this->store->purge();
         // 3. Optionally pruge any stacked loaders.
@@ -1111,6 +1107,17 @@ class cache implements cache_loader {
         return true;
     }
 
+    /**
+     * Purge the static acceleration cache.
+     */
+    protected function static_acceleration_purge() {
+        $this->staticaccelerationarray = array();
+        if ($this->staticaccelerationsize !== false) {
+            $this->staticaccelerationkeys = array();
+            $this->staticaccelerationcount = 0;
+        }
+    }
+
     /**
      * Returns the timestamp from the first request for the time from the cache API.
      *
index 26ab145..255cbf3 100644 (file)
@@ -187,6 +187,35 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertEquals('test data 1', $cache->get('contest'));
     }
 
+    /**
+     * Tests set_identifiers resets identifiers and static cache
+     */
+    public function test_set_identifiers() {
+        $instance = cache_config_testing::instance();
+        $instance->phpunit_add_definition('phpunit/identifier', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'identifier',
+            'simplekeys' => true,
+            'simpledata' => true,
+            'staticacceleration' => true
+        ));
+        $cache = cache::make('phpunit', 'identifier', array('area'));
+        $this->assertTrue($cache->set('contest', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('contest'));
+
+        $cache->set_identifiers(array());
+        $this->assertFalse($cache->get('contest'));
+        $this->assertTrue($cache->set('contest', 'empty ident'));
+        $this->assertEquals('empty ident', $cache->get('contest'));
+
+        $cache->set_identifiers(array('area'));
+        $this->assertEquals('test data 1', $cache->get('contest'));
+
+        $cache->set_identifiers(array());
+        $this->assertEquals('empty ident', $cache->get('contest'));
+    }
+
     /**
      * Tests the default application cache
      */
@@ -306,15 +335,17 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertTrue($cache->set($key, $dataobject));
         $this->assertEquals($dataobject, $cache->get($key));
 
-        $specobject = new cache_phpunit_dummy_object('red', 'blue');
+        $starttime = microtime(true);
+        $specobject = new cache_phpunit_dummy_object('red', 'blue', $starttime);
         $this->assertTrue($cache->set($key, $specobject));
         $result = $cache->get($key);
         $this->assertInstanceOf('cache_phpunit_dummy_object', $result);
         $this->assertEquals('red_ptc_wfc', $result->property1);
         $this->assertEquals('blue_ptc_wfc', $result->property2);
+        $this->assertGreaterThan($starttime, $result->propertytime);
 
         // Test array of objects.
-        $specobject = new cache_phpunit_dummy_object('red', 'blue');
+        $specobject = new cache_phpunit_dummy_object('red', 'blue', $starttime);
         $data = new cacheable_object_array(array(
             clone($specobject),
             clone($specobject),
@@ -328,6 +359,8 @@ class core_cache_testcase extends advanced_testcase {
             $this->assertInstanceOf('cache_phpunit_dummy_object', $item);
             $this->assertEquals('red_ptc_wfc', $item->property1);
             $this->assertEquals('blue_ptc_wfc', $item->property2);
+            // Ensure that wake from cache is called in all cases.
+            $this->assertGreaterThan($starttime, $item->propertytime);
         }
 
         // Test set many.
@@ -1847,6 +1880,43 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertEquals('D', $cache->phpunit_static_acceleration_get('d'));
         $this->assertEquals('E', $cache->phpunit_static_acceleration_get('e'));
 
+        // Store a cacheable_object, get many times and ensure each time wake_for_cache is used.
+        // Both get and get_many are tested.  Two cache entries are used to ensure the times aren't
+        // confused with multiple calls to get()/get_many().
+        $startmicrotime = microtime(true);
+        $cacheableobject = new cache_phpunit_dummy_object(1, 1, $startmicrotime);
+        $cacheableobject2 = new cache_phpunit_dummy_object(2, 2, $startmicrotime);
+        $this->assertTrue($cache->set('a', $cacheableobject));
+        $this->assertTrue($cache->set('b', $cacheableobject2));
+        $staticaccelerationreturntime = $cache->phpunit_static_acceleration_get('a')->propertytime;
+        $staticaccelerationreturntimeb = $cache->phpunit_static_acceleration_get('b')->propertytime;
+        $this->assertGreaterThan($startmicrotime, $staticaccelerationreturntime, 'Restore time of static must be newer.');
+
+        // Reset the static cache without resetting backing store.
+        $cache->phpunit_static_acceleration_purge();
+
+        // Get the value from the backend store, populating the static cache.
+        $cachevalue = $cache->get('a');
+        $this->assertInstanceOf('cache_phpunit_dummy_object', $cachevalue);
+        $this->assertGreaterThan($staticaccelerationreturntime, $cachevalue->propertytime);
+        $backingstorereturntime = $cachevalue->propertytime;
+
+        $results = $cache->get_many(array('b'));
+        $this->assertInstanceOf('cache_phpunit_dummy_object', $results['b']);
+        $this->assertGreaterThan($staticaccelerationreturntimeb, $results['b']->propertytime);
+        $backingstorereturntimeb = $results['b']->propertytime;
+
+        // Obtain the value again and confirm that static cache is using wake_from_cache.
+        // Upon failure, the times are not adjusted as wake_from_cache is skipped as the
+        // value is stored serialized in the static acceleration cache.
+        $cachevalue = $cache->phpunit_static_acceleration_get('a');
+        $this->assertInstanceOf('cache_phpunit_dummy_object', $cachevalue);
+        $this->assertGreaterThan($backingstorereturntime, $cachevalue->propertytime);
+
+        $results = $cache->get_many(array('b'));
+        $this->assertInstanceOf('cache_phpunit_dummy_object', $results['b']);
+        $this->assertGreaterThan($backingstorereturntimeb, $results['b']->propertytime);
+
         /** @var cache_phpunit_application $cache */
         $cache = cache::make('phpunit', 'accelerated2');
         $this->assertInstanceOf('cache_phpunit_application', $cache);
index 53255a5..7b05b2e 100644 (file)
@@ -309,6 +309,9 @@ class cache_config_phpunittest extends cache_config_testing {
 /**
  * Dummy object for testing cacheable object interface and interaction
  *
+ * Wake from cache needs specific testing at times to ensure that during multiple
+ * cache get() requests it's possible to verify that it's getting woken each time.
+ *
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -323,21 +326,27 @@ class cache_phpunit_dummy_object extends stdClass implements cacheable_object {
      * @var string
      */
     public $property2;
+    /**
+     * Test property time for verifying wake is run at each get() call.
+     * @var float
+     */
+    public $propertytime;
     /**
      * Constructor
      * @param string $property1
      * @param string $property2
      */
-    public function __construct($property1, $property2) {
+    public function __construct($property1, $property2, $propertytime = null) {
         $this->property1 = $property1;
         $this->property2 = $property2;
+        $this->propertytime = $propertytime === null ? microtime(true) : $propertytime;
     }
     /**
      * Prepares this object for caching
      * @return array
      */
     public function prepare_to_cache() {
-        return array($this->property1.'_ptc', $this->property2.'_ptc');
+        return array($this->property1.'_ptc', $this->property2.'_ptc', $this->propertytime);
     }
     /**
      * Returns this object from the cache
@@ -345,7 +354,15 @@ class cache_phpunit_dummy_object extends stdClass implements cacheable_object {
      * @return cache_phpunit_dummy_object
      */
     public static function wake_from_cache($data) {
-        return new cache_phpunit_dummy_object(array_shift($data).'_wfc', array_shift($data).'_wfc');
+        $time = null;
+        if (!is_null($data[2])) {
+            // Windows 32bit microtime() resolution is 15ms, we ensure the time has moved forward.
+            do {
+                $time = microtime(true);
+            } while ($time == $data[2]);
+
+        }
+        return new cache_phpunit_dummy_object(array_shift($data).'_wfc', array_shift($data).'_wfc', $time);
     }
 }
 
@@ -426,6 +443,17 @@ class cache_phpunit_application extends cache_application {
     public function phpunit_static_acceleration_get($key) {
         return $this->static_acceleration_get($key);
     }
+
+    /**
+     * Purges only the static acceleration while leaving the rest of the store in tack.
+     *
+     * Used for behaving like you have loaded 2 pages, and reset static while the backing store
+     * still contains all the same data.
+     *
+     */
+    public function phpunit_static_acceleration_purge() {
+        $this->static_acceleration_purge();
+    }
 }
 
 /**
index ce55027..713e233 100644 (file)
@@ -7,6 +7,6 @@
     "require-dev": {
         "phpunit/phpunit": "5.4.*",
         "phpunit/dbUnit": "1.4.*",
-        "moodlehq/behat-extension": "3.32.1"
+        "moodlehq/behat-extension": "3.32.2"
     }
 }
index 6fe8310..5810eda 100644 (file)
@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "7f0c2a252624902575a3ed1bdc237644",
-    "content-hash": "bcc157487e77ec6ad4163f1661abac97",
+    "hash": "902ce5735f2446cf9bcc305c0d8a191b",
+    "content-hash": "b60ecd0f5b6430a10ada7e4d4c38e73b",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.2.0",
+            "version": "6.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "d094e337976dff9d8e2424e8485872194e768662"
+                "reference": "3f808fba627f2c5b69e2501217bf31af349c1427"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d094e337976dff9d8e2424e8485872194e768662",
-                "reference": "d094e337976dff9d8e2424e8485872194e768662",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/3f808fba627f2c5b69e2501217bf31af349c1427",
+                "reference": "3f808fba627f2c5b69e2501217bf31af349c1427",
                 "shasum": ""
             },
             "require": {
-                "guzzlehttp/promises": "~1.0",
-                "guzzlehttp/psr7": "~1.1",
-                "php": ">=5.5.0"
+                "guzzlehttp/promises": "^1.0",
+                "guzzlehttp/psr7": "^1.3.1",
+                "php": ">=5.5"
             },
             "require-dev": {
                 "ext-curl": "*",
-                "phpunit/phpunit": "~4.0",
-                "psr/log": "~1.0"
+                "phpunit/phpunit": "^4.0",
+                "psr/log": "^1.0"
             },
             "type": "library",
             "extra": {
                 "rest",
                 "web service"
             ],
-            "time": "2016-03-21 20:02:09"
+            "time": "2016-07-15 17:22:37"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.32.1",
+            "version": "v3.32.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "f8305058ce9140864c23c9b667e3d7d487fdc006"
+                "reference": "cdf11394f55576f849db750cac9836f2efc17d4c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/f8305058ce9140864c23c9b667e3d7d487fdc006",
-                "reference": "f8305058ce9140864c23c9b667e3d7d487fdc006",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/cdf11394f55576f849db750cac9836f2efc17d4c",
+                "reference": "cdf11394f55576f849db750cac9836f2efc17d4c",
                 "shasum": ""
             },
             "require": {
                 "Behat",
                 "moodle"
             ],
-            "time": "2016-06-20 07:56:08"
+            "time": "2016-08-12 01:58:24"
         },
         {
             "name": "myclabs/deep-copy",
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "4.0.0",
+            "version": "4.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "900370c81280cc0d942ffbc5912d80464eaee7e9"
+                "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/900370c81280cc0d942ffbc5912d80464eaee7e9",
-                "reference": "900370c81280cc0d942ffbc5912d80464eaee7e9",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5f3f7e736d6319d5f1fc402aff8b026da26709a3",
+                "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3",
                 "shasum": ""
             },
             "require": {
                 "phpunit/php-text-template": "~1.2",
                 "phpunit/php-token-stream": "^1.4.2",
                 "sebastian/code-unit-reverse-lookup": "~1.0",
-                "sebastian/environment": "^1.3.2",
+                "sebastian/environment": "^1.3.2 || ^2.0",
                 "sebastian/version": "~1.0|~2.0"
             },
             "require-dev": {
                 "testing",
                 "xunit"
             ],
-            "time": "2016-06-03 05:03:56"
+            "time": "2016-07-26 14:39:29"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "5.4.6",
+            "version": "5.4.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "2f1fc94b77ea6418bd6a06c64a1dac0645fbce59"
+                "reference": "3132365e1430c091f208e120b8845d39c25f20e6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2f1fc94b77ea6418bd6a06c64a1dac0645fbce59",
-                "reference": "2f1fc94b77ea6418bd6a06c64a1dac0645fbce59",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3132365e1430c091f208e120b8845d39c25f20e6",
+                "reference": "3132365e1430c091f208e120b8845d39c25f20e6",
                 "shasum": ""
             },
             "require": {
                 "myclabs/deep-copy": "~1.3",
                 "php": "^5.6 || ^7.0",
                 "phpspec/prophecy": "^1.3.1",
-                "phpunit/php-code-coverage": "^4.0",
+                "phpunit/php-code-coverage": "^4.0.1",
                 "phpunit/php-file-iterator": "~1.4",
                 "phpunit/php-text-template": "~1.2",
                 "phpunit/php-timer": "^1.0.6",
                 "testing",
                 "xunit"
             ],
-            "time": "2016-06-16 06:01:15"
+            "time": "2016-07-26 14:48:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
         },
         {
             "name": "psr/http-message",
-            "version": "1.0",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/http-message.git",
-                "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298"
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298",
-                "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
             "keywords": [
                 "http",
                 "http-message",
                 "request",
                 "response"
             ],
-            "time": "2015-05-04 20:22:00"
+            "time": "2016-08-06 14:39:51"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "dcf41ed026b0499254385b5c88f03247b2ba010b"
+                "reference": "d2a07cc11c5fa94820240b1e67592ffb18e347b9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/dcf41ed026b0499254385b5c88f03247b2ba010b",
-                "reference": "dcf41ed026b0499254385b5c88f03247b2ba010b",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/d2a07cc11c5fa94820240b1e67592ffb18e347b9",
+                "reference": "d2a07cc11c5fa94820240b1e67592ffb18e347b9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:41:56"
+            "time": "2016-07-26 08:04:17"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "0d0ac77c336eb73f35bebdf3e1f3695ac741bbc9"
+                "reference": "817f09b4c37b7688fa4342cb4642d8f2d81c1097"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/0d0ac77c336eb73f35bebdf3e1f3695ac741bbc9",
-                "reference": "0d0ac77c336eb73f35bebdf3e1f3695ac741bbc9",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/817f09b4c37b7688fa4342cb4642d8f2d81c1097",
+                "reference": "817f09b4c37b7688fa4342cb4642d8f2d81c1097",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:41:56"
+            "time": "2016-07-10 08:05:47"
         },
         {
             "name": "symfony/config",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "bcf5aebabc95b56e370e13d78565f74c7d8726dc"
+                "reference": "a7630397b91be09cdd2fe57fd13612e258700598"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/bcf5aebabc95b56e370e13d78565f74c7d8726dc",
-                "reference": "bcf5aebabc95b56e370e13d78565f74c7d8726dc",
+                "url": "https://api.github.com/repos/symfony/config/zipball/a7630397b91be09cdd2fe57fd13612e258700598",
+                "reference": "a7630397b91be09cdd2fe57fd13612e258700598",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:41:56"
+            "time": "2016-07-26 08:04:17"
         },
         {
             "name": "symfony/console",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "747154aa69b0f83cd02fc9aa554836dee417631a"
+                "reference": "f9e638e8149e9e41b570ff092f8007c477ef0ce5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/747154aa69b0f83cd02fc9aa554836dee417631a",
-                "reference": "747154aa69b0f83cd02fc9aa554836dee417631a",
+                "url": "https://api.github.com/repos/symfony/console/zipball/f9e638e8149e9e41b570ff092f8007c477ef0ce5",
+                "reference": "f9e638e8149e9e41b570ff092f8007c477ef0ce5",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 07:02:31"
+            "time": "2016-07-26 08:04:17"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "b7272b65f2f46cbe77def7d33916f2613669c508"
+                "reference": "6abd4952d07042d11bbb8122f3b57469691acdb5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b7272b65f2f46cbe77def7d33916f2613669c508",
-                "reference": "b7272b65f2f46cbe77def7d33916f2613669c508",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6abd4952d07042d11bbb8122f3b57469691acdb5",
+                "reference": "6abd4952d07042d11bbb8122f3b57469691acdb5",
                 "shasum": ""
             },
             "require": {
             "require-dev": {
                 "symfony/config": "~2.8|~3.0",
                 "symfony/expression-language": "~2.8|~3.0",
-                "symfony/yaml": "~2.8|~3.0"
+                "symfony/yaml": "~2.8.7|~3.0.7|~3.1.1|~3.2"
             },
             "suggest": {
                 "symfony/config": "",
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:42:25"
+            "time": "2016-07-28 11:13:48"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "99ec4a23330fcd0c8667095f3ef7aa204ffd9dc0"
+                "reference": "c7b9b8db3a6f2bac76dcd9a9db5446f2591897f9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/99ec4a23330fcd0c8667095f3ef7aa204ffd9dc0",
-                "reference": "99ec4a23330fcd0c8667095f3ef7aa204ffd9dc0",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7b9b8db3a6f2bac76dcd9a9db5446f2591897f9",
+                "reference": "c7b9b8db3a6f2bac76dcd9a9db5446f2591897f9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:41:56"
+            "time": "2016-07-26 08:04:17"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "7f9839ede2070f53e7e2f0849b9bd14748c434c5"
+                "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7f9839ede2070f53e7e2f0849b9bd14748c434c5",
-                "reference": "7f9839ede2070f53e7e2f0849b9bd14748c434c5",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c0c00c80b3a69132c4e55c3e7db32b4a387615e5",
+                "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:41:56"
+            "time": "2016-07-19 10:45:57"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "322da5f0910d8aa0b25fa65ffccaba68dbddb890"
+                "reference": "bb29adceb552d202b6416ede373529338136e84f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/322da5f0910d8aa0b25fa65ffccaba68dbddb890",
-                "reference": "322da5f0910d8aa0b25fa65ffccaba68dbddb890",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/bb29adceb552d202b6416ede373529338136e84f",
+                "reference": "bb29adceb552d202b6416ede373529338136e84f",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:41:56"
+            "time": "2016-07-20 05:44:26"
         },
         {
             "name": "symfony/polyfill-mbstring",
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.8",
+            "version": "v2.8.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "89f33c16796415ccfd8bb3cf8d520cbb79899bfe"
+                "reference": "d20332e43e8774ff8870b394f3dd6020cc7f8e0c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/89f33c16796415ccfd8bb3cf8d520cbb79899bfe",
-                "reference": "89f33c16796415ccfd8bb3cf8d520cbb79899bfe",
+                "url": "https://api.github.com/repos/symfony/process/zipball/d20332e43e8774ff8870b394f3dd6020cc7f8e0c",
+                "reference": "d20332e43e8774ff8870b394f3dd6020cc7f8e0c",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:29:29"
+            "time": "2016-07-28 11:13:19"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "d63a94528530c3ea5ff46924c8001cec4a398609"
+                "reference": "7713ddf81518d0823b027fe74ec390b80f6b6536"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/d63a94528530c3ea5ff46924c8001cec4a398609",
-                "reference": "d63a94528530c3ea5ff46924c8001cec4a398609",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/7713ddf81518d0823b027fe74ec390b80f6b6536",
+                "reference": "7713ddf81518d0823b027fe74ec390b80f6b6536",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:41:56"
+            "time": "2016-07-26 08:04:17"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.1.2",
+            "version": "v3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "2884c26ce4c1d61aebf423a8b912950fe7c764de"
+                "reference": "1819adf2066880c7967df7180f4f662b6f0567ac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/2884c26ce4c1d61aebf423a8b912950fe7c764de",
-                "reference": "2884c26ce4c1d61aebf423a8b912950fe7c764de",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/1819adf2066880c7967df7180f4f662b6f0567ac",
+                "reference": "1819adf2066880c7967df7180f4f662b6f0567ac",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2016-06-29 05:41:56"
+            "time": "2016-07-17 14:02:08"
         },
         {
             "name": "webmozart/assert",
-            "version": "1.0.2",
+            "version": "1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde"
+                "reference": "bb2d123231c095735130cc8f6d31385a44c7b308"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde",
-                "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308",
+                "reference": "bb2d123231c095735130cc8f6d31385a44c7b308",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": "^5.3.3|^7.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.6"
+                "phpunit/phpunit": "^4.6",
+                "sebastian/version": "^1.0.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "1.2-dev"
                 }
             },
             "autoload": {
                 "check",
                 "validate"
             ],
-            "time": "2015-08-24 13:29:44"
+            "time": "2016-08-09 15:02:57"
         }
     ],
     "aliases": [],
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 994b37e..a8ac69b 100644 (file)
@@ -1679,6 +1679,12 @@ function set_coursemodule_name($id, $name) {
     $grademodule->modname = $cm->modname;
     grade_update_mod_grades($grademodule);
 
+    // Update calendar events with the new name.
+    $refresheventsfunction = $cm->modname . '_refresh_events';
+    if (function_exists($refresheventsfunction)) {
+        call_user_func($refresheventsfunction, $cm->course);
+    }
+
     return true;
 }
 
@@ -3709,6 +3715,12 @@ function duplicate_module($course, $cm) {
         moveto_module($newcm, $section, $cm);
         moveto_module($cm, $section, $newcm);
 
+        // Update calendar events with the duplicated module.
+        $refresheventsfunction = $newcm->modname . '_refresh_events';
+        if (function_exists($refresheventsfunction)) {
+            call_user_func($refresheventsfunction, $newcm->course);
+        }
+
         // Trigger course module created event. We can trigger the event only if we know the newcmid.
         $event = \core\event\course_module_created::create_from_cm($newcm);
         $event->trigger();
index 4cebfe9..01caa10 100644 (file)
@@ -28,7 +28,7 @@ $string['allowunenrol_desc'] = 'If enabled, course enrolments will be removed wh
 $string['basicsettings'] = 'Basic settings';
 $string['categoryidnumber'] = 'Allow category idnumber';
 $string['categoryidnumber_desc'] = 'If enabled IMS Enterprise will create category with idnumber';
-$string['categoryseparator'] = 'Category Separator Character';
+$string['categoryseparator'] = 'Category separator character';
 $string['categoryseparator_desc'] = 'Required when "Category idnumber" is enabled. Character to separate the category name and idnumber.';
 $string['coursesettings'] = 'Course data options';
 $string['createnewcategories'] = 'Create new (hidden) course categories if not found in Moodle';
diff --git a/enrol/lti/cartridge.php b/enrol/lti/cartridge.php
new file mode 100644 (file)
index 0000000..bf61743
--- /dev/null
@@ -0,0 +1,61 @@
+<?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/>.
+
+/**
+ * Generates an XML IMS Cartridge with the details for the given tool
+ *
+ * @package    enrol_lti
+ * @copyright  2016 John Okely <john@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->dirroot . '/lib/weblib.php');
+
+$toolid = null;
+$token = null;
+
+$filearguments = get_file_argument();
+$arguments = explode('/', trim($filearguments, '/'));
+if (count($arguments) >= 2) { // Can put cartridge.xml at the end, or anything really.
+    list($toolid, $token) = $arguments;
+}
+
+$toolid = optional_param('id', $toolid, PARAM_INT);
+$token = optional_param('token', $token, PARAM_ALPHANUM);
+
+// Only show the cartridge if the token parameter is correct.
+// If we do not compare with a shared secret, someone could very easily
+// guess an id for the enrolment.
+if (!\enrol_lti\helper::verify_tool_token($toolid, $token)) {
+    throw new \moodle_exception('incorrecttoken', 'enrol_lti');
+}
+
+$tool = \enrol_lti\helper::get_lti_tool($toolid);
+
+if (!is_enabled_auth('lti')) {
+    print_error('pluginnotenabled', 'auth', '', get_string('pluginname', 'auth_lti'));
+
+} else if (!enrol_is_enabled('lti')) {
+    print_error('enrolisdisabled', 'enrol_lti');
+
+} else if ($tool->status != ENROL_INSTANCE_ENABLED) {
+    print_error('enrolisdisabled', 'enrol_lti');
+
+} else {
+    header('Content-Type: text/xml; charset=utf-8');
+    echo \enrol_lti\helper::create_cartridge($toolid);
+}
index 83a9b13..aabd9a6 100644 (file)
@@ -380,4 +380,219 @@ class helper {
               </imsx_POXBody>
             </imsx_POXEnvelopeRequest>';
     }
+
+    /**
+     * Returns the url to launch the lti tool.
+     *
+     * @param int $toolid the id of the shared tool
+     * @return moodle_url the url to launch the tool
+     * @since Moodle 3.2
+     */
+    public static function get_launch_url($toolid) {
+        return new \moodle_url('/enrol/lti/tool.php', array('id' => $toolid));
+    }
+
+    /**
+     * Returns the name of the lti enrolment instance, or the name of the course/module being shared.
+     *
+     * @param stdClass $tool The lti tool
+     * @return string The name of the tool
+     * @since Moodle 3.2
+     */
+    public static function get_name($tool) {
+        $name = null;
+
+        if (empty($tool->name)) {
+            $toolcontext = \context::instance_by_id($tool->contextid);
+            $name = $toolcontext->get_context_name();
+        } else {
+            $name = $tool->name;
+        };
+
+        return $name;
+    }
+
+    /**
+     * Returns a description of the course or module that this lti instance points to.
+     *
+     * @param stdClass $tool The lti tool
+     * @return string A description of the tool
+     * @since Moodle 3.2
+     */
+    public static function get_description($tool) {
+        global $DB;
+        $description = '';
+        $context = \context::instance_by_id($tool->contextid);
+        if ($context->contextlevel == CONTEXT_COURSE) {
+            $course = $DB->get_record('course', array('id' => $context->instanceid));
+            $description = $course->summary;
+        } else if ($context->contextlevel == CONTEXT_MODULE) {
+            $cmid = $context->instanceid;
+            $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST);
+            $module = $DB->get_record($cm->modname, array('id' => $cm->instance));
+            $description = $module->intro;
+        }
+        return trim(html_to_text($description));
+    }
+
+    /**
+     * Returns the url to the cartridge representing the tool.
+     *
+     * If you have slash arguments enabled, this will be a nice url ending in cartridge.xml.
+     * If not it will be a php page with some parameters passed.
+     *
+     * @param stdClass $tool The lti tool
+     * @return string The url to the cartridge representing the tool
+     * @since Moodle 3.2
+     */
+    public static function get_cartridge_url($tool) {
+        global $CFG;
+        $url = null;
+
+        $id = $tool->id;
+        $token = self::generate_tool_token($tool->id);
+        if ($CFG->slasharguments) {
+            $url = new \moodle_url('/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml');
+        } else {
+            $url = new \moodle_url('/enrol/lti/cartridge.php',
+                    array(
+                        'id' => $id,
+                        'token' => $token
+                    )
+                );
+        }
+        return $url;
+    }
+
+    /**
+     * Returns a unique hash for this site and this enrolment instance.
+     *
+     * Used to verify that the link to the cartridge has not just been guessed.
+     *
+     * @param int $toolid The id of the shared tool
+     * @return string MD5 hash of combined site ID and enrolment instance ID.
+     * @since Moodle 3.2
+     */
+    public static function generate_tool_token($toolid) {
+        $siteidentifier = get_site_identifier();
+        $checkhash = md5($siteidentifier . '_enrol_lti_' . $toolid);
+        return $checkhash;
+    }
+
+    /**
+     * Verifies that the given token matches the token of the given shared tool.
+     *
+     * @param int $toolid The id of the shared tool
+     * @param string $token hash for this site and this enrolment instance
+     * @return boolean True if the token matches, false if it does not
+     * @since Moodle 3.2
+     */
+    public static function verify_tool_token($toolid, $token) {
+        return $token == self::generate_tool_token($toolid);
+    }
+
+    /**
+     * Returns the parameters of the cartridge as an associative array of partial xpath.
+     *
+     * @param int $toolid The id of the shared tool
+     * @return array Recursive associative array with partial xpath to be concatenated into an xpath expression
+     *     before setting the value.
+     * @since Moodle 3.2
+     */
+    protected static function get_cartridge_parameters($toolid) {
+        global $OUTPUT, $PAGE, $SITE;
+        $PAGE->set_context(\context_system::instance());
+
+        // Get the tool.
+        $tool = self::get_lti_tool($toolid);
+
+        // Work out the name of the tool.
+        $title = self::get_name($tool);
+        $launchurl = self::get_launch_url($toolid);
+        $launchurl = $launchurl->out();
+        $icon = $OUTPUT->favicon();
+        $icon = $icon->out();
+        $securelaunchurl = null;
+        $secureicon = null;
+        $vendorurl = new \moodle_url('/');
+        $vendorurl = $vendorurl->out();
+        $description = self::get_description($tool);
+
+        // If we are a https site, we can add the launch url and icon urls as secure equivalents.
+        if (\is_https()) {
+            $securelaunchurl = $launchurl;
+            $secureicon = $icon;
+        }
+
+        return array(
+                "/cc:cartridge_basiclti_link" => array(
+                    "/blti:title" => $title,
+                    "/blti:description" => $description,
+                    "/blti:extensions" => array(
+                            "/lticm:property[@name='icon_url']" => $icon,
+                            "/lticm:property[@name='secure_icon_url']" => $secureicon
+                        ),
+                    "/blti:launch_url" => $launchurl,
+                    "/blti:secure_launch_url" => $securelaunchurl,
+                    "/blti:icon" => $icon,
+                    "/blti:secure_icon" => $secureicon,
+                    "/blti:vendor" => array(
+                            "/lticp:code" => $SITE->shortname,
+                            "/lticp:name" => $SITE->fullname,
+                            "/lticp:description" => trim(html_to_text($SITE->summary)),
+                            "/lticp:url" => $vendorurl
+                        )
+                )
+            );
+    }
+
+    /**
+     * Traverses a recursive associative array, setting the properties of the corresponding
+     * xpath element.
+     *
+     * @param DOMXPath $xpath The xpath with the xml to modify
+     * @param array $parameters The array of xpaths to search through
+     * @param string $prefix The current xpath prefix (gets longer the deeper into the array you go)
+     * @return void
+     * @since Moodle 3.2
+     */
+    protected static function set_xpath($xpath, $parameters, $prefix = '') {
+        foreach ($parameters as $key => $value) {
+            if (is_array($value)) {
+                self::set_xpath($xpath, $value, $prefix . $key);
+            } else {
+                $result = @$xpath->query($prefix . $key);
+                if ($result) {
+                    $node = $result->item(0);
+                    if ($node) {
+                        if (is_null($value)) {
+                            $node->parentNode->removeChild($node);
+                        } else {
+                            $node->nodeValue = $value;
+                        }
+                    }
+                } else {
+                    throw new \coding_exception('Please check your XPATH and try again.');
+                }
+            }
+        }
+    }
+
+    /**
+     * Create an IMS cartridge for the tool.
+     *
+     * @param int $toolid The id of the shared tool
+     * @return string representing the generated cartridge
+     * @since Moodle 3.2
+     */
+    public static function create_cartridge($toolid) {
+        $cartridge = new \DOMDocument();
+        $cartridge->load(realpath(__DIR__ . '/../xml/imslticc.xml'));
+        $xpath = new \DOMXpath($cartridge);
+        $xpath->registerNamespace('cc', 'http://www.imsglobal.org/xsd/imslticc_v1p0');
+        $parameters = self::get_cartridge_parameters($toolid);
+        self::set_xpath($xpath, $parameters);
+
+        return $cartridge->saveXML();
+    }
 }
index d334483..6ebec61 100644 (file)
@@ -96,12 +96,7 @@ class manage_table extends \table_sql {
      * @return string
      */
     public function col_name($tool) {
-        if (empty($tool->name)) {
-            $toolcontext = \context::instance_by_id($tool->contextid);
-            $name = $toolcontext->get_context_name();
-        } else {
-            $name = $tool->name;
-        };
+        $name = helper::get_name($tool);
 
         return $this->get_display_text($tool, $name);
     }
@@ -113,8 +108,9 @@ class manage_table extends \table_sql {
      * @return string
      */
     public function col_url($tool) {
-        $url = new \moodle_url('/enrol/lti/tool.php', array('id' => $tool->id));
-        return $this->get_display_text($tool, $url);
+        $url = helper::get_cartridge_url($tool);
+
+        return $this->get_copyable_text($tool, $url);
     }
 
     /**
@@ -124,7 +120,7 @@ class manage_table extends \table_sql {
      * @return string
      */
     public function col_secret($tool) {
-        return $this->get_display_text($tool, $tool->secret);
+        return $this->get_copyable_text($tool, $tool->secret);
     }
 
 
@@ -215,4 +211,22 @@ class manage_table extends \table_sql {
 
         return $text;
     }
+
+    /**
+     * Returns text to display in the columns.
+     *
+     * @param \stdClass $tool the tool
+     * @param string $text the text to alter
+     * @return string
+     * @since Moodle 3.2
+     */
+    protected function get_copyable_text($tool, $text) {
+        global $OUTPUT;
+        $copyable = $OUTPUT->render_from_template('core/copy_box', array('text' => $text));
+        if ($tool->status != ENROL_INSTANCE_ENABLED) {
+            return \html_writer::tag('span', $copyable, array('class' => 'dimmed_text', 'style' => 'overflow: scroll'));
+        }
+
+        return $copyable;
+    }
 }
index 8b2cd1e..d8e38e8 100644 (file)
@@ -37,6 +37,7 @@ $string['enrolstartdate_help'] = 'If enabled, users can access from this date on
 $string['frameembeddingnotenabled'] = 'To access the tool, please follow the link below.';
 $string['gradesync'] = 'Grade synchronisation';
 $string['gradesync_help'] = 'Whether grades from the tool are sent to the remote system (LTI consumer).';
+$string['incorrecttoken'] = 'The token was incorrect. Please check the URL and try again, or contact the administrator of this tool.';
 $string['maxenrolled'] = 'Maximum enrolled users';
 $string['maxenrolled_help'] = 'The maximum number of remote users who can access the tool. If set to zero, the number of enrolled users is unlimited.';
 $string['maxenrolledreached'] = 'The maximum number of remote users allowed to access the tool has been reached.';
diff --git a/enrol/lti/styles.css b/enrol/lti/styles.css
new file mode 100644 (file)
index 0000000..9128815
--- /dev/null
@@ -0,0 +1,4 @@
+.copy_box {
+    width: 100%;
+    max-width: 350px;
+}
diff --git a/enrol/lti/tests/fixtures/input.xml b/enrol/lti/tests/fixtures/input.xml
new file mode 100644 (file)
index 0000000..6202832
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+    <firstnode></firstnode>
+    <parentnode>
+        <childnode></childnode>
+    </parentnode>
+    <ambiguous id="0"></ambiguous>
+    <ambiguous id="1"></ambiguous>
+</root>
diff --git a/enrol/lti/tests/fixtures/test_ambiguous_nodes-expected.xml b/enrol/lti/tests/fixtures/test_ambiguous_nodes-expected.xml
new file mode 100644 (file)
index 0000000..8ef4597
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+    <firstnode/>
+    <parentnode>
+        <childnode/>
+    </parentnode>
+    <ambiguous id="0"/>
+    <ambiguous id="1">Content 1</ambiguous>
+</root>
diff --git a/enrol/lti/tests/fixtures/test_correct_xpath-expected.xml b/enrol/lti/tests/fixtures/test_correct_xpath-expected.xml
new file mode 100644 (file)
index 0000000..88c21eb
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+    <firstnode>Content 1</firstnode>
+    <parentnode>
+        <childnode>Content 2</childnode>
+    </parentnode>
+    <ambiguous id="0"/>
+    <ambiguous id="1"/>
+</root>
diff --git a/enrol/lti/tests/fixtures/test_missing_node-expected.xml b/enrol/lti/tests/fixtures/test_missing_node-expected.xml
new file mode 100644 (file)
index 0000000..e4a5619
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+    <firstnode>Content 1</firstnode>
+    <parentnode>
+        <childnode/>
+    </parentnode>
+    <ambiguous id="0"/>
+    <ambiguous id="1"/>
+</root>
diff --git a/enrol/lti/tests/fixtures/test_nodes_removed-expected.xml b/enrol/lti/tests/fixtures/test_nodes_removed-expected.xml
new file mode 100644 (file)
index 0000000..34eb129
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+    
+    <parentnode>
+        
+    </parentnode>
+    <ambiguous id="0"/>
+    <ambiguous id="1"/>
+</root>
index 971fa50..c6c40f7 100644 (file)
@@ -247,6 +247,233 @@ class enrol_lti_helper_testcase extends advanced_testcase {
         $this->assertTrue(isset($tools[$tool3->id]));
     }
 
+    /**
+     * Test getting the launch url of a tool.
+     */
+    public function test_get_launch_url() {
+        $course1 = $this->getDataGenerator()->create_course();
+        $data = new stdClass();
+        $data->courseid = $course1->id;
+        $tool1 = $this->create_tool($data);
+
+        $id = $tool1->id;
+        $launchurl = \enrol_lti\helper::get_launch_url($id);
+        $this->assertEquals('http://www.example.com/moodle/enrol/lti/tool.php?id=' . $id, $launchurl->out());
+    }
+
+    /**
+     * Test getting the cartridge url of a tool.
+     */
+    public function test_get_cartridge_url() {
+        global $CFG;
+
+        $slasharguments = $CFG->slasharguments;
+
+        $CFG->slasharguments = false;
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $data = new stdClass();
+        $data->courseid = $course1->id;
+        $tool1 = $this->create_tool($data);
+
+        $id = $tool1->id;
+        $token = \enrol_lti\helper::generate_tool_token($id);
+        $launchurl = \enrol_lti\helper::get_cartridge_url($tool1);
+        $this->assertEquals('http://www.example.com/moodle/enrol/lti/cartridge.php?id=' . $id . '&amp;token=' . $token,
+                            $launchurl->out());
+
+        $CFG->slasharguments = true;
+
+        $launchurl = \enrol_lti\helper::get_cartridge_url($tool1);
+        $this->assertEquals('http://www.example.com/moodle/enrol/lti/cartridge.php/' . $id . '/' . $token . '/cartridge.xml',
+                            $launchurl->out());
+
+        $CFG->slasharguments = $slasharguments;
+    }
+
+    /**
+     * Test getting the name of a tool.
+     */
+    public function test_get_name() {
+        $course1 = $this->getDataGenerator()->create_course();
+        $data = new stdClass();
+        $data->courseid = $course1->id;
+        $tool1 = $this->create_tool($data);
+
+        $name = \enrol_lti\helper::get_name($tool1);
+        $this->assertEquals('Course: Test course 1', $name);
+
+        $tool1->name = 'Shared course';
+        $name = \enrol_lti\helper::get_name($tool1);
+        $this->assertEquals('Shared course', $name);
+    }
+
+    /**
+     * Test getting the description of a tool.
+     */
+    public function test_get_description() {
+        $course1 = $this->getDataGenerator()->create_course();
+        $data = new stdClass();
+        $data->courseid = $course1->id;
+        $tool1 = $this->create_tool($data);
+
+        $description = \enrol_lti\helper::get_description($tool1);
+        $this->assertContains('Test course 1 Lorem ipsum dolor sit amet', $description);
+
+        $module1 = $this->getDataGenerator()->create_module('assign', array(
+                'course' => $course1->id
+            ));
+        $data = new stdClass();
+        $data->cmid = $module1->cmid;
+        $tool2 = $this->create_tool($data);
+        $description = \enrol_lti\helper::get_description($tool2);
+        $this->assertContains('Test assign 1', $description);
+    }
+
+    /**
+     * Test verifying a tool token.
+     */
+    public function test_verify_tool_token() {
+        $course1 = $this->getDataGenerator()->create_course();
+        $data = new stdClass();
+        $data->courseid = $course1->id;
+        $tool1 = $this->create_tool($data);
+
+        $token = \enrol_lti\helper::generate_tool_token($tool1->id);
+        $this->assertTrue(\enrol_lti\helper::verify_tool_token($tool1->id, $token));
+        $this->assertFalse(\enrol_lti\helper::verify_tool_token($tool1->id, 'incorrect token!'));
+    }
+
+    /**
+     * Data provider for the set_xpath test.
+     */
+    public function set_xpath_provider() {
+        return [
+            "Correct structure" => [
+                "parameters" => [
+                    "/root" => [
+                        "/firstnode" => "Content 1",
+                        "/parentnode" => [
+                            "/childnode" => "Content 2"
+                        ]
+                    ]
+                ],
+                "expected" => "test_correct_xpath-expected.xml"
+            ],
+            "A null value, but no node to remove" => [
+                "parameters" => [
+                    "/root" => [
+                        "/nonexistant" => null,
+                        "/firstnode" => "Content 1"
+                    ]
+                ],
+                "expected" => "test_missing_node-expected.xml"
+            ],
+            "A string value, but no node existing to set" => [
+                "parameters" => [
+                    "/root" => [
+                        "/nonexistant" => "This will not be set",
+                        "/firstnode" => "Content 1"
+                    ]
+                ],
+                "expected" => "test_missing_node-expected.xml"
+            ],
+            "Array but no children exist" => [
+                "parameters" => [
+                    "/root" => [
+                        "/nonexistant" => [
+                            "/alsononexistant" => "This will not be set"
+                        ],
+                        "/firstnode" => "Content 1"
+                    ]
+                ],
+                "expected" => "test_missing_node-expected.xml"
+            ],
+            "Remove nodes" => [
+                "parameters" => [
+                    "/root" => [
+                        "/parentnode" => [
+                            "/childnode" => null
+                        ],
+                        "/firstnode" => null
+                    ]
+                ],
+                "expected" => "test_nodes_removed-expected.xml"
+            ],
+            "Get by attribute" => [
+                "parameters" => [
+                    "/root" => [
+                        "/ambiguous[@id='1']" => 'Content 1'
+                    ]
+                ],
+                "expected" => "test_ambiguous_nodes-expected.xml"
+            ]
+        ];
+    }
+
+    /**
+     * Test set_xpath.
+     * @dataProvider set_xpath_provider
+     * @param array $parameters A hash of parameters represented by a heirarchy of xpath expressions
+     * @param string $expected The name of the fixture file containing the expected result.
+     */
+    public function test_set_xpath($parameters, $expected) {
+        $helper = new ReflectionClass('enrol_lti\\helper');
+        $function = $helper->getMethod('set_xpath');
+        $function->setAccessible(true);
+
+        $document = new \DOMDocument();
+        $document->load(realpath(__DIR__ . '/fixtures/input.xml'));
+        $xpath = new \DOMXpath($document);
+        $function->invokeArgs(null, [$xpath, $parameters]);
+        $result = $document->saveXML();
+        $expected = file_get_contents(realpath(__DIR__ . '/fixtures/' . $expected));
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test set_xpath when an incorrect xpath expression is given.
+     * @expectedException coding_exception
+     */
+    public function test_set_xpath_incorrect_xpath() {
+        $parameters = [
+            "/root" => [
+                "/firstnode" => null,
+                "/parentnode*&#^*#(" => [
+                    "/childnode" => null
+                ],
+            ]
+        ];
+        $helper = new ReflectionClass('enrol_lti\\helper');
+        $function = $helper->getMethod('set_xpath');
+        $function->setAccessible(true);
+
+        $document = new \DOMDocument();
+        $document->load(realpath(__DIR__ . '/fixtures/input.xml'));
+        $xpath = new \DOMXpath($document);
+        $function->invokeArgs(null, [$xpath, $parameters]);
+        $result = $document->saveXML();
+        $expected = file_get_contents(realpath(__DIR__ . '/fixtures/' . $expected));
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test create cartridge.
+     */
+    public function test_create_cartridge() {
+        global $CFG;
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $data = new stdClass();
+        $data->courseid = $course1->id;
+        $tool1 = $this->create_tool($data);
+
+        $cartridge = \enrol_lti\helper::create_cartridge($tool1->id);
+        $this->assertContains('<blti:title>Test LTI</blti:title>', $cartridge);
+        $this->assertContains("<blti:icon>$CFG->wwwroot/theme/image.php/_s/clean/theme/1/favicon</blti:icon>", $cartridge);
+        $this->assertContains("<blti:launch_url>$CFG->wwwroot/enrol/lti/tool.php?id=$tool1->id</blti:launch_url>", $cartridge);
+    }
+
     /**
      * Helper function used to create a tool.
      *
@@ -267,6 +494,12 @@ class enrol_lti_helper_testcase extends advanced_testcase {
             $course = get_course($data->courseid);
         }
 
+        if (!empty($data->cmid)) {
+            $data->contextid = context_module::instance($data->cmid)->id;
+        } else {
+            $data->contextid = context_course::instance($data->courseid)->id;
+        }
+
         // Set it to enabled if no status was specified.
         if (!isset($data->status)) {
             $data->status = ENROL_INSTANCE_ENABLED;
@@ -274,7 +507,6 @@ class enrol_lti_helper_testcase extends advanced_testcase {
 
         // Add some extra necessary fields to the data.
         $data->name = 'Test LTI';
-        $data->contextid = context_course::instance($data->courseid)->id;
         $data->roleinstructor = $studentrole->id;
         $data->rolelearner = $teacherrole->id;
 
index 926e739..1468621 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2016052300; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2016052301; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2016051900; // Requires this Moodle version (3.1)
 $plugin->component = 'enrol_lti'; // Full name of the plugin (used for diagnostics).
diff --git a/enrol/lti/xml/imslticc.xml b/enrol/lti/xml/imslticc.xml
new file mode 100644 (file)
index 0000000..839c57f
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"
+    xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"
+    xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"
+    xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"
+    xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd
+http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd
+http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd
+http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
+    <blti:title></blti:title>
+    <blti:description></blti:description>
+    <blti:extensions platform="org.moodle.lms">
+        <lticm:property name="icon_url"></lticm:property>
+        <lticm:property name="secure_icon_url"></lticm:property>
+    </blti:extensions>
+    <blti:launch_url></blti:launch_url>
+    <blti:secure_launch_url></blti:secure_launch_url>
+    <blti:icon></blti:icon>
+    <blti:secure_icon></blti:secure_icon>
+    <blti:vendor>
+        <lticp:code></lticp:code>
+        <lticp:name></lticp:name>
+        <lticp:description></lticp:description>
+        <lticp:url></lticp:url>
+    </blti:vendor>
+    <test></test>
+</cartridge_basiclti_link>
index d2c5972..4d34810 100644 (file)
@@ -52,9 +52,6 @@ $capabilities = array(
 
         'captype' => 'write',
         'contextlevel' => CONTEXT_COURSE,
-        'archetypes' => array(
-            'manager' => CAP_PROHIBIT,
-        )
     ),
 
     /* Voluntarily unenrol self from course - watch out for data loss. */
index 8be21ea..581de7a 100644 (file)
@@ -39,5 +39,24 @@ function xmldb_enrol_self_upgrade($oldversion) {
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.2.0 release upgrade line.
+    // Put any upgrade step following this.
+
+    if ($oldversion < 2016052301) {
+        global $DB;
+        // Get roles with manager archetype.
+        $managerroles = get_archetype_roles('manager');
+        if (!empty($managerroles)) {
+            // Remove wrong CAP_PROHIBIT from self:holdkey.
+            foreach ($managerroles as $role) {
+                $DB->execute("DELETE
+                                FROM {role_capabilities}
+                               WHERE roleid = ? AND capability = ? AND permission = ?",
+                        array($role->id, 'enrol/self:holdkey', CAP_PROHIBIT));
+            }
+        }
+        upgrade_plugin_savepoint(true, 2016052301, 'enrol', 'self');
+    }
+
     return true;
 }
diff --git a/enrol/self/tests/behat/key_holder.feature b/enrol/self/tests/behat/key_holder.feature
new file mode 100644 (file)
index 0000000..31f4a82
--- /dev/null
@@ -0,0 +1,53 @@
+@enrol @enrol_self
+Feature: Users can be defined as key holders in courses where self enrolment is allowed
+  In order to participate in courses
+  As a user
+  I need to auto enrol me in courses
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | manager1 | Manager | 1 | manager1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And I log in as "admin"
+    And I navigate to "Define roles" node in "Site administration > Users > Permissions"
+    And I click on "Add a new role" "button"
+    And I click on "Continue" "button"
+    And I set the following fields to these values:
+      | Short name | keyholder |
+      | Custom full name | Key holder |
+      | contextlevel50 | 1 |
+      | enrol/self:holdkey | 1 |
+    And I click on "Create this role" "button"
+    And I navigate to "Courses" node in "Site administration > Appearance"
+    And I set the following fields to these values:
+      | Key holder | 1 |
+    And I press "Save changes"
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | manager1 | C1 | keyholder |
+    And I log out
+
+  @javascript
+  Scenario: The key holder name is displayed on site home page
+    Given I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    When I add "Self enrolment" enrolment method with:
+      | Custom instance name | Test student enrolment |
+      | Enrolment key | moodle_rules |
+    And I log out
+    And I log in as "student1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I should see "You should have received this enrolment key from:"
+    And I should see "Manager 1"
+    And I set the following fields to these values:
+      | Enrolment key | moodle_rules |
+    And I press "Enrol me"
+    Then I should see "Topic 1"
+    And I should not see "Enrolment options"
+    And I should not see "Enrol me in this course"
index c2d7ed9..7422312 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016052300;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2016052301;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016051900;        // Requires this Moodle version
 $plugin->component = 'enrol_self';      // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 600;
index 40d49dd..3e568ef 100644 (file)
@@ -161,7 +161,7 @@ class filter_mathjaxloader extends moodle_text_filter {
             $PAGE->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.typeset');
             if ($hasextra) {
                 // If custom dilimeters are used, wrap whole text to prevent autolinking.
-                $text = '<span class="nolink">' + $text + '</span>';
+                $text = '<span class="nolink">' . $text . '</span>';
             } else {
                 if ($hasinline) {
                     // If the default inline TeX delimiters \( \) are present, wrap each pair in nolink.
index 2a6a33c..c1849f2 100644 (file)
@@ -50,9 +50,19 @@ $mform->set_data($data);
 if ($mform->is_cancelled()) {
     redirect($returnurl);
 } else if ($mform->is_submitted() && $mform->is_validated() && !$mform->need_confirm_regrading($controller)) {
-    // everything ok, validated, re-grading confirmed if needed. Make changes to the rubric
-    $controller->update_definition($mform->get_data());
-    redirect($returnurl);
+    // Everything ok, validated, re-grading confirmed if needed. Make changes to the rubric.
+    $data = $mform->get_data();
+    $controller->update_definition($data);
+
+    // If we do not go back to management url and the minscore warning needs to be displayed, display it during redirection.
+    $warning = null;
+    if (!empty($data->returnurl) && $data->returnurl !== $manager->get_management_url()->out(false)) {
+        if (empty($data->rubric['options']['lockzeropoints']) && ($scores = $controller->get_min_max_score()) && $scores['minscore'] <> 0) {
+            $warning = get_string('zerolevelsabsent', 'gradingform_rubric').'<br>'.
+                html_writer::link($manager->get_management_url(), get_string('back'));
+        }
+    }
+    redirect($returnurl, $warning, null, \core\output\notification::NOTIFY_ERROR);
 }
 
 echo $OUTPUT->header();
index cf9455d..bb36c3e 100644 (file)
@@ -44,7 +44,8 @@ $string['err_mintwolevels'] = 'Each criterion must have at least two levels';
 $string['err_nocriteria'] = 'Rubric must contain at least one criterion';
 $string['err_nodefinition'] = 'Level definition can not be empty';
 $string['err_nodescription'] = 'Criterion description can not be empty';
-$string['err_scoreformat'] = 'Number of points for each level must be a valid non-negative number';
+$string['err_novariations'] = 'Criterion levels cannot all be worth the same number of points';
+$string['err_scoreformat'] = 'Number of points for each level must be a valid number';
 $string['err_totalscore'] = 'Maximum number of points possible when graded by the rubric must be more than zero';
 $string['gradingof'] = '{$a} grading';
 $string['level'] = 'Level {$a->definition}, {$a->score} points.';
@@ -52,6 +53,11 @@ $string['leveldelete'] = 'Delete level {$a}';
 $string['leveldefinition'] = 'Level {$a} definition';
 $string['levelempty'] = 'Click to edit level';
 $string['levelsgroup'] = 'Levels group';
+$string['lockzeropoints'] = 'When converting rubric score to points/scale assume that minimum number of points is 0';
+$string['lockzeropoints_help'] = 'Locking the minimum number of points will allow to create rubrics without 0-levels. This may also mean that 0% grade on this rubric is not possible to achieve.<br />
+The maximum score in the rubric is always converted to the maximum grade.<br />
+When this setting is unchecked, the minimum possible score for this rubric will be converted to the minimum grade available in the module (which is zero unless the scale is used).<br />
+<a href="https://docs.moodle.org/en/Rubrics#Grade_calculation">Explanation of rubrics grade calculation</a>';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
 $string['pluginname'] = 'Rubric';
@@ -65,8 +71,9 @@ $string['rubric'] = 'Rubric';
 $string['rubricmapping'] = 'Score to grade mapping rules';
 $string['rubricmappingexplained'] = 'The minimum possible score for this rubric is <b>{$a->minscore} points</b> and it will be converted to the minimum grade available in this module (which is zero unless the scale is used).
     The maximum score <b>{$a->maxscore} points</b> will be converted to the maximum grade.<br />
-    Intermediate scores will be converted respectively and rounded to the nearest available grade.<br />
-    If a scale is used instead of a grade, the score will be converted to the scale elements as if they were consecutive integers.';
+    Intermediate scores will be converted respectively.<br />
+    If a scale is used for grading, the score will be rounded and converted to the scale elements as if they were consecutive integers.<br><br>
+    You can change the method of grade calculation in the Rubic options section below.';
 $string['rubricnotcompleted'] = 'Please choose something for each criterion';
 $string['rubricoptions'] = 'Rubric options';
 $string['rubricstatus'] = 'Current rubric status';
@@ -82,4 +89,6 @@ $string['showscorestudent'] = 'Display points for each level to those being grad
 $string['showscoreteacher'] = 'Display points for each level during evaluation';
 $string['sortlevelsasc'] = 'Sort order for levels:';
 $string['sortlevelsasc0'] = 'Descending by number of points';
-$string['sortlevelsasc1'] = 'Ascending by number of points';
\ No newline at end of file
+$string['sortlevelsasc1'] = 'Ascending by number of points';
+$string['zerolevelsabsent'] = 'Warning: The minimum possible score for this rubric is not 0; this can result in unexpected grades for the activity. To avoid this, each criterion should have a level with 0 points.<br>
+This warning may be ignored if a scale is used for grading, and the minimum levels in the rubric correspond to the minimum value of the scale.';
index 9504b42..39af910 100644 (file)
@@ -143,8 +143,16 @@ class gradingform_rubric_controller extends gradingform_controller {
         // reload the definition from the database
         $currentdefinition = $this->get_definition(true);
 
-        // update rubric data
         $haschanges = array();
+
+        // Check if 'lockzeropoints' option has changed.
+        $newlockzeropoints = $newdefinition->rubric['options']['lockzeropoints'];
+        $currentoptions = $this->get_options();
+        if ((bool)$newlockzeropoints != (bool)$currentoptions['lockzeropoints']) {
+            $haschanges[3] = true;
+        }
+
+        // update rubric data
         if (empty($newdefinition->rubric['criteria'])) {
             $newcriteria = array();
         } else {
@@ -203,10 +211,7 @@ class gradingform_rubric_controller extends gradingform_controller {
             }
             foreach ($levelsdata as $levelid => $level) {
                 if (isset($level['score'])) {
-                    $level['score'] = (float)$level['score'];
-                    if ($level['score']<0) {
-                        $level['score'] = 0;
-                    }
+                    $level['score'] = unformat_float($level['score']);
                 }
                 if (preg_match('/^NEWID\d+$/', $levelid)) {
                     // insert level into DB
@@ -354,6 +359,7 @@ class gradingform_rubric_controller extends gradingform_controller {
     public static function get_default_options() {
         $options = array(
             'sortlevelsasc' => 1,
+            'lockzeropoints' => 1,
             'alwaysshowdefinition' => 1,
             'showdescriptionteacher' => 1,
             'showdescriptionstudent' => 1,
@@ -368,6 +374,9 @@ class gradingform_rubric_controller extends gradingform_controller {
     /**
      * Gets the options of this rubric definition, fills the missing options with default values
      *
+     * The only exception is 'lockzeropoints' - if other options are present in the json string but this
+     * one is absent, this means that the rubric was created before Moodle 3.2 and the 0 value should be used.
+     *
      * @return array
      */
     public function get_options() {
@@ -377,6 +386,11 @@ class gradingform_rubric_controller extends gradingform_controller {
             foreach ($thisoptions as $option => $value) {
                 $options[$option] = $value;
             }
+            if (!array_key_exists('lockzeropoints', $thisoptions)) {
+                // Rubrics created before Moodle 3.2 don't have 'lockzeropoints' option. In this case they should not
+                // assume default value 1 but use "legacy" value 0.
+                $options['lockzeropoints'] = 0;
+            }
         }
         return $options;
     }
@@ -522,7 +536,10 @@ class gradingform_rubric_controller extends gradingform_controller {
             $rubric .= $output->box($this->get_formatted_description(), 'gradingform_rubric-description');
         }
         if (has_capability('moodle/grade:managegradingforms', $page->context)) {
-            $rubric .= $output->display_rubric_mapping_explained($this->get_min_max_score());
+            if (!$options['lockzeropoints']) {
+                // Warn about using grade calculation method where minimum number of points is flexible.
+                $rubric .= $output->display_rubric_mapping_explained($this->get_min_max_score());
+            }
             $rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric');
         } else {
             $rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW_GRADED, 'rubric');
@@ -889,11 +906,19 @@ class gradingform_rubric_instance extends gradingform_instance {
         foreach ($grade['criteria'] as $id => $record) {
             $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score'];
         }
-        $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade);
-        if ($this->get_controller()->get_allow_grade_decimals()) {
-            return $gradeoffset + $mingrade;
+
+        $allowdecimals = $this->get_controller()->get_allow_grade_decimals();
+        $options = $this->get_controller()->get_options();
+
+        if ($options['lockzeropoints']) {
+            // Grade calculation method when 0-level is locked.
+            $grade = max($mingrade, $curscore / $scores['maxscore'] * $maxgrade);
+            return $allowdecimals ? $grade : round($grade, 0);
+        } else {
+            // Alternative grade calculation method.
+            $gradeoffset = ($curscore - $scores['minscore']) / ($scores['maxscore'] - $scores['minscore']) * ($maxgrade - $mingrade);
+            return ($allowdecimals ? $gradeoffset : round($gradeoffset, 0)) + $mingrade;
         }
-        return round($gradeoffset, 0) + $mingrade;
     }
 
     /**
index 72b6b60..4b510d4 100644 (file)
@@ -470,6 +470,9 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
                     $html .= html_writer::tag('label', get_string($option, 'gradingform_rubric'), array('for' => $attrs['id']));
                     break;
             }
+            if (get_string_manager()->string_exists($option.'_help', 'gradingform_rubric')) {
+                $html .= $this->help_icon($option, 'gradingform_rubric');
+            }
             $html .= html_writer::end_tag('div'); // .option
         }
         $html .= html_writer::end_tag('div'); // .options
@@ -633,10 +636,10 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if (!$scores) {
             return $html;
         }
-        $html .= $this->box(
-                html_writer::tag('h4', get_string('rubricmapping', 'gradingform_rubric')).
-                html_writer::tag('div', get_string('rubricmappingexplained', 'gradingform_rubric', (object)$scores))
-                , 'generalbox rubricmappingexplained');
+        if ($scores['minscore'] <> 0) {
+            $html .= $this->output->notification(get_string('zerolevelsabsent', 'gradingform_rubric'), 'error');
+        }
+        $html .= $this->output->notification(get_string('rubricmappingexplained', 'gradingform_rubric', (object)$scores), 'info');
         return $html;
     }
 }
index 54601b5..3d64b5f 100644 (file)
@@ -197,6 +197,7 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
         // iterate through criteria
         $lastaction = null;
         $lastid = null;
+        $overallminscore = $overallmaxscore = 0;
         foreach ($value['criteria'] as $id => $criterion) {
             if ($id == 'addcriterion') {
                 $id = $this->get_next_id(array_keys($value['criteria']));
@@ -221,7 +222,7 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
                 $this->nonjsbuttonpressed = true;
             }
             $levels = array();
-            $maxscore = null;
+            $minscore = $maxscore = null;
             if (array_key_exists('levels', $criterion)) {
                 foreach ($criterion['levels'] as $levelid => $level) {
                     if ($levelid == 'addlevel') {
@@ -231,26 +232,30 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
                             'score' => 0,
                         );
                         foreach ($criterion['levels'] as $lastlevel) {
-                            if (isset($lastlevel['score']) && $level['score'] < $lastlevel['score'] + 1) {
-                                $level['score'] = $lastlevel['score'] + 1;
+                            if (isset($lastlevel['score'])) {
+                                $level['score'] = max($level['score'], ceil(unformat_float($lastlevel['score'])) + 1);
                             }
                         }
                         $this->nonjsbuttonpressed = true;
                     }
                     if (!array_key_exists('delete', $level)) {
+                        $score = unformat_float($level['score'], true);
                         if ($withvalidation) {
                             if (!strlen(trim($level['definition']))) {
                                 $errors['err_nodefinition'] = 1;
                                 $level['error_definition'] = true;
                             }
-                            if (!preg_match('#^[\+]?\d*$#', trim($level['score'])) && !preg_match('#^[\+]?\d*[\.,]\d+$#', trim($level['score']))) {
+                            if ($score === null || $score === false) {
                                 $errors['err_scoreformat'] = 1;
                                 $level['error_score'] = true;
                             }
                         }
                         $levels[$levelid] = $level;
-                        if ($maxscore === null || (float)$level['score'] > $maxscore) {
-                            $maxscore = (float)$level['score'];
+                        if ($minscore === null || $score < $minscore) {
+                            $minscore = $score;
+                        }
+                        if ($maxscore === null || $score > $maxscore) {
+                            $maxscore = $score;
                         }
                     } else {
                         $this->nonjsbuttonpressed = true;
@@ -268,6 +273,8 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
                     $errors['err_nodescription'] = 1;
                     $criterion['error_description'] = true;
                 }
+                $overallmaxscore += $maxscore;
+                $overallminscore += $minscore;
             }
             if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
                 unset($criterion['moveup']);
@@ -307,6 +314,11 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
 
         // create validation error string (if needed)
         if ($withvalidation) {
+            if (!$return['options']['lockzeropoints']) {
+                if ($overallminscore == $overallmaxscore) {
+                    $errors['err_novariations'] = 1;
+                }
+            }
             if (count($errors)) {
                 $rv = array();
                 foreach ($errors as $error => $v) {
index 232e995..933b22b 100644 (file)
@@ -58,7 +58,7 @@ Feature: Rubrics can be created and edited
     And I complete the advanced grading form with these values:
       | Feedback comments | In general... work harder... |
     # Checking that the user grade is correct.
-    And I should see "58.33" in the "Student 1" "table_row"
+    And I should see "65" in the "Student 1" "table_row"
     # Updating the user grade.
     And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
     And I grade by filling the rubric with:
@@ -68,13 +68,13 @@ Feature: Rubrics can be created and edited
     #And the level with "50" points was previously selected for the rubric criterion "Criterion 1"
     #And the level with "20" points is selected for the rubric criterion "Criterion 1"
     And I save the advanced grading form
-    And I should see "22.62" in the "Student 1" "table_row"
+    And I should see "35" in the "Student 1" "table_row"
     And I log out
     # Viewing it as a student.
     And I log in as "student1"
     And I follow "Course 1"
     And I follow "Test assignment 1 name"
-    And I should see "22.62" in the ".feedback" "css_element"
+    And I should see "35" in the ".feedback" "css_element"
     And I should see "Rubric test description" in the ".feedback" "css_element"
     And I should see "In general... work harder..."
     And the level with "10" points is selected for the rubric criterion "Criterion 2"
@@ -97,7 +97,7 @@ Feature: Rubrics can be created and edited
     And I log in as "student1"
     And I follow "Course 1"
     And I follow "Test assignment 1 name"
-    And I should see "22.62" in the ".feedback" "css_element"
+    And I should see "35" in the ".feedback" "css_element"
     And the level with "20" points is selected for the rubric criterion "Criterion 1"
     And I log out
     # Editing a rubric with significant changes.
@@ -105,7 +105,7 @@ Feature: Rubrics can be created and edited
     And I follow "Course 1"
     And I go to "Test assignment 1 name" advanced grading definition page
     And I click on "Move down" "button" in the "Criterion 2" "table_row"
-    And I replace "1" rubric level with "11" in "Criterion 1" criterion
+    And I replace "1" rubric level with "60" in "Criterion 1" criterion
     And I press "Save"
     And I should see "You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded."
     And I press "Continue"
@@ -114,7 +114,7 @@ Feature: Rubrics can be created and edited
     And I log in as "student1"
     And I follow "Course 1"
     And I follow "Test assignment 1 name"
-    And I should see "22.62" in the ".feedback" "css_element"
+    And I should see "35" in the ".feedback" "css_element"
     And the level with "20" points is not selected for the rubric criterion "Criterion 1"
     And I log out
     # Regrade student.
@@ -129,7 +129,7 @@ Feature: Rubrics can be created and edited
     And I log in as "student1"
     And I follow "Course 1"
     And I follow "Test assignment 1 name"
-    And I should see "12.16" in the ".feedback" "css_element"
+    And I should see "31.82" in the ".feedback" "css_element"
     And the level with "20" points is not selected for the rubric criterion "Criterion 1"
     # Hide all rubric info for students
     And I log out
diff --git a/grade/grading/form/rubric/tests/behat/grade_calculation.feature b/grade/grading/form/rubric/tests/behat/grade_calculation.feature
new file mode 100644 (file)
index 0000000..b9ca422
--- /dev/null
@@ -0,0 +1,56 @@
+@gradingform @gradingform_rubric @javascript
+Feature: Converting rubric score to grades
+  In order to use and refine rubrics to grade students
+  As a teacher
+  I need to be able to use different grade settings
+
+  Scenario Outline:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "scales" exist:
+      | name         | scale                                     |
+      | Test scale 1 | Disappointing, Good, Very good, Excellent |
+    And the following "activities" exist:
+      | activity   | name              | intro | course | idnumber    | grade   | advancedgradingmethod_submissions |
+      | assign     | Test assignment 1 | Test  | C1     | assign1     | <grade> | rubric                            |
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I go to "Test assignment 1" advanced grading definition page
+    And I set the following fields to these values:
+      | Name | Assignment 1 rubric |
+      | Description | Rubric test description |
+      | When converting rubric score to points/scale assume that minimum number of points is 0 | <lockzeropoints> |
+    And I define the following rubric:
+      | Criterion 1 | Level 11 | 20 | Level 12 | 25 | Level 13 | 40 | Level 14  | 50  |
+      | Criterion 2 | Level 21 | 20 | Level 22 | 25 | Level 23 | 30 |           |     |
+      | Criterion 3 | Level 31 | 10 | Level 32 | 20 |          |    |           |     |
+    And I press "Save rubric and make it ready"
+    # Grading a student.
+    And I go to "Student 1" "Test assignment 1" activity advanced grading page
+    And I grade by filling the rubric with:
+      | Criterion 1 | 25 |  |
+      | Criterion 2 | 20 |  |
+      | Criterion 3 | 10 |  |
+    And I save the advanced grading form
+    # Checking that the user grade is correct.
+    And I should see "<studentgrade>" in the "student1@example.com" "table_row"
+    And I log out
+
+    Examples:
+      | grade        | lockzeropoints | studentgrade   |
+      | 100          |              1 | 55.00          |
+      | 70           |              1 | 38.50          |
+      | Test scale 1 |              1 | Good           |
+      | 100          |                | 10.00          |
+      | 70           |                | 7.00           |
+      | Test scale 1 |                | Disappointing  |
diff --git a/grade/grading/form/rubric/tests/behat/negative_points.feature b/grade/grading/form/rubric/tests/behat/negative_points.feature
new file mode 100644 (file)
index 0000000..83ee60b
--- /dev/null
@@ -0,0 +1,65 @@
+@gradingform @gradingform_rubric @javascript
+Feature: Rubrics can have levels with negative scores
+  In order to use and refine rubrics to grade students
+  As a teacher
+  I need to be able to penalise for very wrong submissions
+
+  Scenario: Using negative levels in rubrics
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | student3 | Student | 3 | student3@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+      | student3 | C1 | student |
+    And the following "scales" exist:
+      | name         | scale                                     |
+      | Test scale 1 | Disappointing, Good, Very good, Excellent |
+    And the following "activities" exist:
+      | activity   | name              | intro | course | idnumber    | grade   | advancedgradingmethod_submissions |
+      | assign     | Test assignment 1 | Test  | C1     | assign1     | 100     | rubric                            |
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I go to "Test assignment 1" advanced grading definition page
+    And I set the following fields to these values:
+      | Name | Assignment 1 rubric |
+      | Description | Rubric test description |
+    And I define the following rubric:
+      | Criterion 1 | Did not try | -11 | Level 12 | 25 | Level 13 | 40 | Level 14  | 50  |
+      | Criterion 2 | Very bad    | -20 | Level 22 | 25 | Level 23 | 30 |           |     |
+      | Criterion 3 | Level 31    | 10  | Level 32 | 20 |          |    |           |     |
+    And I press "Save rubric and make it ready"
+    # Grading a student.
+    And I go to "Student 1" "Test assignment 1" activity advanced grading page
+    And I grade by filling the rubric with:
+      | Criterion 1 | 25 |  |
+      | Criterion 2 | 30 |  |
+      | Criterion 3 | 10 |  |
+    And I save the advanced grading form
+    And I go to "Student 2" "Test assignment 1" activity advanced grading page
+    And I grade by filling the rubric with:
+      | Criterion 1 | 25 |  |
+      | Criterion 2 | -20 |  |
+      | Criterion 3 | 10 |  |
+    And I save the advanced grading form
+    And I go to "Student 3" "Test assignment 1" activity advanced grading page
+    And I grade by filling the rubric with:
+      | Criterion 1 | -11 |  |
+      | Criterion 2 | -20 |  |
+      | Criterion 3 | 10 |  |
+    And I save the advanced grading form
+    # Checking that the user grade is correct.
+    And I should see "65.00" in the "student1@example.com" "table_row"
+    And I should see "15.00" in the "student2@example.com" "table_row"
+    And I should see "0.00" in the "student3@example.com" "table_row"
+    And I should not see "-" in the "student3@example.com" "table_row"
+    And I log out
index b151a5e..c40d31c 100644 (file)
@@ -38,7 +38,7 @@ $string['clitypevalue'] = 'مقدار را وارد کنید';
 $string['clitypevaluedefault'] = 'مقدار را وارد کنید، برای استفاده از مقدار پیش‌فرض ({$a}) کلید Enter را فشار دهید';
 $string['cliunknowoption'] = 'گزینه‌های شناخته نشده:
   {$a}
-لطفا از گزینه <span style="direction:ltr">--help</span> استفاده کنید.';
+لطفا از گزینه <span dir="ltr" style="display:inline-block;direction:ltr">--help</span> استفاده کنید.';
 $string['cliyesnoprompt'] = 'یکی از گزینه‌های ب (به معنای بلی) یا خ (به نشانه خیر) را تایپ کنید';
 $string['environmentrequireinstall'] = 'باید نصب و فعال باشد';
 $string['environmentrequireversion'] = 'نسخهٔ {$a->needed} لازم است و شما نسخهٔ {$a->current} را دارید';
index 76e7a89..3962c7a 100644 (file)
@@ -30,4 +30,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['cannotfindcomponent'] = 'کامپوننت پیدا نشد';
 $string['missingrequiredfield'] = 'بعضی از فیلدهای ضروری خالی است';
index 197c8ca..622e967 100644 (file)
@@ -33,7 +33,9 @@ defined('MOODLE_INTERNAL') || die();
 $string['admindirname'] = 'دایرکتوری مدیر';
 $string['availablelangs'] = 'بسته‌های زبانی موجود';
 $string['chooselanguagehead'] = 'انتخاب زبان';
-$string['chooselanguagesub'] = 'لطفاً زبانی را به جهت استفاده در حین نصب انتخاب نمائید. زبان سایت و کاربر را در صفحه‌ای دیگر می‌توانید انتخاب کنید.';
+$string['chooselanguagesub'] = 'لطفاً زبانی را به جهت استفاده در حین نصب انتخاب نمایید. زبانی که در این صفحه انتخاب می‌کنید به عنوان زبان پیش‌فرض سایت نیز مورد استفاده قرار خواهد گرفت. البته می‌توانید بعداً آن را تغییر دهید.';
+$string['databasehost'] = 'میزبان پایگاه داده';
+$string['databasename'] = 'نام پایگاه داده';
 $string['dataroot'] = 'دایرکتوری داده';
 $string['dbprefix'] = 'پیشوند جدول‌ها';
 $string['dirroot'] = 'دایرکتوری مودل';
index 9630a2a..99cb67c 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'زبان';
+$string['moodlelogo'] = 'لوگوی مودل';
 $string['next'] = 'ادامه';
 $string['previous'] = 'قبلی';
 $string['reload'] = 'بررسی مجدد';
index 24e76d7..b7fe047 100644 (file)
@@ -40,3 +40,4 @@ $string['cliunknowoption'] = 'Невизначені опції: {$a}. Будь
 $string['cliyesnoprompt'] = 'натисніть y (означає так) або n (означає ні)';
 $string['environmentrequireinstall'] = 'повинен бути встановлений і включений';
 $string['environmentrequireversion'] = 'потрібна версія {$a->needed}, ви запускаєте {$a->current}';
+$string['upgradekeyset'] = 'Оновлення ключа (залишіть порожнім, якщо не встановлено)';
index 1ca623c..07612fb 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Мова інтерфейсу';
+$string['moodlelogo'] = 'Логотип Moodle';
 $string['next'] = 'Далі';
 $string['previous'] = 'Назад';
 $string['reload'] = 'Оновити';
index 58448b6..52f555b 100644 (file)
@@ -151,7 +151,7 @@ $string['configbloglevel'] = 'This setting allows you to restrict the level to w
 $string['configcalendarcustomexport'] = 'Enable custom date range export of calendar';
 $string['configcalendarexportsalt'] = 'This random text is used for improving of security of authentication tokens used for exporting of calendars. Please note that all current tokens are invalidated if you change this hash salt.';
 $string['configcookiehttponly'] = 'Enables new PHP 5.2.0 feature - browsers are instructed to send cookie with real http requests only, cookies should not be accessible by scripting languages. This is not supported in all browsers and it may not be fully compatible with current code. It helps to prevent some types of XSS attacks.';
-$string['configcookiesecure'] = 'If server is accepting only https connections it is recommended to enable sending of secure cookies. If enabled please make sure that web server is not accepting http:// or set up permanent redirection to https:// address. When <em>wwwroot</em> address does not start with https:// this setting is turned off automatically.';
+$string['configcookiesecure'] = 'If server is accepting only https connections it is recommended to enable sending of secure cookies. If enabled please make sure that web server is not accepting http:// or set up permanent redirection to https:// address and ideally send HSTS headers. When <em>wwwroot</em> address does not start with https:// this setting is ignored.';
 $string['configcountry'] = 'If you set a country here, then this country will be selected by default on new user accounts.  To force users to choose a country, just leave this unset.';
 $string['configcourseoverviewfilesext'] = 'A comma-separated list of allowed course summary files extensions.';
 $string['configcourseoverviewfileslimit'] = 'The maximum number of files that can be attached to a course summary.';
@@ -586,7 +586,7 @@ $string['ignore'] = 'Ignore';
 $string['includemoduleuserdata'] = 'Include module user data';
 $string['incompatibleblocks'] = 'Incompatible blocks';
 $string['indexdata'] = 'Index data';
-$string['indexinginfo'] = 'The recommended way to index your site\'s contents is using "Global search indexing" scheduled task which runs automatically by Cron.';
+$string['indexinginfo'] = 'The recommended way to index your site\'s contents is by using the \'Global search indexing\' scheduled task.';
 $string['installhijacked'] = 'Installation must be finished from the original IP address, sorry.';
 $string['installsessionerror'] = 'Can not initialise PHP session, please verify that your browser accepts cookies.';
 $string['intlrecommended'] = 'Intl extension is used to improve internationalization support, such as locale aware sorting.';
@@ -657,6 +657,11 @@ $string['loginpageautofocus_help'] = 'Enabling this option improves usability of
 $string['loginpasswordautocomplete'] = 'Prevent password autocompletion on login form';
 $string['loginpasswordautocomplete_help'] = 'If enabled, users are not allowed to save their account password in their browser.';
 $string['loglifetime'] = 'Keep logs for';
+$string['logo'] = 'Logo';
+$string['logo_desc'] = 'A full logo. The image format must be PNG or JPEG.';
+$string['logocompact'] = 'Compact logo';
+$string['logocompact_desc'] = 'A compact version of the logo. The image format must be PNG or JPEG.';
+$string['logossettings'] = 'Logos';
 $string['logstorenotrequired'] = 'Log store not required';
 $string['logstoressupported'] = 'Log stores that support this report';
 $string['longtimewarning'] = '<b>Please note that this process can take a long time.</b>';
@@ -950,7 +955,7 @@ $string['searchindexupdated'] = 'Search engine contents have been updated';
 $string['searchinsettings'] = 'Search in settings';
 $string['searchlastrun'] = 'Last run (time, # docs, # records, # ignores)';
 $string['searchnotavailable'] = 'Search is not available';
-$string['searchreindexed'] = 'All site\'s contents have been reindexed';
+$string['searchreindexed'] = 'All site contents have been reindexed.';
 $string['searchreindexindex'] = 'Reindex all site contents';
 $string['searchresults'] = 'Search results';
 $string['searchsetupinfo'] = 'Search setup';
index 584bf4c..d1a3925 100644 (file)
@@ -669,22 +669,22 @@ $string['showanalysisicon'] = 'Show grade analysis icon';
 $string['showanalysisicon_desc'] = 'Whether to show grade analysis icon by default. If the activity module supports it, the grade analysis icon links to a page with more detailed explanation of the grade and how it was obtained.';
 $string['showanalysisicon_help'] = 'If the activity module supports it, the grade analysis icon links to a page with more detailed explanation of the grade and how it was obtained.';
 $string['showaverage'] = 'Show average';
-$string['showaverage_help'] = 'Show the average column? Students may be able to estimate other student\'s grades if the average is calculated from a small number of grades. For performance reasons the average is approximate if it is dependent on any hidden items.';
+$string['showaverage_help'] = 'Whether to show an average column. Note that participants may be able to estimate others\' grades if the average is calculated from a small number of grades. For performance reasons the average is approximate if it is dependent on any hidden items.';
 $string['showcontributiontocoursetotal'] = 'Show contribution to course total';
 $string['showcontributiontocoursetotal_help'] = 'Whether to show a column of percentages indicating how much each grade item contributes to the user\'s course total percentage (after weighting is applied).';
 $string['showfeedback'] = 'Show feedback';
-$string['showfeedback_help'] = 'Show the feedback column?';
+$string['showfeedback_help'] = 'Whether to show a column for feedback.';
 $string['showgrade'] = 'Show grades';
-$string['showgrade_help'] = 'Show the grade column?';
+$string['showgrade_help'] = 'Whether to show a column for grades.';
 $string['showingaggregatesonly'] = 'Showing aggregates only';
 $string['showingfullmode'] = 'Showing full view';
 $string['showinggradesonly'] = 'Showing grades only';
 $string['showlettergrade'] = 'Show letter grades';
-$string['showlettergrade_help'] = 'Show the letter grade column?';
+$string['showlettergrade_help'] = 'Whether to show a column for letter grades.';
 $string['showrange'] = 'Show ranges';
-$string['showrange_help'] = 'Show the range column?';
+$string['showrange_help'] = 'Whether to show a column for the range.';
 $string['showweight'] = 'Show weightings';
-$string['showweight_help'] = 'Show the grade weight column?';
+$string['showweight_help'] = 'Whether to show a column for the grade weight.';
 $string['rangedecimals'] = 'Range decimal points';
 $string['rangedecimals_help'] = 'The number of decimal points to display for range.';
 $string['showactivityicons'] = 'Show activity icons';
@@ -714,7 +714,7 @@ $string['shownumberofgrades_help'] = 'If enabled, the number of grades used when
 $string['showonlyactiveenrol'] = 'Show only active enrolments';
 $string['showonlyactiveenrol_help'] = 'This setting determines, if only active enrolled users will be visible in gradebook report. If enabled suspended users will not be shown in gradebook.';
 $string['showpercentage'] = 'Show percentage';
-$string['showpercentage_help'] = 'Show the percentage value of each grade item?';
+$string['showpercentage_help'] = 'Whether to show the percentage value of each grade item.';
 $string['showquickfeedback'] = 'Show quick feedback';
 $string['showquickfeedback_help'] = 'If enabled, when editing is turned on, a feedback text input box with a dotted border appears for each grade, allowing the feedback for many grades to be edited at the same time. Changes are saved and highlighted when the update button is clicked.
 
@@ -722,7 +722,7 @@ Note that when feedback is edited in the grader report, an overridden flag is se
 $string['showranges'] = 'Show ranges';
 $string['showranges_help'] = 'If enabled, the grader report will contain an additional row displaying the range for each category and grade item.';
 $string['showrank'] = 'Show rank';
-$string['showrank_help'] = 'Show the position of the student in relation to the rest of the class for each grade item?';
+$string['showrank_help'] = 'Whether to show the position of the student in relation to the rest of the class for each grade item.';
 $string['showuserimage'] = 'Show user profile images';
 $string['showuserimage_help'] = 'Whether to show the user\'s profile image next to the name in the grader report.';
 $string['showverbose'] = 'Show {$a->category} {$a->itemmodule} {$a->itemname}';
index b6e7255..1c8532f 100644 (file)
@@ -1667,7 +1667,7 @@ $string['servererror'] = 'An error occurred whilst communicating with the server
 $string['serverlocaltime'] = 'Server\'s local time';
 $string['setcategorytheme'] = 'Set category theme';
 $string['setpassword'] = 'Set password';
-$string['setpasswordinstructions'] = 'Please enter and repeat your new password below, then click "Set password". <br />Your new password will be saved, and you will be logged in.';
+$string['setpasswordinstructions'] = 'Please enter your new password below, then save changes.';
 $string['settings'] = 'Settings';
 $string['shortname'] = 'Short name'; /* @deprecated MDL-34652 - Use shortnamecourse or shortnameuser or some own context specific string. */
 $string['shortnamecollisionwarning'] = '[*] = This shortname is already in use by a course and will need to be changed upon approval';
@@ -2027,6 +2027,7 @@ $string['yourself'] = 'yourself';
 $string['yourteacher'] = 'your {$a}';
 $string['yourwordforx'] = 'Your word for \'{$a}\'';
 $string['zippingbackup'] = 'Zipping backup';
+$string['deprecatedeventname'] = '{$a} (no longer in use)';
 
 // Deprecated since Moodle 3.1.
 $string['filetoolarge'] = 'is too large to upload';
index a5d25d7..096722d 100644 (file)
@@ -81,6 +81,8 @@ $string['runindexer'] = 'Run indexer (real)';
 $string['runindexertest'] = 'Run indexer test';
 $string['score'] = 'Score';
 $string['search'] = 'Search';
+$string['search:message_received'] = 'Messages - received';
+$string['search:message_sent'] = 'Messages - sent';
 $string['search:mycourse'] = 'My courses';
 $string['search:user'] = 'Users';
 $string['searcharea'] = 'Search area';
index a2c4eb8..19033df 100644 (file)
@@ -4208,15 +4208,32 @@ function get_role_users($roleid, context $context, $parent = false, $fields = ''
     // Adding the fields from $sort that are not present in $fields.
     $sortarray = preg_split('/,\s*/', $sort);
     $fieldsarray = preg_split('/,\s*/', $fields);
+
+    // Discarding aliases from the fields.
+    $fieldnames = array();
+    foreach ($fieldsarray as $key => $field) {
+        list($fieldnames[$key]) = explode(' ', $field);
+    }
+
     $addedfields = array();
     foreach ($sortarray as $sortfield) {
         // Throw away any additional arguments to the sort (e.g. ASC/DESC).
-        list ($sortfield) = explode(' ', $sortfield);
-        if (!in_array($sortfield, $fieldsarray)) {
+        list($sortfield) = explode(' ', $sortfield);
+        list($tableprefix) = explode('.', $sortfield);
+        $fieldpresent = false;
+        foreach ($fieldnames as $fieldname) {
+            if ($fieldname === $sortfield || $fieldname === $tableprefix.'.*') {
+                $fieldpresent = true;
+                break;
+            }
+        }
+
+        if (!$fieldpresent) {
             $fieldsarray[] = $sortfield;
             $addedfields[] = $sortfield;
         }
     }
+
     $fields = implode(', ', $fieldsarray);
     if (!empty($addedfields)) {
         $addedfields = implode(', ', $addedfields);
index 05364fd..d722a74 100644 (file)
Binary files a/lib/amd/build/chart_bar.min.js and b/lib/amd/build/chart_bar.min.js differ
index c035888..a329bcd 100644 (file)
Binary files a/lib/amd/build/chart_output_chartjs.min.js and b/lib/amd/build/chart_output_chartjs.min.js differ
index a2e9030..d34d905 100644 (file)
Binary files a/lib/amd/build/chart_output_htmltable.min.js and b/lib/amd/build/chart_output_htmltable.min.js differ
index aab6464..557d2b3 100644 (file)
Binary files a/lib/amd/build/chart_pie.min.js and b/lib/amd/build/chart_pie.min.js differ
index 2225cbc..6fe2947 100644 (file)
Binary files a/lib/amd/build/inplace_editable.min.js and b/lib/amd/build/inplace_editable.min.js differ
index f95b3a4..37a25bf 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 13c5596..53e2385 100644 (file)
@@ -43,6 +43,14 @@ define(['core/chart_base'], function(Base) {
      */
     Bar.prototype._horizontal = false;
 
+    /**
+     * Whether the bars should be stacked or not.
+     *
+     * @type {Bool}
+     * @protected
+     */
+    Bar.prototype._stacked = false;
+
     /** @override */
     Bar.prototype.TYPE = 'bar';
 
@@ -50,6 +58,7 @@ define(['core/chart_base'], function(Base) {
     Bar.prototype.create = function(Klass, data) {
         var chart = Base.prototype.create.apply(this, arguments);
         chart.setHorizontal(data.horizontal);
+        chart.setStacked(data.stacked);
         return chart;
     };
 
@@ -69,6 +78,15 @@ define(['core/chart_base'], function(Base) {
         return this._horizontal;
     };
 
+    /**
+     * Get whether the bars should be stacked or not.
+     *
+     * @returns {Bool}
+     */
+    Bar.prototype.getStacked = function() {
+        return this._stacked;
+    };
+
     /**
      * Set whether the bars should be displayed horizontally or not.
      *
@@ -84,6 +102,16 @@ define(['core/chart_base'], function(Base) {
         this._horizontal = Boolean(horizontal);
     };
 
+    /**
+     * Set whether the bars should be stacked or not.
+     *
+     * @method setStacked
+     * @param {Bool} stacked True if the chart should be stacked or false otherwise.
+     */
+    Bar.prototype.setStacked = function(stacked) {
+        this._stacked = Boolean(stacked);
+    };
+
     return Bar;
 
 });
index d738479..9665a37 100644 (file)
@@ -99,11 +99,12 @@ define([
     };
 
     /**
-     * Get the chart type.
+     * Get the chart type and handles the Chart.js specific chart types.
      *
-     * It also handles the bar charts positioning, deciding if the bars should be displayed horizontally.
-     * Otherwise, get the chart TYPE value.
+     * By default returns the current chart TYPE value. Also does the handling of specific chart types, for example
+     * check if the bar chart should be horizontal and the pie chart should be displayed as a doughnut.
      *
+     * @method getChartType
      * @returns {String} the chart type.
      * @protected
      */
@@ -113,6 +114,9 @@ define([
         // Bars can be displayed vertically and horizontally, defining horizontalBar type.
         if (this._chart.getType() === Bar.prototype.TYPE && this._chart.getHorizontal() === true) {
             type = 'horizontalBar';
+        } else if (this._chart.getType() === Pie.prototype.TYPE && this._chart.getDoughnut() === true) {
+            // Pie chart can be displayed as doughnut.
+            type = 'doughnut';
         }
 
         return type;
@@ -195,6 +199,7 @@ define([
                     return axisLabels[index] || '';
                 };
             }
+            config.options.scales.xAxes[i].stacked = this._isStacked();
         }.bind(this));
 
         this._chart.getYAxes().forEach(function(axis, i) {
@@ -209,6 +214,7 @@ define([
                     return axisLabels[parseInt(value, 10)] || '';
                 };
             }
+            config.options.scales.yAxes[i].stacked = this._isStacked();
         }.bind(this));
 
         config.options.tooltips = {
@@ -301,6 +307,23 @@ define([
         return smooth;
     };
 
+    /**
+     * Verify if the bar chart is stacked or not.
+     *
+     * @protected
+     * @returns {Bool}
+     */
+    Output.prototype._isStacked = function() {
+        var stacked = false;
+
+        // Stacking is (currently) only supported for bar charts.
+        if (this._chart.getType() === Bar.prototype.TYPE) {
+            stacked = this._chart.getStacked();
+        }
+
+        return stacked;
+    };
+
     /** @override */
     Output.prototype.update = function() {
         $.extend(true, this._config, this._makeConfig());
index 2ab02a1..3275e6e 100644 (file)
@@ -67,7 +67,7 @@ define([
             rowCount = series[0].getCount();
 
         // Identify the table.
-        tbl.addClass('chart-output-htmltable');
+        tbl.addClass('chart-output-htmltable generaltable');
 
         // Set the caption.
         if (c.getTitle() !== null) {
index 632e109..bce2ef2 100644 (file)
@@ -38,6 +38,21 @@ define(['core/chart_base'], function(Base) {
     /** @override */
     Pie.prototype.TYPE = 'pie';
 
+    /**
+     * Whether the chart should be displayed as doughnut or not.
+     *
+     * @type {Bool}
+     * @protected
+     */
+    Pie.prototype._doughnut = null;
+
+    /** @override */
+    Pie.prototype.create = function(Klass, data) {
+        var chart = Base.prototype.create.apply(this, arguments);
+        chart.setDoughnut(data.doughnut);
+        return chart;
+    };
+
     /**
      * Overridden to add appropriate colors to the series.
      *
@@ -54,6 +69,26 @@ define(['core/chart_base'], function(Base) {
         return Base.prototype.addSeries.apply(this, arguments);
     };
 
+    /**
+     * Get whether the chart should be displayed as doughnut or not.
+     *
+     * @method getDoughnut
+     * @returns {Bool}
+     */
+    Pie.prototype.getDoughnut = function() {
+        return this._doughnut;
+    };
+
+    /**
+     * Set whether the chart should be displayed as doughnut or not.
+     *
+     * @method setDoughnut
+     * @param {Bool} doughnut True for doughnut type, false for pie.
+     */
+    Pie.prototype.setDoughnut = function(doughnut) {
+        this._doughnut = Boolean(doughnut);
+    };
+
     /**
      * Validate a series.
      *
index 77e7c5e..4170825 100644 (file)
@@ -128,7 +128,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                     inputelement = $('<input type="text"/>').
                         attr('id', uniqueId('id_inplacevalue_', 20)).
                         attr('value', el.attr('data-value')).
-                        attr('aria-describedby', instr.attr('id')),
+                        attr('aria-describedby', instr.attr('id')).
+                        addClass('ignoredirty'),
                     lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>').
                         attr('for', inputelement.attr('id'));
                 el.html('').append(instr).append(lbl).append(inputelement);
index 6eeeea5..7f45924 100644 (file)
@@ -175,6 +175,7 @@ define(['core/mustache',
             attributes: [
                 {name: 'src', value: url},
                 {name: 'alt', value: helper(text)},
+                {name: 'title', value: helper(text)},
                 {name: 'class', value: 'smallicon'}
             ]
         };
index 5276703..3fd7d2b 100644 (file)
@@ -764,6 +764,17 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
             // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check
             // is faster than to send the 4 xpath queries for each step.
             if (!$this->getSession()->getDriver()->find($joinedxpath)) {
+                // Check if we have recorded any errors in driver process.
+                $phperrors = behat_get_shutdown_process_errors();
+                if (!empty($phperrors)) {
+                    foreach ($phperrors as $error) {
+                        $errnostring = behat_get_error_string($error['type']);
+                        $msgs[] = $errnostring . ": " .$error['message'] . " at " . $error['file'] . ": " . $error['line'];
+                    }
+                    $msg = "PHP errors found:\n" . implode("\n", $msgs);
+                    throw new \Exception(htmlentities($msg));
+                }
+
                 return;
             }
 
@@ -776,8 +787,20 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
                 if (empty($errorinfoboxes)) {
                     $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny');
                 }
-                $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
-                    $this->get_debug_text($errorinfoboxes[1]->getHtml());
+
+                // If errorinfoboxes is empty, try find ajax/JS exception in dialogue.
+                if (empty($errorinfoboxes)) {
+                    $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.moodle-exception-message');
+
+                    // If ajax/JS exception.
+                    if ($errorinfoboxes) {
+                        $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml());
+                    }
+
+                } else {
+                    $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
+                        $this->get_debug_text($errorinfoboxes[1]->getHtml());
+                }
 
                 $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo;
                 throw new \Exception(html_entity_decode($msg));
index 54049f5..6b237f2 100644 (file)
@@ -25,9 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once(__DIR__ . '/../lib.php');
-require_once(__DIR__ . '/behat_command.php');
-require_once(__DIR__ . '/../../testing/classes/tests_finder.php');
+require_once(__DIR__ . '/behat_config_util.php');
 
 /**
  * Behat configuration manager
@@ -47,6 +45,24 @@ class behat_config_manager {
      */
     public static $autoprofileconversion = false;
 
+    /**
+     * @var behat_config_util keep object of behat_config_util for use.
+     */
+    public static $behatconfigutil = null;
+
+    /**
+     * Returns behat_config_util.
+     *
+     * @return behat_config_util
+     */
+    private static function get_behat_config_util() {
+        if (!self::$behatconfigutil) {
+            self::$behatconfigutil = new behat_config_util();
+        }
+
+        return self::$behatconfigutil;
+    }
+
     /**
      * Updates a config file
      *
@@ -59,9 +75,14 @@ class behat_config_manager {
      * @param  string $component Restricts the obtained steps definitions to the specified component
      * @param  string $testsrunner If the config file will be used to run tests
      * @param  string $tags features files including tags.
+     * @param  bool   $themesuitewithallfeatures if only theme specific features need to be included in the suite.
+     * @param  int    $parallelruns number of parallel runs.
+     * @param  int    $run current run for which config needs to be updated.
      * @return void
      */
-    public static function update_config_file($component = '', $testsrunner = true, $tags = '') {
+    public static function update_config_file($component = '', $testsrunner = true, $tags = '',
+        $themesuitewithallfeatures = false, $parallelruns = 0, $run = 0) {
+
         global $CFG;
 
         // Behat must have a separate behat.yml to have access to the whole set of features and steps definitions.
@@ -72,49 +93,36 @@ class behat_config_manager {
             $configfilepath = self::get_steps_list_config_filepath();
         }
 
-        // Gets all the components with features.
-        $features = array();
-        $components = tests_finder::get_components_with_tests('features');
-        if ($components) {
-            foreach ($components as $componentname => $path) {
-                $path = self::clean_path($path) . self::get_behat_tests_path();
-                if (empty($featurespaths[$path]) && file_exists($path)) {
-
-                    // Standarizes separator (some dirs. comes with OS-dependant separator).
-                    $uniquekey = str_replace('\\', '/', $path);
-                    $featurespaths[$uniquekey] = $path;
-                }
-            }
-            foreach ($featurespaths as $path) {
-                $additional = glob("$path/*.feature");
-                $features = array_merge($features, $additional);
-            }
-        }
+        $behatconfigutil = self::get_behat_config_util();
+        $behatconfigutil->set_theme_suite_to_include_core_features($themesuitewithallfeatures);
+        $behatconfigutil->set_tag_for_feature_filter($tags);
 
-        // Optionally include features from additional directories.
-        if (!empty($CFG->behat_additionalfeatures)) {
-            $features = array_merge($features, array_map("realpath", $CFG->behat_additionalfeatures));
+        // Gets all the components with features, if running the tests otherwise not required.
+        $features = array();
+        if ($testsrunner) {
+            $features = $behatconfigutil->get_components_features();
         }
 
         // Gets all the components with steps definitions.
-        $stepsdefinitions = array();
-        $steps = self::get_components_steps_definitions();
-        if ($steps) {
-            foreach ($steps as $key => $filepath) {
-                if ($component == '' || $component === $key) {
-                    $stepsdefinitions[$key] = $filepath;
-                }
-            }
-        }
-
+        $stepsdefinitions = $behatconfigutil->get_components_contexts($component);
         // We don't want the deprecated steps definitions here.
         if (!$testsrunner) {
             unset($stepsdefinitions['behat_deprecated']);
         }
 
+        // Get current run.
+        if (empty($run) && ($run !== false) && !empty($CFG->behatrunprocess)) {
+            $run = $CFG->behatrunprocess;
+        }
+
+        // Get number of parallel runs if not passed.
+        if (empty($parallelruns) && ($parallelruns !== false)) {
+            $parallelruns = self::get_parallel_test_runs();
+        }
+
         // Behat config file specifing the main context class,
         // the required Behat extensions and Moodle test wwwroot.
-        $contents = self::get_config_file_contents(self::get_features_with_tags($features, $tags), $stepsdefinitions);
+        $contents = $behatconfigutil->get_config_file_contents($features, $stepsdefinitions, $tags, $parallelruns, $run);
 
         // Stores the file.
         if (!file_put_contents($configfilepath, $contents)) {
@@ -129,55 +137,13 @@ class behat_config_manager {
      * @param array $features set of feature files.
      * @param string $tags list of tags (currently support && only.)
      * @return array filtered list of feature files with tags.
+     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
+     * @todo MDL-55365 This will be deleted in Moodle 3.6.
      */
     public static function get_features_with_tags($features, $tags) {
-        if (empty($tags)) {
-            return $features;
-        }
-        $newfeaturelist = array();
-        // Split tags in and and or.
-        $tags = explode('&&', $tags);
-        $andtags = array();
-        $ortags = array();
-        foreach ($tags as $tag) {
-            // Explode all tags seperated by , and add it to ortags.
-            $ortags = array_merge($ortags, explode(',', $tag));
-            // And tags will be the first one before comma(,).
-            $andtags[] = preg_replace('/,.*/', '', $tag);
-        }
 
-        foreach ($features as $featurefile) {
-            $contents = file_get_contents($featurefile);
-            $includefeature = true;
-            foreach ($andtags as $tag) {
-                // If negitive tag, then ensure it don't exist.
-                if (strpos($tag, '~') !== false) {
-                    $tag = substr($tag, 1);
-                    if ($contents && strpos($contents, $tag) !== false) {
-                        $includefeature = false;
-                        break;
-                    }
-                } else if ($contents && strpos($contents, $tag) === false) {
-                    $includefeature = false;
-                    break;
-                }
-            }
-
-            // If feature not included then check or tags.
-            if (!$includefeature && !empty($ortags)) {
-                foreach ($ortags as $tag) {
-                    if ($contents && (strpos($tag, '~') === false) && (strpos($contents, $tag) !== false)) {
-                        $includefeature = true;
-                        break;
-                    }
-                }
-            }
-
-            if ($includefeature) {
-                $newfeaturelist[] = $featurefile;
-            }
-        }
-        return $newfeaturelist;
+        debugging('Use of get_features_with_tags is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
+        return self::get_behat_config_util()->filtered_features_with_tags($features, $tags);
     }
 
     /**
@@ -189,32 +155,14 @@ class behat_config_manager {
      * it from the steps definitions web interface
      *
      * @return array
+     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
+     * @todo MDL-55365 This will be deleted in Moodle 3.6.
      */
     public static function get_components_steps_definitions() {
 
-        $components = tests_finder::get_components_with_tests('stepsdefinitions');
-        if (!$components) {
-            return false;
-        }
-
-        $stepsdefinitions = array();
-        foreach ($components as $componentname => $componentpath) {
-            $componentpath = self::clean_path($componentpath);
-
-            if (!file_exists($componentpath . self::get_behat_tests_path())) {
-                continue;
-            }
-            $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path());
-            $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
-
-            // All behat_*.php inside behat_config_manager::get_behat_tests_path() are added as steps definitions files.
-            foreach ($regite as $file) {
-                $key = $file->getBasename('.php');
-                $stepsdefinitions[$key] = $file->getPathname();
-            }
-        }
-
-        return $stepsdefinitions;
+        debugging('Use of get_components_steps_definitions is deprecated, please see behat_config_util::get_components_contexts',
+            DEBUG_DEVELOPER);
+        return self::get_behat_config_util()->get_components_contexts();
     }
 
     /**
@@ -361,91 +309,13 @@ class behat_config_manager {
      * @param array $features The system feature files
      * @param array $stepsdefinitions The system steps definitions
      * @return string
+     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
+     * @todo MDL-55365 This will be deleted in Moodle 3.6.
      */
     protected static function get_config_file_contents($features, $stepsdefinitions) {
-        global $CFG;
-
-        // We require here when we are sure behat dependencies are available.
-        require_once($CFG->dirroot . '/vendor/autoload.php');
-
-        $selenium2wdhost = array('wd_host' => 'http://localhost:4444/wd/hub');
-
-        $parallelruns = self::get_parallel_test_runs();
-        // If parallel run, then only divide features.
-        if (!empty($CFG->behatrunprocess) && !empty($parallelruns)) {
-            // Attempt to split into weighted buckets using timing information, if available.
-            if ($alloc = self::profile_guided_allocate($features, max(1, $parallelruns), $CFG->behatrunprocess)) {
-                $features = $alloc;
-            } else {
-                // Divide the list of feature files amongst the parallel runners.
-                srand(crc32(floor(time() / 3600 / 24) . var_export($features, true)));
-                shuffle($features);
-                // Pull out the features for just this worker.
-                if (count($features)) {
-                    $features = array_chunk($features, ceil(count($features) / max(1, $parallelruns)));
-                    // Check if there is any feature file for this process.
-                    if (!empty($features[$CFG->behatrunprocess - 1])) {
-                        $features = $features[$CFG->behatrunprocess - 1];
-                    } else {
-                        $features = null;
-                    }
-                }
-            }
-            // Set proper selenium2 wd_host if defined.
-            if (!empty($CFG->behat_parallel_run[$CFG->behatrunprocess - 1]['wd_host'])) {
-                $selenium2wdhost = array('wd_host' => $CFG->behat_parallel_run[$CFG->behatrunprocess - 1]['wd_host']);
-            }
-        }
-
-        // It is possible that it has no value as we don't require a full behat setup to list the step definitions.
-        if (empty($CFG->behat_wwwroot)) {
-            $CFG->behat_wwwroot = 'http://itwillnotbeused.com';
-        }
-
-        // Comments use black color, so failure path is not visible. Using color other then black/white is safer.
-        // https://github.com/Behat/Behat/pull/628.
-        $config = array(
-            'default' => array(
-                'formatters' => array(
-                    'moodle_progress' => array(
-                        'output_styles' => array(
-                            'comment' => array('magenta'))
-                        )
-                ),
-                'suites' => array(
-                    'default' => array(
-                        'paths' => $features,
-                        'contexts' => array_keys($stepsdefinitions)
-                    )
-                ),
-                'extensions' => array(
-                    'Behat\MinkExtension' => array(
-                        'base_url' => $CFG->behat_wwwroot,
-                        'goutte' => null,
-                        'selenium2' => $selenium2wdhost
-                    ),
-                    'Moodle\BehatExtension' => array(
-                        'moodledirroot' => $CFG->dirroot,
-                        'steps_definitions' => $stepsdefinitions
-                    )
-                )
-            )
-        );
-
-        // In case user defined overrides respect them over our default ones.
-        if (!empty($CFG->behat_config)) {
-            foreach ($CFG->behat_config as $profile => $values) {
-                $config = self::merge_config($config, self::merge_behat_config($profile, $values));
-            }
-        }
-        // Check for Moodle custom ones.
-        if (!empty($CFG->behat_profiles) && is_array($CFG->behat_profiles)) {
-            foreach ($CFG->behat_profiles as $profile => $values) {
-                $config = self::merge_config($config, self::get_behat_profile($profile, $values));
-            }
-        }
 
-        return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
+        debugging('Use of get_config_file_contents is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
+        return self::get_behat_config_util()->get_config_file_contents($features, $stepsdefinitions);
     }
 
     /**
@@ -454,41 +324,13 @@ class behat_config_manager {
      * @param string $profile profile name
      * @param array $values values for profile
      * @return array
+     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
+     * @todo MDL-55365 This will be deleted in Moodle 3.6.
      */
     protected static function merge_behat_config($profile, $values) {
-        // Only add profile which are compatible with Behat 3.x
-        // Just check if any of Bheat 2.5 config is set. Not checking for 3.x as it might have some other configs
-        // Like : rerun_cache etc.
-        if (!isset($values['filters']['tags']) && !isset($values['extensions']['Behat\MinkExtension\Extension'])) {
-            return array($profile => $values);
-        }
-
-        // Parse 2.5 format and get related values.
-        $oldconfigvalues = array();
-        if (isset($values['extensions']['Behat\MinkExtension\Extension'])) {
-            $extensionvalues = $values['extensions']['Behat\MinkExtension\Extension'];
-            if (isset($extensionvalues['selenium2']['browser'])) {
-                $oldconfigvalues['browser'] = $extensionvalues['selenium2']['browser'];
-            }
-            if (isset($extensionvalues['selenium2']['wd_host'])) {
-                $oldconfigvalues['wd_host'] = $extensionvalues['selenium2']['wd_host'];
-            }
-            if (isset($extensionvalues['capabilities'])) {
-                $oldconfigvalues['capabilities'] = $extensionvalues['capabilities'];
-            }
-        }
 
-        if (isset($values['filters']['tags'])) {
-            $oldconfigvalues['tags'] = $values['filters']['tags'];
-        }
-
-        if (!empty($oldconfigvalues)) {
-            self::$autoprofileconversion = true;
-            return self::get_behat_profile($profile, $oldconfigvalues);
-        }
-
-        // If nothing set above then return empty array.
-        return array();
+        debugging('Use of merge_behat_config is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
+        self::get_behat_config_util()->get_behat_config_for_profile($profile, $values);
     }
 
     /**
@@ -569,64 +411,8 @@ class behat_config_manager {
      */
     protected static function profile_guided_allocate($features, $nbuckets, $instance) {
 
-        $behattimingfile = defined('BEHAT_FEATURE_TIMING_FILE') &&
-            @filesize(BEHAT_FEATURE_TIMING_FILE) ? BEHAT_FEATURE_TIMING_FILE : false;
-
-        if (!$behattimingfile || !$behattimingdata = @json_decode(file_get_contents($behattimingfile), true)) {
-            // No data available, fall back to relying on steps data.
-            $stepfile = "";
-            if (defined('BEHAT_FEATURE_STEP_FILE') && BEHAT_FEATURE_STEP_FILE) {
-                $stepfile = BEHAT_FEATURE_STEP_FILE;
-            }
-            // We should never get this. But in case we can't do this then fall back on simple splitting.
-            if (empty($stepfile) || !$behattimingdata = @json_decode(file_get_contents($stepfile), true)) {
-                return false;
-            }
-        }
-
-        arsort($behattimingdata); // Ensure most expensive is first.
-
-        $realroot = realpath(__DIR__.'/../../../').'/';
-        $defaultweight = array_sum($behattimingdata) / count($behattimingdata);
-        $weights = array_fill(0, $nbuckets, 0);
-        $buckets = array_fill(0, $nbuckets, array());
-        $totalweight = 0;
-
-        // Re-key the features list to match timing data.
-        foreach ($features as $k => $file) {
-            $key = str_replace($realroot, '', $file);
-            $features[$key] = $file;
-            unset($features[$k]);
-            if (!isset($behattimingdata[$key])) {
-                $behattimingdata[$key] = $defaultweight;
-            }
-        }
-
-        // Sort features by known weights; largest ones should be allocated first.
-        $behattimingorder = array();
-        foreach ($features as $key => $file) {
-            $behattimingorder[$key] = $behattimingdata[$key];
-        }
-        arsort($behattimingorder);
-
-        // Finally, add each feature one by one to the lightest bucket.
-        foreach ($behattimingorder as $key => $weight) {
-            $file = $features[$key];
-            $lightbucket = array_search(min($weights), $weights);
-            $weights[$lightbucket] += $weight;
-            $buckets[$lightbucket][] = $file;
-            $totalweight += $weight;
-        }
-
-        if ($totalweight && !defined('BEHAT_DISABLE_HISTOGRAM') && $instance == $nbuckets) {
-            echo "Bucket weightings:\n";
-            foreach ($weights as $k => $weight) {
-                echo $k + 1 . ": " . str_repeat('*', 70 * $nbuckets * $weight / $totalweight) . PHP_EOL;
-            }
-        }
-
-        // Return the features for this worker.
-        return $buckets[$instance - 1];
+        debugging('Use of profile_guided_allocate is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
+        return self::get_behat_config_util()->profile_guided_allocate($features, $nbuckets, $instance);
     }
 
     /**
@@ -637,34 +423,13 @@ class behat_config_manager {
      * @param mixed $config The node of the default config
      * @param mixed $localconfig The node of the local config
      * @return mixed The merge result
+     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
+     * @todo MDL-55365 This will be deleted in Moodle 3.6.
      */
     protected static function merge_config($config, $localconfig) {
 
-        if (!is_array($config) && !is_array($localconfig)) {
-            return $localconfig;
-        }
-
-        // Local overrides also deeper default values.
-        if (is_array($config) && !is_array($localconfig)) {
-            return $localconfig;
-        }
-
-        foreach ($localconfig as $key => $value) {
-
-            // If defaults are not as deep as local values let locals override.
-            if (!is_array($config)) {
-                unset($config);
-            }
-
-            // Add the param if it doesn't exists or merge branches.
-            if (empty($config[$key])) {
-                $config[$key] = $value;
-            } else {
-                $config[$key] = self::merge_config($config[$key], $localconfig[$key]);
-            }
-        }
-
-        return $config;
+        debugging('Use of merge_config is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
+        return self::get_behat_config_util()->merge_config($config, $localconfig);
     }
 
     /**
@@ -673,28 +438,25 @@ class behat_config_manager {
      * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/
      * @param string $path
      * @return string The string without the last /tests part
+     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
+     * @todo MDL-55365 This will be deleted in Moodle 3.6.
      */
     protected final static function clean_path($path) {
 
-        $path = rtrim($path, DIRECTORY_SEPARATOR);
-
-        $parttoremove = DIRECTORY_SEPARATOR . 'tests';
-
-        $substr = substr($path, strlen($path) - strlen($parttoremove));
-        if ($substr == $parttoremove) {
-            $path = substr($path, 0, strlen($path) - strlen($parttoremove));
-        }
-
-        return rtrim($path, DIRECTORY_SEPARATOR);
+        debugging('Use of clean_path is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
+        return self::get_behat_config_util()->clean_path($path);
     }
 
     /**
      * The relative path where components stores their behat tests
      *
      * @return string
+     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
+     * @todo MDL-55365 This will be deleted in Moodle 3.6.
      */
     protected final static function get_behat_tests_path() {
-        return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat';
+        debugging('Use of get_behat_tests_path is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
+        return self::get_behat_config_util()->get_behat_tests_path();
     }
 
 }
diff --git a/lib/behat/classes/behat_config_util.php b/lib/behat/classes/behat_config_util.php
new file mode 100644 (file)
index 0000000..1dcc33c
--- /dev/null
@@ -0,0 +1,1264 @@
+<?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/>.
+
+/**
+ * Utils to set Behat config
+ *
+ * @package    core
+ * @copyright  2016 Rajesh Taneja
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/../lib.php');
+require_once(__DIR__ . '/behat_command.php');
+require_once(__DIR__ . '/../../testing/classes/tests_finder.php');
+
+/**
+ * Behat configuration manager
+ *
+ * Creates/updates Behat config files getting tests
+ * and steps from Moodle codebase
+ *
+ * @package    core
+ * @copyright  2016 Rajesh Taneja
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_config_util {
+
+    /**
+     * @var array list of features in core.
+     */
+    private $features;
+
+    /**
+     * @var array list of contexts in core.
+     */
+    private $contexts;
+
+    /**
+     * @var array list of theme specific contexts.
+     */
+    private $themecontexts;
+
+    /**
+     * @var array list of all contexts in theme suite.
+     */
+    private $themesuitecontexts;
+
+    /**
+     * @var array list of overridden theme contexts.
+     */
+    private $overriddenthemescontexts;
+
+    /**
+     * @var array list of components with tests.
+     */
+    private $componentswithtests;
+
+    /**
+     * @var bool keep track of theme to return suite with all core features included or not.
+     */
+    private $themesuitewithallfeatures = false;
+
+    /**
+     * @var string filter features which have tags.
+     */
+    private $tags = '';
+
+    /**
+     * @var int number of parallel runs.
+     */
+    private $parallelruns = 0;
+
+    /**
+     * @var int current run.
+     */
+    private $currentrun = 0;
+
+    /**
+     * Set value for theme suite to include all core features. This should be used if your want all core features to be
+     * run with theme.
+     *
+     * @param bool $val
+     */
+    public function set_theme_suite_to_include_core_features($val) {
+        $this->themesuitewithallfeatures = $val;
+    }
+
+    /**
+     * Set the value for tags, so features which are returned will be using filtered by this.
+     *
+     * @param string $tags
+     */
+    public function set_tag_for_feature_filter($tags) {
+        $this->tags = $tags;
+    }
+
+    /**
+     * Set parallel run to be used for generating config.
+     *
+     * @param int $parallelruns number of parallel runs.
+     * @param int $currentrun current run
+     */
+    public function set_parallel_run($parallelruns, $currentrun) {
+
+        if ($parallelruns < $currentrun) {
+            behat_error(BEHAT_EXITCODE_REQUIREMENT,
+                'Parallel runs('.$parallelruns.') should be more then current run('.$currentrun.')');
+        }
+
+        $this->parallelruns = $parallelruns;
+        $this->currentrun = $currentrun;
+    }
+
+    /**
+     * Return parallel runs
+     *
+     * @return int number of parallel runs.
+     */
+    public function get_number_of_parallel_run() {
+        // Get number of parallel runs if not passed.
+        if (empty($this->parallelruns) && ($this->parallelruns !== false)) {
+            $this->parallelruns = behat_config_manager::get_parallel_test_runs();
+        }
+
+        return $this->parallelruns;
+    }
+
+    /**
+     * Return current run
+     *
+     * @return int current run.
+     */
+    public function get_current_run() {
+        global $CFG;
+
+        // Get number of parallel runs if not passed.
+        if (empty($this->currentrun) && ($this->currentrun !== false) && !empty($CFG->behatrunprocess)) {
+            $this->currentrun = $CFG->behatrunprocess;
+        }
+
+        return $this->currentrun;
+    }
+
+    /**
+     * Return list of features.
+     *
+     * @param string $tags tags.
+     * @return array
+     */
+    public function get_components_features($tags = '') {
+        global $CFG;
+
+        // If we already have a list created then just return that, as it's up-to-date.
+        // If tags are passed then it's a new filter of features we need.
+        if (!empty($this->features) && empty($tags)) {
+            return $this->features;
+        }
+
+        // Gets all the components with features.
+        $features = array();
+        $featurespaths = array();
+        $components = $this->get_components_with_tests();
+
+        if ($components) {
+            foreach ($components as $componentname => $path) {
+                $path = $this->clean_path($path) . self::get_behat_tests_path();
+                if (empty($featurespaths[$path]) && file_exists($path)) {
+                    list($key, $featurepath) = $this->get_clean_feature_key_and_path($path);
+                    $featurespaths[$key] = $featurepath;
+                }
+            }
+            foreach ($featurespaths as $path) {
+                $additional = glob("$path/*.feature");
+
+                $additionalfeatures = array();
+                foreach ($additional as $featurepath) {
+                    list($key, $path) = $this->get_clean_feature_key_and_path($featurepath);
+                    $additionalfeatures[$key] = $path;
+                }
+
+                $features = array_merge($features, $additionalfeatures);
+            }
+        }
+
+        // Optionally include features from additional directories.
+        if (!empty($CFG->behat_additionalfeatures)) {
+            $additional = array_map("realpath", $CFG->behat_additionalfeatures);
+            $additionalfeatures = array();
+            foreach ($additional as $featurepath) {
+                list($key, $path) = $this->get_clean_feature_key_and_path($featurepath);
+                $additionalfeatures[$key] = $path;
+            }
+            $features = array_merge($features, $additionalfeatures);
+        }
+
+        $this->features = $features;
+
+        return $this->filtered_features_with_tags($features, $tags);
+    }
+
+    /**
+     * Return feature key for featurepath
+     *
+     * @param string $featurepath
+     * @return array key and featurepath.
+     */
+    public function get_clean_feature_key_and_path($featurepath) {
+        global $CFG;
+
+        // Fix directory path.
+        $featurepath = str_replace('\\', DIRECTORY_SEPARATOR, $featurepath);
+        $featurepath = str_replace('/', DIRECTORY_SEPARATOR, $featurepath);
+
+        if (testing_is_cygwin()) {
+            $featurepath = str_replace('\\', '/', $featurepath);
+        }
+
+        $key = basename($featurepath, '.feature');
+
+        // Get relative path.
+        $featuredirname = str_replace($CFG->dirroot . DIRECTORY_SEPARATOR , '', $featurepath);
+        // Get 5 levels of feature path to ensure we have a unique key.
+        for ($i = 0; $i < 5; $i++) {
+            if (($featuredirname = dirname($featuredirname)) && $featuredirname !== '.') {
+                if ($basename = basename($featuredirname)) {
+                    $key .= '_' . $basename;
+                }
+            }
+        }
+
+        return array($key, $featurepath);
+    }
+
+    /**
+     * Get component contexts.
+     *
+     * @param string $component component name.
+     * @return array
+     */
+    private function get_component_contexts($component) {
+
+        if (empty($component)) {
+            return $this->contexts;
+        }
+
+        $componentcontexts = array();
+        foreach ($this->contexts as $key => $path) {
+            if ($component == '' || $component === $key) {
+                $componentcontexts[$key] = $path;
+            }
+        }
+
+        return $componentcontexts;
+    }
+
+    /**
+     * Gets the list of Moodle behat contexts
+     *
+     * Class name as a key and the filepath as value
+     *
+     * Externalized from update_config_file() to use
+     * it from the steps definitions web interface
+     *
+     * @param  string $component Restricts the obtained steps definitions to the specified component
+     * @return array
+     */
+    public function get_components_contexts($component = '') {
+
+        // If we already have a list created then just return that, as it's up-to-date.
+        if (!empty($this->contexts)) {
+            return $this->get_component_contexts($component);
+        }
+
+        $components = $this->get_components_with_tests();
+
+        $this->contexts = array();
+        foreach ($components as $componentname => $componentpath) {
+            $componentpath = self::clean_path($componentpath);
+
+            if (!file_exists($componentpath . self::get_behat_tests_path())) {
+                continue;
+            }
+            $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path());
+            $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
+
+            // All behat_*.php inside self::get_behat_tests_path() are added as steps definitions files.
+            foreach ($regite as $file) {
+                $key = $file->getBasename('.php');
+                $this->contexts[$key] = $file->getPathname();
+            }
+        }
+
+        return $this->get_component_contexts($component);
+    }
+
+    /**
+     * Behat config file specifing the main context class,
+     * the required Behat extensions and Moodle test wwwroot.
+     *
+     * @param array $features The system feature files
+     * @param array $contexts The system steps definitions
+     * @param string $tags filter features with specified tags.
+     * @param int $parallelruns number of parallel runs.
+     * @param int $currentrun current run for which config file is needed.
+     * @return string
+     */
+    public function get_config_file_contents($features = '', $contexts = '', $tags = '', $parallelruns = 0, $currentrun = 0) {
+        global $CFG;
+
+        // Set current run and parallel run.
+        if (!empty($parallelruns) && !empty($currentrun)) {
+            $this->set_parallel_run($parallelruns, $currentrun);
+        }
+
+        // If tags defined then use them. This is for BC.
+        if (!empty($tags)) {
+            $this->set_tag_for_feature_filter($tags);
+        }
+
+        // If features not passed then get it. Empty array means we don't need to include features.
+        if (empty($features) && !is_array($features)) {
+            $features = $this->get_components_features();
+        } else {
+            $this->features = $features;
+        }
+
+        // If stepdefinitions not passed then get the list.
+        if (empty($contexts)) {
+            $this->get_components_contexts();
+        } else {
+            $this->contexts = $contexts;
+        }
+
+        // We require here when we are sure behat dependencies are available.
+        require_once($CFG->dirroot . '/vendor/autoload.php');
+
+        $config = $this->build_config();
+
+        $config = $this->merge_behat_config($config);
+
+        $config = $this->merge_behat_profiles($config);
+
+        return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
+    }
+
+    /**
+     * Search feature files for set of tags.
+     *
+     * @param array $features set of feature files.
+     * @param string $tags list of tags (currently support && only.)
+     * @return array filtered list of feature files with tags.
+     */
+    public function filtered_features_with_tags($features = '', $tags = '') {
+
+        // This is for BC. Features if not passed then we already have a list in this object.
+        if (empty($features)) {
+            $features = $this->features;
+        }
+
+        // If no tags defined then return full list.
+        if (empty($tags) && empty($this->tags)) {
+            return $features;
+        }
+
+        // If no tags passed by the caller, then it's already set.
+        if (empty($tags)) {
+            $tags = $this->tags;
+        }
+
+        $newfeaturelist = array();
+        // Split tags in and and or.
+        $tags = explode('&&', $tags);
+        $andtags = array();
+        $ortags = array();
+        foreach ($tags as $tag) {
+       &nbs