Merge branch 'MDL-49242-master' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 10 Mar 2015 23:11:56 +0000 (00:11 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 10 Mar 2015 23:11:56 +0000 (00:11 +0100)
118 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]
admin/tool/log/store/database/classes/log/store.php
admin/tool/log/store/legacy/classes/log/store.php
admin/tool/log/store/legacy/tests/store_test.php
admin/tool/log/store/standard/classes/log/store.php
admin/tool/log/store/standard/tests/store_test.php
admin/tool/log/tests/manager_test.php
backup/util/helper/backup_cron_helper.class.php
composer.json
config-dist.php
course/lib.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/classes/dml/recordset_walk.php [new file with mode: 0644]
lib/classes/log/sql_internal_reader.php
lib/classes/log/sql_internal_table_reader.php [new file with mode: 0644]
lib/classes/log/sql_reader.php [new file with mode: 0644]
lib/classes/log/sql_select_reader.php
lib/classes/task/send_failed_login_notifications_task.php
lib/clilib.php
lib/db/services.php
lib/dml/mariadb_native_moodle_database.php
lib/dml/tests/recordset_walk_test.php [new file with mode: 0644]
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/autosave.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/statslib.php
lib/tablelib.php
lib/testing/classes/test_lock.php
lib/testing/lib.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/grouplib_test.php
lib/upgrade.txt
lib/upgradelib.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/tests/behat/group_annotations.feature
mod/choice/tests/behat/block_editing.feature
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
plagiarism/lib.php
rating/index.php
report/log/classes/renderable.php
report/log/classes/table_log.php
report/log/lib.php
report/log/locallib.php
report/loglive/classes/renderable.php
report/loglive/classes/table_log.php
report/loglive/lib.php
report/outline/index.php
report/outline/lib.php
report/outline/locallib.php
report/participation/index.php
report/participation/lib.php
report/participation/locallib.php
report/stats/lib.php
theme/switchdevice.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 c394d0b..0bbffb2 100644 (file)
@@ -25,7 +25,7 @@
 namespace logstore_database\log;
 defined('MOODLE_INTERNAL') || die();
 
