Merge branch 'MDL-48682_master' of git://github.com/dmonllao/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 10 Mar 2015 08:25:30 +0000 (08:25 +0000)
committerDan Poltawski <dan@moodle.com>
Tue, 10 Mar 2015 08:25:30 +0000 (08:25 +0000)
79 files changed:
admin/environment.xml
admin/tool/behat/cli/init.php
admin/tool/behat/cli/run.php [new file with mode: 0644]
admin/tool/behat/cli/util.php
admin/tool/behat/cli/util_single_run.php [new file with mode: 0644]
composer.json
config-dist.php
grade/report/grader/lib.php
lang/en/admin.php
lang/en/group.php
lib/behat/classes/behat_command.php
lib/behat/classes/behat_config_manager.php
lib/behat/lib.php
lib/clilib.php
lib/db/services.php
lib/dml/mariadb_native_moodle_database.php
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js
lib/editor/atto/plugins/link/yui/src/button/js/button.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/selection.js
lib/gradelib.php
lib/grouplib.php
lib/moodlelib.php
lib/questionlib.php
lib/setup.php
lib/setuplib.php
lib/testing/classes/test_lock.php
lib/testing/lib.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_hooks.php
lib/tests/grouplib_test.php
lib/upgradelib.php
mod/data/backup/moodle2/backup_data_stepslib.php
mod/data/db/install.xml
mod/data/db/upgrade.php
mod/data/edit.php
mod/data/field.php
mod/data/field/checkbox/field.class.php
mod/data/field/checkbox/mod.html
mod/data/field/date/field.class.php
mod/data/field/file/field.class.php
mod/data/field/file/mod.html
mod/data/field/latlong/field.class.php
mod/data/field/latlong/mod.html
mod/data/field/menu/field.class.php
mod/data/field/menu/mod.html
mod/data/field/multimenu/field.class.php
mod/data/field/multimenu/mod.html
mod/data/field/number/mod.html
mod/data/field/picture/field.class.php
mod/data/field/picture/mod.html
mod/data/field/radiobutton/field.class.php
mod/data/field/radiobutton/mod.html
mod/data/field/text/mod.html
mod/data/field/textarea/field.class.php
mod/data/field/textarea/mod.html
mod/data/field/url/field.class.php
mod/data/field/url/mod.html
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/styles.css
mod/data/tests/behat/add_entries.feature
mod/data/tests/behat/required_entries.feature [new file with mode: 0644]
mod/data/tests/behat/view_entries.feature
mod/data/version.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/multichoice.php
mod/lesson/pagetypes/numerical.php
mod/lesson/pagetypes/shortanswer.php
mod/lesson/pagetypes/truefalse.php
rating/index.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php

index 38ec017..27a0b5a 100644 (file)
       </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>
index 5547b56..52f783f 100644 (file)
@@ -40,61 +40,126 @@ define('CACHE_DISABLE_ALL', true);
 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);
 }
 
diff --git a/admin/tool/behat/cli/run.php b/admin/tool/behat/cli/run.php
new file mode 100644 (file)
index 0000000..c83259f
--- /dev/null
@@ -0,0 +1,361 @@
+<?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;
+    }
+}
index d843bf2..1bdb72d 100644 (file)
@@ -15,7 +15,7 @@
 // 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
@@ -30,45 +30,58 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
     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
 ";
@@ -78,66 +91,324 @@ if (!empty($options['help'])) {
     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;
+}
diff --git a/admin/tool/behat/cli/util_single_run.php b/admin/tool/behat/cli/util_single_run.php
new file mode 100644 (file)
index 0000000..fa90359
--- /dev/null
@@ -0,0 +1,273 @@
+<?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;
+}
index af77f59..60864a8 100644 (file)
@@ -8,6 +8,6 @@
     "require-dev": {
         "phpunit/phpunit": "3.7.*",
         "phpunit/dbUnit": "1.2.*",
-        "moodlehq/behat-extension": "1.29.2"
+        "moodlehq/behat-extension": "1.29.3"
     }
 }
index fc59406..4973d52 100644 (file)
@@ -724,6 +724,50 @@ $CFG->admin = 'admin';
 // 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
 //=========================================================================
