Merge branch 'MDL-55632-master' of git://github.com/andrewnicols/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 12 Sep 2016 17:12:26 +0000 (19:12 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 12 Sep 2016 17:12:26 +0000 (19:12 +0200)
260 files changed:
.csslintrc [deleted file]
.stylelintrc
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/monitor/lang/en/tool_monitor.php
backup/import.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_controller_dbops.class.php
backup/util/dbops/restore_dbops.class.php
badges/badge.php
badges/criteria/award_criteria_activity.php
badges/criteria/award_criteria_course.php
badges/criteria/award_criteria_manual.php
badges/tests/behat/award_badge.feature
badges/tests/events_test.php
badges/view.php
blocks/navigation/tests/behat/participants_link.feature [new file with mode: 0644]
blocks/recent_activity/tests/behat/structural_changes.feature
blocks/rss_client/templates/feed.mustache
blog/rsslib.php
cache/classes/definition.php
cache/classes/loaders.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
calendar/export.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/preferences.php [deleted file]
calendar/tests/behat/calendar_lookahead.feature
calendar/upgrade.txt
calendar/view.php
config-dist.php
course/externallib.php
course/lib.php
course/tests/courselib_test.php
course/tests/externallib_test.php
enrol/imsenterprise/lang/en/enrol_imsenterprise.php
enrol/instances.php
enrol/lti/lang/en/enrol_lti.php
filter/mathjaxloader/filter.php
filter/mediaplugin/tests/filter_test.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/badges.php
lang/en/grades.php
lang/en/moodle.php
lang/en/search.php
lib/accesslib.php
lib/adminlib.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/behat_field_manager.php
lib/behat/behat_files.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/form_field/behat_form_filemanager.php
lib/behat/lib.php
lib/classes/chart_bar.php
lib/classes/chart_pie.php
lib/classes/component.php
lib/classes/event/badge_listing_viewed.php [new file with mode: 0644]
lib/classes/event/badge_viewed.php [new file with mode: 0644]
lib/classes/event/search_indexed.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/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/externallib.php
lib/formslib.php
lib/grade/grade_item.php
lib/gradelib.php
lib/medialib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/phpunit/bootstrap.php
lib/phpunit/classes/util.php
lib/requirejs/moodle-config.js
lib/sessionlib.php
lib/setup.php
lib/statslib.php
lib/tablelib.php
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/htmlpurifier_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/weblib_test.php
lib/upgrade.txt
lib/weblib.php
lib/xhprof/xhprof_html/callgraph.php
lib/xhprof/xhprof_html/index.php
lib/xhprof/xhprof_moodle.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
login/forgot_password.php
login/signup_form.php
mod/assign/assignmentplugin.php
mod/assign/backup/moodle2/backup_assign_stepslib.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
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/upgrade.txt
mod/choice/mod_form.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/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/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/feedback/item/multichoice/lib.php
mod/feedback/item/multichoicerated/lib.php
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/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/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/report.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/create_form.php
mod/wiki/lib.php
mod/wiki/locallib.php
mod/wiki/pagelib.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
mod/workshop/lib.php
question/category_class.php
question/format.php
question/tests/generator/lib.php
report/log/styles.css
report/participation/index.php
report/security/lang/en/report_security.php
report/security/locallib.php
search/engine/solr/classes/document.php
search/engine/solr/classes/engine.php
search/engine/solr/tests/engine_test.php
search/tests/generator/lib.php
theme/bootstrapbase/layout/columns2.php
theme/bootstrapbase/layout/columns3.php
theme/bootstrapbase/layout/secure.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/lang/en/theme_clean.php
theme/clean/layout/columns2.php
theme/clean/layout/columns3.php
theme/clean/layout/secure.php
theme/more/lang/en/theme_more.php
user/calendar.php [new file with mode: 0644]
user/classes/form/calendar_form.php [moved from calendar/preferences_form.php with 52% similarity]
user/editadvanced_form.php
user/editlib.php
version.php
webservice/lib.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 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 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 af3cf3d..04681c9 100644 (file)
@@ -145,7 +145,6 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
             if (!empty($precheckresults['errors'])) { // If errors are found, terminate the import.
                 fulldelete($tempdestination);
 
-                echo $OUTPUT->header();
                 echo $renderer->precheck_notices($precheckresults);
                 echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id)));
                 echo $OUTPUT->footer();
index 06aa423..0b5fb9b 100644 (file)
@@ -105,6 +105,12 @@ abstract class backup_controller_dbops extends backup_dbops {
             throw new backup_dbops_exception('backup_controller_dbops_nonexisting');
         }
         $controller = unserialize(base64_decode($controllerrec->controller));
