+++ /dev/null
---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,
"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",
"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",
"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",
// "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));
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);
'help' => false,
'fromrun' => 1,
'torun' => 0,
+ 'run-with-theme' => false,
),
array(
'j' => 'parallel',
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
}
}
+$themesuitewithallfeatures = '';
+if ($options['run-with-theme']) {
+ $themesuitewithallfeatures = '--run-with-theme="true"';
+}
+
// Changing the cwd to admin/tool/behat/cli.
$cwd = getcwd();
$output = null;
// 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";
} 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);
} 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);
// 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);
'tags' => '',
'profile' => '',
'feature' => '',
+ 'suite' => '',
'fromrun' => 1,
'torun' => 0,
'single-run' => false,
+ 'run-with-theme' => false,
),
array(
'h' => 'help',
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
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");
}
$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'];
// 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;
} 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;
'updatesteps' => false,
'fromrun' => 1,
'torun' => 0,
+ 'run-with-theme' => false,
),
array(
'h' => 'help',
'j' => 'parallel',
- 'm' => 'maxruns'
+ 'm' => 'maxruns',
)
);
--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
} 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);
}
'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',
)
);
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
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).
behat_util::install_site();
// This is only displayed once for parallel install.
- if (empty($options['run'])) {
+ if (empty($run)) {
mtrace("Acceptance tests site installed");
}
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 .
} 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");
}
$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.
);
$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']);
);
$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']);
);
$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
--- /dev/null
+<?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
$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';
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();
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');
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');
// 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";
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();
$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;
}
$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;
}
* @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']));
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);
}
}
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"
$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();
+
+ }
}
$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)));
$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);
} 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();
--- /dev/null
+@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"
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"
{{$title}}
{{#title}}
- <div class="title">{{feedtitle}}</div>
+ <div class="title">{{title}}</div>
{{/title}}
{{/title}}
* @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');
}
}
- 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;
}
$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.
$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;
}
}
*/
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.
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.
*
$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
*/
$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),
$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.
$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);
/**
* 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
*/
* @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
* @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);
}
}
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();
+ }
}
/**
$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);
* 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) {
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"));
}
/**
// 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');
+++ /dev/null
-<?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
| 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"
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,
// //
/////////////////////////////////////////////////////////////////////////////
-// 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');
$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);
// $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.
//
//
//=========================================================================
);
}
+ /**
+ * 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();
+ }
}
$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;
}
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();
$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;
+}
$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);
+ }
}
$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);
+ }
+ }
+ }
}
$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';
$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')));
$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.';
$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.
$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&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');
// 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'));
}
$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.';
$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';
$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';
// 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 {
}
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
public static function get_default_options() {
$options = array(
'sortlevelsasc' => 1,
+ 'lockzeropoints' => 1,
'alwaysshowdefinition' => 1,
'showdescriptionteacher' => 1,
'showdescriptionstudent' => 1,
/**
* 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() {
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;
}
$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');
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;
}
/**
$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
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;
}
}
'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;
// 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();
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:
#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"
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.
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"
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.
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
--- /dev/null
+@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 |
--- /dev/null
+@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
$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} را دارید';
defined('MOODLE_INTERNAL') || die();
+$string['cannotfindcomponent'] = 'کامپوننت پیدا نشد';
$string['missingrequiredfield'] = 'بعضی از فیلدهای ضروری خالی است';
$string['admindirname'] = 'دایرکتوری مدیر';
$string['availablelangs'] = 'بستههای زبانی موجود';
$string['chooselanguagehead'] = 'انتخاب زبان';
-$string['chooselanguagesub'] = 'لطفاً زبانی را به جهت استفاده در حین نصب انتخاب نمائید. زبان سایت و کاربر را در صفحهای دیگر میتوانید انتخاب کنید.';
+$string['chooselanguagesub'] = 'لطفاً زبانی را به جهت استفاده در حین نصب انتخاب نمایید. زبانی که در این صفحه انتخاب میکنید به عنوان زبان پیشفرض سایت نیز مورد استفاده قرار خواهد گرفت. البته میتوانید بعداً آن را تغییر دهید.';
+$string['databasehost'] = 'میزبان پایگاه داده';
+$string['databasename'] = 'نام پایگاه داده';
$string['dataroot'] = 'دایرکتوری داده';
$string['dbprefix'] = 'پیشوند جدولها';
$string['dirroot'] = 'دایرکتوری مودل';
defined('MOODLE_INTERNAL') || die();
$string['language'] = 'زبان';
+$string['moodlelogo'] = 'لوگوی مودل';
$string['next'] = 'ادامه';
$string['previous'] = 'قبلی';
$string['reload'] = 'بررسی مجدد';
$string['cliyesnoprompt'] = 'натисніть y (означає так) або n (означає ні)';
$string['environmentrequireinstall'] = 'повинен бути встановлений і включений';
$string['environmentrequireversion'] = 'потрібна версія {$a->needed}, ви запускаєте {$a->current}';
+$string['upgradekeyset'] = 'Оновлення ключа (залишіть порожнім, якщо не встановлено)';
defined('MOODLE_INTERNAL') || die();
$string['language'] = 'Мова інтерфейсу';
+$string['moodlelogo'] = 'Логотип Moodle';
$string['next'] = 'Далі';
$string['previous'] = 'Назад';
$string['reload'] = 'Оновити';
$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.';
$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.';
$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';
$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';
$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';
$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';
$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.
$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}';
$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';
$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';
// 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);
$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);
}
}
*/
Bar.prototype._horizontal = false;
+ /**
+ * Whether the bars should be stacked or not.
+ *
+ * @type {Bool}
+ * @protected
+ */
+ Bar.prototype._stacked = false;
+
/** @override */
Bar.prototype.TYPE = 'bar';
Bar.prototype.create = function(Klass, data) {
var chart = Base.prototype.create.apply(this, arguments);
chart.setHorizontal(data.horizontal);
+ chart.setStacked(data.stacked);
return chart;
};
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.
*
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;
});
};
/**
- * 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
*/
// 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;
return axisLabels[index] || '';
};
}
+ config.options.scales.xAxes[i].stacked = this._isStacked();
}.bind(this));
this._chart.getYAxes().forEach(function(axis, i) {
return axisLabels[parseInt(value, 10)] || '';
};
}
+ config.options.scales.yAxes[i].stacked = this._isStacked();
}.bind(this));
config.options.tooltips = {
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());
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) {
/** @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.
*
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.
*
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);
attributes: [
{name: 'src', value: url},
{name: 'alt', value: helper(text)},
+ {name: 'title', value: helper(text)},
{name: 'class', value: 'smallicon'}
]
};
// 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;
}
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));
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) {
if (empty($filepickerelement)) {
$filepickercontainer = $this->find(
'xpath',
- "//*[@class=\"form-filemanager\"]",
+ "//*[@data-fieldtype=\"filemanager\"]",
$exception
);
} else {
$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
);
}
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
*/
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
*
* @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.
$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)) {
* @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);
}
/**
* 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();
}
/**
* @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);
}
/**
* @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);
}
/**
*/
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);
}
/**
* @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);
}
/**
* @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();
}
}
--- /dev/null
+<?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
*
* @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;
}
* 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);
}
/**
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
/**
* Moodle selectors manager.