index daaeddc..f6f1821 100644 (file)
@@ -1345,7 +1345,7 @@ class grade_report_grader extends grade_report {
      * @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
@@ -1377,7 +1377,11 @@ class grade_report_grader extends grade_report {
             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');
index 69dbb49..35c0ef1 100644 (file)
@@ -1067,6 +1067,7 @@ $string['uninstallplugin'] = 'Uninstall';
 $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';
index 14060bb..5536a7a 100644 (file)
@@ -159,6 +159,8 @@ $string['nousersinrole'] = 'There are no suitable users in the selected role';
 $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';
index a7060bd..ce6c744 100644 (file)
@@ -44,12 +44,19 @@ class behat_command {
 
     /**
      * 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)) {
@@ -73,23 +80,29 @@ class behat_command {
      * 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;
     }
 
     /**
index d6c8520..f3b8680 100644 (file)
@@ -53,9 +53,10 @@ class behat_config_manager {
      *
      * @param  string $component Restricts the obtained steps definitions to the specified component
      * @param  string $testsrunner If the config file will be used to run tests
+     * @param  string $tags features files including tags.
      * @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.
@@ -79,7 +80,10 @@ class behat_config_manager {
                     $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.
@@ -105,7 +109,7 @@ class behat_config_manager {
 
         // 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)) {
@@ -114,6 +118,40 @@ class behat_config_manager {
 
     }
 
+    /**
+     * 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
      *
@@ -169,12 +207,22 @@ class behat_config_manager {
     /**
      * 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()) {
@@ -184,6 +232,99 @@ class behat_config_manager {
         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.
@@ -198,12 +339,37 @@ class behat_config_manager {
         // 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(
@@ -217,12 +383,13 @@ class behat_config_manager {
                     '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
@@ -242,6 +409,79 @@ class behat_config_manager {
         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
      *
index 86718a9..7a422ed 100644 (file)
@@ -41,6 +41,11 @@ define('BEHAT_EXITCODE_INSTALL', 254);
 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
  *
@@ -224,6 +229,7 @@ function behat_check_config_vars() {
         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);
@@ -272,6 +278,57 @@ function behat_is_test_site() {
     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
  *
@@ -306,3 +363,99 @@ function behat_is_requested_url($url) {
 
     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
index 32272de..be869e3 100644 (file)
@@ -175,3 +175,4 @@ function cli_error($text, $errorcode=1) {
     fwrite(STDERR, "\n");
     die($errorcode);
 }
+
index 1b60c7a..0631dd8 100644 (file)
@@ -1006,6 +1006,7 @@ $services = array(
             'gradereport_user_get_grades_table',
             'core_group_get_course_user_groups',
             'core_user_remove_user_device',
+            'core_course_get_courses'
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index 12dc9eb..ffc7796 100644 (file)
@@ -101,24 +101,4 @@ class mariadb_native_moodle_database extends mysqli_native_moodle_database {
         }
         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;
-    }
 }
index 838fe81..eeadd60 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js differ
index ba3cf0c..8d1e8f1 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js differ
index 838fe81..eeadd60 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js differ
index 0215e86..50e91fe 100644 (file)
@@ -109,7 +109,7 @@ Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.Edit
     _displayDialogue: function() {
         // Store the current selection.
         this._currentSelection = this.get('host').getSelection();
-        if (this._currentSelection === false || this._currentSelection.collapsed) {
+        if (this._currentSelection === false) {
             return;
         }
 
@@ -165,11 +165,11 @@ Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.Edit
     },
 
     /**
-     * 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) {
@@ -178,9 +178,9 @@ Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.Edit
                 .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();
         }
@@ -200,8 +200,6 @@ Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.Edit
             anchornodes,
             value;
 
-        var host = this.get('host');
-
         e.preventDefault();
         this.getDialogue({
             focusAfterHide: null
@@ -211,8 +209,6 @@ Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.Edit
 
         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();
@@ -221,11 +217,8 @@ Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.Edit
                 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) {
@@ -246,6 +239,41 @@ Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.Edit
         }
     },
 
+    /**
+     * 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.
      *
index fbd5c05..829d055 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 17f3959..aa206ae 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 2a54f7b..28a4cfb 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 788692f..12086a3 100644 (file)
@@ -378,6 +378,7 @@ EditorSelection.prototype = {
      *
      * @method insertContentAtFocusPoint
      * @param {String} html
+     * @return {Node} The YUI Node object added to the DOM.
      */
     insertContentAtFocusPoint: function(html) {
         var selection = rangy.getSelection(),
@@ -390,6 +391,7 @@ EditorSelection.prototype = {
             range.deleteContents();
             range.insertNode(node.getDOMNode());
         }
+        return node;
     }
 
 };
index cb9170a..6dfa227 100644 (file)
@@ -1014,6 +1014,8 @@ function grade_recover_history_grades($userid, $courseid) {
  * @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);
 
index 98ac4be..3f3a233 100644 (file)
@@ -513,8 +513,11 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
     $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);
     }