+        if (!is_object($controller)) {
+            // The controller field of the table did not contain a serialized object.
+            // It is made empty after it has been used successfully, it is likely that
+            // the user has pressed the browser back button at some point.
+            throw new backup_dbops_exception('backup_controller_dbops_loading_invalid_controller');
+        }
         // Check checksum is ok. Sounds silly but it isn't ;-)
         if (!$controller->is_checksum_correct($controllerrec->checksum)) {
             throw new backup_dbops_exception('backup_controller_dbops_loading_checksum_mismatch');
index b68358e..5691a92 100644 (file)
@@ -95,6 +95,12 @@ abstract class restore_controller_dbops extends restore_dbops {
             throw new backup_dbops_exception('restore_controller_dbops_nonexisting');
         }
         $controller = unserialize(base64_decode($controllerrec->controller));
+        if (!is_object($controller)) {
+            // The controller field of the table did not contain a serialized object.
+            // It is made empty after it has been used successfully, it is likely that
+            // the user has pressed the browser back button at some point.
+            throw new backup_dbops_exception('restore_controller_dbops_loading_invalid_controller');
+        }
         // Check checksum is ok. Sounds silly but it isn't ;-)
         if (!$controller->is_checksum_correct($controllerrec->checksum)) {
             throw new backup_dbops_exception('restore_controller_dbops_loading_checksum_mismatch');
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";
index 4d38580..662dc25 100644 (file)
@@ -71,4 +71,16 @@ echo $OUTPUT->header();
 
 echo $output->render($badge);
 
+// Trigger event, badge viewed.
+$other = array('badgeid' => $badge->badgeid, 'badgehash' => $id);
+$eventparams = array('context' => $PAGE->context, 'other' => $other);
+
+// If the badge does not belong to this user, log it appropriately.
+if (($badge->recipient->id != $USER->id)) {
+    $eventparams['relateduserid'] = $badge->recipient->id;
+}
+
+$event = \core\event\badge_viewed::create($eventparams);
+$event->trigger();
+
 echo $OUTPUT->footer();
index ca09936..171361b 100644 (file)
@@ -48,7 +48,7 @@ class award_criteria_activity extends award_criteria {
 
         $this->course = $DB->get_record_sql('SELECT c.id, c.enablecompletion, c.cacherev, c.startdate
                         FROM {badge} b INNER JOIN {course} c ON b.courseid = c.id
-                        WHERE b.id = :badgeid ', array('badgeid' => $this->badgeid));
+                        WHERE b.id = :badgeid ', array('badgeid' => $this->badgeid), MUST_EXIST);
         $this->courseid = $this->course->id;
     }
 
index 8862507..c8ae554 100644 (file)
@@ -50,7 +50,7 @@ class award_criteria_course extends award_criteria {
 
         $this->course = $DB->get_record_sql('SELECT c.id, c.enablecompletion, c.cacherev, c.startdate
                         FROM {badge} b INNER JOIN {course} c ON b.courseid = c.id
-                        WHERE b.id = :badgeid ', array('badgeid' => $this->badgeid));
+                        WHERE b.id = :badgeid ', array('badgeid' => $this->badgeid), MUST_EXIST);
         $this->courseid = $this->course->id;
     }
 
@@ -95,7 +95,7 @@ class award_criteria_course extends award_criteria {
      * @return string
      */
     public function get_details($short = '') {
-        global $DB;
+        global $DB, $OUTPUT;
         $param = reset($this->params);
 
         $course = $DB->get_record('course', array('id' => $param['course']));
index fd0ad2e..51a7959 100644 (file)
@@ -207,12 +207,15 @@ class award_criteria_manual extends award_criteria {
             return array($join, $where, $params);
         } else {
             foreach ($this->params as $param) {
-                $join .= " LEFT JOIN {badge_manual_award} bma{$param['role']} ON
-                          bma{$param['role']}.recipientid = u.id AND
-                          bma{$param['role']}.issuerrole = :issuerrole{$param['role']} ";
-                $where .= " AND bma{$param['role']}.issuerrole IS NOT NULL ";
+                $roledata[] = " bma.issuerrole = :issuerrole{$param['role']} ";
                 $params["issuerrole{$param['role']}"] = $param['role'];
             }
+            if (!empty($roledata)) {
+                $extraon = implode(' AND ', $roledata);
+                $join = " JOIN {badge_manual_award} bma ON bma.recipientid = u.id
+                          AND bma.badgeid = :badgeid{$this->badgeid} AND ({$extraon})";
+                $params["badgeid{$this->badgeid}"] = $this->badgeid;
+            }
             return array($join, $where, $params);
         }
     }
index bbc28ae..508fd42 100644 (file)
@@ -227,3 +227,86 @@ Feature: Award badges
     And I log in as "student1"
     And I follow "Profile" in the user menu
     Then I should see "Course Badge"
+
+  @javascript
+  Scenario: All of the selected roles can award badges
+    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 |
+    And the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    # Create course badge 1.
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge 1 |
+      | Description | Course badge description |
+      | issuername | Tester of course badge |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the field "type" to "Manual issue by role"
+    And I expand all fieldsets
+    # Set to ANY of the roles awards badge.
+    And I set the field "Teacher" to "1"
+    And I set the field "Any of the selected roles awards the badge" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    # Award course badge 1 to student 1.
+    And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
+    When I press "Award badge"
+    And I follow "Course Badge 1"
+    And I follow "Recipients (1)"
+    Then I should see "Recipients (1)"
+    # Add course badge 2.
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge 2 |
+      | Description | Course badge description |
+      | issuername | Tester of course badge |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the field "type" to "Manual issue by role"
+    And I expand all fieldsets
+    # Set to ALL of the selected roles award badge.
+    And I set the field "Teacher" to "1"
+    And I set the field "All of the selected roles award the badge" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    # Award course badge 2 to student 2.
+    And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)"
+    When I press "Award badge"
+    And I follow "Course Badge 2"
+    And I follow "Recipients (1)"
+    Then I should see "Recipients (1)"
+    And I log out
+    And I trigger cron
+    # Student 1 should have just course badge 1.
+    And I log in as "student1"
+    And I follow "Profile" in the user menu
+    When I follow "Course 1"
+    Then I should see "Course Badge 1"
+    And I should not see "Course Badge 2"
+    And I log out
+    # Student 2 should have just course badge 2.
+    And I log in as "student2"
+    And I follow "Profile" in the user menu
+    When I follow "Course 1"
+    Then I should see "Course Badge 2"
+    Then I should not see "Course Badge 1"
index cfe5215..e6c464f 100644 (file)
@@ -310,4 +310,66 @@ class core_badges_events_testcase extends core_badges_badgeslib_testcase {
         $sink->close();
 
     }
+
+    /**
+     * Test the badge viewed event.
+     *
+     * There is no external API for viewing a badge, so the unit test will simply
+     * create and trigger the event and ensure data is returned as expected.
+     */
+    public function test_badge_viewed() {
+
+        $badge = new badge($this->badgeid);
+        // Trigger an event: badge viewed.
+        $other = array('badgeid' => $badge->id, 'badgehash' => '12345678');
+        $eventparams = array(
+            'context' => $badge->get_context(),
+            'other' => $other,
+        );
+
+        $event = \core\event\badge_viewed::create($eventparams);
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_viewed', $event);
+        $this->assertEquals('12345678', $event->other['badgehash']);
+        $this->assertEquals($badge->id, $event->other['badgeid']);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Test the badge listing viewed event.
+     *
+     * There is no external API for viewing a badge, so the unit test will simply
+     * create and trigger the event and ensure data is returned as expected.
+     */
+    public function test_badge_listing_viewed() {
+
+        // Trigger an event: badge listing viewed.
+        $context = context_system::instance();
+        $eventparams = array(
+            'context' => $context,
+            'other' => array('badgetype' => BADGE_TYPE_SITE)
+        );
+
+        $event = \core\event\badge_listing_viewed::create($eventparams);
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_listing_viewed', $event);
+        $this->assertEquals(BADGE_TYPE_SITE, $event->other['badgetype']);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
 }
index efa53c8..5516bfd 100644 (file)
@@ -66,6 +66,7 @@ if ($type == BADGE_TYPE_SITE) {
     $PAGE->set_pagelayout('admin');
     $PAGE->set_heading($SITE->fullname);
     $title = get_string('sitebadges', 'badges');
+    $eventotherparams = array('badgetype' => BADGE_TYPE_SITE);
 } else {
     require_login($course);
     $coursename = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
@@ -73,6 +74,7 @@ if ($type == BADGE_TYPE_SITE) {
     $PAGE->set_context(context_course::instance($course->id));
     $PAGE->set_pagelayout('incourse');
     $PAGE->set_heading($coursename);
+    $eventotherparams = array('badgetype' => BADGE_TYPE_COURSE, 'courseid' => $course->id);
 }
 
 require_capability('moodle/badges:viewbadges', $PAGE->context);
@@ -104,5 +106,9 @@ if ($totalcount) {
 } else {
     echo $output->notification(get_string('nobadges', 'badges'));
 }
+// Trigger event, badge listing viewed.
+$eventparams = array('context' => $PAGE->context, 'other' => $eventotherparams);
+$event = \core\event\badge_listing_viewed::create($eventparams);
+$event->trigger();
 
 echo $output->footer();
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 a43dbb7..9f6d8da 100644 (file)
@@ -156,7 +156,8 @@ Feature: View structural changes in recent activity block
     And I should see "Added Forum" in the "Recent activity" "block"
     And I should see "ForumNew" in the "Recent activity" "block"
     And I log out
-    # Update forum as a teacher
+    # Update forum as a teacher after a second to ensure we have a new timestamp for recent activity.
+    And I wait "1" seconds
     And I log in as "teacher1"
     And I follow "Course 1"
     And I follow "ForumNew"
index ad9ae3e..a69f3e8 100644 (file)
@@ -66,7 +66,7 @@
 
 {{$title}}
     {{#title}}
-        <div class="title">{{feedtitle}}</div>
+        <div class="title">{{title}}</div>
     {{/title}}
 {{/title}}
 
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 7de7d5c..979c892 100644 (file)
@@ -114,7 +114,6 @@ $PAGE->navbar->add($pagetitle);
 $PAGE->set_title($course->shortname.': '.get_string('calendar', 'calendar').': '.$pagetitle);
 $PAGE->set_heading($course->fullname);
 $PAGE->set_pagelayout('standard');
-$PAGE->set_button(calendar_preferences_button($course));
 
 $renderer = $PAGE->get_renderer('core_calendar');
 $calendar->add_sidecalendar_blocks($renderer);
index c675c65..3c2aa92 100644 (file)
@@ -1658,6 +1658,8 @@ function calendar_get_default_courses() {
  * Display calendar preference button
  *
  * @param stdClass $course course object
+ * @deprecated since Moodle 3.2
+ * @todo MDL-55875 This will be deleted in Moodle 3.6.
  * @return string return preference button in html
  */
 function calendar_preferences_button(stdClass $course) {
@@ -1667,8 +1669,9 @@ function calendar_preferences_button(stdClass $course) {
     if (!isloggedin() || isguestuser()) {
         return '';
     }
+    debugging('This should no longer be used, the calendar preferences are now linked to the user preferences page');
 
-    return $OUTPUT->single_button(new moodle_url('/calendar/preferences.php', array('course' => $course->id)), get_string("preferences", "calendar"));
+    return $OUTPUT->single_button(new moodle_url('/user/calendar.php'), get_string("preferences", "calendar"));
 }
 
 /**
index a83b90f..3e4e0e9 100644 (file)
@@ -110,7 +110,6 @@ $subscriptions = $DB->get_records_sql($sql, $params);
 // Print title and header.
 $PAGE->set_title("$course->shortname: ".get_string('calendar', 'calendar').": ".get_string('subscriptions', 'calendar'));
 $PAGE->set_heading($course->fullname);
-$PAGE->set_button(calendar_preferences_button($course));
 
 $renderer = $PAGE->get_renderer('core_calendar');
 
diff --git a/calendar/preferences.php b/calendar/preferences.php
deleted file mode 100644 (file)
index aeb502d..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-// preferences.php - user prefs for calendar
-
-require_once('../config.php');
-require_once($CFG->dirroot.'/calendar/lib.php');
-require_once($CFG->dirroot.'/calendar/preferences_form.php');
-
-$courseid = required_param('course', PARAM_INT);
-$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
-
-$PAGE->set_url(new moodle_url('/calendar/preferences.php', array('course' => $courseid)));
-$PAGE->set_pagelayout('standard');
-
-require_login($course);
-
-if ($courseid == SITEID) {
-    $viewurl = new moodle_url('/calendar/view.php', array('view' => 'month'));
-} else {
-    $viewurl = new moodle_url('/calendar/view.php', array('view' => 'month', 'course' => $courseid));
-}
-navigation_node::override_active_url($viewurl);
-
-$defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
-if (isset($CFG->calendar_lookahead)) {
-    $defaultlookahead = intval($CFG->calendar_lookahead);
-}
-$defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS;
-if (isset($CFG->calendar_maxevents)) {
-    $defaultmaxevents = intval($CFG->calendar_maxevents);
-}
-
-$prefs = new stdClass;
-$prefs->timeformat = get_user_preferences('calendar_timeformat', '');
-$prefs->startwday  = calendar_get_starting_weekday();
-$prefs->maxevents  = get_user_preferences('calendar_maxevents', $defaultmaxevents);
-$prefs->lookahead  = get_user_preferences('calendar_lookahead', $defaultlookahead);
-$prefs->persistflt = get_user_preferences('calendar_persistflt', 0);
-
-$form = new calendar_preferences_form($PAGE->url);
-$form->set_data($prefs);
-
-if ($form->is_cancelled()) {
-    redirect($viewurl);
-} else if ($form->is_submitted() && $form->is_validated() && confirm_sesskey()) {
-    $data = $form->get_data();
-    if ($data->timeformat != CALENDAR_TF_12 && $data->timeformat != CALENDAR_TF_24) {
-        $data->timeformat = '';
-    }
-    set_user_preference('calendar_timeformat', $data->timeformat);
-
-    $data->startwday = intval($data->startwday);
-    if ($data->startwday < 0 || $data->startwday > 6) {
-        $data->startwday = abs($data->startwday % 7);
-    }
-    set_user_preference('calendar_startwday', $data->startwday);
-
-    if (intval($data->maxevents) >= 1) {
-        set_user_preference('calendar_maxevents', $data->maxevents);
-    }
-
-    if (intval($data->lookahead) >= 1) {
-        set_user_preference('calendar_lookahead', $data->lookahead);
-    }
-
-    set_user_preference('calendar_persistflt', intval($data->persistflt));
-    redirect($viewurl, get_string('changessaved'), 1);
-    exit;
-}
-
-$strcalendar = get_string('calendar', 'calendar');
-$strpreferences = get_string('calendarpreferences', 'calendar');
-
-$PAGE->navbar->add($strpreferences);
-$PAGE->set_pagelayout('admin');
-$PAGE->set_title("$course->shortname: $strcalendar: $strpreferences");
-$PAGE->set_heading($course->fullname);
-
-echo $OUTPUT->header();
-echo $OUTPUT->heading($strpreferences);
-echo $OUTPUT->box_start('generalbox boxaligncenter');
-$form->display();
-echo $OUTPUT->box_end();
-echo $OUTPUT->footer();
\ No newline at end of file
index e95c018..449574f 100644 (file)
@@ -28,11 +28,13 @@ Feature: Limit displayed upcoming events
       | Event title       | Two months away event |
     When I follow "C1"
     Then I should not see "Two months away event"
-    And I follow "Go to calendar"
-    And I click on "Preferences" "button"
+    And I am on site homepage
+    And I follow "Preferences" in the user menu
+    And I follow "Calendar preferences"
     And I set the following fields to these values:
       | Upcoming events look-ahead | 3 months |
     And I press "Save changes"
     And I wait to be redirected
-    And I follow "C1"
+    And I am on site homepage
+    And I follow "Course 1"
     And I should see "Two months away event"
index 55d272a..4a37e2c 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /calendar/* ,
 information provided here is intended especially for developers.
 
+=== 3.2 ===
+* calendar_preferences_button() is now depreciated.  Calendar preferences have been moved to the user preferences page.
+
 === 2.9 ===
 default values changes in code:
 * core_calendar_external::get_calendar_events_parameters() 'timeend' default option changed; now, by default,
index e37e3ac..fe8c3e9 100644 (file)
 //                                                                         //
 /////////////////////////////////////////////////////////////////////////////
 
-//  Display the calendar page.
+/**
+ * Display the calendar page.
+ * @copyright 2003 Jon Papaioannou
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package core_calendar
+ */
 
 require_once('../config.php');
 require_once($CFG->dirroot.'/course/lib.php');
@@ -117,7 +122,6 @@ switch($view) {
 $PAGE->set_pagelayout('standard');
 $PAGE->set_title("$course->shortname: $strcalendar: $pagetitle");
 $PAGE->set_heading($COURSE->fullname);
-$PAGE->set_button(calendar_preferences_button($course));
 
 $renderer = $PAGE->get_renderer('core_calendar');
 $calendar->add_sidecalendar_blocks($renderer, true, $view);
index ac9297e..c835d98 100644 (file)
@@ -648,6 +648,7 @@ $CFG->admin = 'admin';
 // $CFG->phpunit_prefix = 'phpu_';
 // $CFG->phpunit_dataroot = '/home/example/phpu_moodledata';
 // $CFG->phpunit_directorypermissions = 02777; // optional
+// $CFG->phpunit_profilingenabled = true; // optional to profile PHPUnit runs.
 //
 //
 //=========================================================================
index ec6f7c9..8b50256 100644 (file)
@@ -2611,4 +2611,160 @@ class core_course_external extends external_api {
         );
     }
 
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.2
+     */
+    public static function get_user_navigation_options_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'Course id.')),
+            )
+        );
+    }
+
+    /**
+     * Return a list of navigation options in a set of courses that are avaialable or not for the current user.
+     *
+     * @param array $courseids a list of course ids
+     * @return array of warnings and the options availability
+     * @since Moodle 3.2
+     * @throws moodle_exception
+     */
+    public static function get_user_navigation_options($courseids) {
+        global $CFG;
+        require_once($CFG->dirroot . '/course/lib.php');
+
+        // Parameter validation.
+        $params = self::validate_parameters(self::get_user_navigation_options_parameters(), array('courseids' => $courseids));
+        $courseoptions = array();
+
+        list($courses, $warnings) = external_util::validate_courses($params['courseids'], array(), true);
+
+        if (!empty($courses)) {
+            foreach ($courses as $course) {
+                // Fix the context for the frontpage.
+                if ($course->id == SITEID) {
+                    $course->context = context_system::instance();
+                }
+                $navoptions = course_get_user_navigation_options($course->context, $course);
+                $options = array();
+                foreach ($navoptions as $name => $available) {
+                    $options[] = array(
+                        'name' => $name,
+                        'available' => $available,
+                    );
+                }
+
+                $courseoptions[] = array(
+                    'id' => $course->id,
+                    'options' => $options
+                );
+            }
+        }
+
+        $result = array(
+            'courses' => $courseoptions,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.2
+     */
+    public static function get_user_navigation_options_returns() {
+        return new external_single_structure(
+            array(
+                'courses' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'Course id'),
+                            'options' => new external_multiple_structure(
+                                new external_single_structure(
+                                    array(
+                                        'name' => new external_value(PARAM_ALPHANUMEXT, 'Option name'),
+                                        'available' => new external_value(PARAM_BOOL, 'Whether the option is available or not'),
+                                    )
+                                )
+                            )
+                        )
+                    ), 'List of courses'
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.2
+     */
+    public static function get_user_administration_options_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'Course id.')),
+            )
+        );
+    }
+
+    /**
+     * Return a list of administration options in a set of courses that are available or not for the current user.
+     *
+     * @param array $courseids a list of course ids
+     * @return array of warnings and the options availability
+     * @since Moodle 3.2
+     * @throws moodle_exception
+     */
+    public static function get_user_administration_options($courseids) {
+        global $CFG;
+        require_once($CFG->dirroot . '/course/lib.php');
+
+        // Parameter validation.
+        $params = self::validate_parameters(self::get_user_administration_options_parameters(), array('courseids' => $courseids));
+        $courseoptions = array();
+
+        list($courses, $warnings) = external_util::validate_courses($params['courseids'], array(), true);
+
+        if (!empty($courses)) {
+            foreach ($courses as $course) {
+                $adminoptions = course_get_user_administration_options($course, $course->context);
+                $options = array();
+                foreach ($adminoptions as $name => $available) {
+                    $options[] = array(
+                        'name' => $name,
+                        'available' => $available,
+                    );
+                }
+
+                $courseoptions[] = array(
+                    'id' => $course->id,
+                    'options' => $options
+                );
+            }
+        }
+
+        $result = array(
+            'courses' => $courseoptions,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.2
+     */
+    public static function get_user_administration_options_returns() {
+        return self::get_user_navigation_options_returns();
+    }
 }
index 994b37e..0ed11fc 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();
@@ -4061,3 +4073,115 @@ function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcon
                 $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages);
     }
 }
