Merge branch 'MDL-57846-master' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 6 Feb 2017 23:12:30 +0000 (00:12 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 6 Feb 2017 23:12:30 +0000 (00:12 +0100)
66 files changed:
admin/tool/behat/cli/run.php
admin/tool/behat/cli/util.php
admin/tool/behat/cli/util_single_run.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/version.php
admin/tool/usertours/tests/manager_test.php
blocks/rss_client/block_rss_client.php
blocks/rss_client/classes/output/footer.php
blocks/rss_client/lang/en/block_rss_client.php
blocks/rss_client/managefeeds.php
blocks/rss_client/templates/footer.mustache
comment/comment.js
comment/lib.php
enrol/lti/classes/helper.php
enrol/lti/classes/manage_table.php
enrol/lti/index.php
enrol/lti/lang/en/deprecated.txt
enrol/lti/lang/en/enrol_lti.php
enrol/lti/templates/copy_grid.mustache
enrol/lti/tests/helper_test.php
enrol/self/lib.php
grade/report/grader/lib.php
grade/report/grader/tests/behat/switch_views.feature
lib/accesslib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/behat/classes/behat_command.php
lib/behat/classes/behat_config_manager.php
lib/behat/classes/behat_config_util.php
lib/behat/classes/util.php
lib/behat/lib.php
lib/classes/external/exporter.php
lib/db/services.php
lib/form/button.php
lib/phpunit/classes/util.php
lib/setup.php
lib/tests/accesslib_test.php
lib/tests/exporter_test.php
message/output/popup/tests/externallib_test.php
message/tests/externallib_test.php
mod/assign/classes/output/grading_app.php
mod/assign/lib.php
mod/assign/tests/lib_test.php
mod/forum/classes/output/big_search_form.php
mod/forum/search.php
mod/forum/templates/big_search_form.mustache
mod/scorm/datamodels/aicc.js
mod/scorm/datamodels/scorm_12.js
mod/scorm/datamodels/scorm_12.php
mod/scorm/datamodels/scorm_13.js
mod/workshop/locallib.php
question/engine/questionattempt.php
question/engine/tests/walkthrough_test.php [new file with mode: 0644]
question/type/gapselect/questionbase.php
question/type/match/question.php
question/type/multichoice/question.php
question/type/questionbase.php
question/type/upgrade.txt
rss/file.php
theme/boost/templates/core/tabtree.mustache
theme/boost/templates/mod_forum/big_search_form.mustache
user/tests/externallib_test.php
version.php

index a228caf..63e19d2 100644 (file)
@@ -54,6 +54,7 @@ list($options, $unrecognised) = cli_get_params(
         'fromrun'  => 1,
         'torun'    => 0,
         'single-run' => false,
+        'rerun' => 0,
     ),
     array(
         'h' => 'help',
@@ -77,6 +78,7 @@ Options:
 --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)
+--rerun            Re-run scenarios that failed during last execution.
 
 -h, --help         Print out this help
 
@@ -91,11 +93,31 @@ if (!empty($options['help'])) {
     exit(0);
 }
 
-$parallelrun = behat_config_manager::get_parallel_test_runs($options['fromrun']);
+$parallelrun = behat_config_manager::get_behat_run_config_value('parallel');
 
-// Default torun is maximum parallel runs.
-if (empty($options['torun'])) {
-    $options['torun'] = $parallelrun;
+// Check if the options provided are valid to run behat.
+if ($parallelrun === false) {
+    // Parallel run should not have fromrun or torun options greater than 1.
+    if (($options['fromrun'] > 1) || ($options['torun'] > 1)) {
+        echo "Test site is not initialized  for parallel run." . PHP_EOL;
+        exit(1);
+    }
+} else {
+    // Ensure fromrun is within limits of initialized test site.
+    if (!empty($options['fromrun']) && ($options['fromrun'] > $parallelrun)) {
+        echo "From run (" . $options['fromrun'] . ") is more than site with parallel runs (" . $parallelrun . ")" . PHP_EOL;
+        exit(1);
+    }
+
+    // Default torun is maximum parallel runs and should be less than equal to parallelruns.
+    if (empty($options['torun'])) {
+        $options['torun'] = $parallelrun;
+    } else {
+        if ($options['torun'] > $parallelrun) {
+            echo "To run (" . $options['torun'] . ") is more than site with parallel runs (" . $parallelrun . ")" . PHP_EOL;
+            exit(1);
+        }
+    }
 }
 
 // Capture signals and ensure we clean symlinks.
@@ -127,7 +149,7 @@ if ($options['profile']) {
         exit(1);
     }
 
-    $extraopts[] = '--profile="' . $profile . '"';
+    $extraopts['profile'] = '--profile="' . $profile . '"';
     // By default, profile tags will be used.
     if (!empty($CFG->behat_config[$profile]['filters']['tags'])) {
         $tags = $CFG->behat_config[$profile]['filters']['tags'];
@@ -137,117 +159,159 @@ if ($options['profile']) {
 // Command line tags have precedence (std behat behavior).
 if ($options['tags']) {
     $tags = $options['tags'];
-    $extraopts[] = '--tags="' . $tags . '"';
+    $extraopts['tags'] = '--tags="' . $tags . '"';
 }
 
 // Add suite option if specified.
 if ($options['suite']) {
-    $extraopts[] = '--suite="' . $options['suite'] . '"';
+    $extraopts['suite'] = '--suite="' . $options['suite'] . '"';
 }
 
 // Feature should be added to last, for behat command.
 if ($options['feature']) {
-    $extraopts[] = $options['feature'];
+    $extraopts['feature'] = $options['feature'];
     // Only run 1 process as process.
     // Feature file is picked from absolute path provided, so no need to check for behat.yml.
     $options['torun'] = $options['fromrun'];
 }
 
 // Set of options to pass to behat.
-$extraopts = implode(' ', $extraopts);
+$extraoptstr = implode(' ', $extraopts);
+
+// If rerun is passed then ensure we just run the failed processes.
+$lastfailedstatus = 0;
+$lasttorun = $options['torun'];
+$lastfromrun = $options['fromrun'];
+if ($options['rerun']) {
+    // Get last combined failed status.
+    $lastfailedstatus = behat_config_manager::get_behat_run_config_value('lastcombinedfailedstatus');
+    $lasttorun = behat_config_manager::get_behat_run_config_value('lasttorun');
+    $lastfromrun = behat_config_manager::get_behat_run_config_value('lastfromrun');
+
+    if ($lastfailedstatus !== false) {
+        $extraoptstr .= ' --rerun';
+    }
 
-// If empty parallelrun then just check with user if it's a run single behat test.
+    // If torun is less than last torun, then just set this to min last to run and similar for fromrun.
+    if ($options['torun'] < $lasttorun) {
+        $options['torun'];
+    }
+    if ($options['fromrun'] > $lastfromrun) {
+        $options['fromrun'];
+    }
+    unset($options['rerun']);
+}
+
+$cmds = array();
+$exitcodes = array();
+$status = 0;
+$verbose = empty($options['verbose']) ? false : true;
+
+// Execute behat run commands.
 if (empty($parallelrun)) {
     $cwd = getcwd();
     chdir(__DIR__);
     $runtestscommand = behat_command::get_behat_command(false, false, true);
     $runtestscommand .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
-    $runtestscommand .= ' ' . $extraopts;
+    $runtestscommand .= ' ' . $extraoptstr;
     echo "Running single behat site:" . PHP_EOL;
-    passthru("php $runtestscommand", $code);
+    passthru("php $runtestscommand", $status);
     chdir($cwd);
-    exit($code);
-}
-
-$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;
+} else {
 
-    // Options parameters to be added to each run.
-    $myopts = !empty($options['replace']) ? str_replace($options['replace'], $i, $extraopts) : $extraopts;
+    echo "Running " . ($options['torun'] - $options['fromrun'] + 1) . " parallel behat sites:" . PHP_EOL;
 
-    $behatcommand = behat_command::get_behat_command(false, false, true);
-    $behatconfigpath = behat_config_manager::get_behat_cli_config_filepath($i);
+    for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
+        $lastfailed = 1 & $lastfailedstatus >> ($i - 1);
 
-    // 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;
-}
+        // Bypass if not failed in last run.
+        if ($lastfailedstatus && !$lastfailed && ($i <= $lasttorun) && ($i >= $lastfromrun)) {
+            continue;
+        }
 
-if (empty($cmds)) {
-    echo "No commands to execute " . PHP_EOL;
-    exit(1);
-}
+        $CFG->behatrunprocess = $i;
 
-// 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);
-}
+        // Options parameters to be added to each run.
+        $myopts = !empty($options['replace']) ? str_replace($options['replace'], $i, $extraoptstr) : $extraoptstr;
 
-// Execute all commands, relative to moodle root directory.
-$processes = cli_execute_parallel($cmds, __DIR__ . "/../../../../");
-$stoponfail = empty($options['stop-on-failure']) ? false : true;
+        $behatcommand = behat_command::get_behat_command(false, false, true);
+        $behatconfigpath = behat_config_manager::get_behat_cli_config_filepath($i);
 
-// Print header.
-print_process_start_info($processes);
+        // 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;
+    }
 
-// 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;
+    if (empty($cmds)) {
+        echo "No commands to execute " . PHP_EOL;
+        exit(1);
+    }
 
-ksort($exitcodes);
+    // 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);
+    }
 
-// Print exit info from each run.
-// Status bits contains pass/fail status of parallel runs.
-$status = 0;
-$processcounter = 0;
-foreach ($exitcodes as $exitcode) {
-    if ($exitcode) {
-        $status |= (1 << $processcounter);
+    // Save torun and from run, so it can be used to detect if it was executed in last run.
+    behat_config_manager::set_behat_run_config_value('lasttorun', $options['torun']);
+    behat_config_manager::set_behat_run_config_value('lastfromrun', $options['fromrun']);
+
+    // Execute all commands, relative to moodle root directory.
+    $processes = cli_execute_parallel($cmds, __DIR__ . "/../../../../");
+    $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 to finish run.
+    $time = round(microtime(true) - $time, 1);
+    echo "Finished in " . gmdate("G\h i\m s\s", $time) . PHP_EOL . PHP_EOL;
+    ksort($exitcodes);
+
+    // Print exit info from each run.
+    // Status bits contains pass/fail status of parallel runs.
+    foreach ($exitcodes as $name => $exitcode) {
+        if ($exitcode) {
+            $runno = str_replace(BEHAT_PARALLEL_SITE_NAME, '', $name);
+            $status |= (1 << ($runno - 1));
+        }
     }
-    $processcounter++;
+
+    // Print each process information.
+    print_each_process_info($processes, $verbose, $status);
 }
 
-// Run finished. Show exit code and output from individual process.
-$verbose = empty($options['verbose']) ? false : true;
-$verbose = $verbose || !empty($status);
-
-// Show exit code from each process, if any process failed.
-if ($verbose) {
-    // Echo exit codes.
-    echo "Exit codes for each behat run: " . PHP_EOL;
-    foreach ($exitcodes as $run => $exitcode) {
-        echo $run . ": " . $exitcode . PHP_EOL;
-    }
+// Show exit code from each process, if any process failed and how to rerun failed process.
+if ($verbose || $status) {
+    // Save final exit code containing which run failed.
+    behat_config_manager::set_behat_run_config_value('lastcombinedfailedstatus', $status);
 
     // Show failed re-run commands.
     if ($status) {
-        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;
+        if (!empty($cmds)) {
+            // Echo exit codes.
+            echo "Exit codes for each behat run: " . PHP_EOL;
+            foreach ($exitcodes as $run => $exitcode) {
+                echo $run . ": " . $exitcode . PHP_EOL;
             }
         }
+
+        echo "To re-run failed processes, you can use following command:" . PHP_EOL;
+        unset($extraopts['fromrun']);
+        unset($extraopts['torun']);
+        $extraopts['rerun'] = '--rerun';
+        if (!empty($options['replace'])) {
+            $extraopts['replace'] =  '--replace="' . $options['replace'] . '"';
+        }
+        $extraoptstr = implode(' ', $extraopts);
+        echo behat_command::get_behat_command(true, true, true) . " " . $extraoptstr . PHP_EOL;
     }
     echo PHP_EOL;
 }
 
-print_each_process_info($processes, $verbose);
-
 // Remove site symlink if necessary.
 behat_config_manager::drop_parallel_site_links();
 
@@ -364,23 +428,47 @@ function print_combined_run_output($processes, $stoponfail = false) {
  * @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) {
+function print_each_process_info($processes, $verbose = false, $status = 0) {
     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;
-                }
+
+        } else if ($status) {
+            // Only show failed o/p.
+            $runno = str_replace(BEHAT_PARALLEL_SITE_NAME, '', $name);
+            if ((1 << ($runno - 1)) & $status) {
+                echo $process->getOutput();
+                echo $process->getErrorOutput();
+            } else {
+                echo get_status_lines_from_run_op($process);
             }
+
+        } else {
+            echo get_status_lines_from_run_op($process);
         }
         echo PHP_EOL;
     }
 }