@@ -526,11 +529,7 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
         $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');
@@ -562,6 +561,55 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
     }
 }
 
+/**
+ * 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.
@@ -587,9 +635,7 @@ function groups_allgroups_course_menu($course, $urlroot, $update = false, $activ
         $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.
@@ -665,8 +711,11 @@ function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallpartic
     $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
     }
@@ -678,11 +727,7 @@ function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallpartic
         $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');
index 2747982..7d8ea2f 100644 (file)
@@ -3143,6 +3143,15 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
         }
     }
 
+    // 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) {
@@ -3156,14 +3165,6 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
         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);
 }
index eb7c577..912557c 100644 (file)
@@ -1833,6 +1833,9 @@ function question_pluginfile($course, $context, $component, $filearea, $args, $f
 
     $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');
index d0a7760..6b093d3 100644 (file)
@@ -80,7 +80,13 @@ if (defined('BEHAT_SITE_RUNNING')) {
     // 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();
@@ -89,10 +95,11 @@ if (defined('BEHAT_SITE_RUNNING')) {
         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);
index 399308b..848df4a 100644 (file)
@@ -1365,7 +1365,7 @@ function make_writable_directory($dir, $exceptiononerror = true) {
     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)) {
index 710cdea..169241e 100644 (file)
@@ -52,7 +52,6 @@ class test_lock {
      */
     public static function acquire($framework) {
         global $CFG;
-
         $datarootpath = $CFG->{$framework . '_dataroot'} . '/' . $framework;
         $lockfile = $datarootpath . '/lock';
         if (!file_exists($datarootpath)) {
index c089c6a..bbb93d3 100644 (file)
@@ -156,6 +156,9 @@ function testing_error($errorcode, $text = '') {
 
     // 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);
 }
 
index f7a5a52..fcde598 100644 (file)
@@ -32,6 +32,7 @@ use Behat\Behat\Context\Step\Given as Given,
     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;
@@ -159,6 +160,19 @@ class behat_forms extends behat_base {
         $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.
      *
index 6e2653f..5df77bb 100644 (file)
@@ -31,6 +31,8 @@ require_once(__DIR__ . '/../../behat/behat_base.php');
 
 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,
@@ -84,17 +86,25 @@ class behat_hooks extends behat_base {
      */
     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
@@ -153,9 +163,65 @@ class behat_hooks extends behat_base {
         }
     }
 
+    /**
+     * 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
      */
@@ -244,9 +310,10 @@ class behat_hooks extends behat_base {
      * 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();
@@ -268,9 +335,10 @@ class behat_hooks extends behat_base {
      * 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.
@@ -303,9 +371,10 @@ class behat_hooks extends behat_base {
      *
      * 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.
index 5c0d0f3..b200fb9 100644 (file)
@@ -838,4 +838,73 @@ class core_grouplib_testcase extends advanced_testcase {
         $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)));
+    }
 }
index 492a628..237b063 100644 (file)
@@ -2203,3 +2203,27 @@ function upgrade_fix_missing_root_folders_draft() {
     $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;
+}
index 5fe9ee6..385e66b 100644 (file)
@@ -50,7 +50,7 @@ class backup_data_activity_structure_step extends backup_activity_structure_step
         $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'));
 
index 37b46f1..c4a5c2c 100644 (file)
@@ -1,5 +1,5 @@
 <?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"
 >
@@ -54,6 +54,7 @@
         <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>
index 61c57d5..c5251ea 100644 (file)
@@ -137,7 +137,18 @@ function xmldb_data_upgrade($oldversion) {
     // 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;
+}
index 6dd59f7..7350cca 100644 (file)
@@ -34,8 +34,14 @@ $rid   = optional_param('rid', 0, PARAM_INT);    //record id
 $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 !== '') {
@@ -152,105 +158,121 @@ if ($rid) {
 $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
@@ -300,9 +322,18 @@ if ($data->addtemplate){
 
         // 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;
     }
@@ -313,6 +344,9 @@ if ($data->addtemplate){
     $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').'" />';
index e567fff..77e3a29 100644 (file)
@@ -146,6 +146,7 @@ switch ($mode) {
 
                 $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})) {
@@ -264,7 +265,13 @@ if (($mode == 'new') && (!empty($newtype)) && confirm_sesskey()) {          ///
     } 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);
 
@@ -273,21 +280,28 @@ if (($mode == 'new') && (!empty($newtype)) && confirm_sesskey()) {          ///
 
                 $field = data_get_field($ff, $data);
 
-                $table->data[] = array(
-
-                '<a href="field.php?mode=display&amp;d='.$data->id.
-                '&amp;fid='.$field->field->id.'&amp;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().'&nbsp;'.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.'&amp;mode=display&amp;fid='.$field->field->id.'&amp;sesskey='.sesskey().'">'.
-                '<img src="'.$OUTPUT->pix_url('t/edit') . '" class="iconsmall" alt="'.get_string('edit').'" title="'.get_string('edit').'" /></a>'.
-                '&nbsp;'.
-                '<a href="field.php?d='.$data->id.'&amp;mode=delete&amp;fid='.$field->field->id.'&amp;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() . '&nbsp;' . 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'))) .
+                        '&nbsp;' .
+                        html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))),
                 );
             }
         }