+
+/**
+ * Return an object with the list of navigation options in a course that are avaialable or not for the current user.
+ * This function also handles the frontpage course.
+ *
+ * @param  stdClass $context context object (it can be a course context or the system context for frontpage settings)
+ * @param  stdClass $course  the course where the settings are being rendered (only used when $context is set to frontpage)
+ * @return stdClass          the navigation options in a course and their availability status
+ * @since  Moodle 3.2
+ */
+function course_get_user_navigation_options($context, $course = null) {
+    global $CFG;
+
+    $isloggedin = isloggedin();
+    $isguestuser = isguestuser();
+    $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM;
+
+    if ($isfrontpage) {
+        $sitecontext = $context;
+    } else {
+        $sitecontext = context_system::instance();
+    }
+
+    $options = new stdClass;
+    $options->blogs = !empty($CFG->enableblogs) &&
+                        ($CFG->bloglevel == BLOG_GLOBAL_LEVEL ||
+                        ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser)))
+                        && has_capability('moodle/blog:view', $sitecontext);
+
+    $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context);
+
+    // Frontpage settings?
+    if ($isfrontpage) {
+        if ($course->id == SITEID) {
+            $options->participants = has_capability('moodle/site:viewparticipants', $sitecontext);
+        } else {
+            $options->participants = has_capability('moodle/course:viewparticipants', context_course::instance($course->id));
+        }
+
+        $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
+        $options->tags = !empty($CFG->usetags) && $isloggedin;
+        $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
+        $options->calendar = $isloggedin;
+    } else {
+        $options->participants = has_capability('moodle/course:viewparticipants', $context);
+        $options->badges = !empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges) &&
+                            has_capability('moodle/badges:viewbadges', $context);
+    }
+    return $options;
+}
+
+/**
+ * Return an object with the list of administration options in a course that are available or not for the current user.
+ * This function also handles the frontpage settings.
+ *
+ * @param  stdClass $course  course object (for frontpage it should be a clone of $SITE)
+ * @param  stdClass $context context object (course context)
+ * @return stdClass          the administration options in a course and their availability status
+ * @since  Moodle 3.2
+ */
+function course_get_user_administration_options($course, $context) {
+    global $CFG;
+    $isfrontpage = $course->id == SITEID;
+
+    $options = new stdClass;
+    $options->update = has_capability('moodle/course:update', $context);
+    $options->filters = has_capability('moodle/filter:manage', $context) &&
+                        count(filter_get_available_in_context($context)) > 0;
+    $options->reports = has_capability('moodle/site:viewreports', $context);
+    $options->backup = has_capability('moodle/backup:backupcourse', $context);
+    $options->restore = has_capability('moodle/restore:restorecourse', $context);
+    $options->files = $course->legacyfiles == 2 and has_capability('moodle/course:managefiles', $context);
+
+    if (!$isfrontpage) {
+        $options->tags = has_capability('moodle/course:tag', $context);
+        $options->gradebook = has_capability('moodle/grade:manage', $context);
+        $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context);
+        $options->badges = !empty($CFG->enablebadges);
+        $options->import = has_capability('moodle/restore:restoretargetimport', $context);
+        $options->publish = has_capability('moodle/course:publish', $context);
+        $options->reset = has_capability('moodle/course:reset', $context);
+        $options->roles = has_capability('moodle/role:switchroles', $context);
+
+        // Add view grade report is permitted.
+        $grades = false;
+        if (has_capability('moodle/grade:viewall', $context)) {
+            $grades = true;
+        } else if (!empty($course->showgrades)) {
+            $reports = core_component::get_plugin_list('gradereport');
+            if (is_array($reports) && count($reports) > 0) {  // Get all installed reports.
+                arsort($reports);   // User is last, we want to test it first.
+                foreach ($reports as $plugin => $plugindir) {
+                    if (has_capability('gradereport/'.$plugin.':view', $context)) {
+                        // Stop when the first visible plugin is found.
+                        $grades = true;
+                        break;
+                    }
+                }
+            }
+        }
+        $options->grades = $grades;
+    } else {
+        // Set default options to false.
+        $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades');
+
+        foreach ($listofoptions as $option) {
+            $options->$option = false;
+        }
+    }
+
+    return $options;
+}
index 4c9ed73..f1a1b8a 100644 (file)
@@ -2920,4 +2920,208 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertNotEmpty($res->prevpageurl);
         $this->assertEmpty($res->nextpageurl);
     }