-class store implements \tool_log\log\writer, \core\log\sql_select_reader {
+class store implements \tool_log\log\writer, \core\log\sql_reader {
     use \tool_log\helper\store,
         \tool_log\helper\reader,
         \tool_log\helper\buffered_writer {
@@ -173,28 +173,71 @@ class store implements \tool_log\log\writer, \core\log\sql_select_reader {
         $records = $this->extdb->get_records_select($dbtable, $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
 
         foreach ($records as $data) {
-            $extra = array('origin' => $data->origin, 'realuserid' => $data->realuserid, 'ip' => $data->ip);
-            $data = (array)$data;
-            $id = $data['id'];
-            $data['other'] = unserialize($data['other']);
-            if ($data['other'] === false) {
-                $data['other'] = array();
-            }
-            unset($data['origin']);
-            unset($data['ip']);
-            unset($data['realuserid']);
-            unset($data['id']);
-
-            $event = \core\event\base::restore($data, $extra);
-            // Add event to list if it's valid.
-            if ($event) {
-                $events[$id] = $event;
+            if ($event = $this->get_log_event($data)) {
+                $events[$data->id] = $event;
             }
         }
 
         return $events;
     }
 
+    /**
+     * Fetch records using given criteria returning a Traversable object.
+     *
+     * Note that the traversable object contains a moodle_recordset, so
+     * remember that is important that you call close() once you finish
+     * using it.
+     *
+     * @param string $selectwhere
+     * @param array $params
+     * @param string $sort
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return \core\dml\recordset_walk|\core\event\base[]
+     */
+    public function get_events_select_iterator($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
+        if (!$this->init()) {
+            return array();
+        }
+
+        if (!$dbtable = $this->get_config('dbtable')) {
+            return array();
+        }
+
+        $sort = self::tweak_sort_by_id($sort);
+
+        $recordset = $this->extdb->get_recordset_select($dbtable, $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
+
+        return new \core\dml\recordset_walk($recordset, array($this, 'get_log_event'));
+    }
+
+    /**
+     * Returns an event from the log data.
+     *
+     * @param stdClass $data Log data
+     * @return \core\event\base
+     */
+    public function get_log_event($data) {
+
+        $extra = array('origin' => $data->origin, 'ip' => $data->ip, 'realuserid' => $data->realuserid);
+        $data = (array)$data;
+        $id = $data['id'];
+        $data['other'] = unserialize($data['other']);
+        if ($data['other'] === false) {
+            $data['other'] = array();
+        }
+        unset($data['origin']);
+        unset($data['ip']);
+        unset($data['realuserid']);
+        unset($data['id']);
+
+        if (!$event = \core\event\base::restore($data, $extra)) {
+            return null;
+        }
+
+        return $event;
+    }
+
     /**
      * Get number of events present for the given select clause.
      *
index 846e4ab..7aa2056 100644 (file)
@@ -26,7 +26,7 @@ namespace logstore_legacy\log;
 
 defined('MOODLE_INTERNAL') || die();
 
-class store implements \tool_log\log\store, \core\log\sql_select_reader {
+class store implements \tool_log\log\store, \core\log\sql_reader {
     use \tool_log\helper\store,
         \tool_log\helper\reader;
 
@@ -47,7 +47,7 @@ class store implements \tool_log\log\store, \core\log\sql_select_reader {
     const CRUD_REGEX = "/(crud).*?(<>|=|!=).*?'(.*?)'/s";
 
     /**
-     * This method contains mapping required for Moodle core to make legacy store compatible with other sql_select_reader based
+     * This method contains mapping required for Moodle core to make legacy store compatible with other sql_reader based
      * queries.
      *
      * @param string $selectwhere Select statment
@@ -104,7 +104,7 @@ class store implements \tool_log\log\store, \core\log\sql_select_reader {
         $events = array();
 
         foreach ($records as $data) {
-            $events[$data->id] = \logstore_legacy\event\legacy_logged::restore_legacy($data);
+            $events[$data->id] = $this->get_log_event($data);
         }
 
         $records->close();
@@ -112,6 +112,48 @@ class store implements \tool_log\log\store, \core\log\sql_select_reader {
         return $events;
     }
 
+    /**
+     * Fetch records using given criteria returning a Traversable object.
+     *
+     * Note that the traversable object contains a moodle_recordset, so
+     * remember that is important that you call close() once you finish
+     * using it.
+     *
+     * @param string $selectwhere
+     * @param array $params
+     * @param string $sort
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return \Traversable|\core\event\base[]
+     */
+    public function get_events_select_iterator($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
+        global $DB;
+
+        $sort = self::tweak_sort_by_id($sort);
+
+        // Replace the query with hardcoded mappings required for core.
+        list($selectwhere, $params, $sort) = self::replace_sql_legacy($selectwhere, $params, $sort);
+
+        try {
+            $recordset = $DB->get_recordset_select('log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
+        } catch (\moodle_exception $ex) {
+            debugging("error converting legacy event data " . $ex->getMessage() . $ex->debuginfo, DEBUG_DEVELOPER);
+            return new \EmptyIterator;
+        }
+
+        return new \core\dml\recordset_walk($recordset, array($this, 'get_log_event'));
+    }
+
+    /**
+     * Returns an event from the log data.
+     *
+     * @param stdClass $data Log data
+     * @return \core\event\base
+     */
+    public function get_log_event($data) {
+        return \logstore_legacy\event\legacy_logged::restore_legacy($data);
+    }
+
     public function get_events_select_count($selectwhere, array $params) {
         global $DB;
 
index e571a23..7fb6353 100644 (file)
@@ -50,7 +50,7 @@ class logstore_legacy_store_testcase extends advanced_testcase {
         $this->assertEquals(array('logstore_legacy'), array_keys($stores));
         $store = $stores['logstore_legacy'];
         $this->assertInstanceOf('logstore_legacy\log\store', $store);
-        $this->assertInstanceOf('core\log\sql_select_reader', $store);
+        $this->assertInstanceOf('core\log\sql_reader', $store);
         $this->assertTrue($store->is_logging());
 
         $logs = $DB->get_records('log', array(), 'id ASC');
index 44c66de..fe93ff5 100644 (file)
@@ -26,7 +26,7 @@ namespace logstore_standard\log;
 
 defined('MOODLE_INTERNAL') || die();
 
-class store implements \tool_log\log\writer, \core\log\sql_internal_reader {
+class store implements \tool_log\log\writer, \core\log\sql_internal_table_reader {
     use \tool_log\helper\store,
         \tool_log\helper\buffered_writer,
         \tool_log\helper\reader;
@@ -75,22 +75,8 @@ class store implements \tool_log\log\writer, \core\log\sql_internal_reader {
         $records = $DB->get_recordset_select('logstore_standard_log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
 
         foreach ($records as $data) {
-            $extra = array('origin' => $data->origin, 'ip' => $data->ip, 'realuserid' => $data->realuserid);
-            $data = (array)$data;
-            $id = $data['id'];
-            $data['other'] = unserialize($data['other']);
-            if ($data['other'] === false) {
-                $data['other'] = array();
-            }
-            unset($data['origin']);
-            unset($data['ip']);
-            unset($data['realuserid']);
-            unset($data['id']);
-
-            $event = \core\event\base::restore($data, $extra);
-            // Add event to list if it's valid.
-            if ($event) {
-                $events[$id] = $event;
+            if ($event = $this->get_log_event($data)) {
+                $events[$data->id] = $event;
             }
         }
 
@@ -99,6 +85,57 @@ class store implements \tool_log\log\writer, \core\log\sql_internal_reader {
         return $events;
     }
 
+    /**
+     * Fetch records using given criteria returning a Traversable object.
+     *
+     * Note that the traversable object contains a moodle_recordset, so
+     * remember that is important that you call close() once you finish
+     * using it.
+     *
+     * @param string $selectwhere
+     * @param array $params
+     * @param string $sort
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return \core\dml\recordset_walk|\core\event\base[]
+     */
+    public function get_events_select_iterator($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
+        global $DB;
+
+        $sort = self::tweak_sort_by_id($sort);
+
+        $recordset = $DB->get_recordset_select('logstore_standard_log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
+
+        return new \core\dml\recordset_walk($recordset, array($this, 'get_log_event'));
+    }
+
+    /**
+     * Returns an event from the log data.
+     *
+     * @param stdClass $data Log data
+     * @return \core\event\base
+     */
+    public function get_log_event($data) {
+
+        $extra = array('origin' => $data->origin, 'ip' => $data->ip, 'realuserid' => $data->realuserid);
+        $data = (array)$data;
+        $id = $data['id'];
+        $data['other'] = unserialize($data['other']);
+        if ($data['other'] === false) {
+            $data['other'] = array();
+        }
+        unset($data['origin']);
+        unset($data['ip']);
+        unset($data['realuserid']);
+        unset($data['id']);
+
+        if (!$event = \core\event\base::restore($data, $extra)) {
+            return null;
+        }
+
+        return $event;
+    }
+
     public function get_events_select_count($selectwhere, array $params) {
         global $DB;
         return $DB->count_records_select('logstore_standard_log', $selectwhere, $params);
index 08d70d4..0ff4f6a 100644 (file)
@@ -218,4 +218,59 @@ class logstore_standard_store_testcase extends advanced_testcase {
             $this->assertContains($expectedreport, $reports);
         }
     }
+
+    /**
+     * Test sql_reader::get_events_select_iterator.
+     * @return void
+     */
+    public function test_events_traversable() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->preventResetByRollback();
+        $this->setAdminUser();
+
+        set_config('enabled_stores', 'logstore_standard', 'tool_log');
+
+        $manager = get_log_manager(true);
+        $stores = $manager->get_readers();
+        $store = $stores['logstore_standard'];
+
+        $events = $store->get_events_select_iterator('', array(), '', 0, 0);
+        $this->assertFalse($events->valid());
+
+        // Here it should be already closed, but we should be allowed to
+        // over-close it without exception.
+        $events->close();
+
+        $user = $this->getDataGenerator()->create_user();
+        for ($i = 0; $i < 1000; $i++) {
+            \core\event\user_created::create_from_userid($user->id)->trigger();
+        }
+        $store->flush();
+
+        // Check some various sizes get the right number of elements.
+        $this->assertEquals(1, iterator_count($store->get_events_select_iterator('', array(), '', 0, 1)));
+        $this->assertEquals(2, iterator_count($store->get_events_select_iterator('', array(), '', 0, 2)));
+
+        $iterator = $store->get_events_select_iterator('', array(), '', 0, 500);
+        $this->assertInstanceOf('\core\event\base', $iterator->current());
+        $this->assertEquals(500, iterator_count($iterator));
+        $iterator->close();
+
+        // Look for non-linear memory usage for the iterator version.
+        $mem = memory_get_usage();
+        $events = $store->get_events_select('', array(), '', 0, 0);
+        $delta1 = memory_get_usage() - $mem;
+        $events = $store->get_events_select_iterator('', array(), '', 0, 0);
+        $delta2 = memory_get_usage() - $mem;
+        $this->assertInstanceOf('\Traversable', $events);
+        $events->close();
+
+        $this->assertLessThan($delta1 / 10, $delta2);
+
+        set_config('enabled_stores', '', 'tool_log');
+        get_log_manager(true);
+    }
+
 }
index 1e2e726..e9e3164 100644 (file)
@@ -48,24 +48,24 @@ class tool_log_manager_testcase extends advanced_testcase {
         $this->assertCount(2, $stores);
         foreach ($stores as $key => $store) {
             $this->assertInternalType('string', $key);
-            $this->assertInstanceOf('core\log\sql_select_reader', $store);
+            $this->assertInstanceOf('core\log\sql_reader', $store);
         }
 
-        $stores = $manager->get_readers('core\log\sql_internal_reader');
+        $stores = $manager->get_readers('core\log\sql_internal_table_reader');
         $this->assertInternalType('array', $stores);
         $this->assertCount(1, $stores);
         foreach ($stores as $key => $store) {
             $this->assertInternalType('string', $key);
             $this->assertSame('logstore_standard', $key);
-            $this->assertInstanceOf('core\log\sql_internal_reader', $store);
+            $this->assertInstanceOf('core\log\sql_internal_table_reader', $store);
         }
 
-        $stores = $manager->get_readers('core\log\sql_select_reader');
+        $stores = $manager->get_readers('core\log\sql_reader');
         $this->assertInternalType('array', $stores);
         $this->assertCount(2, $stores);
         foreach ($stores as $key => $store) {
             $this->assertInternalType('string', $key);
-            $this->assertInstanceOf('core\log\sql_select_reader', $store);
+            $this->assertInstanceOf('core\log\sql_reader', $store);
         }
     }
 }
index 21bebe8..2834f91 100644 (file)
@@ -656,7 +656,7 @@ abstract class backup_cron_automated_helper {
      */
     protected static function is_course_modified($courseid, $since) {
         $logmang = get_log_manager();
-        $readers = $logmang->get_readers('core\log\sql_select_reader');
+        $readers = $logmang->get_readers('core\log\sql_reader');
         $where = "courseid = :courseid and timecreated > :since and crud <> 'r'";
         $params = array('courseid' => $courseid, 'since' => $since);
         foreach ($readers as $reader) {
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 0dabb51..8a043f1 100644 (file)
@@ -2437,7 +2437,7 @@ function can_delete_course($courseid) {
     }
 
     $logmanger = get_log_manager();
-    $readers = $logmanger->get_readers('\core\log\sql_select_reader');
+    $readers = $logmanger->get_readers('\core\log\sql_reader');
     $reader = reset($readers);
 
     if (empty($reader)) {
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..d82f2db 100644 (file)
@@ -44,12 +44,24 @@ 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 not set then return empty string.
+        if (!isset($CFG->behat_dataroot)) {
+            return "";
+        }
+
+        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 +85,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
diff --git a/lib/classes/dml/recordset_walk.php b/lib/classes/dml/recordset_walk.php
new file mode 100644 (file)
index 0000000..7e9ea69
--- /dev/null
@@ -0,0 +1,161 @@
+<?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/>.
+
+/**
+ * Applies the same callback to all recorset records.
+ *
+ * @since      Moodle 2.9
+ * @package    core
+ * @category   dml
+ * @copyright  2015 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\dml;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Iterator that walks through a moodle_recordset applying the provided function.
+ *
+ * The internal recordset can be closed using the close() function.
+ *
+ * Note that consumers of this class are responsible of closing the recordset,
+ * although there are some implicit closes under some ciscumstances:
+ * - Once all recordset records have been iterated
+ * - The object is destroyed
+ *
+ * @since      Moodle 2.9
+ * @package    core
+ * @category   dml
+ * @copyright  2015 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class recordset_walk implements \Iterator {
+
+    /**
+     * @var \moodle_recordset The recordset.
+     */
+    protected $recordset;
+
+    /**
+     * @var callable The callback.
+     */
+    protected $callback;
+
+    /**
+     * @var array|false Extra params for the callback.
+     */
+    protected $callbackextra;
+
+    /**
+     * Create a new iterator applying the callback to each record.
+     *
+     * @param \moodle_recordset $recordset Recordset to iterate.
+     * @param callable $callback Apply this function to each record. If using a method, it should be public.
+     * @param array $callbackextra Array of arguments to pass to the callback.
+     */
+    public function __construct(\moodle_recordset $recordset, callable $callback, $callbackextra = false) {
+        $this->recordset = $recordset;
+        $this->callback = $callback;
+        $this->callbackextra = $callbackextra;
+    }
+
+    /**
+     * Closes the recordset.
+     *
+     * @return void
+     */
+    public function __destruct() {
+        $this->close();
+    }
+
+    /**
+     * Returns the current element after applying the callback.
+     *
+     * @return mixed|bool The returned value type will depend on the callback.
+     */
+    public function current() {
+
+        if (!$this->recordset->valid()) {
+            return false;
+        }
+
+        if (!$record = $this->recordset->current()) {
+            return false;
+        }
+
+        // Apply callback and return.
+        if ($this->callbackextra) {
+            return call_user_func($this->callback, $record);
+        } else {
+            return call_user_func($this->callback, $record, $this->callbackextra);
+        }
+    }
+
+    /**
+     * Moves the internal pointer to the next record.
+     *
+     * @return void
+     */
+    public function next() {
+        return $this->recordset->next();
+    }
+
+    /**
+     * Returns current record key.
+     *
+     * @return int
+     */
+    public function key() {
+        return $this->recordset->key();
+    }
+
+    /**
+     * Returns whether the current position is valid or not.
+     *
+     * If we reached the end of the recordset we close as we
+     * don't allow rewinds. Doing do so we reduce the chance
+     * of unclosed recordsets.
+     *
+     * @return bool
+     */
+    public function valid() {
+        if (!$valid = $this->recordset->valid()) {
+            $this->close();
+        }
+        return $valid;
+    }
+
+    /**
+     * Rewind is not supported.
+     *
+     * @return void
+     */
+    public function rewind() {
+        // No rewind as it is not implemented in moodle_recordset.
+        return;
+    }
+
+    /**
+     * Closes the recordset.
+     *
+     * @return void
+     */
+    public function close() {
+        $this->recordset->close();
+    }
+}
index dbd1b82..d5fcc3d 100644 (file)
@@ -26,11 +26,20 @@ namespace core\log;
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * Sql internal reader.
+ *
+ * @deprecated since Moodle 2.9 MDL-48595 - please do not use this interface any more.
+ * @see        sql_reader
+ * @todo       MDL-49291 This will be deleted in Moodle 3.1.
+ * @package    core
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 interface sql_internal_reader extends sql_select_reader {
 
     /**
-     * Returns name of the table or database view that
-     * holds the log data in standardised format.
+     * Returns name of the table or database view that holds the log data in standardised format.
      *
      * Note: this table must be used for reading only,
      * it is strongly recommended to use this in complex reports only.
diff --git a/lib/classes/log/sql_internal_table_reader.php b/lib/classes/log/sql_internal_table_reader.php
new file mode 100644 (file)
index 0000000..fd94eb7
--- /dev/null
@@ -0,0 +1,51 @@
+<?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/>.
+
+/**
+ * Log storage sql internal table reader interface.
+ *
+ * @package    core
+ * @copyright  2015 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\log;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Sql internal table reader.
+ *
+ * Replaces sql_internal_reader and extends sql_reader.
+ *
+ * @since      Moodle 2.9
+ * @package    core
+ * @copyright  2015 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface sql_internal_table_reader extends sql_reader {
+
+    /**
+     * Returns name of the table or database view that
+     * holds the log data in standardised format.
+     *
+     * Note: this table must be used for reading only,
+     * it is strongly recommended to use this in complex reports only.
+     *
+     * @return string
+     */
+    public function get_internal_log_table_name();
+}
diff --git a/lib/classes/log/sql_reader.php b/lib/classes/log/sql_reader.php
new file mode 100644 (file)
index 0000000..92aa452
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Log iterator reader interface.
+ *
+ * @package    core
+ * @copyright  2015 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\log;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Log iterator reader interface.
+ *
+ * Replaces sql_select_reader adding functions
+ * to return iterators.
+ *
+ * @since      Moodle 2.9
+ * @package    core
+ * @copyright  2015 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface sql_reader extends reader {
+
+    /**
+     * Fetch records using given criteria.
+     *
+     * @param string $selectwhere
+     * @param array $params
+     * @param string $sort
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return \core\event\base[]
+     */
+    public function get_events_select($selectwhere, array $params, $sort, $limitfrom, $limitnum);
+
+    /**
+     * Return number of events matching given criteria.
+     *
+     * @param string $selectwhere
+     * @param array $params
+     * @return int
+     */
+    public function get_events_select_count($selectwhere, array $params);
+
+    /**
+     * Fetch records using the given criteria returning an traversable list of events.
+     *
+     * Note that the returned object is Traversable, not Iterator, as we are returning
+     * EmptyIterator if we know there are no events, and EmptyIterator does not implement
+     * Countable {@link https://bugs.php.net/bug.php?id=60577} so valid() should be checked
+     * in any case instead of a count().
+     *
+     * Also note that the traversable object contains a recordset and it is very important
+     * that you close it after using it.
+     *
+     * @param string $selectwhere
+     * @param array $params
+     * @param string $sort
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return \Traversable|\core\event\base[] Returns an iterator containing \core\event\base objects.
+     */
+    public function get_events_select_iterator($selectwhere, array $params, $sort, $limitfrom, $limitnum);
+
+    /**
+     * Returns an event from the log data.
+     *
+     * @param stdClass $data Log data
+     * @return \core\event\base
+     */
+    public function get_log_event($data);
+}
index 216399e..65b94e1 100644 (file)
@@ -26,6 +26,16 @@ namespace core\log;
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * Sql select reader.
+ *
+ * @deprecated since Moodle 2.9 MDL-48595 - please do not use this interface any more.
+ * @see        sql_reader
+ * @todo       MDL-49291 This will be deleted in Moodle 3.1.
+ * @package    core
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 interface sql_select_reader extends reader {
     /**
      * Fetch records using given criteria.
index 3fbe3b9..2f18c90 100644 (file)
@@ -72,7 +72,7 @@ class send_failed_login_notifications_task extends scheduled_task {
         // Get all the IPs with more than notifyloginthreshold failures since lastnotifyfailure
         // and insert them into the cache_flags temp table.
         $logmang = get_log_manager();
-        $readers = $logmang->get_readers('\core\log\sql_internal_reader');
+        $readers = $logmang->get_readers('\core\log\sql_internal_table_reader');
         $reader = reset($readers);
         $readername = key($readers);
         if (empty($reader) || empty($readername)) {
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;
-    }
 }
diff --git a/lib/dml/tests/recordset_walk_test.php b/lib/dml/tests/recordset_walk_test.php
new file mode 100644 (file)
index 0000000..bd9c804
--- /dev/null
@@ -0,0 +1,124 @@
+<?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/>.
+
+/**
+ * Test \core\dml\recordset_walk.
+ *
+ * @package    core
+ * @category   dml
+ * @copyright  2015 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test case for recordset_walk.
+ *
+ * @package    core
+ * @category   dml
+ * @copyright  2015 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_recordset_walk_testcase extends advanced_testcase {
+
+    public function setUp() {
+        parent::setUp();
+        $this->resetAfterTest();
+    }
+
+    public function test_no_data() {
+        global $DB;
+
+        $recordset = $DB->get_recordset('assign');
+        $walker = new \core\dml\recordset_walk($recordset, array($this, 'simple_callback'));
+        $this->assertEquals(0, iterator_count($walker));
+        $this->assertFalse($walker->valid());
+        foreach ($walker as $data) {
+            // No error here.
+        }
+        $walker->close();
+    }
+
+    public function test_simple_callback() {
+        global $DB;
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $courses = array();
+        for ($i = 0; $i < 10; $i++) {
+            $courses[$i] = $generator->create_instance(array('course' => SITEID));
+        }
+
+        // Simple iteration.
+        $recordset = $DB->get_recordset('assign');
+        $walker = new \core\dml\recordset_walk($recordset, array($this, 'simple_callback'));
+        $this->assertEquals(10, iterator_count($walker));
+        foreach ($walker as $data) {
+            // Checking that the callback is being executed on each iteration.
+            $this->assertEquals($data->id . ' potatoes', $data->newfield);
+        }
+        // No exception if we double-close.
+        $walker->close();
+    }
+
+    public function test_extra_params_callback() {
+        global $DB;
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $courses = array();
+        for ($i = 0; $i < 10; $i++) {
+            $courses[$i] = $generator->create_instance(array('course' => SITEID));
+        }
+
+        // Iteration with extra callback arguments.
+        $recordset = $DB->get_recordset('assign');
+        $walker = new \core\dml\recordset_walk(
+            $recordset,
+            array($this, 'extra_callback'),
+            array('brown' => 'onions')
+        );
+        $this->assertEquals(10, iterator_count($walker));
+        foreach ($walker as $data) {
+            // Checking that the callback is being executed on each
+            // iteration and the param is being passed.
+            $this->assertEquals('onions', $data->brown);
+        }
+        $walker->close();
+    }
+
+    /**
+     * Simple callback requiring 1 row fields.
+     *
+     * @param stdClass $data
+     * @return \Traversable
+     */
+    public function simple_callback($data) {
+        $data->newfield = $data->id . ' potatoes';
+        return $data;
+    }
+
+    /**
+     * Callback requiring 1 row fields + other params.
+     *
+     * @param stdClass $data
+     * @param mixed $extra
+     * @return \Traversable
+     */
+    public function extra_callback($data, $extra) {
+        $data->brown = $extra['brown'];
+        return $data;
+    }
+}
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..52d04bf 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..6a50ded 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..f01c730 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 600cf12..a549c80 100644 (file)
@@ -107,6 +107,7 @@ EditorAutosave.prototype = {
      */
     setupAutosave: function() {
         var draftid = -1,
+            form,
             optiontype = null,
             options = this.get('filepickeroptions'),
             params,
@@ -185,7 +186,10 @@ EditorAutosave.prototype = {
         Y.later(delay, this, this.saveDraft, false, true);
 
         // Now setup the listener for form submission.
-        this.textarea.ancestor('form').on('submit', this.resetAutosave, this);
+        form = this.textarea.ancestor('form');
+        if (form) {
+            form.on('submit', this.resetAutosave, this);
+        }
         return this;
     },
 
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 e77d3ec..65cb7c4 100644 (file)
@@ -949,7 +949,7 @@ function stats_get_start_from($str) {
             $stores = $manager->get_readers();
             $firstlog = false;
             foreach ($stores as $store) {
-                if ($store instanceof \core\log\sql_internal_reader) {
+                if ($store instanceof \core\log\sql_internal_table_reader) {
                     $logtable = $store->get_internal_log_table_name();
                     if (!$logtable) {
                         continue;
@@ -1767,7 +1767,7 @@ function stats_temp_table_fill($timestart, $timeend) {
     $manager = get_log_manager();
     $stores = $manager->get_readers();
     foreach ($stores as $store) {
-        if ($store instanceof \core\log\sql_internal_reader) {
+        if ($store instanceof \core\log\sql_internal_table_reader) {
             $logtable = $store->get_internal_log_table_name();
             if (!$logtable) {
                 continue;
index 2e6960f..134671a 100644 (file)
@@ -1345,7 +1345,7 @@ class table_sql extends flexible_table {
      */
     public $sql = NULL;
     /**
-     * @var array Data fetched from the db.
+     * @var array|\Traversable Data fetched from the db.
      */
     public $rawdata = NULL;
 
@@ -1374,14 +1374,27 @@ class table_sql extends flexible_table {
      * processing each col using either col_{columnname} method or other_cols
      * method or if other_cols returns NULL then put the data straight into the
      * table.
+     *
+     * @return void
      */
     function build_table() {
-        if ($this->rawdata) {
-            foreach ($this->rawdata as $row) {
-                $formattedrow = $this->format_row($row);
-                $this->add_data_keyed($formattedrow,
-                        $this->get_row_class($row));
-            }
+
+        if ($this->rawdata instanceof \Traversable && !$this->rawdata->valid()) {
+            return;
+        }
+        if (!$this->rawdata) {
+            return;
+        }
+
+        foreach ($this->rawdata as $row) {
+            $formattedrow = $this->format_row($row);
+            $this->add_data_keyed($formattedrow,
+                $this->get_row_class($row));
+        }
+
+        if ($this->rawdata instanceof \core\dml\recordset_walk ||
+                $this->rawdata instanceof moodle_recordset) {
+            $this->rawdata->close();
         }
     }
 
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 d8d5264..2cdd394 100644 (file)
@@ -982,7 +982,7 @@ class behat_general extends behat_base {
      * Example: I change window size to "small" or I change window size to "1024x768"
      *
      * @throws ExpectationException
-     * @Then /^I change window size to "([^"](small|medium|large|\d+x\d+))"$/
+     * @Then /^I change window size to "(small|medium|large|\d+x\d+)"$/
      * @param string $windowsize size of the window (small|medium|large|wxh).
      */
     public function i_change_window_size_to($windowsize) {
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 671df7d..47907a3 100644 (file)
@@ -20,6 +20,8 @@ information provided here is intended especially for developers.
 * html_table: new API for adding captions to tables (new field, $table->caption) and subsequently hiding said captions from sighted users using accesshide (enabled using $table->captionhide).
 * The authorization procedure in the mdeploy.php script has been improved. The script
   now relies on the main config.php when deploying an available update.
+* sql_internal_reader and sql_select_reader interfaces have been deprecated in favour of sql_internal_table_reader
+  and sql_reader which use iterators to be more memory efficient.
 
 === 2.8 ===
 
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 b49f680..d047b5c 100644 (file)
@@ -131,6 +131,7 @@ Feature: In an assignment, teacher can annotate PDF files during grading
     And I follow "View/grade all submissions"
     And I click on "Grade" "link" in the "Student 2" "table_row"
     And I follow "Launch PDF editor..."
+    And I change window size to "large"
     And I click on ".stampbutton" "css_element"
     And I click on ".drawingcanvas" "css_element"
     And I click on "Close" "button"
index 1bdb027..31fc64f 100644 (file)
@@ -53,6 +53,7 @@ Feature: In a group assignment, teacher can annotate PDF files for all users
     And I follow "View/grade all submissions"
     And I click on "Grade" "link" in the "Submitted for grading" "table_row"
     And I follow "Launch PDF editor..."
+    And I change window size to "large"
     And I click on ".navigate-next-button" "css_element"
     And I click on ".stampbutton" "css_element"
     And I click on ".drawingcanvas" "css_element"
index e4adf70..5ee5741 100644 (file)
@@ -4,13 +4,10 @@ Feature: Add choice activity
   As a teacher or admin
   I need to add remove block from the choice page
 
-# This tests that the hacky block editing is not borked by legacy forms in choice activity.
-
+  # This tests that the hacky block editing is not borked by legacy forms in choice activity.
   @javascript
-  Scenario: Add a choice activity and complete the activity as a student
-    Given the following "users" exist:
-      | username | firstname | lastname | email |
-    And the following "courses" exist:
+  Scenario: Add a choice activity as admin and check blog menu block should contain link.
+    Given the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And I log in as "admin"
@@ -22,22 +19,19 @@ Feature: Add choice activity
       | option[0] | Option 1 |
       | option[1] | Option 2 |
     And I follow "Choice name 1"
-
-    When I add the "Blog menu" block
-    Then I should see "View all entries about this Choice"
-
+    And I add the "Blog menu" block
+    And I should see "View all entries about this Choice"
     When I configure the "Blog menu" block
     And I press "Save changes"
     Then I should see "View all entries about this Choice"
-
-    When I open the "Blog menu" blocks action menu
+    And I open the "Blog menu" blocks action menu
     And I click on "Delete" "link" in the "Blog menu" "block"
     And I press "Yes"
-    Then I should not see "View all entries about this Choice"
+    And I should not see "View all entries about this Choice"
     And I should see "Choice Description 1"
 
   @javascript
-  Scenario: Add a choice activity and complete the activity as a student
+  Scenario: Add a choice activity as teacher and check blog menu block contain choice link.
     Given the following "users" exist:
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@asd.com |
@@ -58,22 +52,19 @@ Feature: Add choice activity
       | option[0] | Option 1 |
       | option[1] | Option 2 |
     And I follow "Choice name 1"
-
-    When I add the "Blog menu" block
-    Then I should see "View all entries about this Choice"
-
+    And I add the "Blog menu" block
+    And I should see "View all entries about this Choice"
     When I configure the "Blog menu" block
     And I press "Save changes"
     Then I should see "View all entries about this Choice"
-
-    When I open the "Blog menu" blocks action menu
+    And I open the "Blog menu" blocks action menu
     And I click on "Delete" "link" in the "Blog menu" "block"
     And I press "Yes"
-    Then I should not see "View all entries about this Choice"
+    And I should not see "View all entries about this Choice"
     And I should see "Choice Description 1"
 
   @javascript
-  Scenario: Add a choice activity and complete the activity as a student
+  Scenario: Add a choice activity as teacher (with dual role) and check blog menu block contain choice link.
     Given the following "users" exist:
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@asd.com |
@@ -93,16 +84,13 @@ Feature: Add choice activity
       | option[0] | Option 1 |
       | option[1] | Option 2 |
     And I follow "Choice name 1"
-
-    When I add the "Blog menu" block
-    Then I should see "View all entries about this Choice"
-
+    And I add the "Blog menu" block
+    And I should see "View all entries about this Choice"
     When I configure the "Blog menu" block
     And I press "Save changes"
     Then I should see "View all entries about this Choice"
-
-    When I open the "Blog menu" blocks action menu
+    And I open the "Blog menu" blocks action menu
     And I click on "Delete" "link" in the "Blog menu" "block"
     And I press "Yes"
-    Then I should not see "View all entries about this Choice"
+    And I should not see "View all entries about this Choice"
     And I should see "Choice Description 1"
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 8e6feca..003961a 100644 (file)
@@ -42,10 +42,9 @@ class plagiarism_plugin {
     }
 
     /**
-     * hook to allow plagiarism specific information to be displayed beside a submission 
+     * hook to allow plagiarism specific information to be displayed beside a submission
      * @param array  $linkarraycontains all relevant information for the plugin to generate a link
      * @return string
-     * 
      */
     public function get_links($linkarray) {
         return '';
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 380eb38..6fe3fea 100644 (file)
@@ -160,17 +160,17 @@ class report_log_renderable implements renderable {
     }
 
     /**
-     * Get a list of enabled sql_select_reader objects/name
+     * Get a list of enabled sql_reader objects/name
      *
      * @param bool $nameonly if true only reader names will be returned.
-     * @return array core\log\sql_select_reader object or name.
+     * @return array core\log\sql_reader object or name.
      */
     public function get_readers($nameonly = false) {
         if (!isset($this->logmanager)) {
             $this->logmanager = get_log_manager();
         }
 
-        $readers = $this->logmanager->get_readers('core\log\sql_select_reader');
+        $readers = $this->logmanager->get_readers('core\log\sql_reader');
         if ($nameonly) {
             foreach ($readers as $pluginname => $reader) {
                 $readers[$pluginname] = $reader->get_name();
index 8df6db9..25ac3df 100644 (file)
@@ -36,7 +36,11 @@ class report_log_table_log extends table_sql {
     /** @var array list of user fullnames shown in report */
     private $userfullnames = array();
 
-    /** @var array list of course short names shown in report */
+    /**
+     * @deprecated since Moodle 2.9 MDL-48595 - please do not use this argument any more.
+     * @todo MDL-49291 This will be deleted in 3.1
+     * @var array list of course short names shown in report.
+     */
     private $courseshortnames = array();
 
     /** @var array list of context name shown in report */
@@ -54,7 +58,7 @@ class report_log_table_log extends table_sql {
      *     - int userid: user id
      *     - int|string modid: Module id or "site_errors" to view site errors
      *     - int groupid: Group id
-     *     - \core\log\sql_select_reader logreader: reader from which data will be fetched.
+     *     - \core\log\sql_reader logreader: reader from which data will be fetched.
      *     - int edulevel: educational level.
      *     - string action: view action
      *     - int date: Date from which logs to be viewed.
@@ -94,10 +98,15 @@ class report_log_table_log extends table_sql {
     /**
      * Generate the course column.
      *
+     * @deprecated since Moodle 2.9 MDL-48595 - please do not use this function any more.
+     * @todo MDL-49291 This will be deleted in 3.1
      * @param stdClass $event event data.
      * @return string HTML for the course column.
      */
     public function col_course($event) {
+
+        debugging('col_course() is deprecated, there is no such column', DEBUG_DEVELOPER);
+
         if (empty($event->courseid) || empty($this->courseshortnames[$event->courseid])) {
             return '-';
         } else {
@@ -105,6 +114,39 @@ class report_log_table_log extends table_sql {
         }
     }
 
+    /**
+     * Gets the user full name.
+     *
+     * This function is useful because, in the unlikely case that the user is
+     * not already loaded in $this->userfullnames it will fetch it from db.
+     *
+     * @since Moodle 2.9
+     * @param int $userid
+     * @return string|false
+     */
+    protected function get_user_fullname($userid) {
+        global $DB;
+
+        if (!empty($this->userfullnames[$userid])) {
+            return $this->userfullnames[$userid];
+        }
+
+        // We already looked for the user and it does not exist.
+        if ($this->userfullnames[$userid] === false) {
+            return false;
+        }
+
+        // If we reach that point new users logs have been generated since the last users db query.
+        list($usql, $uparams) = $DB->get_in_or_equal($userid);
+        $sql = "SELECT id," . get_all_user_name_fields(true) . " FROM {user} WHERE id " . $usql;
+        if (!$user = $DB->get_records_sql($sql, $uparams)) {
+            return false;
+        }
+
+        $this->userfullnames[$userid] = fullname($user);
+        return $this->userfullnames[$userid];
+    }
+
     /**
      * Generate the time column.
      *
@@ -129,25 +171,29 @@ class report_log_table_log extends table_sql {
         // Add username who did the action.
         if (!empty($logextra['realuserid'])) {
             $a = new stdClass();
-            $params = array('id' => $logextra['realuserid']);
-            if ($event->courseid) {
-                $params['course'] = $event->courseid;
+            if (!$a->realusername = $this->get_user_fullname($logextra['realuserid'])) {
+                $a->realusername = '-';
+            }
+            if (!$a->asusername = $this->get_user_fullname($event->userid)) {
+                $a->asusername = '-';
             }
-            $a->realusername = $this->userfullnames[$logextra['realuserid']];
-            $a->asusername = $this->userfullnames[$event->userid];
             if (empty($this->download)) {
+                $params = array('id' => $logextra['realuserid']);
+                if ($event->courseid) {
+                    $params['course'] = $event->courseid;
+                }
                 $a->realusername = html_writer::link(new moodle_url('/user/view.php', $params), $a->realusername);
                 $params['id'] = $event->userid;
                 $a->asusername = html_writer::link(new moodle_url('/user/view.php', $params), $a->asusername);
             }
             $username = get_string('eventloggedas', 'report_log', $a);
-        } else if (!empty($event->userid) && !empty($this->userfullnames[$event->userid])) {
-            $params = array('id' => $event->userid);
-            if ($event->courseid) {
-                $params['course'] = $event->courseid;
-            }
-            $username = $this->userfullnames[$event->userid];
+
+        } else if (!empty($event->userid) && $username = $this->get_user_fullname($event->userid)) {
             if (empty($this->download)) {
+                $params = array('id' => $event->userid);
+                if ($event->courseid) {
+                    $params['course'] = $event->courseid;
+                }
                 $username = html_writer::link(new moodle_url('/user/view.php', $params), $username);
             }
         } else {
@@ -164,13 +210,12 @@ class report_log_table_log extends table_sql {
      */
     public function col_relatedfullnameuser($event) {
         // Add affected user.
-        if (!empty($event->relateduserid) && isset($this->userfullnames[$event->relateduserid])) {
-            $params = array('id' => $event->relateduserid);
-            if ($event->courseid) {
-                $params['course'] = $event->courseid;
-            }
-            $username = $this->userfullnames[$event->relateduserid];
+        if (!empty($event->relateduserid) && $username = $this->get_user_fullname($event->relateduserid)) {
             if (empty($this->download)) {
+                $params = array('id' => $event->relateduserid);
+                if ($event->courseid) {
+                    $params['course'] = $event->courseid;
+                }
                 $username = html_writer::link(new moodle_url('/user/view.php', $params), $username);
             }
         } else {
@@ -463,26 +508,43 @@ class report_log_table_log extends table_sql {
             $this->pageable(false);
         }
 
-        $this->rawdata = $this->filterparams->logreader->get_events_select($selector, $params, $this->filterparams->orderby,
-                $this->get_page_start(), $this->get_page_size());
+        // Get the users and course data.
+        $this->rawdata = $this->filterparams->logreader->get_events_select_iterator($selector, $params,
+            $this->filterparams->orderby, $this->get_page_start(), $this->get_page_size());
+
+        // Update list of users which will be displayed on log page.
+        $this->update_users_used();
+
+        // Get the events. Same query than before; even if it is not likely, logs from new users
+        // may be added since last query so we will need to work around later to prevent problems.
+        // In almost most of the cases this will be better than having two opened recordsets.
+        $this->rawdata = $this->filterparams->logreader->get_events_select_iterator($selector, $params,
+            $this->filterparams->orderby, $this->get_page_start(), $this->get_page_size());
 
         // Set initial bars.
         if ($useinitialsbar && !$this->is_downloading()) {
             $this->initialbars($total > $pagesize);
         }
 
-        // Update list of users and courses list which will be displayed on log page.
-        $this->update_users_and_courses_used();
     }
 
     /**
      * Helper function to create list of course shortname and user fullname shown in log report.
+     *
      * This will update $this->userfullnames and $this->courseshortnames array with userfullname and courseshortname (with link),
      * which will be used to render logs in table.
+     *
+     * @deprecated since Moodle 2.9 MDL-48595 - please do not use this function any more.
+     * @todo MDL-49291 This will be deleted in 3.1
+     * @see self::update_users_used()
      */
     public function update_users_and_courses_used() {
         global $SITE, $DB;
 
+        debugging('update_users_and_courses_used() is deprecated, please use update_users_used() instead.', DEBUG_DEVELOPER);
+
+        // We should not call self::update_users_used() as would have to iterate twice around the list of logs.
+
         $this->userfullnames = array();
         $this->courseshortnames = array($SITE->id => $SITE->shortname);
         $userids = array();
@@ -491,14 +553,14 @@ class report_log_table_log extends table_sql {
         // Get list of userids and courseids which will be shown in log report.
         foreach ($this->rawdata as $event) {
             $logextra = $event->get_logextra();
-            if (!empty($event->userid) && !in_array($event->userid, $userids)) {
-                $userids[] = $event->userid;
+            if (!empty($event->userid) && empty($userids[$event->userid])) {
+                $userids[$event->userid] = $event->userid;
             }
-            if (!empty($logextra['realuserid']) && !in_array($logextra['realuserid'], $userids)) {
-                $userids[] = $logextra['realuserid'];
+            if (!empty($logextra['realuserid']) && empty($userids[$logextra['realuserid']])) {
+                $userids[$logextra['realuserid']] = $logextra['realuserid'];
             }
-            if (!empty($event->relateduserid) && !in_array($event->relateduserid, $userids)) {
-                $userids[] = $event->relateduserid;
+            if (!empty($event->relateduserid) && empty($userids[$event->relateduserid])) {
+                $userids[$event->relateduserid] = $event->relateduserid;
             }
 
             if (!empty($event->courseid) && ($event->courseid != $SITE->id) && !in_array($event->courseid, $courseids)) {
@@ -506,6 +568,12 @@ class report_log_table_log extends table_sql {
             }
         }
 
+        // Closing it just in case, we can not rewind moodle recordsets anyway.
+        if ($this->rawdata instanceof \core\dml\recordset_walk ||
+                $this->rawdata instanceof moodle_recordset) {
+            $this->rawdata->close();
+        }
+
         // Get user fullname and put that in return list.
         if (!empty($userids)) {
             list($usql, $uparams) = $DB->get_in_or_equal($userids);
@@ -513,6 +581,13 @@ class report_log_table_log extends table_sql {
                     $uparams);
             foreach ($users as $userid => $user) {
                 $this->userfullnames[$userid] = fullname($user);
+                unset($userids[$userid]);
+            }
+
+            // We fill the array with false values for the users that don't exist anymore
+            // in the database so we don't need to query the db again later.
+            foreach ($userids as $userid) {
+                $this->userfullnames[$userid] = false;
             }
         }
 
@@ -537,4 +612,53 @@ class report_log_table_log extends table_sql {
             }
         }
     }
+
+    /**
+     * Helper function to create list of user fullnames shown in log report.
+     *
+     * This will update $this->userfullnames array with userfullname,
+     * which will be used to render logs in table.
+     *
+     * @since   Moodle 2.9
+     * @return  void
+     */
+    protected function update_users_used() {
+        global $DB;
+
+        $this->userfullnames = array();
+        $userids = array();
+
+        // For each event cache full username.
+        // Get list of userids which will be shown in log report.
+        foreach ($this->rawdata as $event) {
+            $logextra = $event->get_logextra();
+            if (!empty($event->userid) && empty($userids[$event->userid])) {
+                $userids[$event->userid] = $event->userid;
+            }
+            if (!empty($logextra['realuserid']) && empty($userids[$logextra['realuserid']])) {
+                $userids[$logextra['realuserid']] = $logextra['realuserid'];
+            }
+            if (!empty($event->relateduserid) && empty($userids[$event->relateduserid])) {
+                $userids[$event->relateduserid] = $event->relateduserid;
+            }
+        }
+        $this->rawdata->close();
+
+        // Get user fullname and put that in return list.
+        if (!empty($userids)) {
+            list($usql, $uparams) = $DB->get_in_or_equal($userids);
+            $users = $DB->get_records_sql("SELECT id," . get_all_user_name_fields(true) . " FROM {user} WHERE id " . $usql,
+                    $uparams);
+            foreach ($users as $userid => $user) {
+                $this->userfullnames[$userid] = fullname($user);
+                unset($userids[$userid]);
+            }
+
+            // We fill the array with false values for the users that don't exist anymore
+            // in the database so we don't need to query the db again later.
+            foreach ($userids as $userid) {
+                $this->userfullnames[$userid] = false;
+            }
+        }
+    }
 }
index 867da4d..a26a3b6 100644 (file)
@@ -48,7 +48,7 @@ function report_log_extend_navigation_course($navigation, $course, $context) {
  * @return bool returns true if the store is supported by the report, false otherwise.
  */
 function report_log_supports_logstore($instance) {
-    if ($instance instanceof \core\log\sql_select_reader) {
+    if ($instance instanceof \core\log\sql_reader) {
         return true;
     }
     return false;
index 8d2d37b..2300483 100644 (file)
@@ -52,8 +52,8 @@ function report_log_print_graph($course, $userid, $type, $date=0, $logreader='')
     } else {
         $reader = $readers[$logreader];
     }
-    // If reader is not a sql_internal_reader and not legacy store then don't show graph.
-    if (!($reader instanceof \core\log\sql_internal_reader) && !($reader instanceof logstore_legacy\log\store)) {
+    // If reader is not a sql_internal_table_reader and not legacy store then don't show graph.
+    if (!($reader instanceof \core\log\sql_internal_table_reader) && !($reader instanceof logstore_legacy\log\store)) {
         return array();
     }
 
@@ -82,8 +82,8 @@ function report_log_usercourse($userid, $courseid, $coursestart, $logreader = ''
         $reader = $readers[$logreader];
     }
 
-    // If reader is not a sql_internal_reader and not legacy store then return.
-    if (!($reader instanceof \core\log\sql_internal_reader) && !($reader instanceof logstore_legacy\log\store)) {
+    // If reader is not a sql_internal_table_reader and not legacy store then return.
+    if (!($reader instanceof \core\log\sql_internal_table_reader) && !($reader instanceof logstore_legacy\log\store)) {
         return array();
     }
 
@@ -134,8 +134,8 @@ function report_log_userday($userid, $courseid, $daystart, $logreader = '') {
         $reader = $readers[$logreader];
     }
 
-    // If reader is not a sql_internal_reader and not legacy store then return.
-    if (!($reader instanceof \core\log\sql_internal_reader) && !($reader instanceof logstore_legacy\log\store)) {
+    // If reader is not a sql_internal_table_reader and not legacy store then return.
+    if (!($reader instanceof \core\log\sql_internal_table_reader) && !($reader instanceof logstore_legacy\log\store)) {
         return array();
     }
 
index 06f2eaa..2c72b9b 100644 (file)
@@ -124,18 +124,18 @@ class report_loglive_renderable implements renderable {
     }
 
     /**
-     * Get a list of enabled sql_select_reader objects/name
+     * Get a list of enabled sql_reader objects/name
      *
      * @param bool $nameonly if true only reader names will be returned.
      *
-     * @return array core\log\sql_select_reader object or name.
+     * @return array core\log\sql_reader object or name.
      */
     public function get_readers($nameonly = false) {
         if (!isset($this->logmanager)) {
             $this->logmanager = get_log_manager();
         }
 
-        $readers = $this->logmanager->get_readers('core\log\sql_select_reader');
+        $readers = $this->logmanager->get_readers('core\log\sql_reader');
         if ($nameonly) {
             foreach ($readers as $pluginname => $reader) {
                 $readers[$pluginname] = $reader->get_name();
@@ -227,4 +227,4 @@ class report_loglive_renderable implements renderable {
         }
         return $this->tablelog;
     }
-}
\ No newline at end of file
+}