index 7571d07..8b7c0a6 100644 (file)
@@ -26,20 +26,33 @@ class data_field_checkbox extends data_field_base {
 
     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) {
@@ -211,5 +224,22 @@ class data_field_checkbox extends data_field_base {
         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;
+    }
 
+}
index a881292..c6dfcaa 100644 (file)
@@ -7,6 +7,10 @@
         <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>
index 2fa245d..fc5f4a9 100644 (file)
@@ -34,10 +34,18 @@ class data_field_date extends data_field_base {
     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();
@@ -128,5 +136,3 @@ class data_field_date extends data_field_base {
 
 
 }
-
-
index 5fd2d21..b1cee24 100644 (file)
@@ -25,7 +25,7 @@
 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;
@@ -36,7 +36,10 @@ class data_field_file extends data_field_base {
         $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);
@@ -62,10 +65,18 @@ class data_field_file extends data_field_base {
             $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 .= '&nbsp;' . 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.'" />';
@@ -83,7 +94,6 @@ class data_field_file extends data_field_base {
 
         $output = $PAGE->get_renderer('core', 'files');
         $html .= $output->render($fm);
-
         $html .= '</fieldset>';
         $html .= '</div>';
 
@@ -204,6 +214,24 @@ class data_field_file extends data_field_base {
         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;
+    }
 
+}
index 8f8945c..2f67d9a 100644 (file)
@@ -7,6 +7,10 @@
         <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>
index e6108ee..d89df71 100644 (file)
@@ -43,12 +43,17 @@ class data_field_latlong extends data_field_base {
     );
     // 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;
@@ -57,8 +62,21 @@ class data_field_latlong extends data_field_base {
         $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>';
@@ -223,6 +241,15 @@ class data_field_latlong extends data_field_base {
         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 == '');
+    }
 
+}
index fb34fb4..1b0466d 100644 (file)
@@ -7,6 +7,10 @@
         <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">
index 4fb611b..c53e3cb 100644 (file)
@@ -26,17 +26,19 @@ class data_field_menu extends data_field_base {
 
     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);
@@ -47,7 +49,14 @@ class data_field_menu extends data_field_base {
             }
         }
 