+
+    /**
+     * Test course_get_user_navigation_options for frontpage.
+     */
+    public function test_course_get_user_navigation_options_for_frontpage() {
+        global $CFG, $SITE, $DB;
+        $this->resetAfterTest();
+        $context = context_system::instance();
+        $course = clone $SITE;
+        $this->setAdminUser();
+
+        $navoptions = course_get_user_navigation_options($context, $course);
+        $this->assertTrue($navoptions->blogs);
+        $this->assertTrue($navoptions->notes);
+        $this->assertTrue($navoptions->participants);
+        $this->assertTrue($navoptions->badges);
+        $this->assertTrue($navoptions->tags);
+        $this->assertFalse($navoptions->search);
+        $this->assertTrue($navoptions->calendar);
+
+        // Enable global search now.
+        $CFG->enableglobalsearch = 1;
+        $navoptions = course_get_user_navigation_options($context, $course);
+        $this->assertTrue($navoptions->search);
+
+        // Now try with a standard user.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $navoptions = course_get_user_navigation_options($context, $course);
+        $this->assertTrue($navoptions->blogs);
+        $this->assertFalse($navoptions->notes);
+        $this->assertFalse($navoptions->participants);
+        $this->assertTrue($navoptions->badges);
+        $this->assertTrue($navoptions->tags);
+        $this->assertTrue($navoptions->search);
+        $this->assertTrue($navoptions->calendar);
+
+        // Standar using viewing frontpage settings from a course where is enrolled.
+        $course = self::getDataGenerator()->create_course();
+        // Create a viewer user.
+        $viewer = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($viewer->id, $course->id, $studentrole->id);
+        $this->setUser($viewer);
+
+        $navoptions = course_get_user_navigation_options($context, $course);
+        $this->assertTrue($navoptions->blogs);
+        $this->assertFalse($navoptions->notes);
+        $this->assertTrue($navoptions->participants);
+        $this->assertTrue($navoptions->badges);
+        $this->assertTrue($navoptions->tags);
+        $this->assertTrue($navoptions->search);
+        $this->assertTrue($navoptions->calendar);
+    }
+
+    /**
+     * Test course_get_user_navigation_options for managers in a normal course.
+     */
+    public function test_course_get_user_navigation_options_for_managers() {
+        global $CFG;
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $this->setAdminUser();
+
+        $navoptions = course_get_user_navigation_options($context);
+        $this->assertTrue($navoptions->blogs);
+        $this->assertTrue($navoptions->notes);
+        $this->assertTrue($navoptions->participants);
+        $this->assertTrue($navoptions->badges);
+    }
+
+    /**
+     * Test course_get_user_navigation_options for students in a normal course.
+     */
+    public function test_course_get_user_navigation_options_for_students() {
+        global $DB, $CFG;
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        $navoptions = course_get_user_navigation_options($context);
+        $this->assertTrue($navoptions->blogs);
+        $this->assertFalse($navoptions->notes);
+        $this->assertTrue($navoptions->participants);
+        $this->assertTrue($navoptions->badges);
+
+        // Disable some options.
+        $CFG->badges_allowcoursebadges = 0;
+        $CFG->enableblogs = 0;
+        // Disable view participants capability.
+        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $context);
+        $context->mark_dirty();
+
+        $navoptions = course_get_user_navigation_options($context);
+        $this->assertFalse($navoptions->blogs);
+        $this->assertFalse($navoptions->notes);
+        $this->assertFalse($navoptions->participants);
+        $this->assertFalse($navoptions->badges);
+    }
+
+    /**
+     * Test course_get_user_administration_options for frontpage.
+     */
+    public function test_course_get_user_administration_options_for_frontpage() {
+        global $CFG, $SITE;
+        $this->resetAfterTest();
+        $course = clone $SITE;
+        $context = context_course::instance($course->id);
+        $this->setAdminUser();
+
+        $adminoptions = course_get_user_administration_options($course, $context);
+        $this->assertTrue($adminoptions->update);
+        $this->assertTrue($adminoptions->filters);
+        $this->assertTrue($adminoptions->reports);
+        $this->assertTrue($adminoptions->backup);
+        $this->assertTrue($adminoptions->restore);
+        $this->assertFalse($adminoptions->files);
+        $this->assertFalse($adminoptions->tags);
+
+        // Now try with a standard user.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $adminoptions = course_get_user_administration_options($course, $context);
+        $this->assertFalse($adminoptions->update);
+        $this->assertFalse($adminoptions->filters);
+        $this->assertFalse($adminoptions->reports);
+        $this->assertFalse($adminoptions->backup);
+        $this->assertFalse($adminoptions->restore);
+        $this->assertFalse($adminoptions->files);
+        $this->assertFalse($adminoptions->tags);
+
+    }
+
+    /**
+     * Test course_get_user_administration_options for managers in a normal course.
+     */
+    public function test_course_get_user_administration_options_for_managers() {
+        global $CFG;
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $this->setAdminUser();
+
+        $adminoptions = course_get_user_administration_options($course, $context);
+        $this->assertTrue($adminoptions->update);
+        $this->assertTrue($adminoptions->filters);
+        $this->assertTrue($adminoptions->reports);
+        $this->assertTrue($adminoptions->backup);
+        $this->assertTrue($adminoptions->restore);
+        $this->assertFalse($adminoptions->files);
+        $this->assertTrue($adminoptions->tags);
+        $this->assertTrue($adminoptions->gradebook);
+        $this->assertFalse($adminoptions->outcomes);
+        $this->assertTrue($adminoptions->badges);
+        $this->assertTrue($adminoptions->import);
+        $this->assertTrue($adminoptions->publish);
+        $this->assertTrue($adminoptions->reset);
+        $this->assertTrue($adminoptions->roles);
+        $this->assertTrue($adminoptions->grades);
+    }
+
+    /**
+     * Test course_get_user_administration_options for students in a normal course.
+     */
+    public function test_course_get_user_administration_options_for_students() {
+        global $DB, $CFG;
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+        $adminoptions = course_get_user_administration_options($course, $context);
+
+        $this->assertFalse($adminoptions->update);
+        $this->assertFalse($adminoptions->filters);
+        $this->assertFalse($adminoptions->reports);
+        $this->assertFalse($adminoptions->backup);
+        $this->assertFalse($adminoptions->restore);
+        $this->assertFalse($adminoptions->files);
+        $this->assertFalse($adminoptions->tags);
+        $this->assertFalse($adminoptions->gradebook);
+        $this->assertFalse($adminoptions->outcomes);
+        $this->assertTrue($adminoptions->badges);
+        $this->assertFalse($adminoptions->import);
+        $this->assertFalse($adminoptions->publish);
+        $this->assertFalse($adminoptions->reset);
+        $this->assertFalse($adminoptions->roles);
+        $this->assertTrue($adminoptions->grades);
+
+        $CFG->enablebadges = false;
+        $adminoptions = course_get_user_administration_options($course, $context);
+        $this->assertFalse($adminoptions->badges);
+    }
 }
