</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
+ <MOODLE version="2.9" requires="2.2">
+ <UNICODE level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unicoderequired" />
+ </FEEDBACK>
+ </UNICODE>
+ <DATABASE level="required">
+ <VENDOR name="mariadb" version="5.5.31" />
+ <VENDOR name="mysql" version="5.5.31" />
+ <VENDOR name="postgres" version="9.1" />
+ <VENDOR name="mssql" version="10.0" />
+ <VENDOR name="oracle" version="10.2" />
+ </DATABASE>
+ <PHP version="5.4.4" level="required">
+ </PHP>
+ <PCREUNICODE level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="pcreunicodewarning" />
+ </FEEDBACK>
+ </PCREUNICODE>
+ <PHP_EXTENSIONS>
+ <PHP_EXTENSION name="iconv" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="iconvrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="mbstring" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="mbstringrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="curl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="curlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="openssl" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="opensslrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="tokenizer" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="tokenizerrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xmlrpc" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="xmlrpcrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="soap" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="soaprecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="ctype" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="ctyperequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="zip" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="ziprequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="zlib" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="gd" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="gdrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="simplexml" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="simplexmlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="spl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="splrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="pcre" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="dom" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xml" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="intl" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="intlrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="json" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="hash" level="required"/>
+ </PHP_EXTENSIONS>
+ <PHP_SETTINGS>
+ <PHP_SETTING name="memory_limit" value="96M" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="settingmemorylimit" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ <PHP_SETTING name="file_uploads" value="1" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="settingfileuploads" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ <PHP_SETTING name="opcache.enable" value="1" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="opcacherecommended" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ </PHP_SETTINGS>
+ <CUSTOM_CHECKS>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unsupporteddbstorageengine" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="quizattemptsupgradedmessage" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ </CUSTOM_CHECKS>
+ </MOODLE>
</COMPATIBILITY_MATRIX>
require_once(__DIR__ . '/../../../../lib/clilib.php');
require_once(__DIR__ . '/../../../../lib/behat/lib.php');
+list($options, $unrecognized) = cli_get_params(
+ array(
+ 'parallel' => 0,
+ 'maxruns' => false,
+ 'help' => false,
+ 'fromrun' => 1,
+ 'torun' => 0,
+ ),
+ array(
+ 'j' => 'parallel',
+ 'm' => 'maxruns',
+ 'h' => 'help',
+ )
+);
+
+// Checking run.php CLI script usage.
+$help = "
+Behat utilities to initialise behat tests
+
+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)
+
+-h, --help Print out this help
+
+Example from Moodle root directory:
+\$ php admin/tool/behat/cli/init.php --parallel=2
+
+More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
+";
+
+if (!empty($options['help'])) {
+ echo $help;
+ exit(0);
+}
+
+// Check which util file to call.
+$utilfile = 'util_single_run.php';
+$paralleloption = "";
+// If parallel run then use utilparallel.
+if ($options['parallel']) {
+ $utilfile = 'util.php';
+ $paralleloption = "";
+ foreach ($options as $option => $value) {
+ if ($value) {
+ $paralleloption .= " --$option=\"$value\"";
+ }
+ }
+}
+
// Changing the cwd to admin/tool/behat/cli.
-chdir(__DIR__);
+$cwd = getcwd();
$output = null;
-exec("php util.php --diag", $output, $code);
-if ($code == 0) {
- echo "Behat test environment already installed\n";
-} else if ($code == BEHAT_EXITCODE_INSTALL) {
+// If behat dependencies not downloaded then do it first, else symfony/process can't be used.
+if ($options['parallel'] && !file_exists(__DIR__ . "/../../../../vendor/autoload.php")) {
+ $code = BEHAT_EXITCODE_COMPOSER;
+} else {
+ chdir(__DIR__);
+ exec("php $utilfile --diag $paralleloption", $output, $code);
+}
+// Check if composer needs to be updated.
+if (($code == BEHAT_EXITCODE_INSTALL) || $code == BEHAT_EXITCODE_REINSTALL || $code == BEHAT_EXITCODE_COMPOSER) {
testing_update_composer_dependencies();
+}
+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 util.php --install", $code);
+ passthru("php $utilfile --install $paralleloption", $code);
if ($code != 0) {
+ chdir($cwd);
exit($code);
}
} else if ($code == BEHAT_EXITCODE_REINSTALL) {
-
- testing_update_composer_dependencies();
-
// Test site data is outdated.
chdir(__DIR__);
- passthru("php util.php --drop", $code);
+ passthru("php $utilfile --drop $paralleloption", $code);
if ($code != 0) {
+ chdir($cwd);
exit($code);
}
- passthru("php util.php --install", $code);
+ chdir(__DIR__);
+ passthru("php $utilfile --install $paralleloption", $code);
if ($code != 0) {
+ chdir($cwd);
exit($code);
}
} else if ($code == BEHAT_EXITCODE_COMPOSER) {
// Missing Behat dependencies.
-
- testing_update_composer_dependencies();
-
// Returning to admin/tool/behat/cli.
chdir(__DIR__);
- passthru("php util.php --install", $code);
+ passthru("php $utilfile --install $paralleloption", $code);
if ($code != 0) {
+ chdir($cwd);
exit($code);
}
} else {
// Generic error, we just output it.
echo implode("\n", $output)."\n";
+ chdir($cwd);
exit($code);
}
// Enable editing mode according to config.php vars.
-passthru("php util.php --enable", $code);
+chdir(__DIR__);
+passthru("php $utilfile --enable $paralleloption", $code);
if ($code != 0) {
+ echo "Error enabling site" . PHP_EOL;
+ chdir($cwd);
exit($code);
}
--- /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/>.
+
+/**
+ * Wrapper to run previously set-up behat tests in parallel.
+ *
+ * @package tool_behat
+ * @copyright 2014 NetSpot Pty Ltd
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+if (isset($_SERVER['REMOTE_ADDR'])) {
+ die(); // No access from web!
+}
+
+define('BEHAT_UTIL', true);
+define('CLI_SCRIPT', true);
+define('ABORT_AFTER_CONFIG', true);
+define('CACHE_DISABLE_ALL', true);
+define('NO_OUTPUT_BUFFERING', true);
+
+require_once(__DIR__ .'/../../../../config.php');
+require_once(__DIR__.'/../../../../lib/clilib.php');
+require_once(__DIR__.'/../../../../lib/behat/lib.php');
+require_once(__DIR__.'/../../../../lib/behat/classes/behat_command.php');
+require_once(__DIR__.'/../../../../lib/behat/classes/behat_config_manager.php');
+
+error_reporting(E_ALL | E_STRICT);
+ini_set('display_errors', '1');
+ini_set('log_errors', '1');
+
+list($options, $unrecognised) = cli_get_params(
+ array(
+ 'stop-on-failure' => 0,
+ 'verbose' => false,
+ 'replace' => false,
+ 'help' => false,
+ 'tags' => '',
+ 'profile' => '',
+ 'fromrun' => 1,
+ 'torun' => 0,
+ ),
+ array(
+ 'h' => 'help',
+ 't' => 'tags',
+ 'p' => 'profile',
+ )
+);
+
+// Checking run.php CLI script usage.
+$help = "
+Behat utilities to run behat tests in parallel
+Options:
+-t, --tags Tags to execute.
+-p, --profile Profile to execute.
+--stop-on-failure Stop on failure in any parallel run.
+--verbose Verbose output
+--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)
+
+-h, --help Print out this help
+
+Example from Moodle root directory:
+\$ php admin/tool/behat/cli/run.php --parallel=2
+
+More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
+";
+
+if (!empty($options['help'])) {
+ echo $help;
+ exit(0);
+}
+
+$parallelrun = behat_config_manager::get_parallel_test_runs($options['fromrun']);
+
+// Default torun is maximum parallel runs.
+if (empty($options['torun'])) {
+ $options['torun'] = $parallelrun;
+}
+
+// Capture signals and ensure we clean symlinks.
+if (extension_loaded('pcntl')) {
+ $disabled = explode(',', ini_get('disable_functions'));
+ if (!in_array('pcntl_signal', $disabled)) {
+ pcntl_signal(SIGTERM, "signal_handler");
+ pcntl_signal(SIGINT, "signal_handler");
+ }
+}
+
+// If empty parallelrun then just check with user if it's a run single behat test.
+if (empty($parallelrun)) {
+ if (cli_input("This is not a parallel site, do you want to run single behat run? (Y/N)", 'n', array('y', 'n')) == 'y') {
+ $runtestscommand = behat_command::get_behat_command();
+ $runtestscommand .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
+ exec("php $runtestscommand", $output, $code);
+ echo implode(PHP_EOL, $output) . PHP_EOL;
+ exit($code);
+ } else {
+ exit(1);
+ }
+}
+
+// Create site symlink if necessary.
+if (!behat_config_manager::create_parallel_site_links($options['fromrun'], $options['torun'])) {
+ echo "Check permissions. If on windows, make sure you are running this command as admin" . PHP_EOL;
+ exit(1);
+}
+
+$time = microtime(true);
+array_walk($unrecognised, function (&$v) {
+ if ($x = preg_filter("#^(-+\w+)=(.+)#", "\$1='\$2'", $v)) {
+ $v = $x;
+ } else if (!preg_match("#^-#", $v)) {
+ $v = escapeshellarg($v);
+ }
+});
+$extraopts = implode(' ', $unrecognised);
+
+$tags = '';
+
+if ($options['profile']) {
+ $profile = $options['profile'];
+ if (empty($CFG->behat_config[$profile]['filters']['tags'])) {
+ echo "Invaid profile passed: " . $profile;
+ exit(1);
+ }
+ $tags = $CFG->behat_config[$profile]['filters']['tags'];
+ $extraopts .= '--profile=\'' . $profile . "'";
+} else if ($options['tags']) {
+ $tags = $options['tags'];
+ $extraopts .= '--tags="' . $tags . '"';
+}
+
+// Update config file if tags defined.
+if ($tags) {
+ // Hack to set proper dataroot and wwwroot.
+ $behatdataroot = $CFG->behat_dataroot;
+ $behatwwwroot = $CFG->behat_wwwroot;
+ for ($i = 1; $i <= $parallelrun; $i++) {
+ $CFG->behatrunprocess = $i;
+ $CFG->behat_dataroot = $behatdataroot . $i;
+ if (!empty($CFG->behat_parallel_run['behat_wwwroot'][$i - 1]['behat_wwwroot'])) {
+ $CFG->behat_wwwroot = $CFG->behat_parallel_run['behat_wwwroot'][$i - 1]['behat_wwwroot'];
+ } else {
+ $CFG->behat_wwwroot = $behatwwwroot . "/" . BEHAT_PARALLEL_SITE_NAME . $i;
+ }
+ behat_config_manager::update_config_file('', true, $tags);
+ }
+ $CFG->behat_dataroot = $behatdataroot;
+ $CFG->behat_wwwroot = $behatwwwroot;
+ unset($CFG->behatrunprocess);
+}
+
+$cmds = array();
+echo "Running " . ($options['torun'] - $options['fromrun'] + 1) . " parallel behat sites:" . PHP_EOL;
+
+for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
+ $CFG->behatrunprocess = $i;
+
+ // Options parameters to be added to each run.
+ $myopts = !empty($options['replace']) ? str_replace($options['replace'], $i, $extraopts) : $extraopts;
+
+ $behatcommand = behat_command::get_behat_command();
+ $behatconfigpath = behat_config_manager::get_behat_cli_config_filepath($i);
+
+ // Command to execute behat run.
+ $cmds[BEHAT_PARALLEL_SITE_NAME . $i] = $behatcommand . ' --config ' . $behatconfigpath . " " . $myopts;
+ echo "[" . BEHAT_PARALLEL_SITE_NAME . $i . "] " . $cmds[BEHAT_PARALLEL_SITE_NAME . $i] . PHP_EOL;
+}
+
+if (empty($cmds)) {
+ echo "No commands to execute " . PHP_EOL;
+ exit(1);
+}
+
+// Execute all commands.
+$processes = cli_execute_parallel($cmds);
+$stoponfail = empty($options['stop-on-failure']) ? false : true;
+
+// Print header.
+print_process_start_info($processes);
+
+// Print combined run o/p from processes.
+$exitcodes = print_combined_run_output($processes, $stoponfail);
+$time = round(microtime(true) - $time, 1);
+echo "Finished in " . gmdate("G\h i\m s\s", $time) . PHP_EOL . PHP_EOL;
+
+
+// Print exit info from each run.
+$status = false;
+foreach ($exitcodes as $exitcode) {
+ $status = (bool)$status || (bool)$exitcode;
+}
+
+// Show exit code from each process, if any process failed.
+if ($status) {
+ echo "Exit codes: " . implode(" ", $exitcodes) . PHP_EOL;
+ echo "To re-run failed processes, you can use following commands:" . PHP_EOL;
+ foreach ($cmds as $name => $cmd) {
+ if (!empty($exitcodes[$name])) {
+ echo "[" . $name . "] " . $cmd . PHP_EOL;
+ }
+ }
+ echo PHP_EOL;
+}
+
+// Run finished. Show exit code and output from individual process.
+$verbose = empty($options['verbose']) ? false : true;
+$verbose = $verbose || $status;
+print_each_process_info($processes, $verbose);
+
+// Remove site symlink if necessary.
+behat_config_manager::drop_parallel_site_links();
+
+exit((int) $status);
+
+/**
+ * Signal handler for terminal exit.
+ *
+ * @param int $signal signal number.
+ */
+function signal_handler($signal) {
+ switch ($signal) {
+ case SIGTERM:
+ case SIGKILL:
+ case SIGINT:
+ // Remove site symlink if necessary.
+ behat_config_manager::drop_parallel_site_links();
+ exit(1);
+ }
+}
+
+/**
+ * Prints header from the first process.
+ *
+ * @param array $processes list of processes to loop though.
+ */
+function print_process_start_info($processes) {
+ $printed = false;
+ // Keep looping though processes, till we get first process o/p.
+ while (!$printed) {
+ usleep(10000);
+ foreach ($processes as $name => $process) {
+ // Exit if any process has stopped.
+ if (!$process->isRunning()) {
+ $printed = true;
+ break;
+ }
+
+ $op = explode(PHP_EOL, $process->getOutput());
+ if (count($op) >= 3) {
+ foreach ($op as $line) {
+ if (trim($line) && (strpos($line, '.') !== 0)) {
+ echo $line . PHP_EOL;
+ }
+ }
+ $printed = true;
+ }
+ }
+ }
+}
+
+/**
+ * Loop though all processes and print combined o/p
+ *
+ * @param array $processes list of processes to loop though.
+ * @param bool $stoponfail Stop all processes and exit if failed.
+ * @return array list of exit codes from all processes.
+ */
+function print_combined_run_output($processes, $stoponfail = false) {
+ $exitcodes = array();
+ $maxdotsonline = 70;
+ $remainingprintlen = $maxdotsonline;
+ $progresscount = 0;
+ while (count($exitcodes) != count($processes)) {
+ usleep(10000);
+ foreach ($processes as $name => $process) {
+ if ($process->isRunning()) {
+ $op = $process->getIncrementalOutput();
+ if (trim($op)) {
+ $update = preg_filter('#^\s*([FS\.\-]+)(?:\s+\d+)?\s*$#', '$1', $op);
+ // Exit process if anything fails.
+ if ($stoponfail && (strpos($update, 'F') !== false)) {
+ $process->stop(0);
+ }
+
+ $strlentoprint = strlen($update);
+
+ // If not enough dots printed on line then just print.
+ if ($strlentoprint < $remainingprintlen) {
+ echo $update;
+ $remainingprintlen = $remainingprintlen - $strlentoprint;
+ } else if ($strlentoprint == $remainingprintlen) {
+ $progresscount += $maxdotsonline;
+ echo $update ." " . $progresscount . PHP_EOL;
+ $remainingprintlen = $maxdotsonline;
+ } else {
+ while ($part = substr($update, 0, $remainingprintlen) > 0) {
+ $progresscount += $maxdotsonline;
+ echo $part . " " . $progresscount . PHP_EOL;
+ $update = substr($update, $remainingprintlen);
+ $remainingprintlen = $maxdotsonline;
+ }
+ }
+ }
+ } else {
+ $exitcodes[$name] = $process->getExitCode();
+ if ($stoponfail && ($exitcodes[$name] != 0)) {
+ foreach ($processes as $l => $p) {
+ $exitcodes[$l] = -1;
+ $process->stop(0);
+ }
+ }
+ }
+ }
+ }
+
+ echo PHP_EOL;
+ return $exitcodes;
+}
+
+/**
+ * Loop though all processes and print combined o/p
+ *
+ * @param array $processes list of processes to loop though.
+ * @param bool $verbose Show verbose output for each process.
+ */
+function print_each_process_info($processes, $verbose = false) {
+ foreach ($processes as $name => $process) {
+ echo "**************** [" . $name . "] ****************" . PHP_EOL;
+ if ($verbose) {
+ echo $process->getOutput();
+ echo $process->getErrorOutput();
+ } else {
+ $op = explode(PHP_EOL, $process->getOutput());
+ foreach ($op as $line) {
+ // Don't print progress .
+ if (trim($line) && (strpos($line, '.') !== 0) && (strpos($line, 'Moodle ') !== 0) &&
+ (strpos($line, 'Server OS ') !== 0) && (strpos($line, 'Started at ') !== 0) &&
+ (strpos($line, 'Browser specific fixes ') !== 0)) {
+ echo $line . PHP_EOL;
+ }
+ }
+ }
+ echo PHP_EOL;
+ }
+}
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * CLI tool with utilities to manage Behat integration in Moodle
+ * CLI tool with utilities to manage parallel Behat integration in Moodle
*
* All CLI utilities uses $CFG->behat_dataroot and $CFG->prefix_dataroot as
* $CFG->dataroot and $CFG->prefix
die(); // No access from web!.
}
-// Basic functions.
+define('BEHAT_UTIL', true);
+define('CLI_SCRIPT', true);
+define('NO_OUTPUT_BUFFERING', true);
+define('IGNORE_COMPONENT_CACHE', true);
+define('ABORT_AFTER_CONFIG', true);
+
+require_once(__DIR__ . '/../../../../config.php');
require_once(__DIR__ . '/../../../../lib/clilib.php');
require_once(__DIR__ . '/../../../../lib/behat/lib.php');
-
+require_once(__DIR__ . '/../../../../lib/behat/classes/behat_command.php');
+require_once(__DIR__ . '/../../../../lib/behat/classes/behat_config_manager.php');
// CLI options.
list($options, $unrecognized) = cli_get_params(
array(
- 'help' => false,
- 'install' => false,
- 'drop' => false,
- 'enable' => false,
- 'disable' => false,
- 'diag' => false
+ 'help' => false,
+ 'install' => false,
+ 'drop' => false,
+ 'enable' => false,
+ 'disable' => false,
+ 'diag' => false,
+ 'parallel' => 0,
+ 'maxruns' => false,
+ 'updatesteps' => false,
+ 'fromrun' => 1,
+ 'torun' => 0,
),
array(
- 'h' => 'help'
+ 'h' => 'help',
+ 'j' => 'parallel',
+ 'm' => 'maxruns'
)
);
-if ($options['install'] or $options['drop']) {
- define('CACHE_DISABLE_ALL', true);
-}
-
// Checking util.php CLI script usage.
$help = "
Behat utilities to manage the test environment
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
+--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
+-j, --parallel Number of parallel behat run operation
+-m, --maxruns Max parallel processes to be executed at one time.
+--updatesteps Update feature step file.
-h, --help Print out this help
Example from Moodle root directory:
-\$ php admin/tool/behat/cli/util.php --enable
+\$ php admin/tool/behat/cli/util.php --enable --parallel=4
More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
";
exit(0);
}
-// Describe this script.
-define('BEHAT_UTIL', true);
-define('CLI_SCRIPT', true);
-define('NO_OUTPUT_BUFFERING', true);
-define('IGNORE_COMPONENT_CACHE', true);
+$cwd = getcwd();
-// Only load CFG from config.php, stop ASAP in lib/setup.php.
-define('ABORT_AFTER_CONFIG', true);
-require_once(__DIR__ . '/../../../../config.php');
+// For drop option check if parallel site.
+if ((empty($options['parallel'])) && $options['drop']) {
+ // Get parallel run info from first run.
+ $options['parallel'] = behat_config_manager::get_parallel_test_runs($options['fromrun']);
+}
+
+// If not a parallel site then open single run.
+if (empty($options['parallel'])) {
+ chdir(__DIR__);
+ // Check if behat is initialised, if not exit.
+ passthru("php util_single_run.php --diag", $status);
+ if ($status) {
+ exit ($status);
+ }
+ $cmd = commands_to_execute($options);
+ $processes = cli_execute_parallel(array($cmd), __DIR__);
+ $status = print_sequential_output($processes, false);
+ chdir($cwd);
+ exit($status);
+}
+
+// Default torun is maximum parallel runs.
+if (empty($options['torun'])) {
+ $options['torun'] = $options['parallel'];
+}
-// Remove error handling overrides done in config.php.
-$CFG->debug = (E_ALL | E_STRICT);
-$CFG->debugdisplay = 1;
-error_reporting($CFG->debug);
-ini_set('display_errors', '1');
-ini_set('log_errors', '1');
+$status = false;
+$cmds = commands_to_execute($options);
-// Finish moodle init.
-define('ABORT_AFTER_CONFIG_CANCEL', true);
-require("$CFG->dirroot/lib/setup.php");
+// Start executing commands either sequential/parallel for options provided.
+if ($options['diag'] || $options['enable'] || $options['disable']) {
+ // Do it sequentially as it's fast and need to be displayed nicely.
+ foreach (array_chunk($cmds, 1, true) as $cmd) {
+ $processes = cli_execute_parallel($cmd, __DIR__);
+ print_sequential_output($processes);
+ }
-raise_memory_limit(MEMORY_HUGE);
+} else if ($options['drop']) {
+ $processes = cli_execute_parallel($cmds, __DIR__);
+ $exitcodes = print_combined_drop_output($processes);
+ foreach ($exitcodes as $exitcode) {
+ $status = (bool)$status || (bool)$exitcode;
+ }
-require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/upgradelib.php');
-require_once($CFG->libdir.'/clilib.php');
-require_once($CFG->libdir.'/installlib.php');
-require_once($CFG->libdir.'/testing/classes/test_lock.php');
+} else if ($options['install']) {
+ // This is intensive compared to behat itself so run them in chunk if option maxruns not set.
+ if ($options['maxruns']) {
+ foreach (array_chunk($cmds, $options['maxruns'], true) as $chunk) {
+ $processes = cli_execute_parallel($chunk, __DIR__);
+ $exitcodes = print_combined_install_output($processes);
+ foreach ($exitcodes as $name => $exitcode) {
+ if ($exitcode != 0) {
+ echo "Failed process [[$name]]" . PHP_EOL;
+ echo $processes[$name]->getOutput();
+ echo PHP_EOL;
+ echo $processes[$name]->getErrorOutput();
+ echo PHP_EOL . PHP_EOL;
+ }
+ $status = (bool)$status || (bool)$exitcode;
+ }
+ }
+ } else {
+ $processes = cli_execute_parallel($cmds, __DIR__);
+ $exitcodes = print_combined_install_output($processes);
+ foreach ($exitcodes as $name => $exitcode) {
+ if ($exitcode != 0) {
+ echo "Failed process [[$name]]" . PHP_EOL;
+ echo $processes[$name]->getOutput();
+ echo PHP_EOL;
+ echo $processes[$name]->getErrorOutput();
+ echo PHP_EOL . PHP_EOL;
+ }
+ $status = (bool)$status || (bool)$exitcode;
+ }
+ }
-if ($unrecognized) {
- $unrecognized = implode("\n ", $unrecognized);
- cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+} else {
+ // We should never reach here.
+ echo $help;
+ exit(1);
}
-// Behat utilities.
-require_once($CFG->libdir . '/behat/classes/util.php');
-require_once($CFG->libdir . '/behat/classes/behat_command.php');
+// Ensure we have success status to show following information.
+if ($status) {
+ echo "Unknown failure $status" . PHP_EOL;
+ exit((int)$status);
+}
-// Run command (only one per time).
+// Show command o/p (only one per time).
if ($options['install']) {
- behat_util::install_site();
- mtrace("Acceptance tests site installed");
+ echo "Acceptance tests site installed for sites:".PHP_EOL;
+
+ // Display all sites which are installed/drop/diabled.
+ for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
+ if (empty($CFG->behat_parallel_run[$i - 1]['behat_wwwroot'])) {
+ echo $CFG->behat_wwwroot . "/" . BEHAT_PARALLEL_SITE_NAME . $i . PHP_EOL;
+ } else {
+ echo $CFG->behat_parallel_run[$i - 1]['behat_wwwroot'] . PHP_EOL;
+ }
+
+ }
} else if ($options['drop']) {
- // Ensure no tests are running.
- test_lock::acquire('behat');
- behat_util::drop_site();
- mtrace("Acceptance tests site dropped");
+ echo "Acceptance tests site dropped for " . $options['parallel'] . " parallel sites" . PHP_EOL;
+
} else if ($options['enable']) {
- behat_util::start_test_mode();
- $runtestscommand = behat_command::get_behat_command(true) .
- ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
- mtrace("Acceptance tests environment enabled on $CFG->behat_wwwroot, to run the tests use:\n " . $runtestscommand);
+ echo "Acceptance tests environment enabled on $CFG->behat_wwwroot, to run the tests use:" . PHP_EOL;
+ echo behat_command::get_behat_command(true, true);
+ echo PHP_EOL;
+
} else if ($options['disable']) {
- behat_util::stop_test_mode();
- mtrace("Acceptance tests environment disabled");
-} else if ($options['diag']) {
- $code = behat_util::get_behat_status();
- exit($code);
+ echo "Acceptance tests environment disabled for " . $options['parallel'] . " parallel sites" . PHP_EOL;
+
} else {
echo $help;
}
+chdir($cwd);
exit(0);
+
+/**
+ * Create commands to be executed for parallel run.
+ *
+ * @param array $options options provided by user.
+ * @return array commands to be executed.
+ */
+function commands_to_execute($options) {
+ $removeoptions = array('maxruns', 'fromrun', 'torun');
+ $cmds = array();
+ $extraoptions = $options;
+ $extra = "";
+
+ // Remove extra options not in util_single_run.php.
+ foreach ($removeoptions as $ro) {
+ $extraoptions[$ro] = null;
+ unset($extraoptions[$ro]);
+ }
+
+ foreach ($extraoptions as $option => $value) {
+ if ($options[$option]) {
+ $extra .= " --$option";
+ if ($value) {
+ $extra .= "=$value";
+ }
+ }
+ }
+
+ if (empty($options['parallel'])) {
+ $cmds = "php util_single_run.php " . $extra;
+ } else {
+ // Create commands which has to be executed for parallel site.
+ for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
+ $prefix = BEHAT_PARALLEL_SITE_NAME . $i;
+ $cmds[$prefix] = "php util_single_run.php " . $extra . " --run=" . $i . " 2>&1";
+ }
+ }
+ return $cmds;
+}
+
+/**
+ * Print drop output merging each run.
+ *
+ * @param array $processes list of processes.
+ * @return array exit codes of each process.
+ */
+function print_combined_drop_output($processes) {
+ $exitcodes = array();
+ $maxdotsonline = 70;
+ $remainingprintlen = $maxdotsonline;
+ $progresscount = 0;
+ echo "Dropping tables:" . PHP_EOL;
+
+ while (count($exitcodes) != count($processes)) {
+ usleep(10000);
+ foreach ($processes as $name => $process) {
+ if ($process->isRunning()) {
+ $op = $process->getIncrementalOutput();
+ if (trim($op)) {
+ $update = preg_filter('#^\s*([FS\.\-]+)(?:\s+\d+)?\s*$#', '$1', $op);
+ $strlentoprint = strlen($update);
+
+ // If not enough dots printed on line then just print.
+ if ($strlentoprint < $remainingprintlen) {
+ echo $update;
+ $remainingprintlen = $remainingprintlen - $strlentoprint;
+ } else if ($strlentoprint == $remainingprintlen) {
+ $progresscount += $maxdotsonline;
+ echo $update . " " . $progresscount . PHP_EOL;
+ $remainingprintlen = $maxdotsonline;
+ } else {
+ while ($part = substr($update, 0, $remainingprintlen) > 0) {
+ $progresscount += $maxdotsonline;
+ echo $part . " " . $progresscount . PHP_EOL;
+ $update = substr($update, $remainingprintlen);
+ $remainingprintlen = $maxdotsonline;
+ }
+ }
+ }
+ } else {
+ // Process exited.
+ $process->clearOutput();
+ $exitcodes[$name] = $process->getExitCode();
+ }
+ }
+ }
+
+ echo PHP_EOL;
+ return $exitcodes;
+}
+
+/**
+ * Print install output merging each run.
+ *
+ * @param array $processes list of processes.
+ * @return array exit codes of each process.
+ */
+function print_combined_install_output($processes) {
+ $exitcodes = array();
+ $line = array();
+
+ // Check what best we can do to accommodate all parallel run o/p on single line.
+ // Windows command line has length of 80 chars, so default we will try fit o/p in 80 chars.
+ if (defined('BEHAT_MAX_CMD_LINE_OUTPUT') && BEHAT_MAX_CMD_LINE_OUTPUT) {
+ $lengthofprocessline = (int)max(10, BEHAT_MAX_CMD_LINE_OUTPUT / count($processes));
+ } else {
+ $lengthofprocessline = (int)max(10, 80 / count($processes));
+ }
+
+ echo "Installing behat site for " . count($processes) . " parallel behat run" . PHP_EOL;
+
+ // Show process name in first row.
+ foreach ($processes as $name => $process) {
+ // If we don't have enough space to show full run name then show runX.
+ if ($lengthofprocessline < strlen($name + 2)) {
+ $name = substr($name, -5);
+ }
+ // One extra padding as we are adding | separator for rest of the data.
+ $line[$name] = str_pad('[' . $name . '] ', $lengthofprocessline + 1);
+ }
+ ksort($line);
+ $tableheader = array_keys($line);
+ echo implode("", $line) . PHP_EOL;
+
+ // Now print o/p from each process.
+ while (count($exitcodes) != count($processes)) {
+ usleep(50000);
+ $poutput = array();
+ // Create child process.
+ foreach ($processes as $name => $process) {
+ if ($process->isRunning()) {
+ $output = $process->getIncrementalOutput();
+ if (trim($output)) {
+ $poutput[$name] = explode(PHP_EOL, $output);
+ }
+ } else {
+ // Process exited.
+ $exitcodes[$name] = $process->getExitCode();
+ }
+ }
+ ksort($poutput);
+
+ // Get max depth of o/p before displaying.
+ $maxdepth = 0;
+ foreach ($poutput as $pout) {
+ $pdepth = count($pout);
+ $maxdepth = $pdepth >= $maxdepth ? $pdepth : $maxdepth;
+ }
+
+ // Iterate over each process to get line to print.
+ for ($i = 0; $i <= $maxdepth; $i++) {
+ $pline = "";
+ foreach ($tableheader as $name) {
+ $po = empty($poutput[$name][$i]) ? "" : substr($poutput[$name][$i], 0, $lengthofprocessline - 1);
+ $po = str_pad($po, $lengthofprocessline);
+ $pline .= "|". $po;
+ }
+ if (trim(str_replace("|", "", $pline))) {
+ echo $pline . PHP_EOL;
+ }
+ }
+ unset($poutput);
+ $poutput = null;
+
+ }
+ echo PHP_EOL;
+ return $exitcodes;
+}
+
+/**
+ * Print install output merging showing one run at a time.
+ * If any process fail then exit.
+ *
+ * @param array $processes list of processes.
+ * @param bool $showprefix show prefix.
+ * @return bool exitcode.
+ */
+function print_sequential_output($processes, $showprefix = true) {
+ $status = false;
+ foreach ($processes as $name => $process) {
+ $shownname = false;
+ while ($process->isRunning()) {
+ $op = $process->getIncrementalOutput();
+ if (trim($op)) {
+ // Show name of the run once for sequential.
+ if ($showprefix && !$shownname) {
+ echo '[' . $name . '] ';
+ $shownname = true;
+ }
+ echo $op;
+ }
+ }
+ // If any error then exit.
+ $exitcode = $process->getExitCode();
+ if ($exitcode != 0) {
+ exit($exitcode);
+ }
+ $status = $status || (bool)$exitcode;
+ }
+ return $status;
+}
--- /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/>.
+
+/**
+ * CLI tool with utilities to manage Behat integration in Moodle
+ *
+ * All CLI utilities uses $CFG->behat_dataroot and $CFG->prefix_dataroot as
+ * $CFG->dataroot and $CFG->prefix
+ *
+ * @package tool_behat
+ * @copyright 2012 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+if (isset($_SERVER['REMOTE_ADDR'])) {
+ die(); // No access from web!.
+}
+
+// Basic functions.
+require_once(__DIR__ . '/../../../../lib/clilib.php');
+require_once(__DIR__ . '/../../../../lib/behat/lib.php');
+
+// CLI options.
+list($options, $unrecognized) = cli_get_params(
+ array(
+ 'help' => false,
+ 'install' => false,
+ 'parallel' => 0,
+ 'run' => '',
+ 'drop' => false,
+ 'enable' => false,
+ 'disable' => false,
+ 'diag' => false,
+ 'tags' => '',
+ 'updatesteps' => false,
+ ),
+ array(
+ 'h' => 'help'
+ )
+);
+
+if ($options['install'] or $options['drop']) {
+ define('CACHE_DISABLE_ALL', true);
+}
+
+// Checking util_single_run.php CLI script usage.
+$help = "
+Behat utilities to manage the test environment
+
+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.
+
+-h, --help Print out this help
+
+Example from Moodle root directory:
+\$ php admin/tool/behat/cli/util_single_run.php --enable
+
+More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
+";
+
+if (!empty($options['help'])) {
+ echo $help;
+ exit(0);
+}
+
+// Describe this script.
+define('BEHAT_UTIL', true);
+define('CLI_SCRIPT', true);
+define('NO_OUTPUT_BUFFERING', true);
+define('IGNORE_COMPONENT_CACHE', true);
+
+// Set run value, to be used by setup for configuring proper CFG variables.
+if ($options['run']) {
+ define('BEHAT_CURRENT_RUN', $options['run']);
+}
+
+// Only load CFG from config.php, stop ASAP in lib/setup.php.
+define('ABORT_AFTER_CONFIG', true);
+require_once(__DIR__ . '/../../../../config.php');
+
+// Remove error handling overrides done in config.php.
+$CFG->debug = (E_ALL | E_STRICT);
+$CFG->debugdisplay = 1;
+error_reporting($CFG->debug);
+ini_set('display_errors', '1');
+ini_set('log_errors', '1');
+
+// Finish moodle init.
+define('ABORT_AFTER_CONFIG_CANCEL', true);
+require("$CFG->dirroot/lib/setup.php");
+
+raise_memory_limit(MEMORY_HUGE);
+
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/upgradelib.php');
+require_once($CFG->libdir.'/clilib.php');
+require_once($CFG->libdir.'/installlib.php');
+require_once($CFG->libdir.'/testing/classes/test_lock.php');
+
+if ($unrecognized) {
+ $unrecognized = implode(PHP_EOL . " ", $unrecognized);
+ cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+// Behat utilities.
+require_once($CFG->libdir . '/behat/classes/util.php');
+require_once($CFG->libdir . '/behat/classes/behat_command.php');
+require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
+
+// Ensure run option is <= parallel run installed.
+if ($options['run']) {
+ if (!$options['parallel']) {
+ $options['parallel'] = behat_config_manager::get_parallel_test_runs();
+ }
+ if (empty($options['parallel']) || $options['run'] > $options['parallel']) {
+ echo "Parallel runs can't be more then ".$options['parallel'].PHP_EOL;
+ exit(1);
+ }
+ $CFG->behatrunprocess = $options['run'];
+}
+
+// Run command (only one per time).
+if ($options['install']) {
+ behat_util::install_site();
+
+ // This is only displayed once for parallel install.
+ if (empty($options['run'])) {
+ mtrace("Acceptance tests site installed");
+ }
+
+} else if ($options['drop']) {
+ // Ensure no tests are running.
+ test_lock::acquire('behat');
+ behat_util::drop_site();
+ // This is only displayed once for parallel install.
+ if (empty($options['run'])) {
+ mtrace("Acceptance tests site dropped");
+ }
+
+} else if ($options['enable']) {
+ if (!empty($options['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'])) {
+ behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
+ }
+ }
+
+ // Enable test mode.
+ behat_util::start_test_mode();
+
+ // This is only displayed once for parallel install.
+ if (empty($options['run'])) {
+ $runtestscommand = behat_command::get_behat_command(true, !empty($options['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 .
+ $runtestscommand);
+ }
+
+} else if ($options['disable']) {
+ behat_util::stop_test_mode();
+ // This is only displayed once for parallel install.
+ if (empty($options['run'])) {
+ mtrace("Acceptance tests environment disabled");
+ }
+
+} else if ($options['diag']) {
+ $code = behat_util::get_behat_status();
+ exit($code);
+
+} else if ($options['updatesteps']) {
+ if (defined('BEHAT_FEATURE_STEP_FILE') && BEHAT_FEATURE_STEP_FILE) {
+ $behatstepfile = BEHAT_FEATURE_STEP_FILE;
+ } else {
+ echo "BEHAT_FEATURE_STEP_FILE is not set, please ensure you set this to writable file" . PHP_EOL;
+ exit(1);
+ }
+
+ // Rewrite config file to ensure we have all the features covered.
+ behat_config_manager::update_config_file();
+
+ // Run behat command to get steps in feature files.
+ $featurestepscmd = behat_command::get_behat_command(true);
+ $featurestepscmd .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
+ $featurestepscmd .= ' --dry-run --format=moodle_step_count';
+ $processes = cli_execute_parallel(array($featurestepscmd), __DIR__ . "/../../../../");
+ $status = print_update_step_output(array_pop($processes), $behatstepfile);
+
+ exit($status);
+} else {
+ echo $help;
+ exit(1);
+}
+
+exit(0);
+
+/**
+ * Print update progress as dots for updating feature file step list.
+ *
+ * @param Process $process process executing update step command.
+ * @param string $featurestepfile feature step file in which steps will be saved.
+ * @return int exitcode.
+ */
+function print_update_step_output($process, $featurestepfile) {
+ $printedlength = 0;
+
+ echo "Updating steps feature file for parallel behat runs" . PHP_EOL;
+
+ // Show progress while running command.
+ while ($process->isRunning()) {
+ usleep(10000);
+ $op = $process->getIncrementalOutput();
+ if (trim($op)) {
+ echo ".";
+ $printedlength++;
+ if ($printedlength > 70) {
+ $printedlength = 0;
+ echo PHP_EOL;
+ }
+ }
+ }
+
+ // If any error then exit.
+ $exitcode = $process->getExitCode();
+ // Output err.
+ if ($exitcode != 0) {
+ echo $process->getErrorOutput();
+ exit($exitcode);
+ }
+
+ // Extract features with step info and save it in file.
+ $featuresteps = $process->getOutput();
+ $featuresteps = explode(PHP_EOL, $featuresteps);
+
+ $realroot = realpath(__DIR__.'/../../../../').'/';
+ foreach ($featuresteps as $featurestep) {
+ if (trim($featurestep)) {
+ $step = explode("::", $featurestep);
+ $step[0] = str_replace($realroot, '', $step[0]);
+ $steps[$step[0]] = $step[1];
+ }
+ }
+ arsort($steps);
+
+ if (!@file_put_contents($featurestepfile, json_encode($steps, JSON_PRETTY_PRINT))) {
+ behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $featurestepfile . ' can not be created');
+ $exitcode = -1;
+ }
+
+ echo PHP_EOL. "Updated step count in " . $featurestepfile . PHP_EOL;
+
+ return $exitcode;
+}
"require-dev": {
"phpunit/phpunit": "3.7.*",
"phpunit/dbUnit": "1.2.*",
- "moodlehq/behat-extension": "1.29.2"
+ "moodlehq/behat-extension": "1.29.3"
}
}
// Example:
// $CFG->behat_faildump_path = '/my/path/to/save/failure/dumps';
//
+// You can specify db, selenium wd_host etc. for behat parallel run by setting following variable.
+// Example:
+// $CFG->behat_parallel_run = array (
+// array (
+// 'dbtype' => 'mysqli',
+// 'dblibrary' => 'native',
+// 'dbhost' => 'localhost',
+// 'dbname' => 'moodletest',
+// 'dbuser' => 'moodle',
+// 'dbpass' => 'moodle',
+// 'behat_prefix' => 'mdl_',
+// 'wd_host' => 'http://127.0.0.1:4444/wd/hub',
+// 'behat_wwwroot' => 'http://127.0.0.1/moodle',
+// 'behat_dataroot' => '/home/example/bht_moodledata'
+// ),
+// );
+//
+// To change name of behat parallel run site, define BEHAT_PARALLEL_SITE_NAME and parallel run sites will be suffixed
+// with this value
+// Example:
+// define('BEHAT_PARALLEL_SITE_NAME', 'behatparallelsite');
+//
+// Command line output for parallel behat install is limited to 80 chars, if you are installing more then 4 sites and
+// want to expand output to more then 80 chars, then define BEHAT_MAX_CMD_LINE_OUTPUT
+// Example:
+// define('BEHAT_MAX_CMD_LINE_OUTPUT', 120);
+//
+// Behat feature files will be distributed randomly between the processes by default. If you have timing file or want
+// to create timing file then define BEHAT_FEATURE_TIMING_FILE with path to timing file. It will be updated for each
+// run with latest time taken to execute feature.
+// Example:
+// define('BEHAT_FEATURE_TIMING_FILE', '/PATH_TO_TIMING_FILE/timing.json');
+//
+// If you don't have timing file and want some stable distribution of features, then you can use step counts to
+// distribute the features. You can generate step file by executing php admin/tool/behat/cli/util.php --updatesteps
+// this will update step file which is defined by BEHAT_FEATURE_STEP_FILE.
+// Example:
+// define('BEHAT_FEATURE_STEP_FILE', '/PATH_TO_FEATURE_STEP_COUNT_FILE/stepcount.json');
+//
+// Feature distribution for each process is displayed as histogram. you can disable it by setting
+// BEHAT_DISABLE_HISTOGRAM
+// Example:
+// define('BEHAT_DISABLE_HISTOGRAM', true);
+//
//=========================================================================
// 12. DEVELOPER DATA GENERATOR
//=========================================================================
* @return array Array of rows for the right part of the report
*/
public function get_right_avg_row($rows=array(), $grouponly=false) {
- global $USER, $DB, $OUTPUT;
+ global $USER, $DB, $OUTPUT, $CFG;
if (!$this->canviewhidden) {
// Totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
// Limit to users with an active enrollment.
- list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
+ $coursecontext = $this->context->get_course_context(true);
+ $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
+ $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
+ $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
+ list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol);
// We want to query both the current context and parent contexts.
list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
$string['unlockaccount'] = 'Unlock account';
$string['unsettheme'] = 'Unset theme';
$string['unsupported'] = 'Unsupported';
+$string['unsupporteddbstorageengine'] = 'The database storage engine being used is no longer supported.';
$string['unsuspenduser'] = 'Activate user account';
$string['updateaccounts'] = 'Update existing accounts';
$string['updatecomponent'] = 'Update component';
$string['number'] = 'Group/member count';
$string['numgroups'] = 'Number of groups';
$string['nummembers'] = 'Members per group';
+$string['mygroups'] = 'My groups';
+$string['othergroups'] = 'Other groups';
$string['overview'] = 'Overview';
$string['potentialmembers'] = 'Potential members: {$a}';
$string['potentialmembs'] = 'Potential members';
/**
* Ensures the behat dir exists in moodledata
+ * @param int $runprocess run process for which behat dir is returned.
* @return string Full path
*/
- public static function get_behat_dir() {
+ public static function get_behat_dir($runprocess = 0) {
global $CFG;
- $behatdir = $CFG->behat_dataroot . '/behat';
+ if (empty($runprocess)) {
+ $behatdir = $CFG->behat_dataroot . '/behat';
+ } else if (isset($CFG->behat_parallel_run[$runprocess - 1]['behat_dataroot'])) {
+ $behatdir = $CFG->behat_parallel_run[$runprocess - 1]['behat_dataroot'] . '/behat';;
+ } else {
+ $behatdir = $CFG->behat_dataroot . $runprocess . '/behat';
+ }
if (!is_dir($behatdir)) {
if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
* normal cmd.exe (in Windows).
*
* @param bool $custombyterm If the provided command should depend on the terminal where it runs
+ * @param bool $parallelrun If parallel run is installed.
* @return string
*/
- public final static function get_behat_command($custombyterm = false) {
+ public final static function get_behat_command($custombyterm = false, $parallerun = false) {
$separator = DIRECTORY_SEPARATOR;
- $exec = 'behat';
+ if (!$parallerun) {
+ $exec = 'behat';
- // Cygwin uses linux-style directory separators.
- if ($custombyterm && testing_is_cygwin()) {
- $separator = '/';
+ // Cygwin uses linux-style directory separators.
+ if ($custombyterm && testing_is_cygwin()) {
+ $separator = '/';
- // MinGW can not execute .bat scripts.
- if (!testing_is_mingw()) {
- $exec = 'behat.bat';
+ // MinGW can not execute .bat scripts.
+ if (!testing_is_mingw()) {
+ $exec = 'behat.bat';
+ }
}
+ $command = 'vendor' . $separator . 'bin' . $separator . $exec;
+ } else {
+ $command = 'php admin' . $separator . 'tool' . $separator . 'behat' . $separator . 'cli' . $separator . 'run.php';
}
- return 'vendor' . $separator . 'bin' . $separator . $exec;
+ return $command;
}
/**
*
* @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.
* @return void
*/
- public static function update_config_file($component = '', $testsrunner = true) {
+ public static function update_config_file($component = '', $testsrunner = true, $tags = '') {
global $CFG;
// Behat must have a separate behat.yml to have access to the whole set of features and steps definitions.
$featurespaths[$uniquekey] = $path;
}
}
- $features = array_values($featurespaths);
+ foreach ($featurespaths as $path) {
+ $additional = glob("$path/*.feature");
+ $features = array_merge($features, $additional);
+ }
}
// Optionally include features from additional directories.
// Behat config file specifing the main context class,
// the required Behat extensions and Moodle test wwwroot.
- $contents = self::get_config_file_contents($features, $stepsdefinitions);
+ $contents = self::get_config_file_contents(self::get_features_with_tags($features, $tags), $stepsdefinitions);
// Stores the file.
if (!file_put_contents($configfilepath, $contents)) {
}
+ /**
+ * 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 static function get_features_with_tags($features, $tags) {
+ if (empty($tags)) {
+ return $features;
+ }
+ $newfeaturelist = array();
+ $tagstosearch = explode('&&', $tags);
+ foreach ($features as $featurefile) {
+ $contents = file_get_contents($featurefile);
+ $includefeature = true;
+ foreach ($tagstosearch 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;
+ }
+ } else if ($contents && strpos($contents, $tag) === false) {
+ $includefeature = false;
+ }
+ }
+ if ($includefeature) {
+ $newfeaturelist[] = $featurefile;
+ }
+ }
+ return $newfeaturelist;
+ }
+
/**
* Gets the list of Moodle steps definitions
*
/**
* Returns the behat config file path used by the behat cli command.
*
+ * @param int $runprocess Runprocess.
* @return string
*/
- public static function get_behat_cli_config_filepath() {
+ public static function get_behat_cli_config_filepath($runprocess = 0) {
global $CFG;
- $command = $CFG->behat_dataroot . DIRECTORY_SEPARATOR . 'behat' . DIRECTORY_SEPARATOR . 'behat.yml';
+ if ($runprocess) {
+ if (isset($CFG->behat_parallel_run[$runprocess - 1 ]['behat_dataroot'])) {
+ $command = $CFG->behat_parallel_run[$runprocess - 1]['behat_dataroot'];
+ } else {
+ $command = $CFG->behat_dataroot . $runprocess;
+ }
+ } else {
+ $command = $CFG->behat_dataroot;
+ }
+ $command .= DIRECTORY_SEPARATOR . 'behat' . DIRECTORY_SEPARATOR . 'behat.yml';
// Cygwin uses linux-style directory separators.
if (testing_is_cygwin()) {
return $command;
}
+ /**
+ * Returns the path to the parallel run file which specifies if parallel test environment is enabled
+ * and how many parallel runs to execute.
+ *
+ * @param int $runprocess run process for which behat dir is returned.
+ * @return string
+ */
+ public final static function get_parallel_test_file_path($runprocess = 0) {
+ return behat_command::get_behat_dir($runprocess) . '/parallel_environment_enabled.txt';
+ }
+
+ /**
+ * Returns number of parallel runs for which site is initialised.
+ *
+ * @param int $runprocess run process for which behat dir is returned.
+ * @return int
+ */
+ public final static function get_parallel_test_runs($runprocess = 0) {
+ $parallelrun = 0;
+ // Get parallel run info from first file and last file.
+ $parallelrunconfigfile = self::get_parallel_test_file_path($runprocess);
+ if (file_exists($parallelrunconfigfile)) {
+ if ($parallel = file_get_contents($parallelrunconfigfile)) {
+ $parallelrun = (int) $parallel;
+ }
+ }
+
+ return $parallelrun;
+ }
+
+ /**
+ * Drops parallel site links.
+ *
+ * @return bool true on success else false.
+ */
+ public final static function drop_parallel_site_links() {
+ global $CFG;
+
+ // Get parallel test runs from first run.
+ $parallelrun = self::get_parallel_test_runs(1);
+
+ if (empty($parallelrun)) {
+ return false;
+ }
+
+ // If parallel run then remove links and original file.
+ clearstatcache();
+ for ($i = 1; $i <= $parallelrun; $i++) {
+ // Don't delete links for specified sites, as they should be accessible.
+ if (!empty($CFG->behat_parallel_run['behat_wwwroot'][$i - 1]['behat_wwwroot'])) {
+ continue;
+ }
+ $link = $CFG->dirroot . '/' . BEHAT_PARALLEL_SITE_NAME . $i;
+ if (file_exists($link) && is_link($link)) {
+ @unlink($link);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create parallel site links.
+ *
+ * @param int $fromrun first run
+ * @param int $torun last run.
+ * @return bool true for sucess, else false.
+ */
+ public final static function create_parallel_site_links($fromrun, $torun) {
+ global $CFG;
+
+ // Create site symlink if necessary.
+ clearstatcache();
+ for ($i = $fromrun; $i <= $torun; $i++) {
+ // Don't create links for specified sites, as they should be accessible.
+ if (!empty($CFG->behat_parallel_run['behat_wwwroot'][$i - 1]['behat_wwwroot'])) {
+ continue;
+ }
+ $link = $CFG->dirroot.'/'.BEHAT_PARALLEL_SITE_NAME.$i;
+ clearstatcache();
+ if (file_exists($link)) {
+ if (!is_link($link) || !is_dir($link)) {
+ echo "File exists at link location ($link) but is not a link or directory!" . PHP_EOL;
+ return false;
+ }
+ } else if (!symlink($CFG->dirroot, $link)) {
+ // Try create link in case it's not already present.
+ echo "Unable to create behat site symlink ($link)" . PHP_EOL;
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Behat config file specifing the main context class,
* the required Behat extensions and Moodle test wwwroot.
// 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)));
+ $features = $features[$CFG->behatrunprocess - 1];
+ }
+ }
+ // 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';
}
$basedir = $CFG->dirroot . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'behat';
+
$config = array(
'default' => array(
'paths' => array(
'Behat\MinkExtension\Extension' => array(
'base_url' => $CFG->behat_wwwroot,
'goutte' => null,
- 'selenium2' => null
+ 'selenium2' => $selenium2wdhost
),
'Moodle\BehatExtension\Extension' => array(
'formatters' => array(
'moodle_progress' => 'Moodle\BehatExtension\Formatter\MoodleProgressFormatter',
- 'moodle_list' => 'Moodle\BehatExtension\Formatter\MoodleListFormatter'
+ 'moodle_list' => 'Moodle\BehatExtension\Formatter\MoodleListFormatter',
+ 'moodle_step_count' => 'Moodle\BehatExtension\Formatter\MoodleStepCountFormatter'
),
'features' => $features,
'steps_definitions' => $stepsdefinitions
return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
}
+ /**
+ * 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 Feature files array, sorted into allocations
+ */
+ 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];
+ }
+
/**
* Overrides default config with local config values
*
define('BEHAT_EXITCODE_COMPOSER', 255);
define('BEHAT_EXITCODE_INSTALLED', 256);
+/**
+ * The behat test site fullname and shortname.
+ */
+define('BEHAT_PARALLEL_SITE_NAME', "behatrun");
+
/**
* Exits with an error code
*
behat_error(BEHAT_EXITCODE_CONFIG,
'Define $CFG->behat_dataroot in config.php');
}
+ clearstatcache();
if (!file_exists($CFG->behat_dataroot)) {
$permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
umask(0);
return false;
}
+/**
+ * Fix variables for parallel behat testing.
+ * - behat_wwwroot = behat_wwwroot{behatrunprocess}
+ * - behat_dataroot = behat_dataroot{behatrunprocess}
+ * - behat_prefix = behat_prefix.{behatrunprocess}_ (For oracle it will be firstletter of prefix and behatrunprocess)
+ **/
+function behat_update_vars_for_process() {
+ global $CFG;
+
+ $allowedconfigoverride = array('dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'behat_prefix',
+ 'behat_wwwroot', 'behat_dataroot');
+ $behatrunprocess = behat_get_run_process();
+ $CFG->behatrunprocess = $behatrunprocess;
+
+ if ($behatrunprocess) {
+ if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_wwwroot'])) {
+ // Set www root for run process.
+ if (isset($CFG->behat_wwwroot) &&
+ !preg_match("#/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess . "\$#", $CFG->behat_wwwroot)) {
+ $CFG->behat_wwwroot .= "/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess;
+ }
+ }
+
+ if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_dataroot'])) {
+ // Set behat_dataroot.
+ if (!preg_match("#" . $behatrunprocess . "\$#", $CFG->behat_dataroot)) {
+ $CFG->behat_dataroot .= $behatrunprocess;
+ }
+ }
+
+ // Set behat_prefix for db, just suffix run process number, to avoid max length exceed.
+ // For oracle only 2 letter prefix is possible.
+ // NOTE: This will not work for parallel process > 9.
+ if ($CFG->dbtype === 'oci') {
+ $CFG->behat_prefix = substr($CFG->behat_prefix, 0, 1);
+ $CFG->behat_prefix .= "{$behatrunprocess}";
+ } else {
+ $CFG->behat_prefix .= "{$behatrunprocess}_";
+ }
+
+ if (!empty($CFG->behat_parallel_run[$behatrunprocess - 1])) {
+ // Override allowed config vars.
+ foreach ($allowedconfigoverride as $config) {
+ if (isset($CFG->behat_parallel_run[$behatrunprocess - 1][$config])) {
+ $CFG->$config = $CFG->behat_parallel_run[$behatrunprocess - 1][$config];
+ }
+ }
+ }
+ }
+}
+
/**
* Checks if the URL requested by the user matches the provided argument
*
return false;
}
+
+/**
+ * Get behat run process from either $_SERVER or command config.
+ *
+ * @return bool|int false if single run, else run process number.
+ */
+function behat_get_run_process() {
+ global $argv, $CFG;
+ $behatrunprocess = false;
+
+ // Get behat run process, if set.
+ if (defined('BEHAT_CURRENT_RUN') && BEHAT_CURRENT_RUN) {
+ $behatrunprocess = BEHAT_CURRENT_RUN;
+ } else if (!empty($_SERVER['REMOTE_ADDR'])) {
+ // Try get it from config if present.
+ if (!empty($CFG->behat_parallel_run)) {
+ foreach ($CFG->behat_parallel_run as $run => $behatconfig) {
+ if (isset($behatconfig['behat_wwwroot']) && behat_is_requested_url($behatconfig['behat_wwwroot'])) {
+ $behatrunprocess = $run + 1; // We start process from 1.
+ break;
+ }
+ }
+ }
+ // Check if parallel site prefix is used.
+ if (empty($behatrunprocess) && preg_match('#/' . BEHAT_PARALLEL_SITE_NAME . '(.+?)/#', $_SERVER['REQUEST_URI'])) {
+ $dirrootrealpath = str_replace("\\", "/", realpath($CFG->dirroot));
+ $serverrealpath = str_replace("\\", "/", realpath($_SERVER['SCRIPT_FILENAME']));
+ $afterpath = str_replace($dirrootrealpath.'/', '', $serverrealpath);
+ if (!$behatrunprocess = preg_filter("#.*/" . BEHAT_PARALLEL_SITE_NAME . "(.+?)/$afterpath#", '$1',
+ $_SERVER['SCRIPT_FILENAME'])) {
+ throw new Exception("Unable to determine behat process [afterpath=" . $afterpath .
+ ", scriptfilename=" . $_SERVER['SCRIPT_FILENAME'] . "]!");
+ }
+ }
+ } else if (defined('BEHAT_TEST') || defined('BEHAT_UTIL')) {
+ if ($match = preg_filter('#--run=(.+)#', '$1', $argv)) {
+ $behatrunprocess = reset($match);
+ } else if ($k = array_search('--config', $argv)) {
+ $behatconfig = str_replace("\\", "/", $argv[$k + 1]);
+ // Try get it from config if present.
+ if (!empty($CFG->behat_parallel_run)) {
+ foreach ($CFG->behat_parallel_run as $run => $parallelconfig) {
+ if (!empty($parallelconfig['behat_dataroot']) &&
+ $parallelconfig['behat_dataroot'] . '/behat/behat.yml' == $behatconfig) {
+
+ $behatrunprocess = $run + 1; // We start process from 1.
+ break;
+ }
+ }
+ }
+ // Check if default behat datroot increment was done.
+ if (empty($behatrunprocess)) {
+ $behatdataroot = str_replace("\\", "/", $CFG->behat_dataroot);
+ $behatrunprocess = preg_filter("#^{$behatdataroot}" . "(.+?)[/|\\\]behat[/|\\\]behat\.yml#", '$1',
+ $behatconfig);
+ }
+ }
+ }
+
+ return $behatrunprocess;
+}
+
+/**
+ * Execute commands in parallel.
+ *
+ * @param array $cmds list of commands to be executed.
+ * @param string $cwd absolute path of working directory.
+ * @return array list of processes.
+ */
+function cli_execute_parallel($cmds, $cwd = null) {
+ require_once(__DIR__ . "/../../vendor/autoload.php");
+
+ $processes = array();
+
+ // Create child process.
+ foreach ($cmds as $name => $cmd) {
+ $process = new Symfony\Component\Process\Process($cmd);
+
+ $process->setWorkingDirectory($cwd);
+ $process->setTimeout(null);
+ $processes[$name] = $process;
+ $processes[$name]->start();
+
+ // If error creating process then exit.
+ if ($processes[$name]->getStatus() !== 'started') {
+ echo "Error starting process: $name";
+ foreach ($processes[$name] as $process) {
+ if ($process) {
+ $process->signal(SIGKILL);
+ }
+ }
+ exit(1);
+ }
+ }
+ return $processes;
+}
\ No newline at end of file
fwrite(STDERR, "\n");
die($errorcode);
}
+
'gradereport_user_get_grades_table',
'core_group_get_course_user_groups',
'core_user_remove_user_device',
+ 'core_course_get_courses'
),
'enabled' => 0,
'restrictedusers' => 0,
}
return true;
}
-
- /**
- * Returns the current db engine.
- *
- * MyISAM is NOT supported!
- *
- * @return string or null MySQL engine name
- */
- public function get_dbengine() {
- if ($this->external) {
- return null;
- }
-
- $engine = parent::get_dbengine();
- if ($engine === 'MyISAM') {
- debugging('MyISAM tables are not supported in MariaDB driver!');
- $engine = 'XtraDB';
- }
- return $engine;
- }
}
_displayDialogue: function() {
// Store the current selection.
this._currentSelection = this.get('host').getSelection();
- if (this._currentSelection === false || this._currentSelection.collapsed) {
+ if (this._currentSelection === false) {
return;
}
},
/**
- * Update the dialogue after an image was selected in the File Picker.
+ * Update the dialogue after a link was selected in the File Picker.
*
* @method _filepickerCallback
* @param {object} params The parameters provided by the filepicker
- * containing information about the image.
+ * containing information about the link.
* @private
*/
_filepickerCallback: function(params) {
.hide();
if (params.url !== '') {
- this.get('host').setSelection(this._currentSelection);
- document.execCommand('unlink', false, null);
- document.execCommand('createLink', false, params.url);
+ // Add the link.
+ this._setLinkOnSelection(params.url);
+
// And mark the text area as updated.
this.markUpdated();
}
anchornodes,
value;
- var host = this.get('host');
-
e.preventDefault();
this.getDialogue({
focusAfterHide: null
value = input.get('value');
if (value !== '') {
- this.editor.focus();
- host.setSelection(this._currentSelection);
// We add a prefix if it is not already prefixed.
value = value.trim();
value = 'http://' + value;
}
- document.execCommand('unlink', false, null);
- document.execCommand('createLink', false, value);
-
- // Now set the target.
- selectednode = host.getSelectionParentNode();
+ // Add the link.
+ selectednode = this._setLinkOnSelection(value);
// Note this is a document fragment and YUI doesn't like them.
if (!selectednode) {
}
},
+ /**
+ * Final step setting the anchor on the selection.
+ *
+ * @private
+ * @method _setLinkOnSelection
+ * @param {String} url URL the link will point to.
+ * @return {Node} The added Node.
+ */
+ _setLinkOnSelection: function(url) {
+ var host = this.get('host'),
+ link,
+ selectednode;
+
+ this.editor.focus();
+ host.setSelection(this._currentSelection);
+
+ if (this._currentSelection[0].collapsed) {
+ // Firefox cannot add links when the selection is empty so we will add it manually.
+ link = Y.Node.create('<a>' + url + '</a>');
+ link.setAttribute('href', url);
+
+ // Add the node and select it to replicate the behaviour of execCommand.
+ selectednode = host.insertContentAtFocusPoint(link.get('outerHTML'));
+ host.setSelection(host.getSelectionFromNode(selectednode));
+ } else {
+ document.execCommand('unlink', false, null);
+ document.execCommand('createLink', false, url);
+
+ // Now set the target.
+ selectednode = host.getSelectionParentNode();
+ }
+
+ return selectednode;
+ },
+
/**
* Look up and down for the nearest anchor tags that are least partly contained in the selection.
*
*
* @method insertContentAtFocusPoint
* @param {String} html
+ * @return {Node} The YUI Node object added to the DOM.
*/
insertContentAtFocusPoint: function(html) {
var selection = rangy.getSelection(),
range.deleteContents();
range.insertNode(node.getDOMNode());
}
+ return node;
}
};
* @return bool true if ok, array of errors if problems found. Grade item id => error message
*/
function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
+ // This may take a very long time.
+ \core_php_time_limit::raise();
$course_item = grade_item::fetch_course_item($courseid);
$context = context_course::instance($course->id);
$aag = has_capability('moodle/site:accessallgroups', $context);
+ $usergroups = array();
if ($groupmode == VISIBLEGROUPS or $aag) {
$allowedgroups = groups_get_all_groups($course->id, 0, $course->defaultgroupingid);
+ // Get user's own groups and put to the top.
+ $usergroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
} else {
$allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
}
$groupsmenu[0] = get_string('allparticipants');
}
- if ($allowedgroups) {
- foreach ($allowedgroups as $group) {
- $groupsmenu[$group->id] = format_string($group->name);
- }
- }
+ $groupsmenu += groups_sort_menu_options($allowedgroups, $usergroups);
if ($groupmode == VISIBLEGROUPS) {
$grouplabel = get_string('groupsvisible');
}
}
+/**
+ * Turn an array of groups into an array of menu options.
+ * @param array $groups of group objects.
+ * @return array groupid => formatted group name.
+ */
+function groups_list_to_menu($groups) {
+ $groupsmenu = array();
+ foreach ($groups as $group) {
+ $groupsmenu[$group->id] = format_string($group->name);
+ }
+ return $groupsmenu;
+}
+
+/**
+ * Takes user's allowed groups and own groups and formats for use in group selector menu
+ * If user has allowed groups + own groups will add to an optgroup
+ * Own groups are removed from allowed groups
+ * @param array $allowedgroups All groups user is allowed to see
+ * @param array $usergroups Groups user belongs to
+ * @return array
+ */
+function groups_sort_menu_options($allowedgroups, $usergroups) {
+ $useroptions = array();
+ if ($usergroups) {
+ $useroptions = groups_list_to_menu($usergroups);
+
+ // Remove user groups from other groups list.
+ foreach ($usergroups as $group) {
+ unset($allowedgroups[$group->id]);
+ }
+ }
+
+ $allowedoptions = array();
+ if ($allowedgroups) {
+ $allowedoptions = groups_list_to_menu($allowedgroups);
+ }
+
+ if ($useroptions && $allowedoptions) {
+ return array(
+ 1 => array(get_string('mygroups', 'group') => $useroptions),
+ 2 => array(get_string('othergroups', 'group') => $allowedoptions)
+ );
+ } else if ($useroptions) {
+ return $useroptions;
+ } else {
+ return $allowedoptions;
+ }
+}
+
/**
* Generates html to print menu selector for course level, listing all groups.
* Note: This api does not do any group mode check use groups_print_course_menu() instead if you want proper checks.
$allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
}
- foreach ($allowedgroups as $group) {
- $groupsmenu[$group->id] = format_string($group->name);
- }
+ $groupsmenu += groups_list_to_menu($allowedgroups);
if ($update) {
// Init activegroup array if necessary.
$context = context_module::instance($cm->id);
$aag = has_capability('moodle/site:accessallgroups', $context);
+ $usergroups = array();
if ($groupmode == VISIBLEGROUPS or $aag) {
$allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid); // any group in grouping
+ // Get user's own groups and put to the top.
+ $usergroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid);
} else {
$allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid); // only assigned groups
}
$groupsmenu[0] = get_string('allparticipants');
}
- if ($allowedgroups) {
- foreach ($allowedgroups as $group) {
- $groupsmenu[$group->id] = format_string($group->name);
- }
- }
+ $groupsmenu += groups_sort_menu_options($allowedgroups, $usergroups);
if ($groupmode == VISIBLEGROUPS) {
$grouplabel = get_string('groupsvisible');
}
}
+ // Set the global $COURSE.
+ // TODO MDL-49434: setting current course/cm should be after the check $cm->uservisible .
+ if ($cm) {
+ $PAGE->set_cm($cm, $course);
+ $PAGE->set_pagelayout('incourse');
+ } else if (!empty($courseorid)) {
+ $PAGE->set_course($course);
+ }
+
// Check visibility of activity to current user; includes visible flag, conditional availability, etc.
if ($cm && !$cm->uservisible) {
if ($preventredirect) {
redirect($url, get_string('activityiscurrentlyhidden'));
}
- // Set the global $COURSE.
- if ($cm) {
- $PAGE->set_cm($cm, $course);
- $PAGE->set_pagelayout('incourse');
- } else if (!empty($courseorid)) {
- $PAGE->set_course($course);
- }
-
// Finally access granted, update lastaccess times.
user_accesstime_log($course->id);
}
$module = $DB->get_field('question_usages', 'component',
array('id' => $qubaid));
+ if (!$module) {
+ send_file_not_found();
+ }
if ($module === 'core_question_preview') {
require_once($CFG->dirroot . '/question/previewlib.php');
// The behat is configured on this server, we need to find out if this is the behat test
// site based on the URL used for access.
require_once(__DIR__ . '/../lib/behat/lib.php');
+
+ // Update config variables for parallel behat runs.
+ behat_update_vars_for_process();
+
if (behat_is_test_site()) {
+ clearstatcache();
+
// Checking the integrity of the provided $CFG->behat_* vars and the
// selected wwwroot to prevent conflicts with production and phpunit environments.
behat_check_config_vars();
if (!file_exists("$CFG->behat_dataroot/behattestdir.txt")) {
if ($dh = opendir($CFG->behat_dataroot)) {
while (($file = readdir($dh)) !== false) {
- if ($file === 'behat' or $file === '.' or $file === '..' or $file === '.DS_Store') {
+ if ($file === 'behat' or $file === '.' or $file === '..' or $file === '.DS_Store' or is_numeric($file)) {
continue;
}
- behat_error(BEHAT_EXITCODE_CONFIG, '$CFG->behat_dataroot directory is not empty, ensure this is the directory where you want to install behat test dataroot');
+ behat_error(BEHAT_EXITCODE_CONFIG, "$CFG->behat_dataroot directory is not empty, ensure this is the " .
+ "directory where you want to install behat test dataroot");
}
closedir($dh);
unset($dh);
umask($CFG->umaskpermissions);
if (!file_exists($dir)) {
- if (!mkdir($dir, $CFG->directorypermissions, true)) {
+ if (!@mkdir($dir, $CFG->directorypermissions, true)) {
clearstatcache();
// There might be a race condition when creating directory.
if (!is_dir($dir)) {
*/
public static function acquire($framework) {
global $CFG;
-
$datarootpath = $CFG->{$framework . '_dataroot'} . '/' . $framework;
$lockfile = $datarootpath . '/lock';
if (!file_exists($datarootpath)) {
// do not write to error stream because we need the error message in PHP exec result from web ui
echo($text."\n");
+ if (isset($_SERVER['REMOTE_ADDR'])) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
exit($errorcode);
}
Behat\Behat\Context\Step\When as When,
Behat\Behat\Context\Step\Then as Then,
Behat\Gherkin\Node\TableNode as TableNode,
+ Behat\Gherkin\Node\PyStringNode as PyStringNode,
Behat\Mink\Element\NodeElement as NodeElement,
Behat\Mink\Exception\ExpectationException as ExpectationException,
Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
$this->set_field_value($field, $value);
}
+ /**
+ * Sets the specified value to the field.
+ *
+ * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to multiline$/
+ * @throws ElementNotFoundException Thrown by behat_base::find
+ * @param string $field
+ * @param PyStringNode $value
+ * @return void
+ */
+ public function i_set_the_field_to_multiline($field, PyStringNode $value) {
+ $this->set_field_value($field, (string)$value);
+ }
+
/**
* Sets the specified value to the field with xpath.
*
use Behat\Behat\Event\SuiteEvent as SuiteEvent,
Behat\Behat\Event\ScenarioEvent as ScenarioEvent,
+ Behat\Behat\Event\FeatureEvent as FeatureEvent,
+ Behat\Behat\Event\OutlineExampleEvent as OutlineExampleEvent,
Behat\Behat\Event\StepEvent as StepEvent,
Behat\Mink\Exception\DriverException as DriverException,
WebDriver\Exception\NoSuchWindow as NoSuchWindow,
*/
protected static $faildumpdirname = false;
+ /**
+ * Keeps track of time taken by feature to execute.
+ *
+ * @var array list of feature timings
+ */
+ protected static $timings = array();
+
/**
* Gives access to moodle codebase, ensures all is ready and sets up the test lock.
*
* Includes config.php to use moodle codebase with $CFG->behat_*
* instead of $CFG->prefix and $CFG->dataroot, called once per suite.
*
+ * @param SuiteEvent $event event before suite.
* @static
* @throws Exception
* @BeforeSuite
*/
- public static function before_suite($event) {
+ public static function before_suite(SuiteEvent $event) {
global $CFG;
// Defined only when the behat CLI command is running, the moodle init setup process will
}
}
+ /**
+ * Gives access to moodle codebase, to keep track of feature start time.
+ *
+ * @param FeatureEvent $event event fired before feature.
+ * @BeforeFeature
+ */
+ public static function before_feature(FeatureEvent $event) {
+ if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
+ return;
+ }
+ $file = $event->getFeature()->getFile();
+ self::$timings[$file] = microtime(true);
+ }
+
+ /**
+ * Gives access to moodle codebase, to keep track of feature end time.
+ *
+ * @param FeatureEvent $event event fired after feature.
+ * @AfterFeature
+ */
+ public static function after_feature(FeatureEvent $event) {
+ if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
+ return;
+ }
+ $file = $event->getFeature()->getFile();
+ self::$timings[$file] = microtime(true) - self::$timings[$file];
+ // Probably didn't actually run this, don't output it.
+ if (self::$timings[$file] < 1) {
+ unset(self::$timings[$file]);
+ }
+ }
+
+ /**
+ * Gives access to moodle codebase, to keep track of suite timings.
+ *
+ * @param SuiteEvent $event event fired after suite.
+ * @AfterSuite
+ */
+ public static function after_suite(SuiteEvent $event) {
+ if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
+ return;
+ }
+ $realroot = realpath(__DIR__.'/../../../').'/';
+ foreach (self::$timings as $k => $v) {
+ $new = str_replace($realroot, '', $k);
+ self::$timings[$new] = round($v, 1);
+ unset(self::$timings[$k]);
+ }
+ if ($existing = @json_decode(file_get_contents(BEHAT_FEATURE_TIMING_FILE), true)) {
+ self::$timings = array_merge($existing, self::$timings);
+ }
+ arsort(self::$timings);
+ @file_put_contents(BEHAT_FEATURE_TIMING_FILE, json_encode(self::$timings, JSON_PRETTY_PRINT));
+ }
+
/**
* Resets the test environment.
*
+ * @param OutlineExampleEvent|ScenarioEvent $event event fired before scenario.
* @throws coding_exception If here we are not using the test database it should be because of a coding error
* @BeforeScenario
*/
* default would be at framework level, which will stop the execution of
* the run.
*
+ * @param StepEvent $event event fired before step.
* @BeforeStep @javascript
*/
- public function before_step_javascript($event) {
+ public function before_step_javascript(StepEvent $event) {
try {
$this->wait_for_pending_js();
* default would be at framework level, which will stop the execution of
* the run.
*
+ * @param StepEvent $event event fired after step.
* @AfterStep @javascript
*/
- public function after_step_javascript($event) {
+ public function after_step_javascript(StepEvent $event) {
global $CFG;
// Save a screenshot if the step failed.
*
* This includes creating an HTML dump of the content if there was a failure.
*
+ * @param StepEvent $event event fired after step.
* @AfterStep
*/
- public function after_step($event) {
+ public function after_step(StepEvent $event) {
global $CFG;
// Save the page content if the step failed.
$this->assertCount(0, $usergroups1[0]);
$this->assertCount(0, $usergroups2[0]);
}
+
+ /**
+ * Create dummy groups array for use in menu tests
+ * @param int $number
+ * @return array
+ */
+ protected function make_group_list($number) {
+ $testgroups = array();
+ for ($a = 0; $a < $number; $a++) {
+ $grp = new stdClass();
+ $grp->id = 100 + $a;
+ $grp->name = 'test group ' . $grp->id;
+ $testgroups[$grp->id] = $grp;
+ }
+ return $testgroups;
+ }
+
+ public function test_groups_sort_menu_options_empty() {
+ $this->assertEquals(array(), groups_sort_menu_options(array(), array()));
+ }
+
+ public function test_groups_sort_menu_options_allowed_goups_only() {
+ $this->assertEquals(array(
+ 100 => 'test group 100',
+ 101 => 'test group 101',
+ ), groups_sort_menu_options($this->make_group_list(2), array()));
+ }
+
+ public function test_groups_sort_menu_options_user_goups_only() {
+ $this->assertEquals(array(
+ 100 => 'test group 100',
+ 101 => 'test group 101',
+ ), groups_sort_menu_options(array(), $this->make_group_list(2)));
+ }
+
+ public function test_groups_sort_menu_options_user_both() {
+ $this->assertEquals(array(
+ 1 => array(get_string('mygroups', 'group') => array(
+ 100 => 'test group 100',
+ 101 => 'test group 101',
+ )),
+ 2 => array(get_string('othergroups', 'group') => array(
+ 102 => 'test group 102',
+ 103 => 'test group 103',
+ )),
+ ), groups_sort_menu_options($this->make_group_list(4), $this->make_group_list(2)));
+ }
+
+ public function test_groups_sort_menu_options_user_both_many_groups() {
+ $this->assertEquals(array(
+ 1 => array(get_string('mygroups', 'group') => array(
+ 100 => 'test group 100',
+ 101 => 'test group 101',
+ )),
+ 2 => array (get_string('othergroups', 'group') => array(
+ 102 => 'test group 102',
+ 103 => 'test group 103',
+ 104 => 'test group 104',
+ 105 => 'test group 105',
+ 106 => 'test group 106',
+ 107 => 'test group 107',
+ 108 => 'test group 108',
+ 109 => 'test group 109',
+ 110 => 'test group 110',
+ 111 => 'test group 111',
+ 112 => 'test group 112',
+ )),
+ ), groups_sort_menu_options($this->make_group_list(13), $this->make_group_list(2)));
+ }
}
$rs->close();
$transaction->allow_commit();
}
+
+/**
+ * This function verifies that the database is not using an unsupported storage engine.
+ *
+ * @param environment_results $result object to update, if relevant
+ * @return environment_results|null updated results object, or null if the storage engine is supported
+ */
+function check_database_storage_engine(environment_results $result) {
+ global $DB;
+
+ // Check if MySQL is the DB family (this will also be the same for MariaDB).
+ if ($DB->get_dbfamily() == 'mysql') {
+ // Get the database engine we will either be using to install the tables, or what we are currently using.
+ $engine = $DB->get_dbengine();
+ // Check if MyISAM is the storage engine that will be used, if so, do not proceed and display an error.
+ if ($engine == 'MyISAM') {
+ $result->setInfo('unsupported_db_storage_engine');
+ $result->setStatus(false);
+ return $result;
+ }
+ }
+
+ return null;
+}
$fields = new backup_nested_element('fields');
$field = new backup_nested_element('field', array('id'), array(
- 'type', 'name', 'description', 'param1', 'param2',
+ 'type', 'name', 'description', 'required', 'param1', 'param2',
'param3', 'param4', 'param5', 'param6',
'param7', 'param8', 'param9', 'param10'));
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/data/db" VERSION="20120122" COMMENT="XMLDB file for Moodle mod/data"
+<XMLDB PATH="mod/data/db" VERSION="20150309" COMMENT="XMLDB file for Moodle mod/data"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<FIELD NAME="type" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
+ <FIELD NAME="required" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Required fields must have a value when inserted by a user"/>
<FIELD NAME="param1" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="param2" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="param3" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="fieldid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="recordid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
- <FIELD NAME="content" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
- <FIELD NAME="content1" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
- <FIELD NAME="content2" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
- <FIELD NAME="content3" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
- <FIELD NAME="content4" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
+ <FIELD NAME="content" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+ <FIELD NAME="content1" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+ <FIELD NAME="content2" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+ <FIELD NAME="content3" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+ <FIELD NAME="content4" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.
- return true;
-}
+ if ($oldversion < 2015030900) {
+ // Define field required to be added to data_fields.
+ $table = new xmldb_table('data_fields');
+ $field = new xmldb_field('required', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'description');
+
+ // Conditionally launch add field required.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+ upgrade_mod_savepoint(true, 2015030900, 'data');
+ }
+ return true;
+}
$cancel = optional_param('cancel', '', PARAM_RAW); // cancel an add
$mode ='addtemplate'; //define the mode for this page, only 1 mode available
+
+
$url = new moodle_url('/mod/data/edit.php');
if ($rid !== 0) {
+ $record = $DB->get_record('data_records', array(
+ 'id' => $rid,
+ 'dataid' => $d,
+ ), '*', MUST_EXIST);
$url->param('rid', $rid);
}
if ($cancel !== '') {
$PAGE->set_title($data->name);
$PAGE->set_heading($course->fullname);
-/// Process incoming data for adding/updating records
+// Process incoming data for adding/updating records.
+
+// Keep track of any notifications.
+$generalnotifications = array();
+$fieldnotifications = array();
+// Process the submitted form.
if ($datarecord = data_submitted() and confirm_sesskey()) {
+ if ($rid) {
+ // Updating an existing record.
- $ignorenames = array('MAX_FILE_SIZE','sesskey','d','rid','saveandview','cancel'); // strings to be ignored in input data
+ // Retrieve the format for the fields.
+ $fields = $DB->get_records('data_fields', array('dataid' => $datarecord->d));
- if ($rid) { /// Update some records
+ // Validate the form to ensure that enough data was submitted.
+ $processeddata = data_process_submission($data, $fields, $datarecord);
- /// All student edits are marked unapproved by default
- $record = $DB->get_record('data_records', array('id'=>$rid));
+ // Add the new notification data.
+ $generalnotifications = array_merge($generalnotifications, $processeddata->generalnotifications);
+ $fieldnotifications = array_merge($fieldnotifications, $processeddata->fieldnotifications);
- /// reset approved flag after student edit
- if (!has_capability('mod/data:approve', $context)) {
- $record->approved = 0;
- }
+ if ($processeddata->validated) {
+ // Enough data to update the record.
- $record->timemodified = time();
- $DB->update_record('data_records', $record);
+ // Obtain the record to be updated.
- /// Update all content
- $field = NULL;
- foreach ($datarecord as $name => $value) {
- if (!in_array($name, $ignorenames)) {
- $namearr = explode('_',$name); // Second one is the field id
- if (empty($field->field) || ($namearr[1] != $field->field->id)) { // Try to reuse classes
- $field = data_get_field_from_id($namearr[1], $data);
- }
- if ($field) {
- $field->update_content($rid, $value, $name);
- }
+ // Reset the approved flag after edit if the user does not have permission to approve their own entries.
+ if (!has_capability('mod/data:approve', $context)) {
+ $record->approved = 0;
}
- }
- // Trigger an event for updating this record.
- $event = \mod_data\event\record_updated::create(array(
- 'objectid' => $rid,
- 'context' => $context,
- 'courseid' => $course->id,
- 'other' => array(
- 'dataid' => $data->id
- )
- ));
- $event->add_record_snapshot('data', $data);
- $event->trigger();
-
- redirect($CFG->wwwroot.'/mod/data/view.php?d='.$data->id.'&rid='.$rid);
-
- } else { /// Add some new records
- ///Empty form checking - you can't submit an empty form!
-
- $emptyform = true; // assume the worst
-
- foreach ($datarecord as $name => $value) {
- if (!in_array($name, $ignorenames)) {
- $namearr = explode('_', $name); // Second one is the field id
- if (empty($field->field) || ($namearr[1] != $field->field->id)) { // Try to reuse classes
- $field = data_get_field_from_id($namearr[1], $data);
- }
- if ($field->notemptyfield($value, $name)) {
- $emptyform = false;
- break; // if anything has content, this form is not empty, so stop now!
- }
+ // Update the parent record.
+ $record->timemodified = time();
+ $DB->update_record('data_records', $record);
+
+ // Update all content.
+ foreach ($processeddata->fields as $fieldname => $field) {
+ $field->update_content($rid, $datarecord->$fieldname, $fieldname);
}
- }
- if ($emptyform){ //nothing gets written to database
- echo $OUTPUT->notification(get_string('emptyaddform','data'));
+ // Trigger an event for updating this record.
+ $event = \mod_data\event\record_updated::create(array(
+ 'objectid' => $rid,
+ 'context' => $context,
+ 'courseid' => $course->id,
+ 'other' => array(
+ 'dataid' => $data->id
+ )
+ ));
+ $event->add_record_snapshot('data', $data);
+ $event->trigger();
+
+ $viewurl = new moodle_url('/mod/data/view.php', array(
+ 'd' => $data->id,
+ 'rid' => $rid,
+ ));
+ redirect($viewurl);
}
- if (!$emptyform && $recordid = data_add_record($data, $currentgroup)) { //add instance to data_record
+ } else {
+ // No recordid was specified - creating a new entry.
- /// Insert a whole lot of empty records to make sure we have them
- $fields = $DB->get_records('data_fields', array('dataid'=>$data->id));
+ // Retrieve the format for the fields.
+ $fields = $DB->get_records('data_fields', array('dataid' => $datarecord->d));
+
+ // Validate the form to ensure that enough data was submitted.
+ $processeddata = data_process_submission($data, $fields, $datarecord);
+
+ // Add the new notification data.
+ $generalnotifications = array_merge($generalnotifications, $processeddata->generalnotifications);
+ $fieldnotifications = array_merge($fieldnotifications, $processeddata->fieldnotifications);
+
+ // Add instance to data_record.
+ if ($processeddata->validated && $recordid = data_add_record($data, $currentgroup)) {
+
+ // Insert a whole lot of empty records to make sure we have them.
+ $records = array();
foreach ($fields as $field) {
$content = new stdClass();
$content->recordid = $recordid;
$content->fieldid = $field->id;
- $DB->insert_record('data_content',$content);
+ $records[] = $content;
}
- /// For each field in the add form, add it to the data_content.
- foreach ($datarecord as $name => $value){
- if (!in_array($name, $ignorenames)) {
- $namearr = explode('_', $name); // Second one is the field id
- if (empty($field->field) || ($namearr[1] != $field->field->id)) { // Try to reuse classes
- $field = data_get_field_from_id($namearr[1], $data);
- }
- if ($field) {
- $field->update_content($recordid, $value, $name);
- }
- }
+ // Bulk insert the records now. Some records may have no data but all must exist.
+ $DB->insert_records('data_content', $records);
+
+ // Add all provided content.
+ foreach ($processeddata->fields as $fieldname => $field) {
+ $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
}
+ // Trigger an event for updating this record.
+ $event = \mod_data\event\record_created::create(array(
+ 'objectid' => $rid,
+ 'context' => $context,
+ 'courseid' => $course->id,
+ 'other' => array(
+ 'dataid' => $data->id
+ )
+ ));
+ $event->add_record_snapshot('data', $data);
+ $event->trigger();
+
if (!empty($datarecord->saveandview)) {
- redirect($CFG->wwwroot.'/mod/data/view.php?d='.$data->id.'&rid='.$recordid);
+ $viewurl = new moodle_url('/mod/data/view.php', array(
+ 'd' => $data->id,
+ 'rid' => $recordid,
+ ));
+ redirect($viewurl);
}
}
}
-} // End of form processing
+}
+// End of form processing.
/// Print the page header
// To skip unnecessary calls to display_add_field().
if (strpos($data->addtemplate, "[[".$field->field->name."]]") !== false) {
+ // Replace the field tag.
$patterns[] = "[[".$field->field->name."]]";
- $replacements[] = $field->display_add_field($rid);
+ $errors = '';
+ if (!empty($fieldnotifications[$field->field->name])) {
+ foreach ($fieldnotifications[$field->field->name] as $notification) {
+ $errors .= $OUTPUT->notification($notification);
+ }
+ }
+ $replacements[] = $errors . $field->display_add_field($rid, $datarecord);
}
+
+ // Replace the field id tag.
$patterns[] = "[[".$field->field->name."#id]]";
$replacements[] = 'field_'.$field->field->id;
}
$newtext = '';
}
+foreach ($generalnotifications as $notification) {
+ echo $OUTPUT->notification($notification);
+}
echo $newtext;
echo '<div class="mdl-align"><input type="submit" name="saveandview" value="'.get_string('saveandview','data').'" />';
$field->field->name = $fieldinput->name;
$field->field->description = $fieldinput->description;
+ $field->field->required = !empty($fieldinput->required) ? 1 : 0;
for ($i=1; $i<=10; $i++) {
if (isset($fieldinput->{'param'.$i})) {
} else { //else print quiz style list of fields
$table = new html_table();
- $table->head = array(get_string('fieldname','data'), get_string('type','data'), get_string('fielddescription', 'data'), get_string('action','data'));
+ $table->head = array(
+ get_string('fieldname', 'data'),
+ get_string('type', 'data'),
+ get_string('required', 'data'),
+ get_string('fielddescription', 'data'),
+ get_string('action', 'data'),
+ );
$table->align = array('left','left','left', 'center');
$table->wrap = array(false,false,false,false);
$field = data_get_field($ff, $data);
- $table->data[] = array(
-
- '<a href="field.php?mode=display&d='.$data->id.
- '&fid='.$field->field->id.'&sesskey='.sesskey().'">'.$field->field->name.'</a>',
+ $baseurl = new moodle_url('/mod/data/field.php', array(
+ 'd' => $data->id,
+ 'fid' => $field->field->id,
+ 'sesskey' => sesskey(),
+ ));
- $field->image().' '.get_string($field->type, 'data'),
+ $displayurl = new moodle_url($baseurl, array(
+ 'mode' => 'display',
+ ));
- shorten_text($field->field->description, 30),
-
- '<a href="field.php?d='.$data->id.'&mode=display&fid='.$field->field->id.'&sesskey='.sesskey().'">'.
- '<img src="'.$OUTPUT->pix_url('t/edit') . '" class="iconsmall" alt="'.get_string('edit').'" title="'.get_string('edit').'" /></a>'.
- ' '.
- '<a href="field.php?d='.$data->id.'&mode=delete&fid='.$field->field->id.'&sesskey='.sesskey().'">'.
- '<img src="'.$OUTPUT->pix_url('t/delete') . '" class="iconsmall" alt="'.get_string('delete').'" title="'.get_string('delete').'" /></a>'
+ $deleteurl = new moodle_url($baseurl, array(
+ 'mode' => 'delete',
+ ));
+ $table->data[] = array(
+ html_writer::link($displayurl, $field->field->name),
+ $field->image() . ' ' . get_string($field->type, 'data'),
+ $field->field->required ? get_string('yes') : get_string('no'),
+ shorten_text($field->field->description, 30),
+ html_writer::link($displayurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))) .
+ ' ' .
+ html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))),
);
}
}
var $type = 'checkbox';
- function display_add_field($recordid=0) {
- global $CFG, $DB;
+ function display_add_field($recordid = 0, $formdata = null) {
+ global $CFG, $DB, $OUTPUT;
$content = array();
- if ($recordid) {
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id;
+ $content = $formdata->$fieldname;
+ } else if ($recordid) {
$content = $DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid));
$content = explode('##', $content);
} else {
$content = array();
}
- $str = '<div title="'.s($this->field->description).'">';
- $str .= '<fieldset><legend><span class="accesshide">'.$this->field->name.'</span></legend>';
+ $str = '<div title="' . s($this->field->description) . '">';
+ $str .= '<fieldset><legend><span class="accesshide">'.$this->field->name;
+ if ($this->field->required) {
+ $str .= '$nbsp;' . get_string('requiredelement', 'form');
+ $str .= '</span></legend>';
+ $str .= '<div>';
+ $str .= html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ $str .= '</div>';
+ } else {
+ $str .= '</span></legend>';
+ }
$i = 0;
foreach (explode("\n", $this->field->param1) as $checkbox) {
return implode('##', $vals);
}
-}
+ /**
+ * Check whether any boxes in the checkbox where checked.
+ *
+ * @param mixed $value The submitted values
+ * @param mixed $name
+ * @return bool
+ */
+ function notemptyfield($value, $name) {
+ $found = false;
+ foreach ($value as $checkboxitem) {
+ if (!empty($checkboxitem)) {
+ $found = true;
+ break;
+ }
+ }
+ return $found;
+ }
+}
<td class="c0"><label for="description"><?php echo get_string('fielddescription', 'data'); ?></label></td>
<td class="c1"><input class="fielddescription" type="text" name="description" id="description" value="<?php p($this->field->description); ?>" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required ? "checked=\"checked\"" : ""); ?>/></td>
+ </tr>
<tr>
<td class="c0" valign="top"><label for="param1"><?php echo get_string('fieldoptions', 'data'); ?></label></td>
<td class="c1"><textarea class="optionstextarea" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
var $month = 0;
var $year = 0;
- function display_add_field($recordid=0) {
+ function display_add_field($recordid = 0, $formdata = null) {
global $DB, $OUTPUT;
- if ($recordid) {
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id . '_day';
+ $day = $formdata->$fieldname;
+ $fieldname = 'field_' . $this->field->id . '_month';
+ $month = $formdata->$fieldname;
+ $fieldname = 'field_' . $this->field->id . '_year';
+ $year = $formdata->$fieldname;
+ $content = make_timestamp($year, $month, $day, 12, 0, 0, 0, false);
+ } else if ($recordid) {
$content = (int)$DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid));
} else {
$content = time();
}
-
-
class data_field_file extends data_field_base {
var $type = 'file';
- function display_add_field($recordid=0) {
+ function display_add_field($recordid = 0, $formdata = null) {
global $CFG, $DB, $OUTPUT, $PAGE, $USER;
$file = false;
$itemid = null;
// editing an existing database entry
- if ($recordid){
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id . '_file';
+ $itemid = $formdata->$fieldname;
+ } else if ($recordid) {
if ($content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) {
file_prepare_draft_area($itemid, $this->context->id, 'mod_data', 'content', $content->id);
$itemid = file_get_unused_draft_itemid();
}
- $html = '';
// database entry label
- $html .= '<div title="'.s($this->field->description).'">';
- $html .= '<fieldset><legend><span class="accesshide">'.$this->field->name.'</span></legend>';
+ $html = '<div title="' . s($this->field->description) . '">';
+ $html .= '<fieldset><legend><span class="accesshide">'.$this->field->name;
+
+ if ($this->field->required) {
+ $html .= ' ' . get_string('requiredelement', 'form') . '</span></legend>';
+ $image = html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ $html .= html_writer::div($image);
+ } else {
+ $html .= '</span></legend>';
+ }
// itemid element
$html .= '<input type="hidden" name="field_'.$this->field->id.'_file" value="'.$itemid.'" />';
$output = $PAGE->get_renderer('core', 'files');
$html .= $output->render($fm);
-
$html .= '</fieldset>';
$html .= '</div>';
return true;
}
-}
-
+ /**
+ * Custom notempty function
+ *
+ * @param string $value
+ * @param string $name
+ * @return bool
+ */
+ function notemptyfield($value, $name) {
+ global $USER;
+
+ $names = explode('_', $name);
+ if ($names[2] == 'file') {
+ $usercontext = context_user::instance($USER->id);
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $value);
+ return count($files) >= 2;
+ }
+ return false;
+ }
+}
<td class="c0"><label for="description"><?php echo get_string('fielddescription', 'data'); ?></label></td>
<td class="c1"><input class="fielddescription" type="text" name="description" id="description" value="<?php p($this->field->description);?>" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required ? "checked=\"checked\"" : ""); ?>/></td>
+ </tr>
<tr>
<td class="c0"><label for="param3">
<?php echo get_string('maxsize', 'data'); ?></label></td>
);
// Other map sources listed at http://kvaleberg.com/extensions/mapsources/index.php?params=51_30.4167_N_0_7.65_W_region:earth
- function display_add_field($recordid=0) {
- global $CFG, $DB;
+ function display_add_field($recordid = 0, $formdata = null) {
+ global $CFG, $DB, $OUTPUT;
$lat = '';
$long = '';
- if ($recordid) {
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id . '_0';
+ $lat = $formdata->$fieldname;
+ $fieldname = 'field_' . $this->field->id . '_1';
+ $long = $formdata->$fieldname;
+ } else if ($recordid) {
if ($content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) {
$lat = $content->content;
$long = $content->content1;
$str = '<div title="'.s($this->field->description).'">';
$str .= '<fieldset><legend><span class="accesshide">'.$this->field->name.'</span></legend>';
$str .= '<table><tr><td align="right">';
- $str .= '<label for="field_'.$this->field->id.'_0">' . get_string('latitude', 'data') . '</label></td><td><input type="text" name="field_'.$this->field->id.'_0" id="field_'.$this->field->id.'_0" value="'.s($lat).'" size="10" />°N</td></tr>';
- $str .= '<tr><td align="right"><label for="field_'.$this->field->id.'_1">' . get_string('longitude', 'data') . '</label></td><td><input type="text" name="field_'.$this->field->id.'_1" id="field_'.$this->field->id.'_1" value="'.s($long).'" size="10" />°E</td></tr>';
+ $str .= '<label for="field_'.$this->field->id.'_0">' . get_string('latitude', 'data');
+ if ($this->field->required) {
+ $str .= html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ }
+ $str .= '</label></td><td><input type="text" name="field_'.$this->field->id.'_0" id="field_'.$this->field->id.'_0" value="';
+ $str .= s($lat).'" size="10" />°N</td></tr>';
+ $str .= '<tr><td align="right"><label for="field_'.$this->field->id.'_1">' . get_string('longitude', 'data');
+ if ($this->field->required) {
+ $str .= html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ }
+ $str .= '</label></td><td><input type="text" name="field_'.$this->field->id.'_1" id="field_'.$this->field->id.'_1" value="';
+ $str .= s($long).'" size="10" />°E</td>';
+ $str .= '</tr>';
$str .= '</table>';
$str .= '</fieldset>';
$str .= '</div>';
return sprintf('%01.4f', $record->content) . ' ' . sprintf('%01.4f', $record->content1);
}
-}
-
+ /**
+ * Check if a field from an add form is empty
+ *
+ * @param mixed $value
+ * @param mixed $name
+ * @return bool
+ */
+ function notemptyfield($value, $name) {
+ return isset($value) && !($value == '');
+ }
+}
<td class="c0"><label for="description"><?php echo get_string('fielddescription', 'data'); ?></label></td>
<td class="c1"><input class="fielddescription" type="text" name="description" id="description" value="<?php p ($this->field->description);?>" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
<tr>
<td class="c0"><label for="param1"><?php echo get_string('latlonglinkservicesdisplayed', 'data'); ?></label></td>
<td class="c1">
var $type = 'menu';
- function display_add_field($recordid=0) {
+ function display_add_field($recordid = 0, $formdata = null) {
global $DB, $OUTPUT;
- if ($recordid){
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id;
+ $content = $formdata->$fieldname;
+ } else if ($recordid) {
$content = $DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid));
$content = trim($content);
} else {
$content = '';
}
-
- $str = '<div title="'.s($this->field->description).'">';
+ $str = '<div title="' . s($this->field->description) . '">';
$options = array();
$rawoptions = explode("\n",$this->field->param1);
}
}
- $str .= html_writer::label(get_string('menuchoose', 'data'), 'field_'.$this->field->id, false, array('class' => 'accesshide'));
+ $str .= '<label for="' . 'field_' . $this->field->id . '">';
+ $str .= html_writer::span($this->field->name, 'accesshide');
+ if ($this->field->required) {
+ $image = html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ $str .= html_writer::div($image);
+ }
+ $str .= '</label>';
$str .= html_writer::select($options, 'field_'.$this->field->id, $content, array(''=>get_string('menuchoose', 'data')), array('id'=>'field_'.$this->field->id));
$str .= '</div>';
}
}
-
-
<td class="c0"><label for="description"><?php echo get_string('fielddescription', 'data'); ?></label></td>
<td class="c1"><input class="fielddescription" type="text" name="description" id="description" value="<?php p($this->field->description);?>" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
<tr>
<td class="c0" valign="top"><label for="param1"><?php echo get_string('fieldoptions', 'data'); ?></label></td>
<td class="c1"><textarea class="optionstextarea" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
var $type = 'multimenu';
- function display_add_field($recordid=0) {
- global $DB;
+ function display_add_field($recordid = 0, $formdata = null) {
+ global $DB, $OUTPUT;
- if ($recordid){
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id;
+ if (isset($formdata->$fieldname)) {
+ $content = $formdata->$fieldname;
+ } else {
+ $content = array();
+ }
+ } else if ($recordid) {
$content = $DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid));
$content = explode('##', $content);
} else {
$str = '<div title="'.s($this->field->description).'">';
$str .= '<input name="field_' . $this->field->id . '[xxx]" type="hidden" value="xxx"/>'; // hidden field - needed for empty selection
- $str .= '<label class="accesshide" for="field_' . $this->field->id . '">' . $this->field->name. '</label>';
+
+ $str .= '<label for="field_' . $this->field->id . '">';
+ $str .= html_writer::span($this->field->name, 'accesshide');
+ if ($this->field->required) {
+ $str .= '<div>';
+ $str .= html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ $str .= '</div>';
+ }
+ $str .= '</label>';
$str .= '<select name="field_' . $this->field->id . '[]" id="field_' . $this->field->id . '" multiple="multiple">';
- foreach (explode("\n",$this->field->param1) as $option) {
+ foreach (explode("\n", $this->field->param1) as $option) {
$option = trim($option);
$str .= '<option value="' . s($option) . '"';
}
return false;
}
-}
+ /**
+ * Check if a field from an add form is empty
+ *
+ * @param mixed $value
+ * @param mixed $name
+ * @return bool
+ */
+ function notemptyfield($value, $name) {
+ unset($value['xxx']);
+ return !empty($value);
+ }
+}
<td class="c0"><label for="description"><?php echo get_string('fielddescription', 'data'); ?></label></td>
<td class="c1"><input class="fielddescription" type="text" name="description" id="description" value="<?php p($this->field->description); ?>" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
<tr>
<td class="c0" valign="top"><label for="param1"><?php echo get_string('fieldoptions', 'data'); ?></label></td>
<td class="c1"><textarea class="optionstextarea" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
<td class="c0"><label for="description"><?php echo get_string('fielddescription', 'data'); ?></label></td>
<td class="c1"><input class="fielddescription" type="text" name="description" id="description" value="<?php p($this->field->description);?>" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
</table>
var $previewwidth = 50;
var $previewheight = 50;
- function display_add_field($recordid=0) {
+ function display_add_field($recordid = 0, $formdata = null) {
global $CFG, $DB, $OUTPUT, $USER, $PAGE;
$file = false;
$itemid = null;
$fs = get_file_storage();
- if ($recordid) {
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id . '_file';
+ $itemid = $formdata->$fieldname;
+ $fieldname = 'field_' . $this->field->id . '_alttext';
+ if (isset($formdata->$fieldname)) {
+ $alttext = $formdata->$fieldname;
+ }
+ } else if ($recordid) {
if ($content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) {
file_prepare_draft_area($itemid, $this->context->id, 'mod_data', 'content', $content->id);
if (!empty($content->content)) {
} else {
$itemid = file_get_unused_draft_itemid();
}
-
- $str = '<div title="'.s($this->field->description).'">';
- $str .= '<fieldset><legend><span class="accesshide">'.$this->field->name.'</span></legend>';
+ $str = '<div title="' . s($this->field->description) . '">';
+ $str .= '<fieldset><legend><span class="accesshide">'.$this->field->name;
+
+ if ($this->field->required) {
+ $str .= ' ' . get_string('requiredelement', 'form') . '</span></legend>';
+ $image = html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ $str .= html_writer::div($image);
+ } else {
+ $str .= '</span></legend>';
+ }
$str .= '<noscript>';
if ($file) {
$src = file_encode_url($CFG->wwwroot.'/pluginfile.php/', $this->context->id.'/mod_data/content/'.$content->id.'/'.$file->get_filename());
function file_ok($path) {
return true;
}
-}
+ /**
+ * Custom notempty function
+ *
+ * @param string $value
+ * @param string $name
+ * @return bool
+ */
+ function notemptyfield($value, $name) {
+ global $USER;
+ $names = explode('_', $name);
+ if ($names[2] == 'file') {
+ $usercontext = context_user::instance($USER->id);
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $value);
+ return count($files) >= 2;
+ }
+ return false;
+ }
+}
<input class="fielddescription" type="text" name="description" id="description" value="<?php p($this->field->description);?>" />
</td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
<tr>
<td class="c0"><label for="param1">
<?php echo get_string('fieldwidthsingleview', 'data');?></label></td>
var $type = 'radiobutton';
- function display_add_field($recordid=0) {
- global $CFG, $DB;
+ function display_add_field($recordid = 0, $formdata = null) {
+ global $CFG, $DB, $OUTPUT;
- if ($recordid){
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id;
+ if (isset($formdata->$fieldname)) {
+ $content = $formdata->$fieldname;
+ } else {
+ $content = '';
+ }
+ } else if ($recordid) {
$content = trim($DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid)));
} else {
$content = '';
}
- $str = '<div title="'.s($this->field->description).'">';
- $str .= '<fieldset><legend><span class="accesshide">'.$this->field->name.'</span></legend>';
+ $str = '<div title="' . s($this->field->description) . '">';
+ $str .= '<fieldset><legend><span class="accesshide">' . $this->field->name;
+
+ if ($this->field->required) {
+ $str .= ' ' . get_string('requiredelement', 'form') . '</span></legend>';
+ $image = html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ $str .= html_writer::div($image);
+ } else {
+ $str .= '</span></legend>';
+ }
$i = 0;
- foreach (explode("\n",$this->field->param1) as $radio) {
+ $requiredstr = '';
+ $options = explode("\n", $this->field->param1);
+ foreach ($options as $radio) {
$radio = trim($radio);
if ($radio === '') {
continue; // skip empty lines
<td class="c0"><label for="description"><?php echo get_string('fielddescription', 'data'); ?></label></td>
<td class="c1"><input class="fielddescription" type="text" name="description" id="description" value="<?php p($this->field->description); ?>" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
<tr>
<td class="c0" valign="top"><label for="param1"><?php echo get_string('fieldoptions', 'data'); ?></label></td>
<td class="c1"><textarea class="optionstextarea" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
<td class="c0"><label for="description"><?php echo get_string('fielddescription', 'data'); ?></label></td>
<td class="c1"><input class="fielddescription" type="text" name="description" id="description" value="<?php p($this->field->description); ?>" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
<tr>
<td class="c0"><label for="param1"><?php echo get_string('fieldallowautolink', 'data'); ?></label></td>
<td class="c1"><input type="checkbox" name="param1" id="param1" <?php if($this->field->param1) {echo 'checked="checked"';} ?> value="1" /></td>
return $options;
}
- function display_add_field($recordid=0) {
+ function display_add_field($recordid = 0, $formdata = null) {
global $CFG, $DB, $OUTPUT, $PAGE;
$text = '';
$format = 0;
-
- $str = '<div title="'.$this->field->description.'">';
+ $str = '<div title="' . s($this->field->description) . '">';
+ $str .= '<label for="field_' . $this->field->id . '">';
+ $str .= html_writer::span($this->field->name, "accesshide");
+ if ($this->field->required) {
+ $str .= html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ }
+ $str .= '</label>';
editors_head_setup();
$options = $this->get_options();
$itemid = $this->field->id;
$field = 'field_'.$itemid;
- if ($recordid && $content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))){
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id . '_content1';
+ if (isset($formdata->$fieldname)) {
+ $format = $formdata->$fieldname;
+ } else {
+ $format = file_get_unused_draft_itemid();
+ }
+ $fieldname = 'field_' . $this->field->id . '_itemid';
+ if (isset($formdata->$fieldname)) {
+ $draftitemid = $formdata->$fieldname;
+ } else {
+ $draftitemid = file_get_unused_draft_itemid();
+ }
+ $fieldname = 'field_' . $this->field->id;
+ if (isset($formdata->$fieldname)) {
+ $text = $formdata->$fieldname;
+ }
+ } else if ($recordid &&
+ $content = $DB->get_record('data_content', array('fieldid' => $this->field->id, 'recordid' => $recordid))) {
$format = $content->content1;
$text = clean_text($content->content, $format);
$text = file_prepare_draft_area($draftitemid, $this->context->id, 'mod_data', 'content', $content->id, $options, $text);
$str .= '<option value="'.s($key).'" '.$selected.'>'.$desc.'</option>';
}
$str .= '</select>';
- $str .= '</div>';
+ $str .= '</div>';
$str .= '</div>';
return $str;
}
function file_ok($relativepath) {
return true;
}
-}
+ /**
+ * Only look at the first item (second is format)
+ *
+ * @param string $value
+ * @param string $name
+ * @return bool
+ */
+ function notemptyfield($value, $name) {
+ $names = explode('_', $name);
+ // Clean first.
+ if (count($names) == 2) {
+ return !empty($value);
+ }
+ return false;
+ }
+}
value="<?php p($this->field->description); ?>" />
</td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
<tr>
<td class="c0"><label for="param2">
<?php echo get_string('fieldwidth', 'data'); ?></label></td>
class data_field_url extends data_field_base {
var $type = 'url';
- function display_add_field($recordid=0) {
+ function display_add_field($recordid = 0, $formdata = null) {
global $CFG, $DB, $OUTPUT, $PAGE;
require_once($CFG->dirroot. '/repository/lib.php'); // necessary for the constants used in args
$straddlink = get_string('choosealink', 'repository');
$url = '';
$text = '';
- if ($recordid) {
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id . '_0';
+ $url = $formdata->$fieldname;
+ $fieldname = 'field_' . $this->field->id . '_1';
+ if (isset($formdata->$fieldname)) {
+ $text = $formdata->$fieldname;
+ }
+ } else if ($recordid) {
if ($content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) {
$url = $content->content;
$text = $content->content1;
}
}
- $str = '<div title="'.s($this->field->description).'">';
+ $str = '<div title="' . s($this->field->description) . '">';
+
+ $label = '<label for="' . $fieldid . '"><span class="accesshide">' . $this->field->name . '</span>';
+ if ($this->field->required) {
+ $label .= html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ }
+ $label .= '</label>';
+
if (!empty($this->field->param1) and empty($this->field->param2)) {
$str .= '<table><tr><td align="right">';
$str .= get_string('url','data').':</td><td>';
- $str .= '<label class="accesshide" for="' . $fieldid . '">'. $this->field->name .'</label>';
+ $str .= $label;
$str .= '<input type="text" name="field_'.$this->field->id.'_0" id="'.$fieldid.'" value="'.$url.'" size="60" />';
$str .= '<button id="filepicker-button-'.$options->client_id.'" style="display:none">'.$straddlink.'</button></td></tr>';
$str .= '<tr><td align="right">'.get_string('text','data').':</td><td><input type="text" name="field_'.$this->field->id.'_1" id="field_'.$this->field->id.'_1" value="'.s($text).'" size="60" /></td></tr>';
$str .= '</table>';
} else {
// Just the URL field
- $str .= '<label class="accesshide" for="' . $fieldid . '">'. $this->field->name .'</label>';
+ $str .= $label;
$str .= '<input type="text" name="field_'.$this->field->id.'_0" id="'.$fieldid.'" value="'.s($url).'" size="60" />';
if (count($options->repositories) > 0) {
$str .= '<button id="filepicker-button-'.$options->client_id.'" class="visibleifjs">'.$straddlink.'</button>';
$module = array('name'=>'data_urlpicker', 'fullpath'=>'/mod/data/data.js', 'requires'=>array('core_filepicker'));
$PAGE->requires->js_init_call('M.data_urlpicker.init', array($options), true, $module);
-
$str .= '</div>';
return $str;
}
}
}
-
-
<td class="c0"><label for="param1"><?php echo get_string('autolinkurl', 'data') ?></label></td>
<td class="c1"><input type="checkbox" name="param1" id="param1" <?php if($this->field->param1) {echo 'checked="checked"';} ?> value="1" /></td>
</tr>
+ <tr>
+ <td class="c0"><label for="required"><?php echo get_string('requiredfield', 'data'); ?></label></td>
+ <td class="c1"><input class="requiredfield" type="checkbox" name="required" id="required" <?php p($this->field->required?"checked=\"checked\"":""); ?>/></td>
+ </tr>
<tr>
<td class="c0"><label for="param3"><?php echo get_string('openlinkinnewwindow', 'datafield_url') ?></label></td>
<td class="c1"><input type="checkbox" name="param3" id="param3" <?php if($this->field->param3) {echo 'checked="checked"';} ?> value="1" /></td>
$string['entrysaved'] = 'Your entry has been saved';
$string['errormustbeteacher'] = 'You need to be a teacher to use this page!';
$string['errorpresetexists'] = 'There is already a preset with the selected name';
+$string['errormustsupplyvalue'] = 'You must supply a value here.';
$string['example'] = 'Database module example';
$string['excel'] = 'Excel';
$string['export'] = 'Export';
$string['recordssaved'] = 'entries saved';
$string['requireapproval'] = 'Approval required';
$string['requireapproval_help'] = 'If enabled, entries require approving by a teacher before they are viewable by everyone.';
+$string['required'] = 'Required';
$string['requiredentries'] = 'Entries required for completion';
$string['requiredentries_help'] = 'The number of entries a student is required to submit before the activity can be considered complete.';
$string['requiredentriestoview'] = 'Entries required before viewing';
$string['requiredentriestoview_help'] = 'The number of entries a student is required to submit before they can view entries from other students.
Note: If entries are required before viewing, the database auto-linking filter should be disabled. This is because the database auto-linking filter can\'t determine whether a user has submitted the required number of entries.';
+$string['requiredfield'] = 'Required field';
$string['resetsettings'] = 'Reset filters';
$string['resettemplate'] = 'Reset template';
$string['resizingimages'] = 'Resizing image thumbnails...';
$this->field->param3 = '';
$this->field->name = '';
$this->field->description = '';
+ $this->field->required = false;
return true;
}
$this->field->name = trim($data->name);
$this->field->description = trim($data->description);
+ $this->field->required = !empty($data->required) ? 1 : 0;
if (isset($data->param1)) {
$this->field->param1 = trim($data->param1);
* @param int $recordid
* @return string
*/
- function display_add_field($recordid=0){
- global $DB;
+ function display_add_field($recordid=0, $formdata=null) {
+ global $DB, $OUTPUT;
- if ($recordid){
+ if ($formdata) {
+ $fieldname = 'field_' . $this->field->id;
+ $content = $formdata->$fieldname;
+ } else if ($recordid) {
$content = $DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid));
} else {
$content = '';
$content='';
}
- $str = '<div title="'.s($this->field->description).'">';
- $str .= '<label class="accesshide" for="field_'.$this->field->id.'">'.$this->field->description.'</label>';
- $str .= '<input class="basefieldinput" type="text" name="field_'.$this->field->id.'" id="field_'.$this->field->id.'" value="'.s($content).'" />';
+ $str = '<div title="' . s($this->field->description) . '">';
+ $str .= '<label for="field_'.$this->field->id.'"><span class="accesshide">'.$this->field->name.'</span>';
+ if ($this->field->required) {
+ $str .= html_writer::img($OUTPUT->pix_url('req'), get_string('requiredelement', 'form'),
+ array('class' => 'req', 'title' => get_string('requiredelement', 'form')));
+ }
+ $str .= '</label><input class="basefieldinput" type="text" name="field_'.$this->field->id.'" id="field_'.$this->field->id;
+ $str .= '" value="'.s($content).'" />';
$str .= '</div>';
return $str;
foreach ($fields as $field) {
if ($form) { // Print forms instead of data
$fieldobj = data_get_field($field, $data);
- $token = $fieldobj->display_add_field($recordid);
+ $token = $fieldobj->display_add_field($recordid, null);
} else { // Just print the tag
$token = '[['.$field->name.']]';
}
}
return false;
}
+
+/**
+ * Check for required fields, and build a list of fields to be updated in a
+ * submission.
+ *
+ * @param $mod stdClass The current recordid - provided as an optimisation.
+ * @param $fields array The field data
+ * @param $datarecord stdClass The submitted data.
+ * @return stdClass containing:
+ * * string[] generalnotifications Notifications for the form as a whole.
+ * * string[] fieldnotifications Notifications for a specific field.
+ * * bool validated Whether the field was validated successfully.
+ * * data_field_base[] fields The field objects to be update.
+ */
+function data_process_submission(stdClass $mod, $fields, stdClass $datarecord) {
+ $result = new stdClass();
+
+ // Empty form checking - you can't submit an empty form.
+ $emptyform = true;
+ $requiredfieldsfilled = true;
+
+ // Store the notifications.
+ $result->generalnotifications = array();
+ $result->fieldnotifications = array();
+
+ // Store the instantiated classes as an optimisation when processing the result.
+ // This prevents the fields being re-initialised when updating.
+ $result->fields = array();
+
+ $submitteddata = array();
+ foreach ($datarecord as $fieldname => $fieldvalue) {
+ if (strpos($fieldname, '_')) {
+ $namearray = explode('_', $fieldname, 3);
+ $fieldid = $namearray[1];
+ if (!isset($submitteddata[$fieldid])) {
+ $submitteddata[$fieldid] = array();
+ }
+ if (count($namearray) === 2) {
+ $subfieldid = 0;
+ } else {
+ $subfieldid = $namearray[2];
+ }
+
+ $fielddata = new stdClass();
+ $fielddata->fieldname = $fieldname;
+ $fielddata->value = $fieldvalue;
+ $submitteddata[$fieldid][$subfieldid] = $fielddata;
+ }
+ }
+
+ // Check all form fields which have the required are filled.
+ foreach ($fields as $fieldrecord) {
+ // Check whether the field has any data.
+ $fieldhascontent = false;
+
+ $field = data_get_field($fieldrecord, $mod);
+ if (isset($submitteddata[$fieldrecord->id])) {
+ foreach ($submitteddata[$fieldrecord->id] as $fieldname => $value) {
+ if ($field->notemptyfield($value->value, $value->fieldname)) {
+ // The field has content and the form is not empty.
+ $fieldhascontent = true;
+ $emptyform = false;
+ }
+ }
+ }
+
+ // If the field is required, add a notification to that effect.
+ if ($field->field->required && !$fieldhascontent) {
+ if (!isset($result->fieldnotifications[$field->field->name])) {
+ $result->fieldnotifications[$field->field->name] = array();
+ }
+ $result->fieldnotifications[$field->field->name][] = get_string('errormustsupplyvalue', 'data');
+ $requiredfieldsfilled = false;
+ }
+
+ if ($fieldhascontent) {
+ // The field has content so it should be updatable.
+ foreach ($submitteddata[$fieldrecord->id] as $value) {
+ $result->fields[$value->fieldname] = $field;
+ }
+ }
+ }
+
+ if ($emptyform) {
+ // The form is empty.
+ $result->generalnotifications[] = get_string('emptyaddform', 'data');
+ }
+
+ $result->validated = $requiredfieldsfilled && !$emptyform;
+
+ return $result;
+}
.dir-rtl .mod-data-default-template .template-field {text-align:left;}
.dir-rtl .mod-data-default-template .template-token {text-align:right;}
.dir-rtl .mod-data-default-template searchcontrols {text-align:left;}
+
+#page-mod-data-edit .req {
+ cursor: help;
+}
When I log in as "student1"
And I follow "Course 1"
And I add an entry to "Test database name" database with:
- | Test field description | Student original entry |
+ | Test field name | Student original entry |
And I press "Save and view"
Then I should see "Student original entry"
And I follow "Edit"
And I set the following fields to these values:
- | Test field description | Student edited entry |
+ | Test field name | Student edited entry |
And I press "Save and view"
And I should see "Student edited entry"
And I add an entry to "Test database name" database with:
- | Test field description | Student second entry |
+ | Test field name | Student second entry |
And I press "Save and add another"
And I add an entry to "Test database name" database with:
- | Test field description | Student third entry |
+ | Test field name | Student third entry |
And I press "Save and view"
And I follow "View list"
And I should see "Student edited entry"
--- /dev/null
+@mod @mod_data
+Feature: Users can be required to specify certain fields when adding entries to database activities
+ In order to constrain user input
+ As a teacher
+ I need to specify certain fields as required when I add entries to databases
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@asd.com |
+ | teacher1 | Teacher | 1 | teacher1@asd.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | data | Test database name | n | C1 | data1 |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I add a "Text input" field to "Test database name" database and I fill the form with:
+ | Field name | Base Text input |
+ | Required | yes |
+ | Field description | Base Text input |
+ And I add a "Checkbox" field to "Test database name" database and I fill the form with:
+ | Field name | Required Checkbox |
+ | Field description | Required Checkbox |
+ | Required | yes |
+ | Options | Required Checkbox Option 1 |
+ And I follow "Fields"
+ And I set the field "newtype" to "Checkbox"
+ And I click on "Go" "button" in the ".fieldadd" "css_element"
+ And I set the following fields to these values:
+ | Field name | Required Two-Option Checkbox |
+ | Field description | Required Two-Option Checkbox |
+ | Required | yes |
+ And I set the field "Options" to multiline
+ """
+ RTOC Option 1
+ RTOC Option 2
+ """
+ And I press "Add"
+ And I add a "Latlong" field to "Test database name" database and I fill the form with:
+ | Field name | Required Latlong |
+ | Field description | Required Latlong |
+ | Required | yes |
+ And I add a "Menu" field to "Test database name" database and I fill the form with:
+ | Field name | Required Menu |
+ | Field description | Required Menu |
+ | Required | yes |
+ | Options | Option 1 |
+ And I add a "Number" field to "Test database name" database and I fill the form with:
+ | Field name | Required Number |
+ | Field description | Required Number |
+ | Required | yes |
+ And I add a "Radio button" field to "Test database name" database and I fill the form with:
+ | Field name | Required Radio |
+ | Field description | Required Radio |
+ | Required | yes |
+ | Options | Required Radio Option 1 |
+ And I add a "Text input" field to "Test database name" database and I fill the form with:
+ | Field name | Required Text input |
+ | Field description | Required Text input |
+ | Required | yes |
+ And I add a "Text area" field to "Test database name" database and I fill the form with:
+ | Field name | Required Text area |
+ | Field description | Required Text area |
+ | Required | yes |
+ And I add a "URL" field to "Test database name" database and I fill the form with:
+ | Field name | Required URL |
+ | Field description | Required URL |
+ | Required | yes |
+ And I add a "Multimenu" field to "Test database name" database and I fill the form with:
+ | Field name | Required Multimenu |
+ | Field description | Required Multimenu |
+ | Required | yes |
+ | Options | Option 1 |
+ And I follow "Fields"
+ And I set the field "newtype" to "Multimenu"
+ And I click on "Go" "button" in the ".fieldadd" "css_element"
+ And I set the following fields to these values:
+ | Field name | Required Two-Option Multimenu |
+ | Field description | Required Two-Option Multimenu |
+ | Required | yes |
+ And I set the field "Options" to multiline
+ """
+ Option 1
+ Option 2
+ """
+ And I press "Add"
+ And I add a "Checkbox" field to "Test database name" database and I fill the form with:
+ | Field name | Not required Checkbox |
+ | Field description | Not required Checkbox |
+ | Options | Not required Checkbox Option 1 |
+ And I add a "Latlong" field to "Test database name" database and I fill the form with:
+ | Field name | Not required Latlong |
+ | Field description | Not required Latlong |
+ And I add a "Menu" field to "Test database name" database and I fill the form with:
+ | Field name | Not required Menu |
+ | Field description | Not required Menu |
+ | Options | Option 1 |
+ And I add a "Number" field to "Test database name" database and I fill the form with:
+ | Field name | Not required Number |
+ | Field description | Not required Number |
+ And I add a "Radio button" field to "Test database name" database and I fill the form with:
+ | Field name | Not required Radio |
+ | Field description | Not required Radio |
+ | Options | Not required Radio Option 1 |
+ And I add a "Text input" field to "Test database name" database and I fill the form with:
+ | Field name | Not required Text input |
+ | Field description | Not required Text input |
+ And I add a "Text area" field to "Test database name" database and I fill the form with:
+ | Field name | Not required Text area |
+ | Field description | Not required Text area |
+ And I add a "URL" field to "Test database name" database and I fill the form with:
+ | Field name | Not required URL |
+ | Field description | Not required URL |
+ And I add a "Multimenu" field to "Test database name" database and I fill the form with:
+ | Field name | Not required Multimenu |
+ | Field description | Not required Multimenu |
+ | Options | Option 1 |
+ And I follow "Templates"
+ And I log out
+
+ Scenario: Students receive errors for empty required fields but not for optional fields
+ When I log in as "student1"
+ And I follow "Course 1"
+ And I add an entry to "Test database name" database with:
+ | Base Text input | Some input to allow us to submit the otherwise empty form |
+ And I press "Save and view"
+ Then ".alert.alert-error" "css_element" should exist in the "Required Checkbox" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Two-Option Checkbox" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Latlong" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Menu" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Number" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Radio" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Text input" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Text area" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required URL" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Multimenu" "table_row"
+ And ".alert.alert-error" "css_element" should exist in the "Required Two-Option Multimenu" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required Checkbox" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required Latlong" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required Menu" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required Number" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required Radio" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required Text input" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required Text area" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required URL" "table_row"
+ And ".alert.alert-error" "css_element" should not exist in the "Not required Multimenu" "table_row"
+ And I follow "View list"
+ And I should see "No entries in database"
+
+ Scenario: Students recieve no error for filled in required fields
+ When I log in as "student1"
+ And I follow "Course 1"
+ And I add an entry to "Test database name" database with:
+ | Base Text input | Some input to allow us to submit the otherwise empty form |
+ | Required Checkbox Option 1 | 1 |
+ | RTOC Option 1 | 1 |
+ | Latitude | 0 |
+ | Longitude | 0 |
+ | Required Menu | 1 |
+ | Required Number | 1 |
+ | Required Radio Option 1 | 1 |
+ | Required Text input | New entry text |
+ | Required Text area | More text |
+ | Required URL | http://example.com/ |
+ | Required Multimenu | 1 |
+ | Required Two-Option Multimenu | 1 |
+ And I press "Save and view"
+ And I follow "View list"
+ Then I should not see "No entries in database"
+ And I should see "New entry text"
+
+ Scenario: Fields refill with data after having an error
+ When I log in as "student1"
+ And I follow "Course 1"
+ And I add an entry to "Test database name" database with:
+ | RTOC Option 1 | 1 |
+ | Latitude | 0 |
+ | Longitude | 0 |
+ | Required Menu | 1 |
+ | Required Number | 1 |
+ | Required Radio Option 1 | 1 |
+ | Required Text input | New entry text |
+ | Required Text area | More text |
+ | Required URL | http://example.com/ |
+ | Required Multimenu | 1 |
+ | Required Two-Option Multimenu | 1 |
+ And I press "Save and view"
+ Then the following fields match these values:
+ | Base Text input | |
+ | Latitude | 0 |
+ | Longitude | 0 |
+ | Required Menu | Option 1 |
+ | Required Number | 1 |
+ | Required Radio Option 1 | 1 |
+ | Required Text input | New entry text |
+ | Required Text area | More text |
+ | Required URL | http://example.com/ |
+ | Required Multimenu | Option 1 |
+ | Required Two-Option Multimenu | Option 1 |
# To generate the default templates.
And I follow "Templates"
And I add an entry to "Test database name" database with:
- | Test field description | Teacher entry 1 |
+ | Test field name | Teacher entry 1 |
And I press "Save and add another"
And I add an entry to "Test database name" database with:
- | Test field description | Teacher entry 2 |
+ | Test field name | Teacher entry 2 |
And I press "Save and add another"
And I add an entry to "Test database name" database with:
- | Test field description | Teacher entry 3 |
+ | Test field name | Teacher entry 3 |
And I press "Save and view"
And I log out
When I log in as "student1"
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2014111000; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2015030900; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2014110400; // Requires this Moodle version
$plugin->component = 'mod_data'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
}
}
+ // Disable shortforms.
+ $mform->setDisableShortforms();
+
$mform->addElement('header', 'pageheader');
$mform->addElement('html', $OUTPUT->container($contents, 'contents'));
$lessonid = $this->_customdata['lessonid'];
$contents = $this->_customdata['contents'];
+ // Disable shortforms.
+ $mform->setDisableShortforms();
+
$mform->addElement('header', 'pageheader');
$mform->addElement('html', $OUTPUT->container($contents, 'contents'));
$attempt->answerid = null;
}
+ // Disable shortforms.
+ $mform->setDisableShortforms();
+
$mform->addElement('header', 'pageheader');
$mform->addElement('html', $OUTPUT->container($contents, 'contents'));
$lessonid = $this->_customdata['lessonid'];
$contents = $this->_customdata['contents'];
+ // Disable shortforms.
+ $mform->setDisableShortforms();
+
$mform->addElement('header', 'pageheader');
$mform->addElement('html', $OUTPUT->container($contents, 'contents'));
$mform = $this->_form;
$contents = $this->_customdata['contents'];
+ // Disable shortforms.
+ $mform->setDisableShortforms();
+
$mform->addElement('header', 'pageheader');
$mform->addElement('html', $OUTPUT->container($contents, 'contents'));
$attrs['size'] = round(strlen($placeholder) * 1.1);
}
+ // Disable shortforms.
+ $mform->setDisableShortforms();
+
$mform->addElement('header', 'pageheader');
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
$attempt->answerid = null;
}
+ // Disable shortforms.
+ $mform->setDisableShortforms();
+
$mform->addElement('header', 'pageheader');
$mform->addElement('html', $OUTPUT->container($contents, 'contents'));
$contextid = required_param('contextid', PARAM_INT);
$component = required_param('component', PARAM_COMPONENT);
-$ratingarea = optional_param('ratingarea', null, PARAM_AREA);
+$ratingarea = required_param('ratingarea', PARAM_AREA);
$itemid = required_param('itemid', PARAM_INT);
$scaleid = required_param('scaleid', PARAM_INT);
$sort = optional_param('sort', '', PARAM_ALPHA);
$url = new moodle_url('/rating/index.php', array('contextid' => $contextid,
'component' => $component,
+ 'ratingarea' => $ratingarea,
'itemid' => $itemid,
'scaleid' => $scaleid));
-if (!empty($ratingarea)) {
- $url->param('ratingarea', $ratingarea);
-}
if (!empty($sort)) {
$url->param('sort', $sort);
}
if (!has_capability('moodle/rating:view', $context)) {
print_error('noviewrate', 'rating');
}
-if (!has_capability('moodle/rating:viewall', $context) and $USER->id != $item->userid) {
- print_error('noviewanyrate', 'rating');
-}
+
+$canviewallratings = has_capability('moodle/rating:viewall', $context);
switch ($sort) {
case 'firstname':
$maxrating = max(array_keys($scalemenu));
foreach ($ratings as $rating) {
+ if (!$canviewallratings and $USER->id != $rating->userid) {
+ continue;
+ }
+
// Undo the aliasing of the user id column from user_picture::fields().
// We could clone the rating object or preserve the rating id if we needed it again
// but we don't.
defined('MOODLE_INTERNAL') || die();
-$version = 2015030500.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2015030900.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '2.9dev (Build: 20150305)'; // Human-friendly version name
+$release = '2.9dev (Build: 20150309)'; // Human-friendly version name
$branch = '29'; // This version's branch.
$maturity = MATURITY_ALPHA; // This version's maturity level.
// Mobile CSS theme and alternative login url.
$siteinfo['mobilecssurl'] = $CFG->mobilecssurl;
+ // Retrieve some advanced features. Only enable/disable ones (bool).
+ $advancedfeatures = array("usecomments", "usetags", "enablenotes", "messaging", "enableblogs",
+ "enablecompletion", "enablebadges");
+ foreach ($advancedfeatures as $feature) {
+ if (isset($CFG->{$feature})) {
+ $siteinfo['advancedfeatures'][] = array(
+ 'name' => $feature,
+ 'value' => (int) $CFG->{$feature}
+ );
+ }
+ }
+ // Special case mnet_dispatcher_mode.
+ $siteinfo['advancedfeatures'][] = array(
+ 'name' => 'mnet_dispatcher_mode',
+ 'value' => ($CFG->mnet_dispatcher_mode == 'strict') ? 1 : 0
+ );
+
return $siteinfo;
}
VALUE_OPTIONAL),
'release' => new external_value(PARAM_TEXT, 'Moodle release number', VALUE_OPTIONAL),
'version' => new external_value(PARAM_TEXT, 'Moodle version number', VALUE_OPTIONAL),
- 'mobilecssurl' => new external_value(PARAM_URL, 'Mobile custom CSS theme', VALUE_OPTIONAL)
+ 'mobilecssurl' => new external_value(PARAM_URL, 'Mobile custom CSS theme', VALUE_OPTIONAL),
+ 'advancedfeatures' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'name' => new external_value(PARAM_ALPHANUMEXT, 'feature name'),
+ 'value' => new external_value(PARAM_INT, 'feature value. Usually 1 means enabled.')
+ ),
+ 'Advanced features availability'
+ ),
+ 'Advanced features availability',
+ VALUE_OPTIONAL
+ )
)
);
}
$this->assertEquals($function['version'], $siteinfo['version']);
$this->assertEquals(1, $siteinfo['downloadfiles']);
$this->assertEquals(1, $siteinfo['uploadfiles']);
+
+ foreach ($siteinfo['advancedfeatures'] as $feature) {
+ if ($feature['name'] == 'mnet_dispatcher_mode') {
+ if ($CFG->mnet_dispatcher_mode == 'off') {
+ $this->assertEquals(0, $feature['value']);
+ } else {
+ $this->assertEquals(1, $feature['value']);
+ }
+ } else {
+ $this->assertEquals($CFG->{$feature['name']}, $feature['value']);
+ }
+ }
+
}
}