-        $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>';
@@ -108,5 +117,3 @@ class data_field_menu extends data_field_base {
     }
 
 }
-
-
index f92fb13..e7c84e3 100644 (file)
@@ -7,6 +7,10 @@
         <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>
index 1f3de9f..75b6bc0 100644 (file)
@@ -26,10 +26,17 @@ class data_field_multimenu extends data_field_base {
 
     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 {
@@ -38,10 +45,19 @@ class data_field_multimenu extends data_field_base {
 
         $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) . '"';
 
@@ -238,5 +254,16 @@ class data_field_multimenu extends data_field_base {
         }
         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);
+    }
+}
index a881292..58db398 100644 (file)
@@ -7,6 +7,10 @@
         <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>
index 831560f..7b1791b 100644 (file)
@@ -7,4 +7,8 @@
         <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>
index a459791..d4ade19 100644 (file)
@@ -27,7 +27,7 @@ class data_field_picture extends data_field_base {
     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;
@@ -37,7 +37,14 @@ class data_field_picture extends data_field_base {
         $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)) {
@@ -64,9 +71,17 @@ class data_field_picture extends data_field_base {
         } 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 .= '&nbsp;' . 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());
@@ -290,6 +305,24 @@ class data_field_picture extends data_field_base {
     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;
+    }
+}
index 99fd424..c008976 100644 (file)
             <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>
index f3115c1..3008fb0 100644 (file)
@@ -26,20 +26,38 @@ class data_field_radiobutton extends data_field_base {
 
     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 .= '&nbsp;' . 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
index a881292..58db398 100644 (file)
@@ -7,6 +7,10 @@
         <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>
index 940ff1c..597e763 100644 (file)
@@ -7,6 +7,10 @@
         <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>
index 51ad6fc..dda933e 100644 (file)
@@ -49,13 +49,19 @@ class data_field_textarea extends data_field_base {
         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();
@@ -63,7 +69,25 @@ class data_field_textarea extends data_field_base {
         $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);
@@ -129,8 +153,8 @@ class data_field_textarea extends data_field_base {
             $str .= '<option value="'.s($key).'" '.$selected.'>'.$desc.'</option>';
         }
         $str .= '</select>';
-        $str .= '</div>';
 
+        $str .= '</div>';
         $str .= '</div>';
         return $str;
     }
@@ -230,5 +254,20 @@ class data_field_textarea extends data_field_base {
     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;
+    }
+}
index e75365a..e3dbc78 100644 (file)
             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>
index eb0c4c5..b89ecce 100644 (file)
@@ -25,7 +25,7 @@
 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
@@ -43,24 +43,39 @@ class data_field_url extends data_field_base {
         $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>';
@@ -72,7 +87,6 @@ class data_field_url extends data_field_base {
 
         $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;
     }
@@ -178,5 +192,3 @@ class data_field_url extends data_field_base {
     }
 
 }
-
-
index e33b467..afb5dc3 100644 (file)
         <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>
index 5b6cc5a..f9ec9dc 100644 (file)
@@ -135,6 +135,7 @@ $string['entry'] = 'Entry';
 $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';
@@ -302,12 +303,14 @@ $string['recordsnotsaved'] = 'No entry was saved. Please check the format of the
 $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...';
index 7adbedd..5c3cc5e 100644 (file)
@@ -137,6 +137,7 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
         $this->field->param3 = '';
         $this->field->name = '';
         $this->field->description = '';
+        $this->field->required = false;
 
         return true;
     }
@@ -152,6 +153,7 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
 
         $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);
@@ -268,10 +270,13 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
      * @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 = '';
@@ -282,9 +287,14 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
             $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;
@@ -548,7 +558,7 @@ function data_generate_default_template(&$data, $template, $recordid=0, $form=fa
         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.']]';
             }
@@ -3751,3 +3761,95 @@ function data_delete_record($recordid, $data, $courseid, $cmid) {
     }
     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;
+}
index 955b3cd..242de98 100644 (file)
@@ -68,3 +68,7 @@
 .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;