index 0e07018..b7fbe2f 100644 (file)
@@ -1755,4 +1755,119 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals('forum', $result['courses'][0]['overviews'][0]['module']);
         $this->assertContains('1 total unread', $result['courses'][0]['overviews'][0]['overviewtext']);
     }
+
+    /**
+     * Test get_user_navigation_options
+     */
+    public function test_get_user_navigation_options() {
+        global $USER;
+
+        $this->resetAfterTest();
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+
+        // Create a viewer user.
+        $viewer = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
+
+        $this->setUser($viewer->id);
+        $courses = array($course1->id , $course2->id, SITEID);
+
+        $result = core_course_external::get_user_navigation_options($courses);
+        $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(3, $result['courses']);
+
+        foreach ($result['courses'] as $course) {
+            $navoptions = new stdClass;
+            foreach ($course['options'] as $option) {
+                $navoptions->{$option['name']} = $option['available'];
+            }
+            if ($course['id'] == SITEID) {
+                $this->assertCount(7, $course['options']);
+                $this->assertTrue($navoptions->blogs);
+                $this->assertFalse($navoptions->notes);
+                $this->assertFalse($navoptions->participants);
+                $this->assertTrue($navoptions->badges);
+                $this->assertTrue($navoptions->tags);
+                $this->assertFalse($navoptions->search);
+                $this->assertTrue($navoptions->calendar);
+            } else {
+                $this->assertCount(4, $course['options']);
+                $this->assertTrue($navoptions->blogs);
+                $this->assertFalse($navoptions->notes);
+                $this->assertTrue($navoptions->participants);
+                $this->assertTrue($navoptions->badges);
+            }
+        }
+    }
+
+    /**
+     * Test get_user_administration_options
+     */
+    public function test_get_user_administration_options() {
+        global $USER;
+
+        $this->resetAfterTest();
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+
+        // Create a viewer user.
+        $viewer = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
+
+        $this->setUser($viewer->id);
+        $courses = array($course1->id , $course2->id, SITEID);
+
+        $result = core_course_external::get_user_administration_options($courses);
+        $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(3, $result['courses']);
+
+        foreach ($result['courses'] as $course) {
+            $adminoptions = new stdClass;
+            foreach ($course['options'] as $option) {
+                $adminoptions->{$option['name']} = $option['available'];
+            }
+            if ($course['id'] == SITEID) {
+                $this->assertCount(15, $course['options']);
+                $this->assertFalse($adminoptions->update);
+                $this->assertFalse($adminoptions->filters);
+                $this->assertFalse($adminoptions->reports);
+                $this->assertFalse($adminoptions->backup);
+                $this->assertFalse($adminoptions->restore);
+                $this->assertFalse($adminoptions->files);
+                $this->assertFalse(!isset($adminoptions->tags));
+                $this->assertFalse($adminoptions->gradebook);
+                $this->assertFalse($adminoptions->outcomes);
+                $this->assertFalse($adminoptions->badges);
+                $this->assertFalse($adminoptions->import);
+                $this->assertFalse($adminoptions->publish);
+                $this->assertFalse($adminoptions->reset);
+                $this->assertFalse($adminoptions->roles);
+                $this->assertFalse($adminoptions->grades);
+            } else {
+                $this->assertCount(15, $course['options']);
+                $this->assertFalse($adminoptions->update);
+                $this->assertFalse($adminoptions->filters);
+                $this->assertFalse($adminoptions->reports);
+                $this->assertFalse($adminoptions->backup);
+                $this->assertFalse($adminoptions->restore);
+                $this->assertFalse($adminoptions->files);
+                $this->assertFalse($adminoptions->tags);
+                $this->assertFalse($adminoptions->gradebook);
+                $this->assertFalse($adminoptions->outcomes);
+                $this->assertTrue($adminoptions->badges);
+                $this->assertFalse($adminoptions->import);
+                $this->assertFalse($adminoptions->publish);
+                $this->assertFalse($adminoptions->reset);
+                $this->assertFalse($adminoptions->roles);
+                $this->assertTrue($adminoptions->grades);
+            }
+        }
+    }
 }
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';
index 52b5142..546da9d 100644 (file)
@@ -225,8 +225,6 @@ foreach ($instances as $instance) {
     $edit = array();
 
     if ($canconfig) {
-        // up/down link
-        $updown = '';
         if ($updowncount > 1) {
             $aurl = new moodle_url($url, array('action'=>'up', 'instance'=>$instance->id));
             $updown[] = $OUTPUT->action_icon($aurl, new pix_icon('t/up', $strup, 'core', array('class' => 'iconsmall')));
index 0579023..d8e38e8 100644 (file)
@@ -37,7 +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'] = 'Token was incorrect please check the URL and try again, or contact the administrator of this tool.';
+$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.';
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 0d3175f..13173ee 100644 (file)
@@ -105,7 +105,7 @@ class filter_mediaplugin_testcase extends advanced_testcase {
         $paddedurl = str_pad($originalurl, 6000, 'z');
         $validpaddedurl = '<p>Some text.</p><pre style="color: rgb(0, 0, 0); line-height: normal;"><span class="mediaplugin mediaplugin_youtube">
 <iframe title="Valid link" width="400" height="300"
-  src="https://www.youtube.com/embed/uUhWl9Lm3OM?rel=0&wmode=transparent" frameborder="0" allowfullscreen="1"></iframe>
+  src="https://www.youtube.com/embed/uUhWl9Lm3OM?rel=0&amp;wmode=transparent" frameborder="0" allowfullscreen="1"></iframe>
 </span></pre><pre style="color: rgb(0, 0, 0); line-height: normal;">';
         $validpaddedurl = str_pad($validpaddedurl, 6000 + (strlen($validpaddedurl) - strlen($originalurl)), 'z');
 
index 71f47b8..c1849f2 100644 (file)
@@ -56,8 +56,8 @@ if ($mform->is_cancelled()) {
 
     // 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)) {
-        if (($scores = $controller->get_min_max_score()) && $scores['minscore'] <> 0) {
+    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'));
         }
index dacd5dd..bb36c3e 100644 (file)
@@ -45,7 +45,7 @@ $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_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 non-negative number';
+$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.';
@@ -53,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';
@@ -66,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';
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 dbaa666..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
@@ -636,10 +639,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if ($scores['minscore'] <> 0) {
             $html .= $this->output->notification(get_string('zerolevelsabsent', 'gradingform_rubric'), 'error');
         }
-        $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');
+        $html .= $this->output->notification(get_string('rubricmappingexplained', 'gradingform_rubric', (object)$scores), 'info');
         return $html;
     }
 }
index 49b0193..3d64b5f 100644 (file)
@@ -232,29 +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 ($minscore === null || (float)$level['score'] < $minscore) {
-                            $minscore = (float)$level['score'];
+                        if ($minscore === null || $score < $minscore) {
+                            $minscore = $score;
                         }
-                        if ($maxscore === null || (float)$level['score'] > $maxscore) {
-                            $maxscore = (float)$level['score'];
+                        if ($maxscore === null || $score > $maxscore) {
+                            $maxscore = $score;
                         }
                     } else {
                         $this->nonjsbuttonpressed = true;
@@ -313,8 +314,10 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
 
         // create validation error string (if needed)
         if ($withvalidation) {
-            if ($overallminscore == $overallmaxscore) {
-                $errors['err_novariations'] = 1;
+            if (!$return['options']['lockzeropoints']) {
+                if ($overallminscore == $overallmaxscore) {
+                    $errors['err_novariations'] = 1;
+                }
             }
             if (count($errors)) {
                 $rv = array();
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 3011ae5..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.';
@@ -658,9 +658,9 @@ $string['loginpasswordautocomplete'] = 'Prevent password autocompletion on login
 $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'] = 'Your full logo. The image format must be PNG or JPEG.';
+$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 your logo, usually this would be your emblem, or iconic symbol. Moodle\'s compact logo is the \'M\' by itself with the cap. The image format must be PNG or JPEG.';
+$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';
@@ -955,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 aaf102b..5d47fe9 100644 (file)
@@ -258,7 +258,9 @@ $string['eventbadgedeleted'] = 'Badge deleted';
 $string['eventbadgedisabled'] = 'Badge disabled';
 $string['eventbadgeduplicated'] = 'Badge duplicated';
 $string['eventbadgeenabled'] = 'Badge enabled';
+$string['eventbadgelistingviewed'] = 'Badge listing viewed';
 $string['eventbadgeupdated'] = 'Badge updated';
+$string['eventbadgeviewed'] = 'Badge viewed';
 $string['evidence'] = 'Evidence';
 $string['existingrecipients'] = 'Existing badge recipients';
 $string['expired'] = 'Expired';
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 7148c61..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';
index f48c7f4..096722d 100644 (file)
@@ -81,8 +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: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 645bf9c..af6015f 100644 (file)
@@ -9220,7 +9220,8 @@ class admin_setting_configstoredfile extends admin_setting {
         $html .= '<input value="" id="'.$id.'" type="hidden" />';
 
         return format_admin_setting($this, $this->visiblename,
-            '<div class="form-filemanager">'.$html.'</div>', $this->description, true, '', '', $query);
+            '<div class="form-filemanager" data-fieldtype="filemanager">'.$html.'</div>',
+            $this->description, true, '', '', $query);
     }
 }
 
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 166fd46..1fb7642 100644 (file)
@@ -228,7 +228,11 @@ class behat_field_manager {
             return 'availability';
         }
 
-        // We look for a parent node with 'felement' class.
+        // We look for a parent node with 'felement' class or data-fieldtype attribute.
+        if ($fieldtype = $fieldnode->getParent()->getAttribute('data-fieldtype')) {
+            return $fieldtype;
+        }
+
         if ($class = $fieldnode->getParent()->getAttribute('class')) {
 
             if (strstr($class, 'felement') != false) {
index 553a118..8e1e9a9 100644 (file)
@@ -67,7 +67,7 @@ class behat_files extends behat_base {
         if (empty($filepickerelement)) {
             $filepickercontainer = $this->find(
                 'xpath',
-                "//*[@class=\"form-filemanager\"]",
+                "//*[@data-fieldtype=\"filemanager\"]",
                 $exception
             );
         } else {
@@ -76,8 +76,7 @@ class behat_files extends behat_base {
             $filepickercontainer = $this->find(
                 'xpath',
                 "//input[./@id = //label[normalize-space(.)=$filepickerelement]/@for]" .
-                    "//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ') or " .
-                    "contains(concat(' ', normalize-space(@class), ' '), ' ffilepicker ')]",
+                    '//ancestor::div[@data-fieldtype="filemanager" or @data-fieldtype="filepicker"]',
                 $exception
             );
         }
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..e646ad8
--- /dev/null
@@ -0,0 +1,1269 @@
+<?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) {
+            // 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;
+    }
+
+    /**
+     * Build config for behat.yml.
+     *
+     * @param int $parallelruns how many parallel runs feature needs to be divided.
+     * @param int $currentrun current run for which features should be returned.
+     * @return array
+     */
+    protected function build_config($parallelruns = 0, $currentrun = 0) {
+        global $CFG;
+
+        if (!empty($parallelruns) && !empty($currentrun)) {
+            $this->set_parallel_run($parallelruns, $currentrun);
+        } else {
+            $currentrun = $this->get_current_run();
+            $parallelruns = $this->get_number_of_parallel_run();
+        }
+
+        $selenium2wdhost = array('wd_host' => 'http://localhost:4444/wd/hub');
+        // If parallel run, then set wd_host if specified.
+        if (!empty($currentrun) && !empty($parallelruns)) {
+            // Set proper selenium2 wd_host if defined.
+            if (!empty($CFG->behat_parallel_run[$currentrun - 1]['wd_host'])) {
+                $selenium2wdhost = array('wd_host' => $CFG->behat_parallel_run[$currentrun - 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';
+        }
+
+        $suites = $this->get_behat_suites($parallelruns, $currentrun);
+
+        $overriddenthemescontexts = $this->get_overridden_theme_contexts();
+        if (!empty($overriddenthemescontexts)) {
+            $allcontexts = array_merge($this->contexts, $overriddenthemescontexts);
+        } else {
+            $allcontexts = $this->contexts;
+        }
+
+        // Remove selectors from step definitions.
+        $themes = $this->get_list_of_themes();
+        foreach ($themes as $theme) {
+            $selectorclass = self::get_behat_theme_selector_override_classname($theme);
+            if (isset($allcontexts[$selectorclass])) {
+                unset($allcontexts[$selectorclass]);
+            }
+        }
+
+        // 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' => $suites,
+                'extensions' => array(
+                    'Behat\MinkExtension' => array(
+                        'base_url' => $CFG->behat_wwwroot,
+                        'goutte' => null,
+                        'selenium2' => $selenium2wdhost
+                    ),
+                    'Moodle\BehatExtension' => array(
+                        'moodledirroot' => $CFG->dirroot,
+                        'steps_definitions' => $allcontexts,
+                    )
+                )
+            )
+        );
+
+        return $config;
+    }
+
+    /**
+     * Divide features between the runs and return list.
+     *
+     * @param array $features list of features to be divided.
+     * @param int $parallelruns how many parallel runs feature needs to be divided.
+     * @param int $currentrun current run for which features should be returned.
+     * @return array
+     */
+    protected function get_features_for_the_run($features, $parallelruns, $currentrun) {
+
+        // If no features are passed then just return.
+        if (empty($features)) {
+            return $features;
+        }
+
+        $allocatedfeatures = $features;
+
+        // If parallel run, then only divide features.
+        if (!empty($currentrun) && !empty($parallelruns)) {
+            // Attempt to split into weighted buckets using timing information, if available.
+            if ($alloc = $this->profile_guided_allocate($features, max(1, $parallelruns), $currentrun)) {
+                $allocatedfeatures = $alloc;
+            } else {
+                // Divide the list of feature files amongst the parallel runners.
+                // 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[$currentrun - 1])) {
+                        $allocatedfeatures = $features[$currentrun - 1];
+                    } else {
+                        $allocatedfeatures = array();
+                    }
+                }
+            }
+        }
+
+        return $allocatedfeatures;
+    }
+
+    /**
+     * Parse $CFG->behat_profile and return the array with required config structure for behat.yml.
+     *
+     * $CFG->behat_profiles = array(
+     *     'profile' = array(
+     *         'browser' => 'firefox',
+     *         'tags' => '@javascript',
+     *         'wd_host' => 'http://127.0.0.1:4444/wd/hub',
+     *         'capabilities' => array(
+     *             'platform' => 'Linux',
+     *             'version' => 44
+     *         )
+     *     )
+     * );
+     *
+     * @param string $profile profile name
+     * @param array $values values for profile.
+     * @return array
+     */
+    protected function get_behat_profile($profile, $values) {
+        // Values should be an array.
+        if (!is_array($values)) {
+            return array();
+        }
+
+        // Check suite values.
+        $behatprofilesuites = array();
+        // Fill tags information.
+        if (isset($values['tags'])) {
+            $behatprofilesuites = array(
+                'suites' => array(
+                    'default' => array(
+                        'filters' => array(
+                            'tags' => $values['tags'],
+                        )
+                    )
+                )
+            );
+        }
+
+        // Selenium2 config values.
+        $behatprofileextension = array();
+        $seleniumconfig = array();
+        if (isset($values['browser'])) {
+            $seleniumconfig['browser'] = $values['browser'];
+        }
+        if (isset($values['wd_host'])) {
+            $seleniumconfig['wd_host'] = $values['wd_host'];
+        }
+        if (isset($values['capabilities'])) {
+            $seleniumconfig['capabilities'] = $values['capabilities'];
+        }
+        if (!empty($seleniumconfig)) {
+            $behatprofileextension = array(
+                'extensions' => array(
+                    'Behat\MinkExtension' => array(
+                        'selenium2' => $seleniumconfig,
+                    )
+                )
+            );
+        }
+
+        return array($profile => array_merge($behatprofilesuites, $behatprofileextension));
+    }
+
+    /**
+     * Attempt to split feature list into fairish buckets using timing information, if available.
+     * Simply add each one to lightest buckets until all files allocated.
+     * PGA = Profile Guided Allocation. I made it up just now.
+     * CAUTION: workers must agree on allocation, do not be random anywhere!
+     *
+     * @param array $features Behat feature files array
+     * @param int $nbuckets Number of buckets to divide into
+     * @param int $instance Index number of this instance
+     * @return array|bool Feature files array, sorted into allocations
+     */
+    public function profile_guided_allocate($features, $nbuckets, $instance) {
+
+        // No profile guided allocation is required in phpunit.
+        if (defined('PHPUNIT_TEST')) {
+            return false;
+        }
+
+        $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 && !defined('PHPUNIT_TEST')) {
+            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];
+    }
+
+    /**
+     * Overrides default config with local config values
+     *
+     * array_merge does not merge completely the array's values
+     *
+     * @param mixed $config The node of the default config
+     * @param mixed $localconfig The node of the local config
+     * @return mixed The merge result
+     */
+    public 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] = $this->merge_config($config[$key], $localconfig[$key]);
+            }
+        }
+
+        return $config;
+    }
+
+    /**
+     * Merges $CFG->behat_config with the one passed.
+     *
+     * @param array $config existing config.
+     * @return array merged config with $CFG->behat_config
+     */
+    public function merge_behat_config($config) {
+        global $CFG;
+
+        // In case user defined overrides respect them over our default ones.
+        if (!empty($CFG->behat_config)) {
+            foreach ($CFG->behat_config as $profile => $values) {
+                $config = $this->merge_config($config, $this->get_behat_config_for_profile($profile, $values));
+            }
+        }
+
+        return $config;
+    }
+
+    /**
+     * Parse $CFG->behat_config and return the array with required config structure for behat.yml
+     *
+     * @param string $profile profile name
+     * @param array $values values for profile
+     * @return array
+     */
+    public function get_behat_config_for_profile($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)) {
+            behat_config_manager::$autoprofileconversion = true;
+            return $this->get_behat_profile($profile, $oldconfigvalues);
+        }
+
+        // If nothing set above then return empty array.
+        return array();
+    }
+
+    /**
+     * Merges $CFG->behat_profiles with the one passed.
+     *
+     * @param array $config existing config.
+     * @return array merged config with $CFG->behat_profiles
+     */
+    public function merge_behat_profiles($config) {
+        global $CFG;
+
+        // Check for Moodle custom ones.
+        if (!empty($CFG->behat_profiles) && is_array($CFG->behat_profiles)) {
+            foreach ($CFG->behat_profiles as $profile => $values) {
+                $config = $this->merge_config($config, $this->get_behat_profile($profile, $values));
+            }
+        }
+
+        return $config;
+    }
+
+    /**
+     * Cleans the path returned by get_components_with_tests() to standarize it
+     *
+     * @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
+     */
+    public final 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);
+    }
+
+    /**
+     * The relative path where components stores their behat tests
+     *
+     * @return string
+     */
+    public static final function get_behat_tests_path() {
+        return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat';
+    }
+
+    /**
+     * Return context name of behat_theme selector to use.
+     *
+     * @param string $themename name of the theme.
+     * @param bool $includeclass if class should be included.
+     * @return string
+     */
+    public static final function get_behat_theme_selector_override_classname($themename, $includeclass = false) {
+        global $CFG;
+
+        $overridebehatclassname = 'behat_theme_'.$themename.'_behat_selectors';
+
+        if ($includeclass) {
+            $themeoverrideselector = $CFG->dirroot . DIRECTORY_SEPARATOR . 'theme' . DIRECTORY_SEPARATOR . $themename .
+                self::get_behat_tests_path() . DIRECTORY_SEPARATOR . $overridebehatclassname . '.php';
+
+            if (file_exists($themeoverrideselector)) {
+                require_once($themeoverrideselector);
+            }
+        }
+
+        return $overridebehatclassname;
+    }
+
+    /**
+     * List of components which contain behat context or features.
+     *
+     * @return array
+     */
+    private function get_components_with_tests() {
+        if (empty($this->componentswithtests)) {
+            $this->componentswithtests = tests_finder::get_components_with_tests('behat');
+        }
+
+        return $this->componentswithtests;
+    }
+
+    /**
+     * Remove list of blacklisted features from the feature list.
+     *
+     * @param array $features list of original features.
+     * @param array|string $blacklist list of features which needs to be removed.
+     * @return array features - blacklisted features.
+     */
+    protected function remove_blacklisted_features_from_list($features, $blacklist) {
+
+        // If no blacklist passed then return.
+        if (empty($blacklist)) {
+            return $features;
+        }
+
+        // If there is no feature in suite then just return what was passed.
+        if (empty($features)) {
+            return $features;
+        }
+
+        if (!is_array($blacklist)) {
+            $blacklist = array($blacklist);
+        }
+
+        // Remove blacklisted features.
+        foreach ($blacklist as $blacklistpath) {
+
+            list($key, $featurepath) = $this->get_clean_feature_key_and_path($blacklistpath);
+
+            if (isset($features[$key])) {
+                $features[$key] = null;
+                unset($features[$key]);
+            } else if (empty($this->tags)) {
+                // If tags not set, then ensure we have a blacklisted feature in core. Else, let user know that
+                // blacklisted feature is invalid.
+                $featurestocheck = $this->get_components_features();
+                if (!isset($featurestocheck[$key]) && !defined('PHPUNIT_TEST')) {
+                    behat_error(BEHAT_EXITCODE_REQUIREMENT, 'Blacklisted feature "' . $blacklistpath . '" not found.');
+                }
+            }
+        }
+
+        return $features;
+    }
+
+    /**
+     * Return list of behat suites. Multiple suites are returned if theme
+     * overrides default step definitions/features.
+     *
+     * @param int $parallelruns number of parallel runs
+     * @param int $currentrun current run.
+     * @return array list of suites.
+     */
+    protected function get_behat_suites($parallelruns = 0, $currentrun = 0) {
+        $features = $this->get_components_features();
+        $contexts = $this->get_components_contexts();
+
+        // Get number of parallel runs and current run.
+        if (!empty($parallelruns) && !empty($currentrun)) {
+            $this->set_parallel_run($parallelruns, $currentrun);
+        } else {
+            $parallelruns = $this->get_number_of_parallel_run();
+            $currentrun = $this->get_current_run();;
+        }
+
+        $blacklistedfeatures = array();
+        $themefeatures = array();
+        $themesuitecontexts = array();
+        $themecontexts = array();
+
+        $themes = $this->get_list_of_themes();
+
+        // Create list of theme suite features and contexts.
+        foreach ($themes as $theme) {
+            // Get theme features.
+            list($blacklistedfeatures[$theme], $themefeatures[$theme]) = $this->get_behat_features_for_theme($theme);
+
+            list($themecontexts[$theme], $themesuitecontexts[$theme]) = $this->get_behat_contexts_for_theme($theme);
+        }
+
+        // Remove list of theme features for default suite, as default suite should not run theme specific features.
+        foreach ($themefeatures as $removethemefeatures) {
+            $features = $this->remove_blacklisted_features_from_list($features, $removethemefeatures);
+        }
+
+        // Remove list of theme features for default suite, as default suite should not run theme specific features.
+        foreach ($themecontexts as $theme => $themeblacklistcontexts) {
+            if ($themeblacklistcontexts) {
+                foreach ($themeblacklistcontexts as $c) {
+                    // Remove theme specific contexts from default contexts.
+                    unset($contexts[$c]);
+
+                    // Remove theme specific contexts from other themes.
+                    foreach ($themes as $currenttheme) {
+                        if (($currenttheme != $theme) && isset($themesuitecontexts[$currenttheme][$c])) {
+                            unset($themesuitecontexts[$currenttheme][$c]);
+                        }
+                    }
+                }
+            }
+        }
+
+        // Return sub-set of features if parallel run.
+        $featuresforrun = $this->get_features_for_the_run($features, $parallelruns, $currentrun);
+
+        // Default suite.
+        $suites = array(
+            'default' => array(
+                'paths' => array_values($featuresforrun),
+                'contexts' => array_keys($contexts),
+            )
+        );
+
+        // Set suite for each theme.
+        foreach ($themes as $theme) {
+            // Get list of features which will be included in theme.
+            // If theme suite with all features is set, then we want all core features to be part of theme suite.
+            if ($this->themesuitewithallfeatures) {
+                // If there is no theme specific feature. Then it's just core features.
+                if (empty($themefeatures[$theme])) {
+                    $themesuitefeatures = $features;
+                } else {
+                    $themesuitefeatures = array_merge($features, $themefeatures[$theme]);
+                }
+            } else {
+                $themesuitefeatures = $themefeatures[$theme];
+            }
+
+            // Remove blacklisted features.
+            $themesuitefeatures = $this->remove_blacklisted_features_from_list($themesuitefeatures, $blacklistedfeatures[$theme]);
+
+            // Return sub-set of features if parallel run.
+            $themesuitefeatures = $this->get_features_for_the_run($themesuitefeatures, $parallelruns, $currentrun);
+
+            // Add suite no matter what. If there is no feature in suite then it will just exist successfully with no
+            // scenarios. But if we don't set this then the user has to know which run doesn't have suite and which run do.
+            $suites = array_merge($suites, array(
+                $theme => array(
+                    'paths'    => array_values($themesuitefeatures),
+                    'contexts' => array_keys($themesuitecontexts[$theme]),
+                )
+            ));
+        }
+
+        return $suites;
+    }
+
+    /**
+     * Return list of themes which can be set in moodle.
+     *
+     * @return array list of themes with tests.
+     */
+    protected function get_list_of_themes() {
+        $selectablethemes = array();
+
+        // Get all themes installed on site.
+        $themes = core_component::get_plugin_list('theme');
+        ksort($themes);
+
+        foreach ($themes as $themename => $themedir) {
+            // Load the theme config.
+            try {
+                $theme = theme_config::load($themename);
+            } catch (Exception $e) {
+                // Bad theme, just skip it for now.
+                continue;
+            }
+            if ($themename !== $theme->name) {
+                // Obsoleted or broken theme, just skip for now.
+                continue;
+            }
+            if ($theme->hidefromselector) {
+                // The theme doesn't want to be shown in the theme selector and as theme
+                // designer mode is switched off we will respect that decision.
+                continue;
+            }
+            if ($themename == theme_config::DEFAULT_THEME) {
+                // Don't include default theme, as default suite will be running with this theme.
+                continue;
+            }
+            $selectablethemes[] = $themename;
+        }
+
+        return $selectablethemes;
+    }
+
+    /**
+     * Returns all the directories having overridden tests.
+     *
+     * @param string $theme name of theme
+     * @param string $testtype The kind of test we are looking for
+     * @return array all directories having tests
+     */
+    protected function get_test_directories_overridden_for_theme($theme, $testtype) {
+        global $CFG;
+
+        $testtypes = array(
+            'contexts' => '|behat_.*\.php$|',
+            'features' => '|.*\.feature$|',
+        );
+        $themetestdir = "/theme/" . $theme . '/tests/behat';
+        $themetestdirfullpath = $CFG->dirroot . $themetestdir;
+
+        // If test directory doesn't exist then return.
+        if (!is_dir($themetestdirfullpath)) {
+            return array();
+        }
+
+        $directoriestosearch = glob($themetestdirfullpath . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR);
+
+        // Include theme directory to find tests.
+        $dirs[realpath($themetestdirfullpath)] = trim(str_replace('/', '_', $themetestdir), '_');
+
+        // Search for tests in valid directories.
+        foreach ($directoriestosearch as $dir) {
+            $dirite = new RecursiveDirectoryIterator($dir);
+            $iteite = new RecursiveIteratorIterator($dirite);
+            $regexp = $testtypes[$testtype];
+            $regite = new RegexIterator($iteite, $regexp);
+            foreach ($regite as $path => $element) {
+                $key = dirname($path);
+                $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_');
+                $dirs[$key] = $value;
+            }
+        }
+        ksort($dirs);
+
+        return array_flip($dirs);
+    }
+
+    /**
+     * Return blacklisted contexts or features for a theme, as defined in blacklist.json.
+     *
+     * @param string $theme themename
+     * @param string $testtype test type (contexts|features)
+     * @return array list of blacklisted contexts or features
+     */
+    protected function get_blacklisted_tests_for_theme($theme, $testtype) {
+        global $CFG;
+
+        $themetestpath = $CFG->dirroot . DIRECTORY_SEPARATOR . "theme" . DIRECTORY_SEPARATOR . $theme .
+            self::get_behat_tests_path();
+
+        if (file_exists($themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json')) {
+            // Blacklist file exist. Leave it for last to clear the feature and contexts.
+            $blacklisttests = @json_decode(file_get_contents($themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json'), true);
+            if (empty($blacklisttests)) {
+                behat_error(BEHAT_EXITCODE_REQUIREMENT, $themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json is empty');
+            }
+
+            // If features or contexts not defined then no problem.
+            if (!isset($blacklisttests[$testtype])) {
+                $blacklisttests[$testtype] = array();
+            }
+            return $blacklisttests[$testtype];
+        }
+
+        return array();
+    }
+
+    /**
+     * Return list of features and step definitions in theme.
+     *
+     * @param string $theme theme name
+     * @param string $testtype test type, either features or contexts
+     * @return array list of contexts $contexts or $features
+     */
+    protected function get_tests_for_theme($theme, $testtype) {
+
+        $tests = array();
+        $testtypes = array(
+            'contexts' => '|behat_.*\.php$|',
+            'features' => '|.*\.feature$|',
+        );
+
+        // Get all the directories having overridden tests.
+        $directories = $this->get_test_directories_overridden_for_theme($theme, $testtype);
+
+        // Get overridden test contexts.
+        foreach ($directories as $dirpath) {
+            // All behat_*.php inside overridden directory.
+            $diriterator = new DirectoryIterator($dirpath);
+            $regite = new RegexIterator($diriterator, $testtypes[$testtype]);
+
+            // 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');
+                $tests[$key] = $file->getPathname();
+            }
+        }
+
+        return $tests;
+    }
+
+    /**
+     * Return list of blacklisted behat features for theme and features defined by theme only.
+     *
+     * @param string $theme theme name.
+     * @return array ($themeblacklistfeatures, $themefeatures)
+     */
+    protected function get_behat_features_for_theme($theme) {
+
+        // Get list of features defined by theme.
+        $themefeatures = $this->get_tests_for_theme($theme, 'features');
+        $themeblacklistfeatures = $this->get_blacklisted_tests_for_theme($theme, 'features');
+
+        // If tags are specified then we just want features with specified tags.
+        if (!empty($this->tags)) {
+            if (!empty($themefeatures)) {
+                $themefeatures = $this->filtered_features_with_tags($themefeatures);
+            }
+        }
+
+        return array($themeblacklistfeatures, $themefeatures);
+    }
+
+    /**
+     * Return list of contexts overridden by themes.
+     *
+     * @return array.
+     */
+    protected function get_overridden_theme_contexts() {
+        if (empty($this->overriddenthemescontexts)) {
+            $this->overriddenthemescontexts = array();
+        }
+
+        return $this->overriddenthemescontexts;
+    }
+
+    /**
+     * Return list of behat contexts for theme and update $this->stepdefinitions list.
+     *
+     * @param string $theme theme name.
+     * @return array list($themecontexts, $themesuitecontexts)
+     */
+    protected function get_behat_contexts_for_theme($theme) {
+
+        // If we already have this list then just return. This will not change by run.
+        if (!empty($this->themecontexts[$theme]) && !empty($this->themesuitecontexts)) {
+            return array(array_keys($this->themecontexts[$theme]), $this->themesuitecontexts[$theme]);
+        }
+
+        if (empty($this->overriddenthemescontexts)) {
+            $this->overriddenthemescontexts = array();
+        }
+
+        $contexts = $this->get_components_contexts();
+
+        // Create list of contexts used by theme suite.
+        $themecontexts = $this->get_tests_for_theme($theme, 'contexts');
+        $blacklistedcontexts = $this->get_blacklisted_tests_for_theme($theme, 'contexts');
+
+        // Theme suite will use all core contexts, except the one overridden by theme.
+        $themesuitecontexts = $contexts;
+
+        foreach ($themecontexts as $context => $path) {
+
+            // If a context in theme starts with behat_theme_{themename}_behat_* then it's overriding core context.
+            if (preg_match('/^behat_theme_'.$theme.'_(\w+)$/', $context, $match)) {
+
+                if (!empty($themesuitecontexts[$match[1]])) {
+                    unset($themesuitecontexts[$match[1]]);
+                }
+
+                // Add this to the list of overridden paths, so it can be added to final contexts list for class resolver.
+                $this->overriddenthemescontexts[$context] = $path;
+            }
+
+            // Don't include behat_selectors.
+            if ($context === self::get_behat_theme_selector_override_classname($theme)) {
+                unset($this->contexts[$context]);
+                unset($themesuitecontexts[$context]);
+                continue;
+            }
+
+            // Add theme specific contexts with suffix to steps definitions.
+            $themesuitecontexts[$context] = $path;
+        }
+
+        // Remove blacklisted contexts.
+        foreach ($blacklistedcontexts as $blacklistpath) {
+            $blacklistcontext = basename($blacklistpath, '.php');
+
+            unset($themesuitecontexts[$blacklistcontext]);
+        }
+
+        // We are only interested in the class name of context.
+        $this->themesuitecontexts[$theme] = $themesuitecontexts;
+        $this->themecontexts[$theme] = $themecontexts;
+
+        return array(array_keys($themecontexts), $themesuitecontexts);
+    }
+}
\ No newline at end of file
index 47d7fb0..99641cd 100644 (file)
@@ -55,8 +55,22 @@ class behat_context_helper {
      *
      * @param Environment $environment
      * @return void
+     * @deprecated since 3.2 MDL-55072 - please use behat_context_helper::set_environment()
+     * @todo MDL-55365 This will be deleted in Moodle 3.6.
      */
     public static function set_session(Environment $environment) {
+        debugging('set_session is deprecated. Please use set_environment instead.', DEBUG_DEVELOPER);
+
+        self::set_environment($environment);
+    }
+
+    /**
+     * Sets behat environment.
+     *
+     * @param Environment $environment
+     * @return void
+     */
+    public static function set_environment(Environment $environment) {
         self::$environment = $environment;
     }
 
@@ -67,17 +81,27 @@ class behat_context_helper {
      * that uses direct API calls; steps returning step chains
      * can not be executed like this.
      *
-     * @throws coding_exception
+     * @throws Behat\Behat\Context\Exception\ContextNotFoundException
      * @param string $classname Context identifier (the class name).
      * @return behat_base
      */
     public static function get($classname) {
 
-        if (!$subcontext = self::$environment->getContext($classname)) {
-            throw coding_exception('The required "' . $classname . '" class does not exist');
+        $suitename = self::$environment->getSuite()->getName();
+        $overridencontextname = 'behat_theme_'.$suitename.'_'.$classname;
+
+        // Check if overridden context class exists.
+        if ($suitename !== 'default') {
+            try {
+                $subcontext = self::$environment->getContext($overridencontextname);
+                return $subcontext;
+            } catch (Behat\Behat\Context\Exception\ContextNotFoundException $e) {
+                // If context not found then it's not overridden.
+            }
         }
 
-        return $subcontext;
+        // Get the actual context.
+        return self::$environment->getContext($classname);
     }
 
     /**
index a528b70..c29ed08 100644 (file)
@@ -23,7 +23,6 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-defined('MOODLE_INTERNAL') || die();
 
 /**
  * Moodle selectors manager.