+
+/**
+ * Extract status information from behat o/p and return.
+ * @param Symfony\Component\Process\Process $process
+ * @return string
+ */
+function get_status_lines_from_run_op(Symfony\Component\Process\Process $process) {
+    $statusstr = '';
+    $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)) {
+            $statusstr .= $line . PHP_EOL;
+        }
+    }
+
+    return $statusstr;
+}
+
index b157ca7..8efa4ed 100644 (file)
@@ -36,11 +36,7 @@ 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(
@@ -103,14 +99,26 @@ if (!empty($options['help'])) {
 
 $cwd = getcwd();
 
+// If Behat parallel site is being initiliased, then define a param to be used to ignore single run install.
+if (!empty($options['parallel'])) {
+    define('BEHAT_PARALLEL_UTIL', true);
+}
+
+require_once(__DIR__ . '/../../../../config.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');
+
 // For drop option check if parallel site.
 if ((empty($options['parallel'])) && ($options['drop']) || $options['updatesteps']) {
-    // Get parallel run info from first run.
-    $options['parallel'] = behat_config_manager::get_parallel_test_runs($options['fromrun']);
+    $options['parallel'] = behat_config_manager::get_behat_run_config_value('parallel');
 }
 
 // If not a parallel site then open single run.
 if (empty($options['parallel'])) {
+    // Set run config value for single run.
+    behat_config_manager::set_behat_run_config_value('singlerun', 1);
+
     chdir(__DIR__);
     // Check if behat is initialised, if not exit.
     passthru("php util_single_run.php --diag", $status);
@@ -147,6 +155,21 @@ if ($options['diag'] || $options['enable'] || $options['disable']) {
         $status = (bool)$status || (bool)$exitcode;
     }
 
+    // Remove run config file.
+    $behatrunconfigfile = behat_config_manager::get_behat_run_config_file_path();
+    if (file_exists($behatrunconfigfile)) {
+        if (!unlink($behatrunconfigfile)) {
+            behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete behat run config file');
+        }
+    }
+
+    // Remove test file path.
+    if (file_exists(behat_util::get_test_file_path())) {
+        if (!unlink(behat_util::get_test_file_path())) {
+            behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test file enable info');
+        }
+    }
+
 } else if ($options['install']) {
     // This is intensive compared to behat itself so run them in chunk if option maxruns not set.
     if ($options['maxruns']) {
@@ -233,6 +256,19 @@ if ($options['install']) {
 } else if ($options['enable']) {
     echo "Acceptance tests environment enabled on $CFG->behat_wwwroot, to run the tests use:" . PHP_EOL;
     echo behat_command::get_behat_command(true, true);
+
+    // Save fromrun and to run information.
+    if (isset($options['fromrun'])) {
+        behat_config_manager::set_behat_run_config_value('fromrun', $options['fromrun']);
+    }
+
+    if (isset($options['torun'])) {
+        behat_config_manager::set_behat_run_config_value('torun', $options['torun']);
+    }
+    if (isset($options['parallel'])) {
+        behat_config_manager::set_behat_run_config_value('parallel', $options['parallel']);
+    }
+
     echo PHP_EOL;
 
 } else if ($options['disable']) {
index 3d0e2db..2e98656 100644 (file)
@@ -143,7 +143,7 @@ if ($options['run']) {
     $run = $options['run'];
     // If parallel option is not passed, then try get it form config.
     if (!$options['parallel']) {
-        $parallel = behat_config_manager::get_parallel_test_runs();
+        $parallel = behat_config_manager::get_behat_run_config_value('parallel');
     } else {
         $parallel = $options['parallel'];
     }
@@ -176,10 +176,7 @@ if ($options['install']) {
 } else if ($options['enable']) {
     if (!empty($parallel)) {
         // Save parallel site info for enable and install options.
-        $filepath = behat_config_manager::get_parallel_test_file_path();
-        if (!file_put_contents($filepath, $parallel)) {
-            behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
-        }
+        behat_config_manager::set_behat_run_config_value('behatsiteenabled', 1);
     }
 
     // Enable test mode.
@@ -200,7 +197,7 @@ if ($options['install']) {
     }
 
 } else if ($options['disable']) {
-    behat_util::stop_test_mode();
+    behat_util::stop_test_mode($run);
     // This is only displayed once for parallel install.
     if (empty($run)) {
         mtrace("Acceptance tests environment disabled");
index dee797a..5e5836b 100644 (file)
@@ -210,6 +210,7 @@ class api {
             $settings->tool_mobile_forcelogout = get_config('tool_mobile', 'forcelogout');
             $settings->tool_mobile_customlangstrings = get_config('tool_mobile', 'customlangstrings');
             $settings->tool_mobile_disabledfeatures = get_config('tool_mobile', 'disabledfeatures');
+            $settings->tool_mobile_custommenuitems = get_config('tool_mobile', 'custommenuitems');
         }
 
         return $settings;
index fca608f..40b1985 100644 (file)
@@ -33,6 +33,15 @@ mm.user.student|Learner|en
 mm.user.student|Aprendiz|es
 </pre>
 For a complete list of string identifiers and more information, see the <a href="{$a}">documentation page</a>.';
+$string['custommenuitems'] = 'Custom menu items';
+$string['custommenuitems_desc'] = 'Additional items can be added to the app\'s main menu by specifying them here. Enter each custom menu item on a new line with format: item text, link URL, link-opening method (inappbrowser, browser or embedded) and language code (optional, for displaying the item to users of the specified language only), separated by pipe characters. For example:
+<pre>
+App\'s help | https://someurl.xyz/help | inappbrowser | en
+Visit our SIS | https://someurl.xyz | browser | en
+My grades | https://someurl.xyz/local/mygrades/index.php | embedded | en
+Mis calificaciones | https://someurl.xyz/local/mygrades/index.php | embedded | es
+</pre>
+Use inappbrowser when you want to open the link in a browser without leaving the app, use browser for opening the link in the device default browser and embedded if you want to display the link embedded in a new page in the app.';
 $string['disabledfeatures'] = 'Disabled features';
 $string['disabledfeatures_desc'] = 'Select here the features you want to disable in the Mobile app for your site. Please note that some features listed here could be already disabled via other site settings. You will have to log out and log in again in the app to see the changes.';
 $string['enablesmartappbanners'] = 'Enable Smart App Banners';
index c55e368..11e3c98 100644 (file)
@@ -100,6 +100,10 @@ if ($hassiteconfig) {
                     new lang_string('disabledfeatures', 'tool_mobile'),
                     new lang_string('disabledfeatures_desc', 'tool_mobile'), array(), $options));
 
+        $temp->add(new admin_setting_configtextarea('tool_mobile/custommenuitems',
+                    new lang_string('custommenuitems', 'tool_mobile'),
+                    new lang_string('custommenuitems_desc', 'tool_mobile'), '', PARAM_RAW, '50', '10'));
+
         $temp->add(new admin_setting_heading('tool_mobile/language',
                     new lang_string('language'), ''));
 
index d8e6aa4..6cf0f49 100644 (file)
@@ -148,6 +148,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             array('name' => 'tool_mobile_forcelogout', 'value' => 0),
             array('name' => 'tool_mobile_customlangstrings', 'value' => ''),
             array('name' => 'tool_mobile_disabledfeatures', 'value' => ''),
+            array('name' => 'tool_mobile_custommenuitems', 'value' => ''),
         );
         $this->assertCount(0, $result['warnings']);
         $this->assertEquals($expected, $result['settings']);
index 5fdb190..c83e26a 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2016120503; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2016120504; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2016112900; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
index 321ff06..bffecb3 100644 (file)
@@ -118,7 +118,7 @@ class tool_usertours_manager_testcase extends advanced_testcase {
         $rcm = $rc->getMethod($function);
         $rcm->setAccessible(true);
 
-        $this->setExpectedException('moodle_exception', 'A required parameter (sesskey) was missing');
+        $this->expectException('moodle_exception');
         $rcm->invokeArgs($manager, $arguments);
     }
 
index e41e68c..47e9e78 100644 (file)
@@ -33,6 +33,9 @@
     /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
     const CLIENT_MAX_SKIPTIME = 43200; // 60 * 60 * 12 seconds.
 
+    /** @var bool track whether any of the output feeds have recorded failures */
+    private $hasfailedfeeds = false;
+
     function init() {
         $this->title = get_string('pluginname', 'block_rss_client');
     }
@@ -59,6 +62,7 @@
      * @return block_rss_client\output\footer|null The renderable footer or null if none should be displayed.
      */
     protected function get_footer($feedrecords) {
+        global $PAGE;
         $footer = null;
 
         if ($this->config->block_rss_client_show_channel_link) {
             }
         }
 
+        if ($this->hasfailedfeeds) {
+            if (has_any_capability(['block/rss_client:manageownfeeds', 'block/rss_client:manageanyfeeds'], $this->context)) {
+                if ($footer === null) {
+                    $footer = new block_rss_client\output\footer();
+                }
+                $manageurl = new moodle_url('/blocks/rss_client/managefeeds.php', ['courseid' => $PAGE->course->id]);
+                $footer->set_failed($manageurl);
+            }
+        }
+
         return $footer;
     }
 
         global $CFG;
         require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
 
+        if ($feedrecord->skipuntil) {
+            // Last attempt to gather this feed via cron failed - do not try to fetch it now.
+            $this->hasfailedfeeds = true;
+            return null;
+        }
+
         $simplepiefeed = new moodle_simplepie($feedrecord->url);
 
         if(isset($CFG->block_rss_client_timeout)){
index 3da2039..c864df3 100644 (file)
@@ -40,16 +40,23 @@ class footer implements \renderable, \templatable {
     /**
      * The link provided in the RSS channel
      *
-     * @var \moodle_url
+     * @var \moodle_url|null
      */
     protected $channelurl;
 
+    /**
+     * Link to manage feeds, only provided if a feed has failed.
+     *
+     * @var \moodle_url|null
+     */
+    protected $manageurl = null;
+
     /**
      * Constructor
      *
-     * @param \moodle_url $channelurl The link provided in the RSS channel
+     * @param \moodle_url $channelurl (optional) The link provided in the RSS channel
      */
-    public function __construct(\moodle_url $channelurl) {
+    public function __construct($channelurl = null) {
         $this->channelurl = $channelurl;
     }
 
@@ -65,6 +72,16 @@ class footer implements \renderable, \templatable {
         return $this;
     }
 
+    /**
+     * Record the fact that there is at least one failed feed (and the URL for viewing
+     * these failed feeds).
+     *
+     * @param \moodle_url $manageurl the URL to link to for more information
+     */
+    public function set_failed(\moodle_url $manageurl) {
+        $this->manageurl = $manageurl;
+    }
+
     /**
      * Get the channel url
      *
@@ -84,6 +101,10 @@ class footer implements \renderable, \templatable {
     public function export_for_template(\renderer_base $output) {
         $data = new \stdClass();
         $data->channellink = clean_param($this->channelurl, PARAM_URL);
+        if ($this->manageurl) {
+            $data->hasfailedfeeds = true;
+            $data->manageurl = clean_param($this->manageurl, PARAM_URL);
+        }
 
         return $data;
     }
index 3fbbdc7..e606979 100644 (file)
@@ -44,6 +44,8 @@ $string['editnewsfeeds'] = 'Edit news feeds';
 $string['editrssblock'] = 'Edit RSS headline block';
 $string['enableautodiscovery'] = 'Enable auto-discovery of feeds?';
 $string['enableautodiscovery_help'] = 'If enabled, feeds on web pages are found automatically. For example, if http://docs.moodle.org is entered, then http://docs.moodle.org/en/index.php?title=Special:RecentChanges&feed=rss would be found.';
+$string['failedfeed'] = 'Feed failed to download - will retry after {$a}';
+$string['failedfeeds'] = 'One or more RSS feeds have failed';
 $string['feed'] = 'Feed';
 $string['feedadded'] = 'News feed added';
 $string['feeddeleted'] = 'News feed deleted';
index 475337c..85cf983 100644 (file)
@@ -114,6 +114,13 @@ foreach($feeds as $feed) {
     $feedinfo = '<div class="title">' . $viewlink . '</div>' .
         '<div class="url">' . html_writer::link($feed->url, $feed->url) .'</div>' .
         '<div class="description">' . $feed->description . '</div>';
+    if ($feed->skipuntil) {
+        $skipuntil = userdate($feed->skipuntil, get_string('strftimedatetime', 'langconfig'));
+        $skipmsg = get_string('failedfeed', 'block_rss_client', $skipuntil);
+        $notification = new \core\output\notification($skipmsg, 'error');
+        $notification->set_show_closebutton(false);
+        $feedinfo .= $OUTPUT->render($notification);
+    }
 
     $editurl = new moodle_url('/blocks/rss_client/editfeed.php?rssid=' . $feed->id . $extraparams);
     $editaction = $OUTPUT->action_icon($editurl, new pix_icon('t/edit', get_string('edit')));
index b1aa373..dd5d0fe 100644 (file)
         "channellink": "https://www.example.com/feeds/rss"
     }
 }}
-<a href="{{{channellink}}}">{{#str}} clientchannellink, block_rss_client {{/str}}</a>
+{{#channellink}}
+    <a href="{{{channellink}}}">{{#str}} clientchannellink, block_rss_client {{/str}}</a>
+    {{#hasfailedfeeds}}<br>{{/hasfailedfeeds}}
+{{/channellink}}
+{{#hasfailedfeeds}}
+    <a href="{{{manageurl}}}">{{#str}} failedfeeds, block_rss_client {{/str}}</a>
+{{/hasfailedfeeds}}
\ No newline at end of file
index 9ae2895..bdcc7bc 100644 (file)
@@ -57,6 +57,12 @@ M.core_comment = {
                         this.view(0);
                         return false;
                     }, this);
+                    // Also handle space/enter key.
+                    handle.on('key', function(e) {
+                        e.preventDefault();
+                        this.view(0);
+                        return false;
+                    }, '13,32', this);
                 }
                 scope.toggle_textarea(false);
             },
@@ -335,6 +341,7 @@ M.core_comment = {
                 );
             },
             view: function(page) {
+                var commenttoggler = Y.one('#comment-link-' + this.client_id);
                 var container = Y.one('#comment-ctrl-'+this.client_id);
                 var ta = Y.one('#dlg-content-'+this.client_id);
                 var img = Y.one('#comment-img-'+this.client_id);
@@ -351,6 +358,9 @@ M.core_comment = {
                     if (img) {
                         img.set('src', M.util.image_url('t/expanded', 'core'));
                     }
+                    if (commenttoggler) {
+                        commenttoggler.setAttribute('aria-expanded', 'true');
+                    }
                 } else {
                     // hide
                     container.setStyle('display', 'none');
@@ -364,6 +374,9 @@ M.core_comment = {
                     if (ta) {
                         ta.set('value','');
                     }
+                    if (commenttoggler) {
+                        commenttoggler.setAttribute('aria-expanded', 'false');
+                    }
                 }
                 if (ta) {
                     //toggle_textarea.apply(ta, [false]);
index 42b3d42..8bc0197 100644 (file)
@@ -460,7 +460,13 @@ class comment {
                 } else {
                     $collapsedimage= 't/collapsed';
                 }
-                $html .= html_writer::start_tag('a', array('class' => 'comment-link', 'id' => 'comment-link-'.$this->cid, 'href' => '#'));
+                $html .= html_writer::start_tag('a', array(
+                    'class' => 'comment-link',
+                    'id' => 'comment-link-'.$this->cid,
+                    'href' => '#',
+                    'role' => 'button',
+                    'aria-expanded' => 'false')
+                );
                 $html .= html_writer::empty_tag('img', array('id' => 'comment-img-'.$this->cid, 'src' => $OUTPUT->pix_url($collapsedimage), 'alt' => $this->linktext, 'title' => $this->linktext));
                 $html .= html_writer::tag('span', $this->linktext.' '.$countstring, array('id' => 'comment-link-text-'.$this->cid));
                 $html .= html_writer::end_tag('a');
index e1abad6..c8fadba 100644 (file)
@@ -385,7 +385,7 @@ class helper {
      * Returns the url to launch the lti tool.
      *
      * @param int $toolid the id of the shared tool
-     * @return moodle_url the url to launch the tool
+     * @return \moodle_url the url to launch the tool
      * @since Moodle 3.2
      */
     public static function get_launch_url($toolid) {
@@ -395,7 +395,7 @@ class helper {
     /**
      * Returns the name of the lti enrolment instance, or the name of the course/module being shared.
      *
-     * @param stdClass $tool The lti tool
+     * @param \stdClass $tool The lti tool
      * @return string The name of the tool
      * @since Moodle 3.2
      */
@@ -415,7 +415,7 @@ class helper {
     /**
      * Returns a description of the course or module that this lti instance points to.
      *
-     * @param stdClass $tool The lti tool
+     * @param \stdClass $tool The lti tool
      * @return string A description of the tool
      * @since Moodle 3.2
      */
@@ -438,8 +438,8 @@ class helper {
     /**
      * Returns the icon of the tool.
      *
-     * @param stdClass $tool The lti tool
-     * @return moodle_url A url to the icon of the tool
+     * @param \stdClass $tool The lti tool
+     * @return \moodle_url A url to the icon of the tool
      * @since Moodle 3.2
      */
     public static function get_icon($tool) {
@@ -453,7 +453,7 @@ class helper {
      * If you have slash arguments enabled, this will be a nice url ending in cartridge.xml.
      * If not it will be a php page with some parameters passed.
      *
-     * @param stdClass $tool The lti tool
+     * @param \stdClass $tool The lti tool
      * @return string The url to the cartridge representing the tool
      * @since Moodle 3.2
      */
@@ -482,7 +482,7 @@ class helper {
      * If you have slash arguments enabled, this will be a nice url ending in cartridge.xml.
      * If not it will be a php page with some parameters passed.
      *
-     * @param stdClass $tool The lti tool
+     * @param \stdClass $tool The lti tool
      * @return string The url to the cartridge representing the tool
      */
     public static function get_proxy_url($tool) {
@@ -616,7 +616,7 @@ class helper {
      * Traverses a recursive associative array, setting the properties of the corresponding
      * xpath element.
      *
-     * @param DOMXPath $xpath The xpath with the xml to modify
+     * @param \DOMXPath $xpath The xpath with the xml to modify
      * @param array $parameters The array of xpaths to search through
      * @param string $prefix The current xpath prefix (gets longer the deeper into the array you go)
      * @return void
index 5936b52..df3c504 100644 (file)
@@ -69,14 +69,14 @@ class manage_table extends \table_sql {
 
         $this->define_columns(array(
             'name',
-            'lti1',
-            'lti2',
+            'launch',
+            'registration',
             'edit'
         ));
         $this->define_headers(array(
             get_string('name'),
-            get_string('lti1', 'enrol_lti'),
-            get_string('lti2', 'enrol_lti'),
+            get_string('launchdetails', 'enrol_lti'),
+            get_string('registrationurl', 'enrol_lti'),
             get_string('edit')
         ));
         $this->collapsible(false);
@@ -87,6 +87,11 @@ class manage_table extends \table_sql {
         $this->ltienabled = enrol_is_enabled('lti');
         $this->canconfig = has_capability('moodle/course:enrolconfig', \context_course::instance($courseid));
         $this->courseid = $courseid;
+
+        // Set help icons.
+        $launchicon = new \help_icon('launchdetails', 'enrol_lti');
+        $regicon = new \help_icon('registrationurl', 'enrol_lti');
+        $this->define_help_for_headers(['1' => $launchicon, '2' => $regicon]);
     }
 
     /**
@@ -102,25 +107,25 @@ class manage_table extends \table_sql {
     }
 
     /**
-     * Generate the LTI1 column.
+     * Generate the launch column.
      *
      * @param \stdClass $tool instance data.
      * @return string
      */
-    public function col_lti1($tool) {
+    public function col_launch($tool) {
         global $OUTPUT;
 
         $url = helper::get_cartridge_url($tool);
 
-        $toolurllabel = get_string('toolurl', 'enrol_lti');
-        $toolurl = $url;
+        $cartridgeurllabel = get_string('cartridgeurl', 'enrol_lti');
+        $cartridgeurl = $url;
         $secretlabel = get_string('secret', 'enrol_lti');
         $secret = $tool->secret;
 
         $data = [
                 "rows" => [
-                    [ "label" => $toolurllabel, "text" => $toolurl, "id" => "toolurl" ],
-                    [ "label" => $secretlabel, "text" => $secret, "id" => "secret" ],
+                    [ "label" => $cartridgeurllabel, "text" => $cartridgeurl, "id" => "cartridgeurl", "hidelabel" => false ],
+                    [ "label" => $secretlabel, "text" => $secret, "id" => "secret", "hidelabel" => false ],
                 ]
             ];
 
@@ -129,22 +134,22 @@ class manage_table extends \table_sql {
     }
 
     /**
-     * Generate the LTI2 column.
+     * Generate the Registration column.
      *
      * @param \stdClass $tool instance data.
      * @return string
      */
-    public function col_lti2($tool) {
+    public function col_registration($tool) {
         global $OUTPUT;
 
         $url = helper::get_proxy_url($tool);
 
-        $toolurllabel = get_string('toolurl', 'enrol_lti');
+        $toolurllabel = get_string("registrationurl", "enrol_lti");
         $toolurl = $url;
 
         $data = [
                 "rows" => [
-                    [ "label" => $toolurllabel, "text" => $toolurl, "id" => "toolurl" ],
+                    [ "label" => $toolurllabel, "text" => $toolurl, "id" => "toolurl" , "hidelabel" => true],
                 ]
             ];
 
@@ -152,7 +157,6 @@ class manage_table extends \table_sql {
         return $return;
     }
 
-
     /**
      * Generate the edit column.
      *
index c0f9581..af96bab 100644 (file)
@@ -99,7 +99,9 @@ if ($action) {
 
 echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('toolsprovided', 'enrol_lti'));
-echo "<p>" . get_string('toolsprovided_help', 'enrol_lti') . "</p>";
+echo "<p>" .get_string('toolsprovided_help', 'enrol_lti') . "</p>";
+echo "<p class=helplink>" . $OUTPUT->doc_link('enrol/lti/index',
+    get_string('morehelp')) ."</p>";
 
 if (\enrol_lti\helper::count_lti_tools(array('courseid' => $courseid)) > 0) {
     $table = new \enrol_lti\manage_table($courseid);
index f4cb27c..f2db034 100644 (file)
@@ -55,14 +55,16 @@ $string['membersyncmodeenrolandunenrol'] = 'Enrol new and unenrol missing users'
 $string['membersyncmodeenrolnew'] = 'Enrol new users';
 $string['membersyncmodeunenrolmissing'] = 'Unenrol missing users';
 $string['notoolsprovided'] = 'No tools provided';
-$string['lti1'] = 'LTI 1';
-$string['lti2'] = 'LTI 2';
+$string['launchdetails'] = 'Launch details';
+$string['launchdetails_help'] = 'A cartridge URL (also called configuration URL) plus secret are required for configuring the tool.';
 $string['lti:config'] = 'Configure \'Publish as LTI tool\' instances';
 $string['lti:unenrol'] = 'Unenrol users from the course';
 $string['opentool'] = 'Open tool';
 $string['pluginname'] = 'Publish as LTI tool';
 $string['pluginname_desc'] = 'The \'Publish as LTI tool\' plugin, together with the LTI authentication plugin, allows remote users to access selected courses and activities. In other words, Moodle functions as an LTI tool provider.';
 $string['registration'] = 'Published tool registration';
+$string['registrationurl'] = 'Registration URL';
+$string['registrationurl_help'] = 'If a registration URL (also called proxy URL) is used, then the tool is automatically configured.';
 $string['remotesystem'] = 'Remote system';
 $string['requirecompletion'] = 'Require course or activity completion prior to grade synchronisation';
 $string['returnurlnotset'] = 'Return URL was not set.';
@@ -77,9 +79,8 @@ $string['successfulregistration'] = 'Successful registration';
 $string['tasksyncgrades'] = 'Publish as LTI tool grade sync';
 $string['tasksyncmembers'] = 'Publish as LTI tool users sync';
 $string['toolsprovided'] = 'Published tools';
-$string['toolsprovided_help'] = 'This is the information you need to paste into any system to add these tools. In other systems the Tool URL may be referred to as a configuration URL or a web link.';
+$string['toolsprovided_help'] = 'A tool may be shared with another site by providing either launch details or a registration URL.';
 $string['tooltobeprovided'] = 'Tool to be published';
 $string['toolurl'] = 'Tool URL';
 $string['userdefaultvalues'] = 'User default values';
-// Deprecated since Moodle 3.2.
 $string['cartridgeurl'] = 'Cartridge URL';
index 005f076..fee6faf 100644 (file)
@@ -26,7 +26,7 @@
     * none
 
     Context variables required for this template:
-    * rows An array of objects with label, text and id
+    * rows An array of objects with label, hidelabel, text and id
 
     Example context (json):
     {
             {
                 "label": "Tool URL",
                 "text": "http://example.com/",
-                "id": "toolurl"
+                "id": "toolurl",
+                "hidelabel": false
             },
             {
                 "label": "Secret",
                 "text": "ABCDEF1234567890",
-                "id": "secret"
+                "id": "secret",
+                "hidelabel": true
             }
         ]
     }
 }}
 {{#rows}}
     <div>
-        <label style="display: inline-block; width: 5em" {{#id}}for={{#quote}}{{{id}}}-{{{uniqid}}}{{/quote}}{{/id}}>{{label}}</label>
+        <label style="display: inline-block; width: 5em"
+            {{#id}}for={{#quote}}{{{id}}}-{{{uniqid}}}{{/quote}}{{/id}} {{#hidelabel}}
+               class="accesshide"{{/hidelabel}}>{{label}}</label>
         <div style="display: inline-block">{{> core/copy_box }}</div>
     </div>
 {{/rows}}
index 6583665..09c5804 100644 (file)
@@ -516,7 +516,7 @@ class enrol_lti_helper_testcase extends advanced_testcase {
         $document->load(realpath(__DIR__ . '/fixtures/input.xml'));
         $xpath = new \DOMXpath($document);
 
-        $this->setExpectedException("coding_exception");
+        $this->expectException('coding_exception');
         $function->invokeArgs(null, [$xpath, $parameters]);
     }
 
index 565f3ef..db8ed4b 100644 (file)
@@ -173,7 +173,7 @@ class enrol_self_plugin extends enrol_plugin {
             }
         }
         // Send welcome message.
-        if ($instance->customint4 !== ENROL_DO_NOT_SEND_EMAIL) {
+        if ($instance->customint4 != ENROL_DO_NOT_SEND_EMAIL) {
             $this->email_welcome_message($instance, $USER);
         }
     }
index 0ecbc4c..ae3e29b 100644 (file)
@@ -45,6 +45,13 @@ class grade_report_grader extends grade_report {
      */
     private $allgrades;
 
+    /**
+     * Contains all grade items expect GRADE_TYPE_NONE.
+     *
+     * @var array $allgradeitems
+     */
+    private $allgradeitems;
+
     /**
      * Array of errors for bulk grades updating.
      * @var array $gradeserror
@@ -520,6 +527,22 @@ class grade_report_grader extends grade_report {
         return $this->users;
     }
 
+    /**
+     * Load all grade items.
+     */
+    protected function get_allgradeitems() {
+        if (!empty($this->allgradeitems)) {
+            return $this->allgradeitems;
+        }
+        $allgradeitems = grade_item::fetch_all(array('courseid' => $this->courseid));
+        // But hang on - don't include ones which are set to not show the grade at all.
+        $this->allgradeitems = array_filter($allgradeitems, function($item) {
+            return $item->gradetype != GRADE_TYPE_NONE;
+        });
+
+        return $this->allgradeitems;
+    }
+
     /**
      * we supply the userids in this query, and get all the grades
      * pulls out all the grades, this does not need to worry about paging
@@ -543,11 +566,16 @@ class grade_report_grader extends grade_report {
                  WHERE g.itemid = gi.id AND gi.courseid = :courseid {$this->userselect}";
 
         $userids = array_keys($this->users);
+        $allgradeitems = $this->get_allgradeitems();
 
         if ($grades = $DB->get_records_sql($sql, $params)) {
             foreach ($grades as $graderec) {
                 $grade = new grade_grade($graderec, false);
-                $this->allgrades[$graderec->userid][$graderec->itemid] = $grade;
+                if (!empty($allgradeitems[$graderec->itemid])) {
+                    // Note: Filter out grades which have a grade type of GRADE_TYPE_NONE.
+                    // Only grades without this type are present in $allgradeitems.
+                    $this->allgrades[$graderec->userid][$graderec->itemid] = $grade;
+                }
                 if (in_array($graderec->userid, $userids) and array_key_exists($graderec->itemid, $this->gtree->get_items())) { // some items may not be present!!
                     $this->grades[$graderec->userid][$graderec->itemid] = $grade;
                     $this->grades[$graderec->userid][$graderec->itemid]->grade_item = $this->gtree->get_item($graderec->itemid); // db caching
@@ -568,6 +596,18 @@ class grade_report_grader extends grade_report {
                 }
             }
         }
+
+        // Pre fill grades for any remaining items which might be collapsed.
+        foreach ($userids as $userid) {
+            foreach ($allgradeitems as $itemid => $gradeitem) {
+                if (!isset($this->allgrades[$userid][$itemid])) {
+                    $this->allgrades[$userid][$itemid] = new grade_grade();
+                    $this->allgrades[$userid][$itemid]->itemid = $itemid;
+                    $this->allgrades[$userid][$itemid]->userid = $userid;
+                    $this->allgrades[$userid][$itemid]->grade_item = $gradeitem;
+                }
+            }
+        }
     }
 
     /**
@@ -922,12 +962,7 @@ class grade_report_grader extends grade_report {
         // grade items (in case one has been hidden) as the course total shown needs to be adjusted for this particular
         // user.
         if (!$this->canviewhidden) {
-            $allgradeitems = grade_item::fetch_all(array('courseid' => $this->courseid));
-
-            // But hang on - don't include ones which are set to not show the grade at all.
-            $allgradeitems = array_filter($allgradeitems, function($item) {
-                return $item->gradetype != GRADE_TYPE_NONE;
-            });
+            $allgradeitems = $this->get_allgradeitems();
         }
 
         foreach ($this->users as $userid => $user) {
index 775f455..54bed2b 100644 (file)
@@ -12,6 +12,7 @@ Feature: We can change what we are viewing on the grader report
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
       | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 1 | student2@example.com |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
@@ -94,6 +95,9 @@ Feature: We can change what we are viewing on the grader report
       | capability | permission |
       | moodle/grade:viewhidden | Prevent |
     And I log out
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student2 | C1 | student |
     And I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "View > Grader report" in the course gradebook
index 3d4b0a7..64126eb 100644 (file)
@@ -3649,9 +3649,13 @@ function get_users_by_capability(context $context, $capability, $fields = '', $s
                 } else {
                     $unions[] = "SELECT userid
                                    FROM {role_assignments}
-                                  WHERE contextid IN ($ctxids)
-                                        AND roleid IN (".implode(',', array_keys($needed[$cap])) .")
-                                        AND roleid NOT IN (".implode(',', array_keys($prohibited[$cap])) .")";
+                                  WHERE contextid IN ($ctxids) AND roleid IN (".implode(',', array_keys($needed[$cap])) .")
+                                        AND userid NOT IN (
+                                            SELECT userid
+                                              FROM {role_assignments}
+                                             WHERE contextid IN ($ctxids)
+                                                    AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . ")
+                                                        )";
                 }
             }
         }
index a29e064..17220a3 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index ca7f034..361d0e1 100644 (file)
@@ -33,7 +33,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         ENTER: 13,
         SPACE: 32,
         ESCAPE: 27,
-        COMMA: 188,
+        COMMA: 44,
         UP: 38
     };
 
@@ -543,14 +543,6 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                     // We handled this event, so prevent it.
                     e.preventDefault();
                     return false;
-                case KEYS.COMMA:
-                    if (options.tags) {
-                        // If we are allowing tags, comma should create a tag (or enter).
-                        createItem(options, state, originalSelect);
-                    }
-                    // We handled this event, so prevent it.
-                    e.preventDefault();
-                    return false;
                 case KEYS.UP:
                     // Choose the previous active item.
                     activatePreviousItem(state);
@@ -581,6 +573,19 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             }
             return true;
         });
+        // Support multi lingual COMMA keycode (44).
+        inputElement.on('keypress', function(e) {
+            if (e.keyCode === KEYS.COMMA) {
+                if (options.tags) {
+                    // If we are allowing tags, comma should create a tag (or enter).
+                    createItem(options, state, originalSelect);
+                }
+                // We handled this event, so prevent it.
+                e.preventDefault();
+                return false;
+            }
+            return true;
+        });
         // Handler used to force set the value from behat.
         inputElement.on('behat:set-value', function() {
             var suggestionsElement = $(document.getElementById(state.suggestionsId));
index 3cefc8f..0070634 100644 (file)
@@ -42,6 +42,22 @@ class behat_command {
      */
     const DOCS_URL = 'http://docs.moodle.org/dev/Acceptance_testing';
 
+    /**
+     * Ensures the behat dir exists in moodledata
+     *
+     * @return string Full path
+     */
+    public static function get_parent_behat_dir() {
+        global $CFG;
+
+        // If not set then return empty string.
+        if (!isset($CFG->behat_dataroot_parent)) {
+            return "";
+        }
+
+        return $CFG->behat_dataroot_parent;
+    }
+
     /**
      * Ensures the behat dir exists in moodledata
      * @param int $runprocess run process for which behat dir is returned.
@@ -55,12 +71,11 @@ class behat_command {
             return "";
         }
 
-        if (empty($runprocess)) {
-            $behatdir = $CFG->behat_dataroot . '/behat';
-        } else if (isset($CFG->behat_parallel_run[$runprocess - 1]['behat_dataroot'])) {
+        // If $CFG->behat_parallel_run starts with index 0 and $runprocess for parallel run starts with 1.
+        if (!empty($runprocess) && 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';
+            $behatdir = $CFG->behat_dataroot . '/behat';
         }
 
         if (!is_dir($behatdir)) {
index 6b237f2..7285e39 100644 (file)
@@ -87,7 +87,7 @@ class behat_config_manager {
 
         // Behat must have a separate behat.yml to have access to the whole set of features and steps definitions.
         if ($testsrunner === true) {
-            $configfilepath = behat_command::get_behat_dir() . '/behat.yml';
+            $configfilepath = behat_command::get_behat_dir($run) . '/behat.yml';
         } else {
             // Alternative for steps definitions filtering, one for each user.
             $configfilepath = self::get_steps_list_config_filepath();
@@ -117,7 +117,7 @@ class behat_config_manager {
 
         // Get number of parallel runs if not passed.
         if (empty($parallelruns) && ($parallelruns !== false)) {
-            $parallelruns = self::get_parallel_test_runs();
+            $parallelruns = self::get_behat_run_config_value('parallel');
         }
 
         // Behat config file specifing the main context class,
@@ -212,31 +212,49 @@ class behat_config_manager {
      * 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';
+    public final static function get_behat_run_config_file_path() {
+        return behat_command::get_parent_behat_dir() . '/run_environment.json';
     }
 
     /**
-     * Returns number of parallel runs for which site is initialised.
+     * Get config for parallel run.
      *
-     * @param int $runprocess run process for which behat dir is returned.
-     * @return int
+     * @param string $key Key to store
+     * @return string|int|array value which is stored.
      */
-    public final static function get_parallel_test_runs($runprocess = 0) {
+    public final static function get_behat_run_config_value($key) {
+        $parallelrunconfigfile = self::get_behat_run_config_file_path();
 
-        $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;
+            if ($parallelrunconfigs = @json_decode(file_get_contents($parallelrunconfigfile), true)) {
+                if (isset($parallelrunconfigs[$key])) {
+                    return $parallelrunconfigs[$key];
+                }
             }
         }
 
-        return $parallelrun;
+        return false;
+    }
+
+    /**
+     * Save/update config for parallel run.
+     *
+     * @param string $key Key to store
+     * @param string|int|array $value to store.
+     */
+    public final static function set_behat_run_config_value($key, $value) {
+        $parallelrunconfigs = array();
+        $parallelrunconfigfile = self::get_behat_run_config_file_path();
+
+        // Get any existing config first.
+        if (file_exists($parallelrunconfigfile)) {
+            $parallelrunconfigs = @json_decode(file_get_contents($parallelrunconfigfile), true);
+        }
+        $parallelrunconfigs[$key] = $value;
+
+        @file_put_contents($parallelrunconfigfile, json_encode($parallelrunconfigs, JSON_PRETTY_PRINT));
     }
 
     /**
@@ -247,8 +265,8 @@ class behat_config_manager {
     public final static function drop_parallel_site_links() {
         global $CFG;
 
-        // Get parallel test runs from first run.
-        $parallelrun = self::get_parallel_test_runs(1);
+        // Get parallel test runs.
+        $parallelrun = self::get_behat_run_config_value('parallel');
 
         if (empty($parallelrun)) {
             return false;
index 8df778e..1546d0b 100644 (file)
@@ -147,7 +147,7 @@ class behat_config_util {
     public function get_number_of_parallel_run() {
         // Get number of parallel runs if not passed.
         if (empty($this->parallelruns) && ($this->parallelruns !== false)) {
-            $this->parallelruns = behat_config_manager::get_parallel_test_runs();
+            $this->parallelruns = behat_config_manager::get_behat_run_config_value('parallel');
         }
 
         return $this->parallelruns;
index 09cf9cc..ab97abb 100644 (file)
@@ -139,8 +139,23 @@ class behat_util extends testing_util {
         }
 
         self::reset_dataroot();
-        self::drop_dataroot();
         self::drop_database(true);
+        self::drop_dataroot();
+    }
+
+    /**
+     * Delete files and directories under dataroot.
+     */
+    public static function drop_dataroot() {
+        global $CFG;
+
+        // As behat directory is now created under default $CFG->behat_dataroot_parent, so remove the whole dir.
+        if ($CFG->behat_dataroot !== $CFG->behat_dataroot_parent) {
+            remove_dir($CFG->behat_dataroot, false);
+        } else {
+            // It should never come here.
+            throw new moodle_exception("Behat dataroot should not be same as parent behat data root.");
+        }
     }
 
     /**
@@ -290,6 +305,7 @@ class behat_util extends testing_util {
         }
 
         $testenvfile = self::get_test_file_path();
+        behat_config_manager::set_behat_run_config_value('behatsiteenabled', 0);
 
         if (!self::is_test_mode_enabled()) {
             echo "Test environment was already disabled\n";
@@ -322,8 +338,8 @@ class behat_util extends testing_util {
      * Returns the path to the file which specifies if test environment is enabled
      * @return string
      */
-    protected final static function get_test_file_path() {
-        return behat_command::get_behat_dir() . '/test_environment_enabled.txt';
+    public final static function get_test_file_path() {
+        return behat_command::get_parent_behat_dir() . '/test_environment_enabled.txt';
     }
 
     /**
index 662a784..1ca3f09 100644 (file)
@@ -274,26 +274,41 @@ function behat_check_config_vars() {
             'Define $CFG->behat_dataroot in config.php');
     }
     clearstatcache();
-    if (!file_exists($CFG->behat_dataroot)) {
+    if (!file_exists($CFG->behat_dataroot_parent)) {
         $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
         umask(0);
-        if (!mkdir($CFG->behat_dataroot, $permissions, true)) {
+        if (!mkdir($CFG->behat_dataroot_parent, $permissions, true)) {
             behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
         }
     }
-    $CFG->behat_dataroot = realpath($CFG->behat_dataroot);
-    if (empty($CFG->behat_dataroot) or !is_dir($CFG->behat_dataroot) or !is_writable($CFG->behat_dataroot)) {
+    $CFG->behat_dataroot_parent = realpath($CFG->behat_dataroot_parent);
+    if (empty($CFG->behat_dataroot_parent) or !is_dir($CFG->behat_dataroot_parent) or !is_writable($CFG->behat_dataroot_parent)) {
         behat_error(BEHAT_EXITCODE_CONFIG,
             '$CFG->behat_dataroot in config.php must point to an existing writable directory');
     }
-    if (!empty($CFG->dataroot) and $CFG->behat_dataroot == realpath($CFG->dataroot)) {
+    if (!empty($CFG->dataroot) and $CFG->behat_dataroot_parent == realpath($CFG->dataroot)) {
         behat_error(BEHAT_EXITCODE_CONFIG,
             '$CFG->behat_dataroot in config.php must be different from $CFG->dataroot');
     }
-    if (!empty($CFG->phpunit_dataroot) and $CFG->behat_dataroot == realpath($CFG->phpunit_dataroot)) {
+    if (!empty($CFG->phpunit_dataroot) and $CFG->behat_dataroot_parent == realpath($CFG->phpunit_dataroot)) {
         behat_error(BEHAT_EXITCODE_CONFIG,
             '$CFG->behat_dataroot in config.php must be different from $CFG->phpunit_dataroot');
     }
+
+    // This request is coming from admin/tool/behat/cli/util.php which will call util_single.php. So just return from
+    // here as we don't need to create a dataroot for single run.
+    if (defined('BEHAT_PARALLEL_UTIL') && BEHAT_PARALLEL_UTIL && empty($CFG->behatrunprocess)) {
+        return;
+    }
+
+    if (!file_exists($CFG->behat_dataroot)) {
+        $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
+        umask(0);
+        if (!mkdir($CFG->behat_dataroot, $permissions, true)) {
+            behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
+        }
+    }
+    $CFG->behat_dataroot = realpath($CFG->behat_dataroot);
 }
 
 /**
@@ -336,6 +351,10 @@ function behat_update_vars_for_process() {
     $behatrunprocess = behat_get_run_process();
     $CFG->behatrunprocess = $behatrunprocess;
 
+    // Data directory will be a directory under parent directory.
+    $CFG->behat_dataroot_parent = $CFG->behat_dataroot;
+    $CFG->behat_dataroot .= '/'. BEHAT_PARALLEL_SITE_NAME;
+
     if ($behatrunprocess) {
         if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_wwwroot'])) {
             // Set www root for run process.
@@ -459,7 +478,7 @@ function behat_get_run_process() {
             }
             // Check if default behat datroot increment was done.
             if (empty($behatrunprocess)) {
-                $behatdataroot = str_replace("\\", "/", $CFG->behat_dataroot);
+                $behatdataroot = str_replace("\\", "/", $CFG->behat_dataroot . '/' . BEHAT_PARALLEL_SITE_NAME);
                 $behatrunprocess = preg_filter("#^{$behatdataroot}" . "(.+?)[/|\\\]behat[/|\\\]behat\.yml#", '$1',
                     $behatconfig);
             }
index f874ae5..dcfeb4e 100644 (file)
@@ -96,7 +96,11 @@ abstract class exporter {
                 }
 
             } else {
-                if (array_key_exists($key, $related) && $related[$key] instanceof $classname) {
+                $scalartypes = ['string', 'int', 'bool', 'float'];
+                $scalarcheck = 'is_' . $classname;
+                if (array_key_exists($key, $related) &&
+                        ((in_array($classname, $scalartypes) && $scalarcheck($related[$key])) ||
+                        ($related[$key] instanceof $classname))) {
                     $this->related[$key] = $related[$key];
                 } else {
                     throw new coding_exception($missingdataerr . $key . ' => ' . $classname);
index 9dbead8..afabd03 100644 (file)
@@ -702,6 +702,7 @@ $functions = array(
         'description' => 'Retrieve the template data for the conversation list',
         'type' => 'read',
         'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_message_data_for_messagearea_contacts' => array(
         'classname' => 'core_message_external',
@@ -710,6 +711,7 @@ $functions = array(
         'description' => 'Retrieve the template data for the contact list',
         'type' => 'read',
         'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_message_data_for_messagearea_messages' => array(
         'classname' => 'core_message_external',
@@ -718,6 +720,7 @@ $functions = array(
         'description' => 'Retrieve the template data for the messages',
         'type' => 'read',
         'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_message_data_for_messagearea_get_most_recent_message' => array(
         'classname' => 'core_message_external',
index 1229b38..768ef07 100644 (file)
@@ -27,6 +27,7 @@
 
 require_once("HTML/QuickForm/button.php");
 require_once(__DIR__ . '/../outputcomponents.php');
+require_once('templatable_form_element.php');
 
 /**
  * HTML class for a button type element
index 6152374..f390f14 100644 (file)
@@ -412,10 +412,12 @@ class phpunit_util extends testing_util {
 
         self::reset_dataroot();
         testing_initdataroot($CFG->dataroot, 'phpunit');
-        self::drop_dataroot();
 
-        // drop all tables
+        // Drop all tables.
         self::drop_database($displayprogress);
+
+        // Drop dataroot.
+        self::drop_dataroot();
     }
 
     /**
index fb16784..0f71dc6 100644 (file)
@@ -84,7 +84,8 @@ if (defined('BEHAT_SITE_RUNNING')) {
     // Update config variables for parallel behat runs.
     behat_update_vars_for_process();
 
-    if (behat_is_test_site()) {
+    // If behat is being installed for parallel run, then we modify params for parallel run only.
+    if (behat_is_test_site() && !(defined('BEHAT_PARALLEL_UTIL') && empty($CFG->behatrunprocess))) {
         clearstatcache();
 
         // Checking the integrity of the provided $CFG->behat_* vars and the
@@ -116,7 +117,8 @@ if (defined('BEHAT_SITE_RUNNING')) {
 
         if (!defined('BEHAT_UTIL') and !defined('BEHAT_TEST')) {
             // Somebody tries to access test site directly, tell them if not enabled.
-            if (!file_exists($CFG->behat_dataroot . '/behat/test_environment_enabled.txt')) {
+            $behatdir = preg_replace("#[/|\\\]" . BEHAT_PARALLEL_SITE_NAME . "\d{0,}$#", '', $CFG->behat_dataroot);
+            if (!file_exists($behatdir . '/test_environment_enabled.txt')) {
                 behat_error(BEHAT_EXITCODE_CONFIG, 'Behat is configured but not enabled on this test site.');
             }
         }
index 15959a6..359ece3 100644 (file)
@@ -2504,6 +2504,12 @@ class core_accesslib_testcase extends advanced_testcase {
 
         assign_capability('mod/page:view', CAP_PREVENT, $allroles['guest'], $systemcontext, true);
 
+        // Prepare for prohibit test.
+        role_assign($allroles['editingteacher'], $testusers[19], context_system::instance());
+        role_assign($allroles['teacher'], $testusers[19], context_course::instance($testcourses[17]));
+        role_assign($allroles['editingteacher'], $testusers[19], context_course::instance($testcourses[17]));
+        assign_capability('moodle/course:update', CAP_PROHIBIT, $allroles['teacher'], context_course::instance($testcourses[17]), true);
+
         accesslib_clear_all_caches_for_unit_testing(); /// Must be done after assign_capability().
 
         // Extra tests for guests and not-logged-in users because they can not be verified by cross checking
@@ -2527,6 +2533,14 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertFalse(has_capability('moodle/course:update', context_course::instance($testcourses[19]), $testusers[9]));
         $this->assertFalse(has_capability('moodle/course:update', $systemcontext, $testusers[9]));
 
+        // Test prohibits.
+        $this->assertTrue(has_capability('moodle/course:update', context_system::instance(), $testusers[19]));
+        $ids = get_users_by_capability(context_system::instance(), 'moodle/course:update', 'u.id');
+        $this->assertArrayHasKey($testusers[19], $ids);
+        $this->assertFalse(has_capability('moodle/course:update', context_course::instance($testcourses[17]), $testusers[19]));
+        $ids = get_users_by_capability(context_course::instance($testcourses[17]), 'moodle/course:update', 'u.id');
+        $this->assertArrayNotHasKey($testusers[19], $ids);
+
         // Test the list of enrolled users.
         $coursecontext = context_course::instance($course1->id);
         $enrolled = get_enrolled_users($coursecontext);
index afd6a39..f456628 100644 (file)
@@ -41,8 +41,22 @@ class core_exporter_testcase extends advanced_testcase {
 
     public function setUp() {
         $s = new stdClass();
-        $this->validrelated = array('simplestdClass' => $s, 'arrayofstdClass' => array($s, $s), 'context' => null);
-        $this->invalidrelated = array('simplestdClass' => 'a string', 'arrayofstdClass' => 5, 'context' => null);
+        $this->validrelated = array(
+            'simplestdClass' => $s,
+            'arrayofstdClass' => array($s, $s),
+            'context' => null,
+            'aint' => 5,
+            'astring' => 'valid string',
+            'abool' => false
+        );
+        $this->invalidrelated = array(
+            'simplestdClass' => 'a string',
+            'arrayofstdClass' => 5,
+            'context' => null,
+            'aint' => false,
+            'astring' => 4,
+            'abool' => 'not a boolean'
+        );
 
         $this->validdata = array('stringA' => 'A string', 'stringAformat' => FORMAT_HTML, 'intB' => 4);
 
@@ -179,7 +193,8 @@ class core_testable_exporter extends \core\external\exporter {
 
     protected static function define_related() {
         // We cache the context so it does not need to be retrieved from the course.
-        return array('simplestdClass' => 'stdClass', 'arrayofstdClass' => 'stdClass[]', 'context' => 'context?');
+        return array('simplestdClass' => 'stdClass', 'arrayofstdClass' => 'stdClass[]', 'context' => 'context?',
+            'astring' => 'string', 'abool' => 'bool', 'aint' => 'int');
     }
 
     protected function get_other_values(renderer_base $output) {
index 137573b..a8ca638 100644 (file)
@@ -57,7 +57,7 @@ class message_popup_externallib_testcase extends advanced_testcase {
     public function test_get_popup_notifications_no_user_exception() {
         $this->resetAfterTest(true);
 
-        $this->setExpectedException('moodle_exception');
+        $this->expectException('moodle_exception');
         $result = message_popup_external::get_popup_notifications(-2132131, false, 0, 0);
     }
 
@@ -72,7 +72,7 @@ class message_popup_externallib_testcase extends advanced_testcase {
         $user = $this->getDataGenerator()->create_user();
 
         $this->setUser($user);
-        $this->setExpectedException('moodle_exception');
+        $this->expectException('moodle_exception');
         $result = message_popup_external::get_popup_notifications($sender->id, false, 0, 0);
     }
 
@@ -141,7 +141,7 @@ class message_popup_externallib_testcase extends advanced_testcase {
     public function test_get_unread_popup_notification_count_invalid_user_exception() {
         $this->resetAfterTest(true);
 
-        $this->setExpectedException('moodle_exception');
+        $this->expectException('moodle_exception');
         $result = message_popup_external::get_unread_popup_notification_count(-2132131, 0);
     }
 
@@ -156,7 +156,7 @@ class message_popup_externallib_testcase extends advanced_testcase {
         $user = $this->getDataGenerator()->create_user();
 
         $this->setUser($user);
-        $this->setExpectedException('moodle_exception');
+        $this->expectException('moodle_exception');
         $result = message_popup_external::get_unread_popup_notification_count($sender->id, 0);
     }
 
index 9b8eab2..6a53627 100644 (file)
@@ -931,11 +931,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $prefs = core_message_external::get_user_notification_preferences();
         $prefs = external_api::clean_returnvalue(core_message_external::get_user_notification_preferences_returns(), $prefs);
         // Check processors.
-        $this->assertCount(2, $prefs['preferences']['processors']);
+        $this->assertGreaterThanOrEqual(2, count($prefs['preferences']['processors']));
         $this->assertEquals($user->id, $prefs['preferences']['userid']);
 
         // Check components.
-        $this->assertCount(8, $prefs['preferences']['components']);
+        $this->assertGreaterThanOrEqual(8, count($prefs['preferences']['components']));
 
         // Check some preferences that we previously set.
         $found = 0;
index df53f22..a056732 100644 (file)
@@ -88,7 +88,7 @@ class grading_app implements templatable, renderable {
         $export->cmid = $this->assignment->get_course_module()->id;
         $export->contextid = $this->assignment->get_context()->id;
         $export->groupid = $this->groupid;
-        $export->name = $this->assignment->get_instance()->name;
+        $export->name = $this->assignment->get_context()->get_context_name();
         $export->courseid = $this->assignment->get_course()->id;
         $export->participants = array();
         $num = 1;
index 78a4ec2..81f13c9 100644 (file)
@@ -758,7 +758,7 @@ function assign_get_grade_details_for_print_overview(&$unmarkedsubmissions, $sql
                                              g.attemptnumber = s.attemptnumber
                                        WHERE
                                              ( g.timemodified is NULL OR
-                                             s.timemodified > g.timemodified OR
+                                             s.timemodified >= g.timemodified OR
                                              g.grade IS NULL ) AND
                                              s.timemodified IS NOT NULL AND
                                              s.status = ? AND
index 34e5219..7ef88c2 100644 (file)
@@ -158,6 +158,13 @@ class mod_assign_lib_testcase extends mod_assign_base_testcase {
         $data = new stdClass();
         $data->grade = '50.0';
         $openassign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
+
+        // The assign_print_overview expects the grade date to be after the submission date.
+        $graderecord = $DB->get_record('assign_grades', array('assignment' => $openassign->get_instance()->id,
+            'userid' => $this->students[0]->id, 'attemptnumber' => 0));
+        $graderecord->timemodified += 1;
+        $DB->update_record('assign_grades', $graderecord);
+
         $overview = array();
         assign_print_overview($courses, $overview);
         $this->assertEquals(1, count($overview));
index 6a77898..8145103 100644 (file)
@@ -148,6 +148,15 @@ class big_search_form implements renderable, templatable {
         $this->words = $value;
     }
 
+    /**
+     * Forum ID setter search criteria.
+     *
+     * @param int $forumid The forum ID.
+     */
+    public function set_forumid($forumid) {
+        $this->forumid = $forumid;
+    }
+
     public function export_for_template(renderer_base $output) {
         $data = new stdClass();
 
@@ -185,6 +194,15 @@ class big_search_form implements renderable, templatable {
                             . html_writer::select_time('hours', 'tohour', $dateto)
                             . html_writer::select_time('minutes', 'tominute', $dateto);
 
+        if ($this->forumid && !empty($this->forumoptions)) {
+            foreach ($this->forumoptions as $index => $option) {
+                if ($option['value'] == $this->forumid) {
+                    $this->forumoptions[$index]['selected'] = true;
+                } else {
+                    $this->forumoptions[$index]['selected'] = false;
+                }
+            }
+        }
         $data->forumoptions = $this->forumoptions;
 
         return $data;
index 931d045..ec399f9 100644 (file)
@@ -318,7 +318,7 @@ echo $OUTPUT->footer();
   * @return void The function prints the form.
   */
 function forum_print_big_search_form($course) {
-    global $PAGE, $words, $subject, $phrase, $user, $userid, $fullwords, $notwords, $datefrom, $dateto, $OUTPUT;
+    global $PAGE, $words, $subject, $phrase, $user, $fullwords, $notwords, $datefrom, $dateto, $forumid;
 
     $renderable = new \mod_forum\output\big_search_form($course, $user);
     $renderable->set_words($words);
@@ -329,6 +329,7 @@ function forum_print_big_search_form($course) {
     $renderable->set_dateto($dateto);
     $renderable->set_subject($subject);
     $renderable->set_user($user);
+    $renderable->set_forumid($forumid);
 
     $output = $PAGE->get_renderer('mod_forum');
     echo $output->render($renderable);
index 6fc760c..f9371f2 100644 (file)
                 <td class="c1">
                     <select name="forumid" id="menuforumid">
                         {{#forumoptions}}
-                            <option value="{{value}}">{{name}}</option>
+                            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
                         {{/forumoptions}}
                     </select>
                 </td>
index 2dfa72b..cc103bb 100644 (file)
@@ -155,7 +155,7 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
 
         for (element in datamodel[scoid]) {
             if (element.match(/\.n\./) == null) {
-                if ((typeof eval('datamodel["' + scoid + '"]["' + element + '"].defaultvalue')) != 'undefined') {
+                if (typeof datamodel[scoid][element].defaultvalue != 'undefined') {
                     eval(element + ' = datamodel["' + scoid + '"]["' + element + '"].defaultvalue;');
                 } else {
                     eval(element + ' = "";');
@@ -233,8 +233,8 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
             if (element != "") {
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
-                    if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].mod') != 'w') {
+                if (typeof datamodel[scoid][elementmodel] != "undefined") {
+                    if (datamodel[scoid][elementmodel].mod != 'w') {
                             element = String(element).replace(expression, "_$1.");
                             elementIndexes = element.split('.');
                         subelement = 'cmi';
@@ -249,21 +249,21 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                             errorCode = "0"; // Need to check if it is the right errorCode
                         }
                     } else {
-                        errorCode = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].readerror');
+                        errorCode = datamodel[scoid][elementmodel].readerror;
                     }
                 } else {
                     childrenstr = '._children';
                     countstr = '._count';
                     if (elementmodel.substr(elementmodel.length - childrenstr.length,elementmodel.length) == childrenstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length - childrenstr.length);
-                        if ((typeof eval('datamodel["' + scoid + '"]["' + parentmodel + '"]')) != "undefined") {
+                        if (typeof datamodel[scoid][parentmodel] != "undefined") {
                             errorCode = "202";
                         } else {
                             errorCode = "201";
                         }
                     } else if (elementmodel.substr(elementmodel.length - countstr.length,elementmodel.length) == countstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length - countstr.length);
-                        if ((typeof eval('datamodel["' + scoid + '"]["' + parentmodel + '"]')) != "undefined") {
+                        if (typeof datamodel[scoid][parentmodel] != "undefined") {
                             errorCode = "203";
                         } else {
                             errorCode = "201";
@@ -287,9 +287,9 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
             if (element != "") {
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
-                    if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].mod') != 'r') {
-                        expression = new RegExp(eval('datamodel["' + scoid + '"]["' + elementmodel + '"].format'));
+                if (typeof datamodel[scoid][elementmodel] != "undefined") {
+                    if (datamodel[scoid][elementmodel].mod != 'r') {
+                        expression = new RegExp(datamodel[scoid][elementmodel].format);
                         value = value + '';
                         matches = value.match(expression);
                         if (matches != null) {
@@ -339,8 +339,8 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                                 if (autocommit && !(AICCapi.timeout)) {
                                     AICCapi.timeout = Y.later(60000, API, 'LMSCommit', [""], false);
                                 }
-                                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"].range')) != "undefined") {
-                                    range = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].range');
+                                if (typeof datamodel[scoid][elementmodel].range != "undefined") {
+                                    range = datamodel[scoid][elementmodel].range;
                                     ranges = range.split('#');
                                     value = value * 1.0;
                                     if ((value >= ranges[0]) && (value <= ranges[1])) {
@@ -348,7 +348,7 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                                         errorCode = "0";
                                         return "true";
                                     } else {
-                                        errorCode = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].writeerror');
+                                        errorCode = datamodel[scoid][elementmodel].writeerror;
                                     }
                                 } else {
                                     if (element == 'cmi.comments') {
@@ -361,10 +361,10 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                                 }
                             }
                         } else {
-                            errorCode = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].writeerror');
+                            errorCode = datamodel[scoid][elementmodel].writeerror;
                         }
                     } else {
-                        errorCode = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].writeerror');
+                        errorCode = datamodel[scoid][elementmodel].writeerror;
                     }
                 } else {
                     errorCode = "201"
@@ -490,11 +490,11 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                 element = parent + '.' + property;
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
-                    if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].mod') != 'r') {
+                if (typeof datamodel[scoid][elementmodel] != "undefined") {
+                    if (datamodel[scoid][elementmodel].mod != 'r') {
                         elementstring = '&' + underscore(element) + '=' + escape(data[property]);
-                        if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"].defaultvalue')) != "undefined") {
-                            if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].defaultvalue') != data[property]) {
+                        if (typeof datamodel[scoid][elementmodel].defaultvalue != "undefined") {
+                            if (datamodel[scoid][elementmodel].defaultvalue != data[property]) {
                                 datastring += elementstring;
                             }
                         } else {
index 2adb083..a6d0358 100644 (file)
@@ -16,7 +16,8 @@
 //
 // SCORM 1.2 API Implementation
 //
-function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebugging, scormauto, scormid, cfgwwwroot, sesskey, scoid, attempt, viewmode, cmid, currentorg, autocommit, masteryoverride) {
+function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebugging, scormauto, scormid, cfgwwwroot, sesskey,
+        scoid, attempt, viewmode, cmid, currentorg, autocommit, masteryoverride, hidetoc) {
 
     var prerequrl = cfgwwwroot + "/mod/scorm/prereqs.php?a=" + scormid + "&scoid=" + scoid + "&attempt=" + attempt + "&mode=" + viewmode + "&currentorg=" + currentorg + "&sesskey=" + sesskey;
     var datamodelurl = cfgwwwroot + "/mod/scorm/datamodel.php";
@@ -147,7 +148,7 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
 
         for (element in datamodel[scoid]) {
             if (element.match(/\.n\./) == null) {
-                if ((typeof eval('datamodel["' + scoid + '"]["' + element + '"].defaultvalue')) != 'undefined') {
+                if (typeof datamodel[scoid][element].defaultvalue != 'undefined') {
                     eval(element + ' = datamodel["' + scoid + '"]["' + element + '"].defaultvalue;');
                 } else {
                     eval(element + ' = "";');
@@ -244,8 +245,8 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
             if (element != "") {
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
-                    if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].mod') != 'w') {
+                if (typeof datamodel[scoid][elementmodel] != "undefined") {
+                    if (datamodel[scoid][elementmodel].mod != 'w') {
                         element = String(element).replace(expression, "_$1.");
                         elementIndexes = element.split('.');
                         subelement = 'cmi';
@@ -263,21 +264,21 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                             errorCode = "0"; // Need to check if it is the right errorCode
                         }
                     } else {
-                        errorCode = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].readerror');
+                        errorCode = datamodel[scoid][elementmodel].readerror;
                     }
                 } else {
                     childrenstr = '._children';
                     countstr = '._count';
                     if (elementmodel.substr(elementmodel.length - childrenstr.length,elementmodel.length) == childrenstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length - childrenstr.length);
-                        if ((typeof eval('datamodel["' + scoid + '"]["' + parentmodel + '"]')) != "undefined") {
+                        if (typeof datamodel[scoid][parentmodel] != "undefined") {
                             errorCode = "202";
                         } else {
                             errorCode = "201";
                         }
                     } else if (elementmodel.substr(elementmodel.length - countstr.length,elementmodel.length) == countstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length - countstr.length);
-                        if ((typeof eval('datamodel["' + scoid + '"]["' + parentmodel + '"]')) != "undefined") {
+                        if (typeof datamodel[scoid][parentmodel] != "undefined") {
                             errorCode = "203";
                         } else {
                             errorCode = "201";
@@ -304,9 +305,9 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
             if (element != "") {
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
-                    if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].mod') != 'r') {
-                        expression = new RegExp(eval('datamodel["' + scoid + '"]["' + elementmodel + '"].format'));
+                if (typeof datamodel[scoid][elementmodel] != "undefined") {
+                    if (datamodel[scoid][elementmodel].mod != 'r') {
+                        expression = new RegExp(datamodel[scoid][elementmodel].format);
                         value = value + '';
                         matches = value.match(expression);
                         if (matches != null) {
@@ -356,8 +357,8 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                                 if (autocommit && !(SCORMapi1_2.timeout)) {
                                     SCORMapi1_2.timeout = Y.later(60000, API, 'LMSCommit', [""], false);
                                 }
-                                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"].range')) != "undefined") {
-                                    range = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].range');
+                                if (typeof datamodel[scoid][elementmodel].range != "undefined") {
+                                    range = datamodel[scoid][elementmodel].range;
                                     ranges = range.split('#');
                                     value = value * 1.0;
                                     if ((value >= ranges[0]) && (value <= ranges[1])) {
@@ -368,7 +369,7 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                                         }
                                         return "true";
                                     } else {
-                                        errorCode = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].writeerror');
+                                        errorCode = datamodel[scoid][elementmodel].writeerror;
                                     }
                                 } else {
                                     if (element == 'cmi.comments') {
@@ -384,10 +385,10 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                                 }
                             }
                         } else {
-                            errorCode = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].writeerror');
+                            errorCode = datamodel[scoid][elementmodel].writeerror;
                         }
                     } else {
-                        errorCode = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].writeerror');
+                        errorCode = datamodel[scoid][elementmodel].writeerror;
                     }
                 } else {
                     errorCode = "201"
@@ -413,12 +414,17 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
         if (param == "") {
             if (Initialized) {
                 result = StoreData(cmi,false);
-                // trigger TOC update
-                var callback = M.mod_scorm.connectPrereqCallback;
-                YUI().use('io-base', function(Y) {
-                    Y.on('io:complete', callback.success, Y);
-                    Y.io(prerequrl);
-                });
+                // Trigger TOC update only if TOC is displayed.
+                // Checks against setting Display course structure in player:
+                // 0 = To the side, 1 = Hidden, 2 = In a drop down menu, 3 = Disabled
+                if (hidetoc !== '3') {
+                    Y.log('Refreshing toc');
+                    var callback = M.mod_scorm.connectPrereqCallback;
+                    YUI().use('io-base', function(Y) {
+                        Y.on('io:complete', callback.success, Y);
+                        Y.io(prerequrl);
+                    });
+                }
                 if (scormdebugging) {
                     LogAPICall("Commit", param, "", 0);
                 }
@@ -557,40 +563,39 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
 
                     // check if this specific element is not defined in the datamodel,
                     // but the generic element name is
-                    if ((eval('typeof datamodel["' + scoid + '"]["' + element + '"]')) == "undefined"
-                        && (eval('typeof datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
+                    if (typeof datamodel[scoid][element] == "undefined" && typeof datamodel[scoid][elementmodel] != "undefined") {
 
                         // add this specific element to the data model (by cloning
                         // the generic element) so we can track changes to it
-                        eval('datamodel["' + scoid + '"]["' + element + '"]=CloneObj(datamodel["' + scoid + '"]["' + elementmodel + '"]);');
+                        datamodel[scoid][element] = CloneObj(datamodel[scoid][elementmodel]);
                     }
 
                     // check if the current element exists in the datamodel
-                    if ((typeof eval('datamodel["' + scoid + '"]["' + element + '"]')) != "undefined") {
+                    if (typeof datamodel[scoid][element] != "undefined") {
 
                         // make sure this is not a read only element
-                        if (eval('datamodel["' + scoid + '"]["' + element + '"].mod') != 'r') {
+                        if (datamodel[scoid][element].mod != 'r') {
 
                             elementstring = '&' + underscore(element) + '=' + encodeURIComponent(data[property]);
 
                             // check if the element has a default value
-                            if ((typeof eval('datamodel["' + scoid + '"]["' + element + '"].defaultvalue')) != "undefined") {
+                            if (typeof datamodel[scoid][element].defaultvalue != "undefined") {
 
                                 // check if the default value is different from the current value
-                                if (eval('datamodel["' + scoid + '"]["' + element + '"].defaultvalue') != data[property]
-                                    || eval('typeof(datamodel["' + scoid + '"]["' + element + '"].defaultvalue)') != typeof(data[property])) {
+                                if (datamodel[scoid][element].defaultvalue != data[property] ||
+                                    typeof datamodel[scoid][element].defaultvalue != typeof data[property]) {
 
                                     // append the URI fragment to the string we plan to commit
                                     datastring += elementstring;
 
                                     // update the element default to reflect the current committed value
-                                    eval('datamodel["' + scoid + '"]["' + element + '"].defaultvalue=data[property];');
+                                    datamodel[scoid][element].defaultvalue = data[property];
                                 }
                             } else {
                                 // append the URI fragment to the string we plan to commit
                                 datastring += elementstring;
                                 // no default value for the element, so set it now
-                                eval('datamodel["' + scoid + '"]["' + element + '"].defaultvalue=data[property];');
+                                datamodel[scoid][element].defaultvalue = data[property];
                             }
                         }
                     }
@@ -660,6 +665,8 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
 
 M.scorm_api = {};
 
-M.scorm_api.init = function(Y, def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebugging, scormauto, scormid, cfgwwwroot, sesskey, scoid, attempt, viewmode, cmid, currentorg, autocommit, masteryoverride) {
-    window.API = new SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebugging, scormauto, scormid, cfgwwwroot, sesskey, scoid, attempt, viewmode, cmid, currentorg, autocommit, masteryoverride);
+M.scorm_api.init = function(Y, def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebugging, scormauto, scormid, cfgwwwroot,
+        sesskey, scoid, attempt, viewmode, cmid, currentorg, autocommit, masteryoverride, hidetoc) {
+    window.API = new SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebugging, scormauto, scormid, cfgwwwroot,
+            sesskey, scoid, attempt, viewmode, cmid, currentorg, autocommit, masteryoverride, hidetoc);
 }
index 30b2d05..70f8814 100644 (file)
@@ -55,11 +55,12 @@ $scorm->autocommit = ($scorm->autocommit === "1") ? true : false;
 $scorm->masteryoverride = ($scorm->masteryoverride === "1") ? true : false;
 $PAGE->requires->js_init_call('M.scorm_api.init', array($def, $cmiobj, $cmiint, $cmistring256, $cmistring4096,
                                                         scorm_debugging($scorm), $scorm->auto, $scorm->id, $CFG->wwwroot,
-                                                        sesskey(), $scoid, $attempt, $mode, $id, $currentorg, $scorm->autocommit, $scorm->masteryoverride));
+                                                        sesskey(), $scoid, $attempt, $mode, $id, $currentorg, $scorm->autocommit,
+                                                        $scorm->masteryoverride, $scorm->hidetoc));
 
 // Pull in the debugging utilities.
 if (scorm_debugging($scorm)) {
     require_once($CFG->dirroot.'/mod/scorm/datamodels/debug.js.php');
     echo html_writer::script('AppendToLog("Moodle SCORM 1.2 API Loaded, Activity: '.
                                 $scorm->name.', SCO: '.$sco->identifier.'", 0);');
-}
\ No newline at end of file
+}
index d735172..e3b02a4 100644 (file)
@@ -252,7 +252,7 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
 
         for (element in datamodel[scoid]) {
             if (element.match(/\.n\./) == null) {
-                if ((typeof eval('datamodel["' + scoid + '"]["' + element + '"].defaultvalue')) != 'undefined') {
+                if (typeof datamodel[scoid][element].defaultvalue != 'undefined') {
                     eval(element + ' = datamodel["' + scoid + '"]["' + element + '"].defaultvalue;');
                 } else {
                     eval(element + ' = "";');
@@ -380,8 +380,8 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
             if (element != "") {
                 var expression = new RegExp(CMIIndex,'g');
                 var elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
-                    if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].mod') != 'w') {
+                if (typeof datamodel[scoid][elementmodel] != "undefined") {
+                    if (datamodel[scoid][elementmodel].mod != 'w') {
 
                         element = String(element).replace(/\.(\d+)\./, ".N$1.");
                         element = element.replace(/\.(\d+)\./, ".N$1.");
@@ -417,7 +417,7 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                     var parentmodel = '';
                     if (elementmodel.substr(elementmodel.length - childrenstr.length,elementmodel.length) == childrenstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length - childrenstr.length);
-                        if ((typeof eval('datamodel["' + scoid + '"]["' + parentmodel + '"]')) != "undefined") {
+                        if (datamodel[scoid][parentmodel] != "undefined") {
                             errorCode = "301";
                             diagnostic = "Data Model Element Does Not Have Children";
                         } else {
@@ -425,7 +425,7 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                         }
                     } else if (elementmodel.substr(elementmodel.length - countstr.length,elementmodel.length) == countstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length - countstr.length);
-                        if ((typeof eval('datamodel["' + scoid + '"]["' + parentmodel + '"]')) != "undefined") {
+                        if (typeof datamodel[scoid][parentmodel] != "undefined") {
                             errorCode = "301";
                             diagnostic = "Data Model Element Cannot Have Count";
                         } else {
@@ -473,10 +473,10 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
             if (element != "") {
                 var expression = new RegExp(CMIIndex,'g');
                 var elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
-                    if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].mod') != 'r') {
-                        if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].format') != 'CMIFeedback') {
-                            expression = new RegExp(eval('datamodel["' + scoid + '"]["' + elementmodel + '"].format'));
+                if (typeof datamodel[scoid][elementmodel] != "undefined") {
+                    if (datamodel[scoid][elementmodel].mod != 'r') {
+                        if (datamodel[scoid][elementmodel].format != 'CMIFeedback') {
+                            expression = new RegExp(datamodel[scoid][elementmodel].format);
                         } else {
                             // cmi.interactions.n.type depending format accept everything at this stage
                             expression = new RegExp(CMIFeedback);
@@ -749,8 +749,8 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                                     SCORMapi1_3.timeout = Y.later(60000, API_1484_11, 'Commit', [""], false);
                                 }
 
-                                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"].range')) != "undefined") {
-                                    range = eval('datamodel["' + scoid + '"]["' + elementmodel + '"].range');
+                                if (typeof datamodel[scoid][elementmodel].range != "undefined") {
+                                    range = datamodel[scoid][elementmodel].range;
                                     ranges = range.split('#');
                                     value = value * 1.0;
                                     if (value >= ranges[0]) {
@@ -1180,11 +1180,12 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                 var element = parent + '.' + property;
                 var expression = new RegExp(CMIIndexStore,'g');
                 var elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"]')) != "undefined") {
-                    if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].mod') != 'r') {
+                if (typeof datamodel[scoid][elementmodel] != "undefined") {
+                    if (datamodel[scoid][elementmodel].mod != 'r') {
                         var elementstring = '&' + underscore(element) + '=' + encodeURIComponent(data[property]);
-                        if ((typeof eval('datamodel["' + scoid + '"]["' + elementmodel + '"].defaultvalue')) != "undefined") {
-                            if (eval('datamodel["' + scoid + '"]["' + elementmodel + '"].defaultvalue') != data[property] || eval('typeof(datamodel["' + scoid + '"]["' + elementmodel + '"].defaultvalue)') != typeof(data[property])) {
+                        if (typeof datamodel[scoid][elementmodel].defaultvalue != "undefined") {
+                            if (datamodel[scoid][elementmodel].defaultvalue != data[property] ||
+                                typeof datamodel[scoid][elementmodel].defaultvalue != typeof data[property]) {
                                 datastring += elementstring;
                             }
                         } else {
index e63250b..9016190 100644 (file)
@@ -3702,7 +3702,7 @@ abstract class workshop_assessment_base {
     protected $fields = array();
 
     /** @var workshop */
-    protected $workshop;
+    public $workshop;
 
     /**
      * Copies the properties of the given database record into properties of $this instance
index bd25280..fa110dd 100644 (file)
@@ -1311,6 +1311,8 @@ class question_attempt {
         if ($finished) {
             $this->finish();
         }
+
+        $this->set_flagged($oldqa->is_flagged());
     }
 
     /**
diff --git a/question/engine/tests/walkthrough_test.php b/question/engine/tests/walkthrough_test.php
new file mode 100644 (file)
index 0000000..79fd021
--- /dev/null
@@ -0,0 +1,63 @@
+<?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/>.
+
+/**
+ * This file contains tests that walks a question through a whole attempt.
+ *
+ * @package core_question
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once(__DIR__ . '/..//lib.php');
+require_once(__DIR__ . '/helpers.php');
+
+
+/**
+ * End-to-end tests of attempting a question.
+ *
+ * @copyright  2017 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_question_walkthrough_testcase extends qbehaviour_walkthrough_test_base {
+
+    public function test_regrade_does_not_lose_flag() {
+
+        // Create a true-false question with correct answer true.
+        $tf = test_question_maker::make_question('truefalse', 'true');
+        $this->start_attempt_at_question($tf, 'deferredfeedback', 2);
+
+        // Process a true answer.
+        $this->process_submission(array('answer' => 1));
+
+        // Finish the attempt.
+        $this->quba->finish_all_questions();
+
+        // Flag the question.
+        $this->get_question_attempt()->set_flagged(true);
+
+        // Now change the correct answer to the question, and regrade.
+        $tf->rightanswer = false;
+        $this->quba->regrade_all_questions();
+
+        // Verify the flag has not been lost.
+        $this->assertTrue($this->get_question_attempt()->is_flagged());
+    }
+}
index 180a448..76a3882 100644 (file)
@@ -321,7 +321,7 @@ abstract class qtype_gapselect_question_base extends question_graded_automatical
     public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
         if ($component == 'question' && in_array($filearea,
                 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
-            return $this->check_combined_feedback_file_access($qa, $options, $filearea);
+            return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args);
 
         } else if ($component == 'question' && $filearea == 'hint') {
             return $this->check_hint_file_access($qa, $options, $args);
index b373fba..4113010 100644 (file)
@@ -342,7 +342,7 @@ class qtype_match_question extends question_graded_automatically_with_countback
 
         } else if ($component == 'question' && in_array($filearea,
                 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
-            return $this->check_combined_feedback_file_access($qa, $options, $filearea);
+            return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args);
 
         } else if ($component == 'question' && $filearea == 'hint') {
             return $this->check_hint_file_access($qa, $options, $args);
index 2c5e17c..09b9493 100644 (file)
@@ -113,7 +113,7 @@ abstract class qtype_multichoice_base extends question_graded_automatically {
     public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
         if ($component == 'question' && in_array($filearea,
                 array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
-            return $this->check_combined_feedback_file_access($qa, $options, $filearea);
+            return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args);
 
         } else if ($component == 'question' && $filearea == 'answer') {
             $answerid = reset($args); // Itemid is answer id.
index 879381e..b6a4b89 100644 (file)
@@ -668,11 +668,18 @@ abstract class question_graded_automatically extends question_with_responses
      * @param question_attempt $qa the question attempt being displayed.
      * @param question_display_options $options the options that control display of the question.
      * @param string $filearea the name of the file area.
+     * @param array $args the remaining bits of the file path.
      * @return bool whether access to the file should be allowed.
      */
-    protected function check_combined_feedback_file_access($qa, $options, $filearea) {
+    protected function check_combined_feedback_file_access($qa, $options, $filearea, $args = null) {
         $state = $qa->get_state();
 
+        if ($args === null) {
+            debugging('You must pass $args as the fourth argument to check_combined_feedback_file_access.',
+                    DEBUG_DEVELOPER);
+            $args = array($this->id); // Fake it for now, so the rest of this method works.
+        }
+
         if (!$state->is_finished()) {
             $response = $qa->get_last_qt_data();
             if (!$this->is_gradable_response($response)) {
index 470629c..6a74061 100644 (file)
@@ -1,5 +1,11 @@
 This files describes API changes for question type plugins.
 
+=== 3.1.5, 3.2.2, 3.3 ===
+
+* If you are using check_combined_feedback_file_access in your check_file_access method,
+  then you must now pass $args as the 4th argument, so the correct permission checks
+  can be performed. If you don't, you will get a developer debug notice.
+
 === 3.1 ===
 
 * The following functions, previously used (exclusively) by upgrade steps are not available
index b0bd7a2..9a31a11 100644 (file)
@@ -115,7 +115,11 @@ if ($token === "$inttoken") {
 }
 
 // Check the context actually exists.
-list($context, $course, $cm) = get_context_info_array($contextid);
+try {
+    list($context, $course, $cm) = get_context_info_array($contextid);
+} catch (dml_missing_record_exception $e) {
+    rss_error();
+}
 
 $PAGE->set_context($context);
 
index 6cf5366..0e281a4 100644 (file)
@@ -1,7 +1,41 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core/tabtree
+
+    Tab tree.
+
+    Example context (json):
+    {
+        "tabs": [{
+            "active": "true",
+            "inactive": "false",
+            "link": [{
+                "link": "http://moodle.org"
+            }],
+            "title": "Moodle.org",
+            "text": "Moodle community"
+        }]
+    }
+}}
 <ul class="nav nav-tabs m-b-1">
   {{#tabs}}
     <li class="nav-item">
-      <a class="nav-link {{#active}}active{{/active}} {{#inactive}}disabled{{/inactive}}" {{#link}}href="{{{link}}}"{{/link}} title={{title}}>{{text}}</a>
+      <a class="nav-link {{#active}}active{{/active}} {{#inactive}}disabled{{/inactive}}" {{#link}}href="{{{link}}}"{{/link}} title={{title}}>{{{text}}}</a>
     </li>
   {{/tabs}}
 </ul>
index c376145..241d842 100644 (file)
                 <td class="c1">
                     <select name="forumid" id="menuforumid" class="form-control">
                         {{#forumoptions}}
-                            <option value="{{value}}">{{name}}</option>
+                            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
                         {{/forumoptions}}
                     </select>
                 </td>
index d7345e7..16405ae 100644 (file)
@@ -868,7 +868,7 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
         $anotheruser = self::getDataGenerator()->create_user();
         $this->setUser($anotheruser);
 
-        $this->setExpectedException('required_capability_exception');
+        $this->expectException('required_capability_exception');
         $result = core_user_external::get_user_preferences('', $user->id);
     }
 
index ba33a9b..b806b7e 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017012700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017020200.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.3dev (Build: 20170127)'; // Human-friendly version name
+$release  = '3.3dev (Build: 20170202)'; // Human-friendly version name
 
 $branch   = '33';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.