+}
index d8182f3..3a2e3a3 100644 (file)
@@ -31,19 +31,19 @@ Feature: Users can add entries to database activities
     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"
diff --git a/mod/data/tests/behat/required_entries.feature b/mod/data/tests/behat/required_entries.feature
new file mode 100644 (file)
index 0000000..40d19c1
--- /dev/null
@@ -0,0 +1,206 @@
+@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            |
index 833ce42..29d7a70 100644 (file)
@@ -27,13 +27,13 @@ Feature: Users can view and search database entries
     # 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"
index 369c277..bb79562 100644 (file)
@@ -24,7 +24,7 @@
 
 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;
index 546a7db..9592180 100644 (file)
@@ -318,6 +318,9 @@ class lesson_display_answer_form_essay extends moodleform {
             }
         }
 
+        // Disable shortforms.
+        $mform->setDisableShortforms();
+
         $mform->addElement('header', 'pageheader');
 
         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
index 29775df..59bfd1e 100644 (file)
@@ -533,6 +533,9 @@ class lesson_display_answer_form_matching extends moodleform {
         $lessonid = $this->_customdata['lessonid'];
         $contents = $this->_customdata['contents'];
 
+        // Disable shortforms.
+        $mform->setDisableShortforms();
+
         $mform->addElement('header', 'pageheader');
 
         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
index 41ca410..501b17b 100644 (file)
@@ -488,6 +488,9 @@ class lesson_display_answer_form_multichoice_singleanswer extends moodleform {
             $attempt->answerid = null;
         }
 
+        // Disable shortforms.
+        $mform->setDisableShortforms();
+
         $mform->addElement('header', 'pageheader');
 
         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
@@ -541,6 +544,9 @@ class lesson_display_answer_form_multichoice_multianswer extends moodleform {
         $lessonid = $this->_customdata['lessonid'];
         $contents = $this->_customdata['contents'];
 
+        // Disable shortforms.
+        $mform->setDisableShortforms();
+
         $mform->addElement('header', 'pageheader');
 
         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
index 9e5414a..f8f98e0 100644 (file)
@@ -276,6 +276,9 @@ class lesson_display_answer_form_numerical extends moodleform {
         $mform = $this->_form;
         $contents = $this->_customdata['contents'];
 
+        // Disable shortforms.
+        $mform->setDisableShortforms();
+
         $mform->addElement('header', 'pageheader');
 
         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
index 798b180..1bcc96f 100644 (file)
@@ -369,6 +369,9 @@ class lesson_display_answer_form_shortanswer extends moodleform {
             $attrs['size'] = round(strlen($placeholder) * 1.1);
         }
 
+        // Disable shortforms.
+        $mform->setDisableShortforms();
+
         $mform->addElement('header', 'pageheader');
         $mform->addElement('hidden', 'id');
         $mform->setType('id', PARAM_INT);
index 8c8056a..ae8fbaf 100644 (file)
@@ -382,6 +382,9 @@ class lesson_display_answer_form_truefalse extends moodleform {
             $attempt->answerid = null;
         }
 
+        // Disable shortforms.
+        $mform->setDisableShortforms();
+
         $mform->addElement('header', 'pageheader');
 
         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
index 42039d7..ae51610 100644 (file)
@@ -28,7 +28,7 @@ require_once("lib.php");
 
 $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);
@@ -39,11 +39,9 @@ require_login($course, false, $cm);
 
 $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);
 }
@@ -60,9 +58,8 @@ if ($popup) {
 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':
@@ -119,6 +116,10 @@ if (!$ratings) {
     $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.
index fee9e93..6af1e52 100644 (file)
 
 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.
index 26e81a5..da4ce3e 100644 (file)
@@ -160,6 +160,23 @@ class core_webservice_external extends external_api {
         // 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;
     }
 
@@ -200,7 +217,18 @@ class core_webservice_external extends external_api {
                                                        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
+                )
             )
         );
     }
index 92399af..801679d 100644 (file)
@@ -100,6 +100,19 @@ class core_webservice_externallib_testcase extends externallib_advanced_testcase
         $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']);
+            }
+        }
+
     }
 
 }