Merge branch 'MDL-53139' of https://github.com/dg711/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 15 Mar 2016 23:12:18 +0000 (00:12 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 15 Mar 2016 23:12:18 +0000 (00:12 +0100)
144 files changed:
admin/settings/server.php
admin/tests/behat/behat_admin.php
admin/tool/behat/cli/run.php
admin/tool/behat/cli/util_single_run.php
admin/tool/behat/tests/behat/manipulate_forms.feature
admin/tool/behat/tests/manager_test.php
admin/tool/langimport/tests/behat/manage_langpacks.feature
auth/manual/tests/behat/auth_manual.feature [new file with mode: 0644]
auth/tests/behat/behat_auth.php
backup/util/ui/tests/behat/behat_backup.php
behat.yml.dist
blocks/tests/behat/behat_blocks.php
calendar/tests/behat/behat_calendar.php
cohort/tests/behat/behat_cohort.php
completion/tests/behat/behat_completion.php
composer.json
composer.lock
config-dist.php
course/tests/behat/behat_course.php
course/tests/behat/category_resort.feature
course/tests/behat/course_resort.feature
enrol/tests/behat/behat_enrol.php
grade/edit/tree/index.php
grade/grading/form/guide/tests/behat/behat_gradingform_guide.php
grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php
grade/grading/tests/behat/behat_grading.php
grade/report/grader/tests/behat/ajax_grader.feature
grade/report/grader/tests/behat/behat_gradereport_grader.php
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_aggregation.feature
group/tests/behat/behat_groups.php
lang/en/cache.php
lib/accesslib.php
lib/behat/behat_base.php
lib/behat/classes/behat_config_manager.php
lib/behat/classes/behat_context_helper.php
lib/behat/classes/behat_selectors.php
lib/behat/form_field/behat_form_checkbox.php
lib/behat/form_field/behat_form_radio.php
lib/behat/form_field/behat_form_select.php
lib/classes/dml/recordset_walk.php
lib/db/caches.php
lib/dml/tests/recordset_walk_test.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
lib/editor/atto/plugins/image/tests/behat/image.feature
lib/form/tests/behat/modgrade_validation.feature
lib/grade/grade_category.php
lib/grade/grade_item.php
lib/gradelib.php
lib/moodlelib.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_permissions.php
lib/tests/behat/behat_transformations.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/xhprof/readme_moodle.txt
lib/xhprof/xhprof_html/callgraph.php
lib/xhprof/xhprof_html/index.php
lib/xhprof/xhprof_html/typeahead.php
message/tests/behat/behat_message.php
message/tests/behat/delete_messages.feature
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/tests/behat/behat_assignfeedback_editpdf.php
mod/assign/locallib.php
mod/assign/tests/behat/comment_inline.feature
mod/choice/tests/behat/behat_mod_choice.php
mod/data/backup/moodle2/backup_data_stepslib.php
mod/data/backup/moodle2/restore_data_stepslib.php
mod/data/classes/external.php
mod/data/db/install.xml
mod/data/db/upgrade.php
mod/data/tests/behat/behat_mod_data.php
mod/data/tests/behat/required_entries.feature
mod/data/tests/externallib_test.php
mod/data/version.php
mod/feedback/tests/behat/behat_mod_feedback.php
mod/feedback/tests/behat/question_types.feature
mod/folder/renderer.php
mod/forum/lib.php
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/upgrade.txt
mod/glossary/tests/behat/behat_mod_glossary.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/tests/externallib_test.php
mod/lti/tests/lib_test.php
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/settings_form_fields_disableif.feature
mod/scorm/classes/event/cmielement_submitted.php [new file with mode: 0644]
mod/scorm/classes/event/scoreraw_submitted.php [new file with mode: 0644]
mod/scorm/classes/event/status_submitted.php [new file with mode: 0644]
mod/scorm/lang/en/scorm.php
mod/scorm/locallib.php
mod/scorm/tests/events_test.php
mod/scorm/version.php
mod/wiki/classes/external.php
mod/wiki/db/services.php
mod/wiki/locallib.php
mod/wiki/tests/externallib_test.php
mod/wiki/upgrade.txt [new file with mode: 0644]
mod/wiki/version.php
mod/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php
mod/workshop/tests/behat/behat_mod_workshop.php
question/behaviour/adaptive/tests/walkthrough_test.php
question/behaviour/adaptivenopenalty/tests/walkthrough_test.php
question/behaviour/interactive/renderer.php
question/behaviour/interactive/tests/walkthrough_test.php
question/behaviour/interactivecountback/tests/walkthrough_test.php
question/behaviour/rendererbase.php
question/behaviour/upgrade.txt
question/engine/tests/helpers.php
question/tests/behat/behat_question.php
question/tests/behat/behat_question_base.php
question/type/calculated/tests/walkthrough_test.php
question/type/calculatedmulti/tests/walkthrough_test.php
question/type/calculatedsimple/tests/walkthrough_test.php
question/type/ddimageortext/tests/behat/preview.feature
question/type/ddimageortext/tests/walkthrough_test.php
question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php
question/type/ddmarker/tests/behat/preview.feature
question/type/ddmarker/tests/walkthrough_test.php
question/type/ddwtos/tests/behat/preview.feature
question/type/ddwtos/tests/walkthrough_test.php
question/type/gapselect/tests/walkthrough_test.php
question/type/match/tests/walkthrough_test.php
question/type/multianswer/tests/walkthrough_test.php
question/type/numerical/tests/walkthrough_test.php
question/type/randomsamatch/tests/walkthrough_test.php
repository/boxnet/cli/migrationv1.php [deleted file]
repository/boxnet/lang/en/repository_boxnet.php
repository/boxnet/lib.php
repository/boxnet/locallib.php
repository/boxnet/migrationv1.php [deleted file]
repository/filesystem/lang/en/repository_filesystem.php
repository/filesystem/lib.php
repository/tests/behat/behat_filepicker.php
repository/upload/tests/behat/behat_repository_upload.php
version.php

index cc43e70..573ed4b 100644 (file)
@@ -18,11 +18,10 @@ $ADMIN->add('server', $temp);
 
 // "supportcontact" settingpage
 $temp = new admin_settingpage('supportcontact', new lang_string('supportcontact','admin'));
-if (isloggedin()) {
-    global $USER;
-    $primaryadminemail = $USER->email;
-    $primaryadminname  = fullname($USER, true);
-
+$primaryadmin = get_admin();
+if ($primaryadmin) {
+    $primaryadminemail = $primaryadmin->email;
+    $primaryadminname  = fullname($primaryadmin, true);
 } else {
     // no defaults during installation - admin user must be created first
     $primaryadminemail = NULL;
index 8c56f7a..8f69e31 100644 (file)
@@ -28,8 +28,7 @@
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 require_once(__DIR__ . '/../../../lib/behat/behat_field_manager.php');
 
-use Behat\Behat\Context\Step\Given as Given,
-    Behat\Gherkin\Node\TableNode as TableNode,
+use Behat\Gherkin\Node\TableNode as TableNode,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
index 6e19659..3df999a 100644 (file)
@@ -120,8 +120,8 @@ $tags = '';
 
 if ($options['profile']) {
     $profile = $options['profile'];
-    if (!isset($CFG->behat_config[$profile])) {
-        echo "Invalid profile passed: " . $profile;
+    if (!isset($CFG->behat_config[$profile]) && !isset($CFG->behat_profiles[$profile])) {
+        echo "Invalid profile passed: " . $profile . PHP_EOL;
         exit(1);
     }
     $extraopts[] = '--profile="' . $profile . '"';
@@ -226,22 +226,27 @@ $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;
 
+ksort($exitcodes);
 
 // Print exit info from each run.
-$status = false;
+// Status bits contains pass/fail status of parallel runs.
+$status = 0;
+$processcounter = 0;
 foreach ($exitcodes as $exitcode) {
-    $status = (bool)$status || (bool)$exitcode;
+    if ($exitcode) {
+        $status |= (1 << $processcounter);
+    }
+    $processcounter++;
 }
 
 // Run finished. Show exit code and output from individual process.
 $verbose = empty($options['verbose']) ? false : true;
-$verbose = $verbose || $status;
+$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;
-    ksort($exitcodes);
     foreach ($exitcodes as $run => $exitcode) {
         echo $run . ": " . $exitcode . PHP_EOL;
     }
@@ -263,7 +268,7 @@ print_each_process_info($processes, $verbose);
 // Remove site symlink if necessary.
 behat_config_manager::drop_parallel_site_links();
 
-exit((int) $status);
+exit($status);
 
 /**
  * Signal handler for terminal exit.
index d523569..1991707 100644 (file)
@@ -173,6 +173,11 @@ if ($options['install']) {
 
     // This is only displayed once for parallel install.
     if (empty($options['run'])) {
+        // Notify user that 2.5 profile has been converted to 3.5.
+        if (behat_config_manager::$autoprofileconversion) {
+            mtrace("2.5 behat profile detected, automatically converted to current 3.x format");
+        }
+
         $runtestscommand = behat_command::get_behat_command(true, !empty($options['run']));
 
         $runtestscommand .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
index 20c3c29..126a800 100644 (file)
@@ -12,7 +12,6 @@ Feature: Forms manipulation
     When I set the field "First name" to "Field value"
     And I set the field "Select a country" to "Japan"
     And I set the field "Unmask" to "1"
-    And I expand all fieldsets
     Then the field "First name" matches value "Field value"
     And the "Select a country" select box should contain "Japan"
     And the field "Unmask" matches value "1"
index 1dc44ef..0f24f38 100644 (file)
@@ -142,11 +142,10 @@ class tool_behat_manager_testcase extends advanced_testcase {
         // YAML decides when is is necessary to wrap strings between single quotes, so not controlled
         // values like paths should not be asserted including the key name as they would depend on the
         // directories values.
-        $this->assertContains($CFG->dirroot . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'behat' . DIRECTORY_SEPARATOR . 'features', $contents);
+        $this->assertContains($CFG->dirroot, $contents);
 
         // Not quoted strings.
         $this->assertContains('micarro: /me/lo/robaron', $contents);
-        $this->assertContains('class: behat_init_context', $contents);
 
         // YAML uses single quotes to wrap URL strings.
         $this->assertContains("base_url: '" . $CFG->behat_wwwroot . "'", $contents);
index 4d518de..6d6f0f0 100644 (file)
@@ -13,10 +13,10 @@ Feature: Manage language packs
   Scenario: Install language pack
     Given I log in as "admin"
     And I navigate to "Language packs" node in "Site administration > Language"
-    When I set the field "Available language packs" to "English - Pirate (en_ar)"
+    When I set the field "Available language packs" to "en_ar"
     And I press "Install selected language pack(s)"
     Then I should see "Language pack 'en_ar' was successfully installed"
-    And the "Installed language packs" select box should contain "English - Pirate (en_ar)"
+    And the "Installed language packs" select box should contain "en_ar"
     And I navigate to "Live logs" node in "Site administration > Reports"
     And I should see "The language pack 'en_ar' was installed."
     And I log out
@@ -35,14 +35,14 @@ Feature: Manage language packs
   Scenario: Try to uninstall language pack
     Given I log in as "admin"
     And I navigate to "Language packs" node in "Site administration > Language"
-    And I set the field "Available language packs" to "English - Pirate (en_ar)"
+    And I set the field "Available language packs" to "en_ar"
     And I press "Install selected language pack(s)"
-    When I set the field "Installed language packs" to "English - Pirate (en_ar)"
+    When I set the field "Installed language packs" to "en_ar"
     And I press "Uninstall selected language pack(s)"
     And I press "Continue"
     Then I should see "Language pack 'en_ar' was uninstalled"
-    And the "Installed language packs" select box should not contain "English - Pirate (en_ar)"
-    And the "Available language packs" select box should contain "English - Pirate (en_ar)"
+    And the "Installed language packs" select box should not contain "en_ar"
+    And the "Available language packs" select box should contain "en_ar"
     And I navigate to "Live logs" node in "Site administration > Reports"
     And I should see "The language pack 'en_ar' was removed."
     And I should see "Language pack uninstalled"
@@ -51,7 +51,7 @@ Feature: Manage language packs
   Scenario: Try to uninstall English language pack
     Given I log in as "admin"
     And I navigate to "Language packs" node in "Site administration > Language"
-    When I set the field "Installed language packs" to "English (en)"
+    When I set the field "Installed language packs" to "en"
     And I press "Uninstall selected language pack(s)"
     Then I should see "The English language pack cannot be uninstalled."
     And I navigate to "Live logs" node in "Site administration > Reports"
diff --git a/auth/manual/tests/behat/auth_manual.feature b/auth/manual/tests/behat/auth_manual.feature
new file mode 100644 (file)
index 0000000..e7970dd
--- /dev/null
@@ -0,0 +1,28 @@
+@auth @auth_manual
+Feature: Test manual authentication works.
+  In order to check manual authentication
+  As a teacher
+  I need to go on login page and enter username and password.
+
+  Background:
+    Given the following "users" exist:
+      | username |
+      | teacher1 |
+
+  @javascript
+  Scenario: Check login works with javascript.
+    Given I am on homepage
+    And I expand navigation bar
+    And I click on "Log in" "link" in the ".logininfo" "css_element"
+    When I set the field "Username" to "teacher1"
+    And I set the field "Password" to "teacher1"
+    When I press "Log in"
+    Then I should see "You are logged in as"
+
+  Scenario: Check login works without javascript.
+    Given I am on homepage
+    And I click on "Log in" "link" in the ".logininfo" "css_element"
+    When I set the field "Username" to "teacher1"
+    And I set the field "Password" to "teacher1"
+    When I press "Log in"
+    Then I should see "You are logged in as"
index 959c2fb..53349b4 100644 (file)
@@ -28,8 +28,8 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
-use Behat\Behat\Context\Step\When as When;
+use Moodle\BehatExtension\Context\Step\Given as Given;
+use Moodle\BehatExtension\Context\Step\When as When;
 
 /**
  * Log in log out steps definitions.
@@ -47,34 +47,16 @@ class behat_auth extends behat_base {
      * @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)"$/
      */
     public function i_log_in_as($username) {
+        // Visit login page.
+        $this->getSession()->visit($this->locate_path('login/index.php'));
 
-        // Running this step using the API rather than a chained step because
-        // we need to see if the 'Log in' link is available or we need to click
-        // the dropdown to expand the navigation bar before.
-        $this->getSession()->visit($this->locate_path('/'));
+        // Enter username and password.
+        $behatforms = behat_context_helper::get('behat_forms');
+        $behatforms->i_set_the_field_to('Username', $this->escape($username));
+        $behatforms->i_set_the_field_to('Password', $this->escape($username));
 
-        // Generic steps (we will prefix them later expanding the navigation dropdown if necessary).
-        $steps = array(
-            new Given('I click on "' . get_string('login') . '" "link" in the ".logininfo" "css_element"'),
-            new Given('I set the field "' . get_string('username') . '" to "' . $this->escape($username) . '"'),
-            new Given('I set the field "' . get_string('password') . '" to "'. $this->escape($username) . '"'),
-            new Given('I press "' . get_string('login') . '"')
-        );
-
-        // If Javascript is disabled we have enough with these steps.
-        if (!$this->running_javascript()) {
-            return $steps;
-        }
-
-        // Wait for the homepage to be ready.
-        $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
-
-        // If it is needed, it expands the navigation bar with the 'Log in' link.
-        if ($clicknavbar = $this->get_expand_navbar_step()) {
-            array_unshift($steps, $clicknavbar);
-        }
-
-        return $steps;
+        // Press log in button.
+        $behatforms->press_button(get_string('login'));
     }
 
     /**
@@ -101,35 +83,4 @@ class behat_auth extends behat_base {
 
         return $steps;
     }
-
-    /**
-     * Returns a step to open the navigation bar if it is needed.
-     *
-     * The top log in and log out links are hidden when middle or small
-     * size windows (or devices) are used. This step returns a step definition
-     * clicking to expand the navbar if it is hidden.
-     *
-     * @return Given|bool A step definition or false if there is no need to show the navbar.
-     */
-    protected function get_expand_navbar_step() {
-
-        // Checking if we need to click the navbar button to show the navigation menu, it
-        // is hidden by default when using clean theme and a medium or small screen size.
-
-        // The DOM and the JS should be all ready and loaded. Running without spinning
-        // as this is a widely used step and we can not spend time here trying to see
-        // a DOM node that is not always there (at the moment clean is not even the
-        // default theme...).
-        $navbuttonjs = "return (
-            Y.one('.btn-navbar') &&
-            Y.one('.btn-navbar').getComputedStyle('display') !== 'none'
-        )";
-
-        // Adding an extra click we need to show the 'Log in' link.
-        if (!$this->getSession()->getDriver()->evaluateScript($navbuttonjs)) {
-            return false;
-        }
-
-        return new Given('I click on ".btn-navbar" "css_element"');
-    }
 }
index bd0f777..410cdef 100644 (file)
@@ -219,7 +219,6 @@ class behat_backup extends behat_base {
             "/descendant::div[@class='restore-course-search']" .
             "/descendant::tr[contains(., $existingcourse)]" .
             "/descendant::input[@type='radio']");
-        $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore into an existing course section.
@@ -250,7 +249,6 @@ class behat_backup extends behat_base {
         $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
             "/descendant::div[@class='restore-course-search']" .
             "/descendant::input[@type='radio']");
-        $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore into an existing course section.
@@ -280,7 +278,6 @@ class behat_backup extends behat_base {
         // Merge without deleting radio option.
         $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
             "/descendant::input[@type='radio'][@name='target'][@value='1']");
-        $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore merging section.
@@ -310,7 +307,6 @@ class behat_backup extends behat_base {
         // Delete contents radio option.
         $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
             "/descendant::input[@type='radio'][@name='target'][@value='0']");
-        $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore merging section.
@@ -419,8 +415,6 @@ class behat_backup extends behat_base {
             return;
         }
 
-        $pageoptions = clone $options;
-
         $rows = $options->getRows();
         $newrows = array();
         foreach ($rows as $k => $data) {
@@ -433,7 +427,8 @@ class behat_backup extends behat_base {
                 $newrows[] = $data;
             }
         }
-        $pageoptions->setRows($newrows);
+        $pageoptions = new TableNode($newrows);
+
         return $pageoptions;
     }
 
index 60cf807..8c7e910 100644 (file)
@@ -1,14 +1,13 @@
 default:
-  paths:
-    features: lib/behat/features
-    bootstrap: lib/behat/features/bootstrap
-  context:
-    class: behat_init_context
+  suites:
+    default:
+      paths: {  }
+      contexts: {  }
   extensions:
-    Behat\MinkExtension\Extension:
+    Behat\MinkExtension:
       base_url: 'http://localhost:8000'
       goutte: null
       selenium2: null
-    Moodle\BehatExtension\Extension:
-      features: {  }
+    Moodle\BehatExtension:
+      moodledirroot: /Should/Change/To/Moodle/www/dir
       steps_definitions: {  }
index a10250d..2d8143c 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
+use Moodle\BehatExtension\Context\Step\Given as Given;
 
 /**
  * Blocks management steps definitions.
index 70e57b2..9dc24a2 100644 (file)
@@ -26,7 +26,7 @@
 // NOTE: no MOODLE_INTERNAL used, this file may be required by behat before including /config.php.
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
+use Moodle\BehatExtension\Context\Step\Given as Given;
 use Behat\Gherkin\Node\TableNode as TableNode;
 
 /**
index 8c860b5..e1fd621 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
+use Moodle\BehatExtension\Context\Step\Given as Given;
 
 /**
  * Steps definitions for cohort actions.
index f1b4215..0bd4e80 100644 (file)
@@ -27,8 +27,8 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given,
-    Behat\Behat\Context\Step\Then,
+use Moodle\BehatExtension\Context\Step\Given,
+    Moodle\BehatExtension\Context\Step\Then,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
index fdff4bd..f578d7a 100644 (file)
@@ -2,6 +2,6 @@
     "require-dev": {
         "phpunit/phpunit": "4.8.*",
         "phpunit/dbUnit": "1.4.*",
-        "moodlehq/behat-extension": "1.31.0"
+        "moodlehq/behat-extension": "3.31.0"
     }
 }
index c97fa52..7ce9df0 100644 (file)
@@ -4,37 +4,41 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "a957f7332dd1d221be96fb356cb9f03d",
-    "content-hash": "463a75022982e6a64bd9fc0513d9b44c",
+    "hash": "769fa23c4b31f60c9fb82d5b23171e0f",
+    "content-hash": "5fca4c69d043cb1f985fc08cd82a64f8",
     "packages": [],
     "packages-dev": [
         {
             "name": "behat/behat",
-            "version": "v2.5.5",
+            "version": "v3.0.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Behat.git",
-                "reference": "c1e48826b84669c97a1efa78459aedfdcdcf2120"
+                "reference": "b35ae3d45332d80c532af69cc36f780a9397a996"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Behat/zipball/c1e48826b84669c97a1efa78459aedfdcdcf2120",
-                "reference": "c1e48826b84669c97a1efa78459aedfdcdcf2120",
+                "url": "https://api.github.com/repos/Behat/Behat/zipball/b35ae3d45332d80c532af69cc36f780a9397a996",
+                "reference": "b35ae3d45332d80c532af69cc36f780a9397a996",
                 "shasum": ""
             },
             "require": {
-                "behat/gherkin": "~2.3.0",
-                "php": ">=5.3.1",
+                "behat/gherkin": "~4.3",
+                "behat/transliterator": "~1.0",
+                "ext-mbstring": "*",
+                "php": ">=5.3.3",
+                "symfony/class-loader": "~2.1",
                 "symfony/config": "~2.3",
-                "symfony/console": "~2.0",
-                "symfony/dependency-injection": "~2.0",
-                "symfony/event-dispatcher": "~2.0",
-                "symfony/finder": "~2.0",
+                "symfony/console": "~2.1",
+                "symfony/dependency-injection": "~2.1",
+                "symfony/event-dispatcher": "~2.1",
                 "symfony/translation": "~2.3",
-                "symfony/yaml": "~2.0"
+                "symfony/yaml": "~2.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "~3.7.19"
+                "phpspec/prophecy-phpunit": "~1.0",
+                "phpunit/phpunit": "~4.0",
+                "symfony/process": "~2.1"
             },
             "suggest": {
                 "behat/mink-extension": "for integration with Mink testing framework",
                 "bin/behat"
             ],
             "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
             "autoload": {
                 "psr-0": {
-                    "Behat\\Behat": "src/"
+                    "Behat\\Behat": "src/",
+                    "Behat\\Testwork": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
             "description": "Scenario-oriented BDD framework for PHP 5.3",
             "homepage": "http://behat.org/",
             "keywords": [
+                "Agile",
                 "BDD",
-                "Behat",
-                "Symfony2"
+                "ScenarioBDD",
+                "Scrum",
+                "StoryBDD",
+                "User story",
+                "business",
+                "development",
+                "documentation",
+                "examples",
+                "symfony",
+                "testing"
             ],
-            "time": "2015-06-01 09:37:55"
+            "time": "2015-02-22 14:10:33"
         },
         {
             "name": "behat/gherkin",
-            "version": "v2.3.5",
+            "version": "v4.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Gherkin.git",
-                "reference": "2b33963da5525400573560c173ab5c9c057e1852"
+                "reference": "1576b485c0f92ef6d27da9c4bbfc57ee30cf6911"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/2b33963da5525400573560c173ab5c9c057e1852",
-                "reference": "2b33963da5525400573560c173ab5c9c057e1852",
+                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/1576b485c0f92ef6d27da9c4bbfc57ee30cf6911",
+                "reference": "1576b485c0f92ef6d27da9c4bbfc57ee30cf6911",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.1",
-                "symfony/finder": "~2.0"
+                "php": ">=5.3.1"
             },
             "require-dev": {
-                "symfony/config": "~2.0",
-                "symfony/translation": "~2.0",
-                "symfony/yaml": "~2.0"
+                "phpunit/phpunit": "~4.0",
+                "symfony/yaml": "~2.1"
             },
             "suggest": {
-                "symfony/config": "If you want to use Config component to manage resources",
-                "symfony/translation": "If you want to use Symfony2 translations adapter",
                 "symfony/yaml": "If you want to parse features, represented in YAML files"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-develop": "2.2-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "BDD",
                 "Behat",
+                "Cucumber",
                 "DSL",
-                "Symfony2",
+                "gherkin",
                 "parser"
             ],
-            "time": "2013-10-15 11:22:17"
+            "time": "2015-12-30 14:47:00"
         },
         {
             "name": "behat/mink",
-            "version": "v1.5.0",
+            "version": "v1.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/Mink.git",
-                "reference": "0769e6d9726c140a54dbf827a438c0f9912749fe"
+                "reference": "6c129030ec2cc029905cf969a56ca8f087b2dfdf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/Mink/zipball/0769e6d9726c140a54dbf827a438c0f9912749fe",
-                "reference": "0769e6d9726c140a54dbf827a438c0f9912749fe",
+                "url": "https://api.github.com/repos/minkphp/Mink/zipball/6c129030ec2cc029905cf969a56ca8f087b2dfdf",
+                "reference": "6c129030ec2cc029905cf969a56ca8f087b2dfdf",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.1",
-                "symfony/css-selector": "~2.0"
+                "symfony/css-selector": "~2.1"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "~2.7"
             },
             "suggest": {
                 "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-develop": "1.5.x-dev"
+                    "dev-master": "1.7.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Mink": "src/"
+                "psr-4": {
+                    "Behat\\Mink\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://everzet.com"
                 }
             ],
-            "description": "Web acceptance testing framework for PHP 5.3",
+            "description": "Browser controller/emulator abstraction for PHP",
             "homepage": "http://mink.behat.org/",
             "keywords": [
                 "browser",
                 "testing",
                 "web"
             ],
-            "time": "2013-04-13 23:39:27"
+            "time": "2015-09-20 20:24:03"
         },
         {
             "name": "behat/mink-browserkit-driver",
-            "version": "v1.1.0",
+            "version": "v1.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/MinkBrowserKitDriver.git",
-                "reference": "63960c8fcad4529faad1ff33e950217980baa64c"
+                "reference": "2650f5420e713e3807c7f09a07370a4f48367bf9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/63960c8fcad4529faad1ff33e950217980baa64c",
-                "reference": "63960c8fcad4529faad1ff33e950217980baa64c",
+                "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/2650f5420e713e3807c7f09a07370a4f48367bf9",
+                "reference": "2650f5420e713e3807c7f09a07370a4f48367bf9",
                 "shasum": ""
             },
             "require": {
-                "behat/mink": "~1.5.0",
-                "php": ">=5.3.1",
-                "symfony/browser-kit": "~2.0",
-                "symfony/dom-crawler": "~2.0"
+                "behat/mink": "~1.7@dev",
+                "php": ">=5.3.6",
+                "symfony/browser-kit": "~2.3|~3.0",
+                "symfony/dom-crawler": "~2.3|~3.0"
             },
             "require-dev": {
-                "silex/silex": "@dev"
+                "silex/silex": "~1.2",
+                "symfony/phpunit-bridge": "~2.7|~3.0"
             },
             "type": "mink-driver",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1.x-dev"
+                    "dev-master": "1.3.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Mink\\Driver": "src/"
+                "psr-4": {
+                    "Behat\\Mink\\Driver\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "browser",
                 "testing"
             ],
-            "time": "2013-04-13 23:46:30"
+            "time": "2016-01-19 16:59:07"
         },
         {
             "name": "behat/mink-extension",
-            "version": "v1.3.3",
+            "version": "v2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/MinkExtension.git",
-                "reference": "b885b9407cba50a954f72c69ed1b2f8d3bc694f8"
+                "reference": "5b4bda64ff456104564317e212c823e45cad9d59"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/b885b9407cba50a954f72c69ed1b2f8d3bc694f8",
-                "reference": "b885b9407cba50a954f72c69ed1b2f8d3bc694f8",
+                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/5b4bda64ff456104564317e212c823e45cad9d59",
+                "reference": "5b4bda64ff456104564317e212c823e45cad9d59",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "~2.5.0",
+                "behat/behat": "~3.0,>=3.0.5",
                 "behat/mink": "~1.5",
                 "php": ">=5.3.2",
-                "symfony/config": "~2.2"
+                "symfony/config": "~2.2|~3.0"
             },
             "require-dev": {
-                "behat/mink-goutte-driver": "~1.0",
-                "fabpot/goutte": "~1.0"
+                "behat/mink-goutte-driver": "~1.1",
+                "phpspec/phpspec": "~2.0"
             },
             "type": "behat-extension",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.1.x-dev"
+                }
+            },
             "autoload": {
                 "psr-0": {
                     "Behat\\MinkExtension": "src/"
                 "MIT"
             ],
             "authors": [
+                {
+                    "name": "Christophe Coevoet",
+                    "email": "stof@notk.org"
+                },
                 {
                     "name": "Konstantin Kudryashov",
-                    "email": "ever.zet@gmail.com",
-                    "homepage": "http://everzet.com"
+                    "email": "ever.zet@gmail.com"
                 }
             ],
             "description": "Mink extension for Behat",
-            "homepage": "http://mink.behat.org",
+            "homepage": "http://extensions.behat.org/mink",
             "keywords": [
                 "browser",
                 "gui",
                 "test",
                 "web"
             ],
-            "time": "2014-05-15 19:27:39"
+            "time": "2016-02-15 07:55:18"
         },
         {
             "name": "behat/mink-goutte-driver",
-            "version": "v1.0.9",
+            "version": "v1.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/MinkGoutteDriver.git",
-                "reference": "fa1b073b48761464feb0b05e6825da44b20118d8"
+                "reference": "c8e254f127d6f2242b994afd4339fb62d471df3f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/fa1b073b48761464feb0b05e6825da44b20118d8",
-                "reference": "fa1b073b48761464feb0b05e6825da44b20118d8",
+                "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/c8e254f127d6f2242b994afd4339fb62d471df3f",
+                "reference": "c8e254f127d6f2242b994afd4339fb62d471df3f",
                 "shasum": ""
             },
             "require": {
-                "behat/mink-browserkit-driver": ">=1.0.5,<1.2.0",
-                "fabpot/goutte": "~1.0.1",
+                "behat/mink": "~1.6@dev",
+                "behat/mink-browserkit-driver": "~1.2@dev",
+                "fabpot/goutte": "~1.0.4|~2.0|~3.1",
                 "php": ">=5.3.1"
             },
+            "require-dev": {
+                "symfony/phpunit-bridge": "~2.7"
+            },
             "type": "mink-driver",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.2.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Mink\\Driver": "src/"
+                "psr-4": {
+                    "Behat\\Mink\\Driver\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "headless",
                 "testing"
             ],
-            "time": "2013-07-03 18:43:54"
+            "time": "2015-09-21 21:31:11"
         },
         {
             "name": "behat/mink-selenium2-driver",
-            "version": "v1.1.1",
+            "version": "v1.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/MinkSelenium2Driver.git",
-                "reference": "bcf1b537de37db6db0822d9e7bd97e600fd7a476"
+                "reference": "bedbf1999c7ba1bc6921b30ee2eadf383e7ff5c9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/bcf1b537de37db6db0822d9e7bd97e600fd7a476",
-                "reference": "bcf1b537de37db6db0822d9e7bd97e600fd7a476",
+                "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/bedbf1999c7ba1bc6921b30ee2eadf383e7ff5c9",
+                "reference": "bedbf1999c7ba1bc6921b30ee2eadf383e7ff5c9",
                 "shasum": ""
             },
             "require": {
-                "behat/mink": "~1.5.0",
-                "instaclick/php-webdriver": "~1.0.12",
+                "behat/mink": "~1.7@dev",
+                "instaclick/php-webdriver": "~1.1",
                 "php": ">=5.3.1"
             },
+            "require-dev": {
+                "symfony/phpunit-bridge": "~2.7"
+            },
             "type": "mink-driver",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1.x-dev"
+                    "dev-master": "1.3.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Mink\\Driver": "src/"
+                "psr-4": {
+                    "Behat\\Mink\\Driver\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "testing",
                 "webdriver"
             ],
-            "time": "2013-06-02 19:09:45"
+            "time": "2015-09-21 21:02:54"
+        },
+        {
+            "name": "behat/transliterator",
+            "version": "v1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Behat/Transliterator.git",
+                "reference": "868e05be3a9f25ba6424c2dd4849567f50715003"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Behat/Transliterator/zipball/868e05be3a9f25ba6424c2dd4849567f50715003",
+                "reference": "868e05be3a9f25ba6424c2dd4849567f50715003",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Behat\\Transliterator": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Artistic-1.0"
+            ],
+            "description": "String transliterator",
+            "keywords": [
+                "i18n",
+                "slug",
+                "transliterator"
+            ],
+            "time": "2015-09-28 16:26:35"
         },
         {
             "name": "doctrine/instantiator",
         },
         {
             "name": "fabpot/goutte",
-            "version": "v1.0.7",
+            "version": "v2.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/Goutte.git",
-                "reference": "794b196e76bdd37b5155cdecbad311f0a3b07625"
+                "reference": "0ad3ee6dc2d0aaa832a80041a1e09bf394e99802"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/794b196e76bdd37b5155cdecbad311f0a3b07625",
-                "reference": "794b196e76bdd37b5155cdecbad311f0a3b07625",
+                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/0ad3ee6dc2d0aaa832a80041a1e09bf394e99802",
+                "reference": "0ad3ee6dc2d0aaa832a80041a1e09bf394e99802",
                 "shasum": ""
             },
             "require": {
-                "ext-curl": "*",
-                "guzzle/http": "~3.1",
-                "php": ">=5.3.0",
+                "guzzlehttp/guzzle": ">=4,<6",
+                "php": ">=5.4.0",
                 "symfony/browser-kit": "~2.1",
                 "symfony/css-selector": "~2.1",
-                "symfony/dom-crawler": "~2.1",
-                "symfony/finder": "~2.1",
-                "symfony/process": "~2.1"
-            },
-            "require-dev": {
-                "guzzle/plugin-history": "~3.1",
-                "guzzle/plugin-mock": "~3.1"
+                "symfony/dom-crawler": "~2.1"
             },
             "type": "application",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "2.0-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Goutte": "."
+                "psr-4": {
+                    "Goutte\\": "Goutte"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 }
             ],
             "description": "A simple PHP Web Scraper",
-            "homepage": "https://github.com/fabpot/Goutte",
+            "homepage": "https://github.com/FriendsOfPHP/Goutte",
             "keywords": [
                 "scraper"
             ],
-            "time": "2014-10-09 15:52:51"
+            "time": "2015-05-05 21:14:57"
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "v3.8.1",
+            "version": "5.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba"
+                "reference": "f3c8c22471cb55475105c14769644a49c3262b93"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba",
-                "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f3c8c22471cb55475105c14769644a49c3262b93",
+                "reference": "f3c8c22471cb55475105c14769644a49c3262b93",
                 "shasum": ""
             },
             "require": {
-                "ext-curl": "*",
-                "php": ">=5.3.3",
-                "symfony/event-dispatcher": ">=2.1"
-            },
-            "replace": {
-                "guzzle/batch": "self.version",
-                "guzzle/cache": "self.version",
-                "guzzle/common": "self.version",
-                "guzzle/http": "self.version",
-                "guzzle/inflection": "self.version",
-                "guzzle/iterator": "self.version",
-                "guzzle/log": "self.version",
-                "guzzle/parser": "self.version",
-                "guzzle/plugin": "self.version",
-                "guzzle/plugin-async": "self.version",
-                "guzzle/plugin-backoff": "self.version",
-                "guzzle/plugin-cache": "self.version",
-                "guzzle/plugin-cookie": "self.version",
-                "guzzle/plugin-curlauth": "self.version",
-                "guzzle/plugin-error-response": "self.version",
-                "guzzle/plugin-history": "self.version",
-                "guzzle/plugin-log": "self.version",
-                "guzzle/plugin-md5": "self.version",
-                "guzzle/plugin-mock": "self.version",
-                "guzzle/plugin-oauth": "self.version",
-                "guzzle/service": "self.version",
-                "guzzle/stream": "self.version"
+                "guzzlehttp/ringphp": "^1.1",
+                "php": ">=5.4.0"
             },
             "require-dev": {
-                "doctrine/cache": "*",
-                "monolog/monolog": "1.*",
-                "phpunit/phpunit": "3.7.*",
-                "psr/log": "1.0.*",
-                "symfony/class-loader": "*",
-                "zendframework/zend-cache": "<2.3",
-                "zendframework/zend-log": "<2.3"
+                "ext-curl": "*",
+                "phpunit/phpunit": "^4.0",
+                "psr/log": "^1.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.8-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Guzzle": "src/",
-                    "Guzzle\\Tests": "tests/"
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "name": "Michael Dowling",
                     "email": "mtdowling@gmail.com",
                     "homepage": "https://github.com/mtdowling"
-                },
-                {
-                    "name": "Guzzle Community",
-                    "homepage": "https://github.com/guzzle/guzzle/contributors"
                 }
             ],
             "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients",
                 "rest",
                 "web service"
             ],
-            "time": "2014-01-28 22:29:15"
+            "time": "2015-05-20 03:47:55"
+        },
+        {
+            "name": "guzzlehttp/ringphp",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/RingPHP.git",
+                "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/dbbb91d7f6c191e5e405e900e3102ac7f261bc0b",
+                "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/streams": "~3.0",
+                "php": ">=5.4.0",
+                "react/promise": "~2.0"
+            },
+            "require-dev": {
+                "ext-curl": "*",
+                "phpunit/phpunit": "~4.0"
+            },
+            "suggest": {
+                "ext-curl": "Guzzle will use specific adapters if cURL is present"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Ring\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
+            "time": "2015-05-20 03:37:09"
+        },
+        {
+            "name": "guzzlehttp/streams",
+            "version": "3.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/streams.git",
+                "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
+                "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Stream\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Provides a simple abstraction over streams of data",
+            "homepage": "http://guzzlephp.org/",
+            "keywords": [
+                "Guzzle",
+                "stream"
+            ],
+            "time": "2014-10-12 19:18:40"
         },
         {
             "name": "instaclick/php-webdriver",
-            "version": "1.0.17",
+            "version": "1.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/instaclick/php-webdriver.git",
-                "reference": "47a6019553a7a5b42d35493276ffc2c9252c53d5"
+                "reference": "0c20707dcf30a32728fd6bdeeab996c887fdb2fb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/47a6019553a7a5b42d35493276ffc2c9252c53d5",
-                "reference": "47a6019553a7a5b42d35493276ffc2c9252c53d5",
+                "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/0c20707dcf30a32728fd6bdeeab996c887fdb2fb",
+                "reference": "0c20707dcf30a32728fd6bdeeab996c887fdb2fb",
                 "shasum": ""
             },
             "require": {
                 "ext-curl": "*",
                 "php": ">=5.3.2"
             },
-            "bin": [
-                "bin/webunit"
-            ],
+            "require-dev": {
+                "satooshi/php-coveralls": "dev-master"
+            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.4.x-dev"
                 }
             },
             "autoload": {
                 {
                     "name": "Anthon Pang",
                     "email": "apang@softwaredevelopment.ca",
-                    "role": "developer"
+                    "role": "Fork Maintainer"
                 }
             ],
             "description": "PHP WebDriver for Selenium 2",
                 "webdriver",
                 "webtest"
             ],
-            "time": "2013-10-04 15:03:51"
+            "time": "2015-06-15 20:19:33"
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v1.31.0",
+            "version": "v3.31.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "b4dd6f7d1ca1c89a44b8b010af8546d7a2808b7b"
+                "reference": "d985e9da29914b0da90d61c47aadc455586eeee5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/b4dd6f7d1ca1c89a44b8b010af8546d7a2808b7b",
-                "reference": "b4dd6f7d1ca1c89a44b8b010af8546d7a2808b7b",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d985e9da29914b0da90d61c47aadc455586eeee5",
+                "reference": "d985e9da29914b0da90d61c47aadc455586eeee5",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "2.5.5",
-                "behat/mink": "1.5.0",
-                "behat/mink-extension": "1.3.3",
-                "behat/mink-goutte-driver": "1.0.9",
-                "behat/mink-selenium2-driver": "1.1.1",
-                "guzzlehttp/guzzle": "~3.1",
+                "behat/behat": "3.0.*",
+                "behat/mink": "~1.7",
+                "behat/mink-extension": "~2.1",
+                "behat/mink-goutte-driver": "~1.2",
+                "behat/mink-selenium2-driver": "~1.3",
                 "php": ">=5.4.4",
-                "symfony/browser-kit": "2.7.5",
-                "symfony/css-selector": "2.7.5",
-                "symfony/dom-crawler": "2.7.5",
-                "symfony/filesystem": "2.7.5",
-                "symfony/finder": "2.7.5"
+                "symfony/process": "2.8.*"
             },
             "type": "library",
             "autoload": {
                 "Behat",
                 "moodle"
             ],
-            "time": "2016-01-05 02:55:24"
+            "time": "2016-03-04 07:15:40"
         },
         {
             "name": "phpdocumentor/reflection-docblock",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "v1.5.0",
+            "version": "v1.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7"
+                "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7",
-                "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972",
+                "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.0.2",
+                "php": "^5.3|^7.0",
                 "phpdocumentor/reflection-docblock": "~2.0",
-                "sebastian/comparator": "~1.1"
+                "sebastian/comparator": "~1.1",
+                "sebastian/recursion-context": "~1.0"
             },
             "require-dev": {
                 "phpspec/phpspec": "~2.0"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.4.x-dev"
+                    "dev-master": "1.5.x-dev"
                 }
             },
             "autoload": {
                 "spy",
                 "stub"
             ],
-            "time": "2015-08-13 10:07:40"
+            "time": "2016-02-15 07:46:21"
         },
         {
             "name": "phpunit/dbunit",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "4.8.21",
+            "version": "4.8.23",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "ea76b17bced0500a28098626b84eda12dbcf119c"
+                "reference": "6e351261f9cd33daf205a131a1ba61c6d33bd483"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea76b17bced0500a28098626b84eda12dbcf119c",
-                "reference": "ea76b17bced0500a28098626b84eda12dbcf119c",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6e351261f9cd33daf205a131a1ba61c6d33bd483",
+                "reference": "6e351261f9cd33daf205a131a1ba61c6d33bd483",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2015-12-12 07:45:58"
+            "time": "2016-02-11 14:56:33"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
             ],
             "time": "2015-10-02 06:51:40"
         },
+        {
+            "name": "react/promise",
+            "version": "v2.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/reactphp/promise.git",
+                "reference": "3aacad8bf10c7d83e6fa2089d413529888c2bedf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/reactphp/promise/zipball/3aacad8bf10c7d83e6fa2089d413529888c2bedf",
+                "reference": "3aacad8bf10c7d83e6fa2089d413529888c2bedf",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "React\\Promise\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jan Sorgalla",
+                    "email": "jsorgalla@gmail.com"
+                }
+            ],
+            "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+            "time": "2016-02-26 19:09:02"
+        },
         {
             "name": "sebastian/comparator",
             "version": "1.2.0",
         },
         {
             "name": "sebastian/environment",
-            "version": "1.3.3",
+            "version": "1.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "6e7133793a8e5a5714a551a8324337374be209df"
+                "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e7133793a8e5a5714a551a8324337374be209df",
-                "reference": "6e7133793a8e5a5714a551a8324337374be209df",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
+                "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
                 "shasum": ""
             },
             "require": {
                 "environment",
                 "hhvm"
             ],
-            "time": "2015-12-02 08:37:27"
+            "time": "2016-02-26 18:40:46"
         },
         {
             "name": "sebastian/exporter",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v2.7.5",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "277a2457776d4cc25706fbdd9d1e4ab2dac884e4"
+                "reference": "6b2085020b4e86fcb7ae44c3ab8ddb91774b33d2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/277a2457776d4cc25706fbdd9d1e4ab2dac884e4",
-                "reference": "277a2457776d4cc25706fbdd9d1e4ab2dac884e4",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/6b2085020b4e86fcb7ae44c3ab8ddb91774b33d2",
+                "reference": "6b2085020b4e86fcb7ae44c3ab8ddb91774b33d2",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.9",
-                "symfony/dom-crawler": "~2.0,>=2.0.5"
+                "symfony/dom-crawler": "~2.0,>=2.0.5|~3.0.0"
             },
             "require-dev": {
-                "symfony/css-selector": "~2.0,>=2.0.5",
-                "symfony/phpunit-bridge": "~2.7",
-                "symfony/process": "~2.0,>=2.0.5"
+                "symfony/css-selector": "~2.0,>=2.0.5|~3.0.0",
+                "symfony/process": "~2.3.34|~2.7,>=2.7.6|~3.0.0"
             },
             "suggest": {
                 "symfony/process": ""
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.7-dev"
+                    "dev-master": "2.8-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\BrowserKit\\": ""
-                }
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2015-09-06 08:36:38"
+            "time": "2016-01-27 11:34:40"
+        },
+        {
+            "name": "symfony/class-loader",
+            "version": "v2.8.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/class-loader.git",
+                "reference": "517ab0554b6a5744d04480cb06873ffbd9442d73"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/517ab0554b6a5744d04480cb06873ffbd9442d73",
+                "reference": "517ab0554b6a5744d04480cb06873ffbd9442d73",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9",
+                "symfony/polyfill-apcu": "~1.1"
+            },
+            "require-dev": {
+                "symfony/finder": "~2.0,>=2.0.5|~3.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.8-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\ClassLoader\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony ClassLoader Component",
+            "homepage": "https://symfony.com",
+            "time": "2016-01-30 15:58:35"
         },
         {
             "name": "symfony/config",
-            "version": "v2.8.1",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "17d4b2e64ce1c6ba7caa040f14469b3c44d7f7d2"
+                "reference": "0f8f94e6a32b5c480024eed5fa5cbd2790d0ad19"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/17d4b2e64ce1c6ba7caa040f14469b3c44d7f7d2",
-                "reference": "17d4b2e64ce1c6ba7caa040f14469b3c44d7f7d2",
+                "url": "https://api.github.com/repos/symfony/config/zipball/0f8f94e6a32b5c480024eed5fa5cbd2790d0ad19",
+                "reference": "0f8f94e6a32b5c480024eed5fa5cbd2790d0ad19",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.9",
                 "symfony/filesystem": "~2.3|~3.0.0"
             },
+            "suggest": {
+                "symfony/yaml": "To use the yaml reference dumper"
+            },
             "type": "library",
             "extra": {
                 "branch-alias": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2015-12-26 13:37:56"
+            "time": "2016-02-22 16:12:45"
         },
         {
             "name": "symfony/console",
-            "version": "v2.8.1",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "2e06a5ccb19dcf9b89f1c6a677a39a8df773635a"
+                "reference": "56cc5caf051189720b8de974e4746090aaa10d44"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/2e06a5ccb19dcf9b89f1c6a677a39a8df773635a",
-                "reference": "2e06a5ccb19dcf9b89f1c6a677a39a8df773635a",
+                "url": "https://api.github.com/repos/symfony/console/zipball/56cc5caf051189720b8de974e4746090aaa10d44",
+                "reference": "56cc5caf051189720b8de974e4746090aaa10d44",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2015-12-22 10:25:57"
+            "time": "2016-02-28 16:20:50"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v2.7.5",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "abe19cc0429a06be0c133056d1f9859854860970"
+                "reference": "8d83ff9777cdbd83e7f90d9c48f4729823791a5e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/abe19cc0429a06be0c133056d1f9859854860970",
-                "reference": "abe19cc0429a06be0c133056d1f9859854860970",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/8d83ff9777cdbd83e7f90d9c48f4729823791a5e",
+                "reference": "8d83ff9777cdbd83e7f90d9c48f4729823791a5e",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.9"
             },
-            "require-dev": {
-                "symfony/phpunit-bridge": "~2.7"
-            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.7-dev"
+                    "dev-master": "2.8-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\CssSelector\\": ""
-                }
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2015-09-22 13:49:29"
+            "time": "2016-01-27 05:14:19"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v2.8.1",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "c5086d186f538c2711b9af6f727be7b0446979cd"
+                "reference": "62251761a7615435b22ccf562384c588b431be44"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c5086d186f538c2711b9af6f727be7b0446979cd",
-                "reference": "c5086d186f538c2711b9af6f727be7b0446979cd",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/62251761a7615435b22ccf562384c588b431be44",
+                "reference": "62251761a7615435b22ccf562384c588b431be44",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2015-12-26 13:37:56"
+            "time": "2016-02-28 16:34:46"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v2.7.5",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "2e185ca136399f902b948694987e62c80099c052"
+                "reference": "e1a4b4c83f5ee6f5902f1d53035e3718909a0c11"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2e185ca136399f902b948694987e62c80099c052",
-                "reference": "2e185ca136399f902b948694987e62c80099c052",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/e1a4b4c83f5ee6f5902f1d53035e3718909a0c11",
+                "reference": "e1a4b4c83f5ee6f5902f1d53035e3718909a0c11",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9"
+                "php": ">=5.3.9",
+                "symfony/polyfill-mbstring": "~1.0"
             },
             "require-dev": {
-                "symfony/css-selector": "~2.3",
-                "symfony/phpunit-bridge": "~2.7"
+                "symfony/css-selector": "~2.8|~3.0.0"
             },
             "suggest": {
                 "symfony/css-selector": ""
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.7-dev"
+                    "dev-master": "2.8-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\DomCrawler\\": ""
-                }
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2015-09-20 21:13:58"
+            "time": "2016-02-28 16:20:50"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v2.8.1",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "a5eb815363c0388e83247e7e9853e5dbc14999cc"
+                "reference": "78c468665c9568c3faaa9c416a7134308f2d85c3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5eb815363c0388e83247e7e9853e5dbc14999cc",
-                "reference": "a5eb815363c0388e83247e7e9853e5dbc14999cc",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/78c468665c9568c3faaa9c416a7134308f2d85c3",
+                "reference": "78c468665c9568c3faaa9c416a7134308f2d85c3",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2015-10-30 20:15:42"
+            "time": "2016-01-27 05:14:19"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v2.7.5",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "a17f8a17c20e8614c15b8e116e2f4bcde102cfab"
+                "reference": "65cb36b6539b1d446527d60457248f30d045464d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/a17f8a17c20e8614c15b8e116e2f4bcde102cfab",
-                "reference": "a17f8a17c20e8614c15b8e116e2f4bcde102cfab",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/65cb36b6539b1d446527d60457248f30d045464d",
+                "reference": "65cb36b6539b1d446527d60457248f30d045464d",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.9"
             },
-            "require-dev": {
-                "symfony/phpunit-bridge": "~2.7"
-            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.7-dev"
+                    "dev-master": "2.8-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
                     "Symfony\\Component\\Filesystem\\": ""
-                }
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2015-09-09 17:42:36"
+            "time": "2016-02-22 15:02:30"
         },
         {
-            "name": "symfony/finder",
-            "version": "v2.7.5",
+            "name": "symfony/polyfill-apcu",
+            "version": "v1.1.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/finder.git",
-                "reference": "8262ab605973afbb3ef74b945daabf086f58366f"
+                "url": "https://github.com/symfony/polyfill-apcu.git",
+                "reference": "d1911e6caeb4b6a4c8e2d5c46b978a66b3745e4c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/8262ab605973afbb3ef74b945daabf086f58366f",
-                "reference": "8262ab605973afbb3ef74b945daabf086f58366f",
+                "url": "https://api.github.com/repos/symfony/polyfill-apcu/zipball/d1911e6caeb4b6a4c8e2d5c46b978a66b3745e4c",
+                "reference": "d1911e6caeb4b6a4c8e2d5c46b978a66b3745e4c",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9"
-            },
-            "require-dev": {
-                "symfony/phpunit-bridge": "~2.7"
+                "php": ">=5.3.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.7-dev"
+                    "dev-master": "1.1-dev"
                 }
             },
             "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Finder\\": ""
-                }
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             ],
             "authors": [
                 {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
                 },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Finder Component",
+            "description": "Symfony polyfill backporting apcu_* functions to lower PHP versions",
             "homepage": "https://symfony.com",
-            "time": "2015-09-19 19:59:23"
+            "keywords": [
+                "apcu",
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2016-01-20 09:13:37"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.0.1",
+            "version": "v1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "49ff736bd5d41f45240cec77b44967d76e0c3d25"
+                "reference": "1289d16209491b584839022f29257ad859b8532d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/49ff736bd5d41f45240cec77b44967d76e0c3d25",
-                "reference": "49ff736bd5d41f45240cec77b44967d76e0c3d25",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/1289d16209491b584839022f29257ad859b8532d",
+                "reference": "1289d16209491b584839022f29257ad859b8532d",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "1.1-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2015-11-20 09:19:13"
+            "time": "2016-01-20 09:13:37"
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.1",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "62c254438b5040bc2217156e1570cf2206e8540c"
+                "reference": "7dedd5b60550f33dca16dd7e94ef8aca8b67bbfe"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/62c254438b5040bc2217156e1570cf2206e8540c",
-                "reference": "62c254438b5040bc2217156e1570cf2206e8540c",
+                "url": "https://api.github.com/repos/symfony/process/zipball/7dedd5b60550f33dca16dd7e94ef8aca8b67bbfe",
+                "reference": "7dedd5b60550f33dca16dd7e94ef8aca8b67bbfe",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2015-12-23 11:03:46"
+            "time": "2016-02-02 13:33:15"
         },
         {
             "name": "symfony/translation",
-            "version": "v2.8.1",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "c1db87c51251167dd91198b9d1edf897773adb4f"
+                "reference": "b7b4ebadd2b5e614ff7d2d6fc63e0ed0578909c7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/c1db87c51251167dd91198b9d1edf897773adb4f",
-                "reference": "c1db87c51251167dd91198b9d1edf897773adb4f",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/b7b4ebadd2b5e614ff7d2d6fc63e0ed0578909c7",
+                "reference": "b7b4ebadd2b5e614ff7d2d6fc63e0ed0578909c7",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2015-12-05 17:37:59"
+            "time": "2016-02-02 09:49:18"
         },
         {
             "name": "symfony/yaml",
-            "version": "v2.8.1",
+            "version": "v2.8.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "ac84cbb98b68a6abbc9f5149eb96ccc7b07b8966"
+                "reference": "2a4ee40acb880c56f29fb1b8886e7ffe94f3b995"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/ac84cbb98b68a6abbc9f5149eb96ccc7b07b8966",
-                "reference": "ac84cbb98b68a6abbc9f5149eb96ccc7b07b8966",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/2a4ee40acb880c56f29fb1b8886e7ffe94f3b995",
+                "reference": "2a4ee40acb880c56f29fb1b8886e7ffe94f3b995",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2015-12-26 13:37:56"
+            "time": "2016-02-23 07:41:20"
         }
     ],
     "aliases": [],
index 8d8d481..cdf3de9 100644 (file)
@@ -673,18 +673,16 @@ $CFG->admin = 'admin';
 // params hierarchy. More info: http://docs.behat.org/guides/7.config.html
 // Example:
 //   $CFG->behat_config = array(
-//       'default' => array(
-//           'formatter' => array(
-//               'name' => 'pretty',
-//               'parameters' => array(
-//                   'decorated' => true,
-//                   'verbose' => false
-//               )
-//           )
-//       ),
 //       'Mac-Firefox' => array(
+//           'suites' => array (
+//               'default' => array(
+//                   'filters' => array(
+//                      'tags' => '~@_file_upload'
+//                   ),
+//               ),
+//           ),
 //           'extensions' => array(
-//               'Behat\MinkExtension\Extension' => array(
+//               'Behat\MinkExtension' => array(
 //                   'selenium2' => array(
 //                       'browser' => 'firefox',
 //                       'capabilities' => array(
@@ -697,7 +695,7 @@ $CFG->admin = 'admin';
 //       ),
 //       'Mac-Safari' => array(
 //           'extensions' => array(
-//               'Behat\MinkExtension\Extension' => array(
+//               'Behat\MinkExtension' => array(
 //                   'selenium2' => array(
 //                       'browser' => 'safari',
 //                       'capabilities' => array(
@@ -709,6 +707,20 @@ $CFG->admin = 'admin';
 //           )
 //       )
 //   );
+// You can also use the following config to override default Moodle configuration for Behat.
+// This config is limited to default suite and will be supported in later versions.
+// It will have precedence over $CFG->behat_config.
+// $CFG->behat_profiles = array(
+//     'phantomjs' => array(
+//         'browser' => 'phantomjs',
+//         'tags' => '~@_file_upload&&~@_alert&&~@_bug_phantomjs',
+//         'wd_host' => 'http://127.0.0.1:4443/wd/hub',
+//         'capabilities' => array(
+//             'platform' => 'Linux',
+//             'version' => 2.1
+//         )
+//     ),
+// );
 //
 // You can force the browser session (not user's sessions) to restart after N seconds. This could
 // be useful if you are using a cloud-based service with time restrictions in the browser side.
index 344a208..09ee8d4 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given,
+use Moodle\BehatExtension\Context\Step\Given as Given,
     Behat\Gherkin\Node\TableNode as TableNode,
     Behat\Mink\Exception\ExpectationException as ExpectationException,
     Behat\Mink\Exception\DriverException as DriverException,
@@ -97,7 +97,7 @@ class behat_course extends behat_base {
                     unset($rows[$key]);
                 }
             }
-            $table->setRows($rows);
+            $table = new TableNode($rows);
 
             // Adding a forced wait until editors are loaded as otherwise selenium sometimes tries clicks on the
             // format field when the editor is being rendered and the click misses the field coordinates.
@@ -585,7 +585,7 @@ class behat_course extends behat_base {
 
             // The 'Hide' button should be available.
             $nohideexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('hide') . '" icon', $this->getSession());
-            $this->find('named', array('link', get_string('hide')), $nohideexception, $activitynode);
+            $this->find('named_partial', array('link', get_string('hide')), $nohideexception, $activitynode);
         }
     }
 
@@ -611,7 +611,7 @@ class behat_course extends behat_base {
 
             // Also 'Show' icon.
             $noshowexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('show') . '" icon', $this->getSession());
-            $this->find('named', array('link', get_string('show')), $noshowexception, $activitynode);
+            $this->find('named_partial', array('link', get_string('show')), $noshowexception, $activitynode);
 
         } else {
 
index dc35898..42dacac 100644 (file)
@@ -105,7 +105,6 @@ Feature: Test we can resort categories in the management interface.
     And I should see the "Course categories and courses" management page
     And I click on <sortby> action for "Master cat" in management category listing
     And a new page should have loaded since I started watching
-    And I start watching to see if a new page loads
     And I should see the "Course categories and courses" management page
     And I should see category listing <cat1> before <cat2>
     And I should see category listing <cat2> before <cat3>
index cee3ccd..f117146 100644 (file)
@@ -86,7 +86,6 @@ Feature: Test we can resort course in the management interface.
     And I should see "Sort by Course time created descending" in the ".course-listing-actions" "css_element"
     And I click on <sortby> "link" in the ".course-listing-actions" "css_element"
     And a new page should have loaded since I started watching
-    And I start watching to see if a new page loads
     And I should see the "Course categories and courses" management page
     And I should see course listing <course1> before <course2>
     And I should see course listing <course2> before <course3>
index 116e0a2..034898c 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given,
+use Moodle\BehatExtension\Context\Step\Given as Given,
     Behat\Gherkin\Node\TableNode as TableNode;
 
 /**
index 49ab967..8384259 100644 (file)
@@ -263,7 +263,7 @@ if (!$moving) {
 if (!$moving && count($grade_edit_tree->categories) > 1) {
     echo '<br /><br />';
     echo '<input type="hidden" name="bulkmove" value="0" id="bulkmoveinput" />';
-    $attributes = array('id'=>'menumoveafter', 'class' => 'ignoredirty');
+    $attributes = array('id'=>'menumoveafter', 'class' => 'ignoredirty singleselect');
     echo html_writer::label(get_string('moveselectedto', 'grades'), 'menumoveafter');
     echo html_writer::select($grade_edit_tree->categories, 'moveafter', '', array(''=>'choosedots'), $attributes);
     $OUTPUT->add_action_handler(new component_action('change', 'submit_bulk_move'), 'menumoveafter');
index 7ce443c..e63fc5d 100644 (file)
@@ -26,9 +26,9 @@
 require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
 
 use Behat\Gherkin\Node\TableNode as TableNode,
-    Behat\Behat\Context\Step\Given as Given,
-    Behat\Behat\Context\Step\When as When,
-    Behat\Behat\Context\Step\Then as Then,
+    Moodle\BehatExtension\Context\Step\Given as Given,
+    Moodle\BehatExtension\Context\Step\When as When,
+    Moodle\BehatExtension\Context\Step\Then as Then,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
     Behat\Mink\Exception\ExpectationException as ExpectationException;
 
index e01387b..778c7ff 100644 (file)
@@ -28,9 +28,7 @@
 require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
 
 use Behat\Gherkin\Node\TableNode as TableNode,
-    Behat\Behat\Context\Step\Given as Given,
-    Behat\Behat\Context\Step\When as When,
-    Behat\Behat\Context\Step\Then as Then,
+    Moodle\BehatExtension\Context\Step\Given as Given,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
     Behat\Mink\Exception\ExpectationException as ExpectationException;
 
@@ -107,6 +105,15 @@ class behat_gradingform_rubric extends behat_base {
                     }
                 }
 
+                // Remove empty criterion, as TableNode might contain them to make table rows equal size.
+                $newcriterion = array();
+                foreach ($criterion as $k => $c) {
+                    if (!empty($c)) {
+                        $newcriterion[$k] = $c;
+                    }
+                }
+                $criterion = $newcriterion;
+
                 // Checking the number of cells.
                 if (count($criterion) % 2 === 0) {
                     throw new ExpectationException(
index 0a0bf07..f76117c 100644 (file)
@@ -28,8 +28,8 @@
 require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
 
 use Behat\Gherkin\Node\TableNode as TableNode,
-    Behat\Behat\Context\Step\Given as Given,
-    Behat\Behat\Context\Step\When as When;
+    Moodle\BehatExtension\Context\Step\Given as Given,
+    Moodle\BehatExtension\Context\Step\When as When;
 
 /**
  * Generic grading methods step definitions.
@@ -93,7 +93,7 @@ class behat_grading extends behat_base {
 
         // Shortcut in case we already are in the grading page.
         $usergradetextliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($usergradetext);
-        if ($this->getSession()->getPage()->find('named', array('link', $usergradetextliteral))) {
+        if ($this->getSession()->getPage()->find('named_partial', array('link', $usergradetextliteral))) {
             return $gradeuserstep;
         }
 
index 880a341..d341ffd 100644 (file)
@@ -25,22 +25,25 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe
       | name       | scale                                  |
       | Test Scale | Disappointing,Good,Very good,Excellent |
     And the following "grade categories" exist:
-      | fullname | course |
-      | Grade Cat | C1 |
+      | fullname  | course |
+      | Grade Cat | C1     |
+    And the following "grade categories" exist:
+      | fullname  | course | gradecategory |
+      | Grade Sub Cat  | C1 | Grade Cat |
     And the following "grade items" exist:
       | itemname | course | locked | gradetype | gradecategory |
-      | Item 1 | C1 | 0 | value | Grade Cat |
+      | Item 1  | C1 | 0 | value | Grade Cat |
       | Item VU | C1 | 0 | value | Grade Cat |
       | Item VL | C1 | 1 | value | Grade Cat |
-      | Item TU | C1 | 0 | text | Grade Cat |
-      | Item TL | C1 | 1 | text | Grade Cat |
-    And the following "grade items" exist:
-      | itemname | course | locked | gradetype | scale | gradecategory |
-      | Item SU | C1 | 0 | scale | Test Scale | Grade Cat |
-      | Item SL | C1 | 1 | scale | Test Scale | Grade Cat |
+      | Item TU | C1 | 0 | text  | Grade Cat |
+      | Item TL | C1 | 1 | text  | Grade Cat |
+      | Item 3  | C1 | 0 | value | Grade Cat |
+      | Calc Item  | C1 | 0 | value | Grade Cat     |
+      | Item VUSub | C1 | 0 | value | Grade Sub Cat |
     And the following "grade items" exist:
-      | itemname | course | locked | gradetype | gradecategory |
-      | Item 3 | C1 | 0 | value | Grade Cat |
+      | itemname   | course | locked | gradetype | scale | gradecategory |
+      | Item SU    | C1 | 0 | scale | Test Scale | Grade Cat |
+      | Item SL    | C1 | 1 | scale | Test Scale | Grade Cat |
     And the following config values are set as admin:
       | grade_report_showaverages | 0 |
       | grade_report_enableajax | 1 |
@@ -71,7 +74,7 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe
     And I set the field "ajaxgrade" to "Very good"
     And I press key "13" in the field "ajaxgrade"
     And the following should exist in the "user-grades" table:
-      | -1-                | -4-      | -5-      | -9-       | -13-         |
+      | -1-                | -6-      | -7-      | -13-      | -16-         |
       | Student 2          | -        | 33.00    | -         | 33.00        |
       | Student 3          | 80.00    | 50.00    | Very good | 133.00       |
     And I click on student "Student 3" for grade item "Item VL"
@@ -89,14 +92,14 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe
     And I set the field "ajaxgrade" to "90"
     And I press key "13" in the field "ajaxgrade"
     And the following should exist in the "user-grades" table:
-      | -1-                | -13-      |
+      | -1-                | -16-      |
       | Student 1          | 90.00     |
     And I navigate to "Grader report" node in "Grade administration"
     And the following should exist in the "user-grades" table:
-      | -1-                | -4-      | -5-      | -9-       | -13-         |
-      | Student 1          | -        | -        | -         | 90.00        |
-      | Student 2          | -        | 33.00    | -         | 33.00        |
-      | Student 3          | 80.00    | 50.00    | Very good | 133.00       |
+      | -1-                | -6-   | -7-   | -13-      | -16-      |
+      | Student 1          | -     | -     | -         | 90.00     |
+      | Student 2          | -     | 33.00 | -         | 33.00     |
+      | Student 3          | 80.00 | 50.00 | Very good | 133.00    |
 
   @javascript
   Scenario: Use the grader report without editing, with AJAX and quick feedback on
@@ -126,8 +129,8 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe
     And I press key "13" in the field "ajaxfeedback"
     And I navigate to "Grader report" node in "Grade administration"
     And the following should exist in the "user-grades" table:
-      | -1-                | -5-      | -9-       | -13-         |
-      | Student 2          | 33.00    | Very good | 36.00        |
+      | -1-       | -7-   | -13-      | -16-  |
+      | Student 2 | 33.00 | Very good | 36.00 |
     And I click on student "Student 3" for grade item "Item TU"
     And the field "ajaxfeedback" matches value "Student 3 TU feedback"
     And I click on student "Student 2" for grade item "Item SU"
@@ -150,8 +153,8 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe
     And I should not see a grade field for "Student 3" and grade item "Course total"
     And I should not see a feedback field for "Student 3" and grade item "Course total"
     And the following should exist in the "user-grades" table:
-      | -1-                | -5-      | -13-    |
-      | Student 2          | 33.00    | 33.00   |
+      | -1-         | -7-      | -16-    |
+      | Student 2   | 33.00    | 33.00   |
 
   @javascript
   Scenario: Use the grader report with editing, with AJAX and quick feedback on, with category override
@@ -179,8 +182,8 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe
     And the grade for "Student 2" in grade item "Course total" should match "53.00"
     And I turn editing mode off
     And the following should exist in the "user-grades" table:
-      | -1-                | -4-      | -5-     | -9-       | -12-     | -13-    |
-      | Student 2          | 30.00    | 20.00   | Very good | 53.00    | 53.00   |
+      | -1-        | -6-      | -7-     | -13-      | -15-     | -16-    |
+      | Student 2  | 30.00    | 20.00   | Very good | 53.00    | 53.00   |
     And I click on student "Student 2" for grade item "Item 1"
     And the field "ajaxfeedback" matches value "Some feedback"
 
@@ -193,20 +196,41 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I turn editing mode on
+    And I change window size to "large"
+    And I set "=[[i1]] + [[i3]] + [[gsc]]" calculation for grade item "Calc Item" with idnumbers:
+      | Item 1        | i1  |
+      | Item 3        | i3  |
+      | Grade Sub Cat | gsc |
     Then I should not see a grade field for "Student 2" and grade item "Course total"
     And I should not see a feedback field for "Student 2" and grade item "Course total"
     And I give the grade "20.00" to the user "Student 2" for the grade item "Item VU"
     And I click away from student "Student 2" and grade item "Item VU" value
+    And the following should exist in the "user-grades" table:
+      | -1-        | -15-   | -16-  |
+      | Student 2  | 20.00  | 20.00 |
     And I give the grade "30.00" to the user "Student 2" for the grade item "Item 1"
     And I click away from student "Student 2" and grade item "Item 1" value
+    And the following should exist in the "user-grades" table:
+      | -1-        | -15-  | -16-  |
+      | Student 2  | 80.00 | 80.00 |
+    And the field "Student 2 Calc Item grade" matches value "30.00"
+    And I give the grade "5.00" to the user "Student 2" for the grade item "Item 3"
+    And I click away from student "Student 2" and grade item "Item 3" value
+    And the following should exist in the "user-grades" table:
+      | -1-        | -15-  | -16- |
+      | Student 2  | 90.00 | 90.00 |
+    And the field "Student 2 Calc Item grade" matches value "35.00"
+    And I give the grade "10.00" to the user "Student 2" for the grade item "Item VUSub"
+    And I click away from student "Student 2" and grade item "Item VUSub" value
+    And the following should exist in the "user-grades" table:
+      | -1-        | -5-   | -15-   | -16-   |
+      | Student 2  | 10.00 | 110.00 | 110.00 |
+    And the field "Student 2 Calc Item grade" matches value "45.00"
     And I give the feedback "Some feedback" to the user "Student 2" for the grade item "Item 1"
     And I click away from student "Student 2" and grade item "Item 1" feedback
-    And the following should exist in the "user-grades" table:
-      | -1-                | -13-     |
-      | Student 2          | 50.00    |
     And I turn editing mode off
     And the following should exist in the "user-grades" table:
-      | -1-                | -4-      | -5-      | -13-         |
-      | Student 2          | 30.00    | 20.00    | 50.00        |
+      | -1-        | -4-   | -6-   | -7-   | -11- | -12-  | -15-   | -16-   |
+      | Student 2  | 10.00 | 30.00 | 20.00 | 5.00 | 45.00 | 110.00 | 110.00 |
     And I click on student "Student 2" for grade item "Item 1"
     And the field "ajaxfeedback" matches value "Some feedback"
index ee12feb..3e2bcd1 100644 (file)
@@ -27,8 +27,8 @@
 
 require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given,
-    Behat\Behat\Context\Step\Then,
+use Moodle\BehatExtension\Context\Step\Given,
+    Moodle\BehatExtension\Context\Step\Then,
     Behat\Mink\Exception\ExpectationException as ExpectationException,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
index 6ee26bf..76ef7f6 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given,
+use Moodle\BehatExtension\Context\Step\Given as Given,
     Behat\Gherkin\Node\TableNode as TableNode;
 
 class behat_grade extends behat_base {
index c51e281..3e2ab94 100644 (file)
@@ -44,6 +44,7 @@ Feature: We can use calculated grade totals
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I turn editing mode on
+    And I change window size to "large"
     And I give the grade "60.00" to the user "Student 1" for the grade item "Test assignment one"
     And I give the grade "20.00" to the user "Student 1" for the grade item "Test assignment two"
     And I give the grade "40.00" to the user "Student 1" for the grade item "Test assignment three"
@@ -58,6 +59,7 @@ Feature: We can use calculated grade totals
       | Hidden | 1 |
     And I set the following settings for grade item "Test assignment eight":
       | Hidden | 1 |
+    And I change window size to "medium"
     And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Grade display type" to "Real (percentage)"
     And I press "Save changes"
index 54aea96..daede1d 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Then;
+use Moodle\BehatExtension\Context\Step\Then;
 use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
@@ -62,6 +62,11 @@ class behat_groups extends behat_base {
         $fulloption = $groupoption->getText();
         $select->selectOption($fulloption);
 
+        // This is needed by some drivers to ensure relevant event is triggred and button is enabled.
+        $script = "Syn.trigger('change', {}, {{ELEMENT}})";
+        $this->getSession()->getDriver()->triggerSynScript($select->getXpath(), $script);
+        $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+
         // Here we don't need to wait for the AJAX response.
         $this->find_button(get_string('adduserstogroup', 'group'))->click();
 
index 6c34d47..a8005ae 100644 (file)
@@ -60,6 +60,7 @@ $string['cachedef_plugin_manager'] = 'Plugin info manager';
 $string['cachedef_questiondata'] = 'Question definitions';
 $string['cachedef_repositories'] = 'Repositories instances data';
 $string['cachedef_search_results'] = 'Search results user data';
+$string['cachedef_grade_categories'] = 'Grade category queries';
 $string['cachedef_string'] = 'Language string cache';
 $string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
index f50c23f..ae3936d 100644 (file)
@@ -4110,8 +4110,7 @@ function sort_by_roleassignment_authority($users, context $context, $roles = arr
  * system is more flexible. If you really need, you can to use this
  * function but consider has_capability() as a possible substitute.
  *
- * The caller function is responsible for including all the
- * $sort fields in $fields param.
+ * All $sort fields are added into $fields if not present there yet.
  *
  * If $roleid is an array or is empty (all roles) you need to set $fields
  * (and $sort by extension) params according to it, as the first field
@@ -4209,6 +4208,22 @@ function get_role_users($roleid, context $context, $parent = false, $fields = ''
         $params = array_merge($params, $sortparams);
     }
 
+    // Adding the fields from $sort that are not present in $fields.
+    $sortarray = preg_split('/,\s*/', $sort);
+    $fieldsarray = preg_split('/,\s*/', $fields);
+    $addedfields = array();
+    foreach ($sortarray as $sortfield) {
+        if (!in_array($sortfield, $fieldsarray)) {
+            $fieldsarray[] = $sortfield;
+            $addedfields[] = $sortfield;
+        }
+    }
+    $fields = implode(', ', $fieldsarray);
+    if (!empty($addedfields)) {
+        $addedfields = implode(', ', $addedfields);
+        debugging('get_role_users() adding '.$addedfields.' to the query result because they were required by $sort but missing in $fields');
+    }
+
     if ($all === null) {
         // Previously null was used to indicate that parameter was not used.
         $all = true;
index efe954b..819dba9 100644 (file)
@@ -102,6 +102,13 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      */
     protected function find($selector, $locator, $exception = false, $node = false, $timeout = false) {
 
+        // Throw exception, so dev knows it is not supported.
+        if ($selector === 'named') {
+            $exception = 'Using the "named" selector is deprecated as of 3.1. '
+                .' Use the "named_partial" or use the "named_exact" selector instead.';
+            throw new ExpectationException($exception, $this->getSession());
+        }
+
         // Returns the first match.
         $items = $this->find_all($selector, $locator, $exception, $node, $timeout);
         return count($items) ? reset($items) : null;
@@ -122,11 +129,18 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      */
     protected function find_all($selector, $locator, $exception = false, $node = false, $timeout = false) {
 
+        // Throw exception, so dev knows it is not supported.
+        if ($selector === 'named') {
+            $exception = 'Using the "named" selector is deprecated as of 3.1. '
+                .' Use the "named_partial" or use the "named_exact" selector instead.';
+            throw new ExpectationException($exception, $this->getSession());
+        }
+
         // Generic info.
         if (!$exception) {
 
             // With named selectors we can be more specific.
-            if ($selector == 'named') {
+            if (($selector == 'named_exact') || ($selector == 'named_partial')) {
                 $exceptiontype = $locator[0];
                 $exceptionlocator = $locator[1];
 
@@ -236,7 +250,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
 
         // Redirecting execution to the find method with the specified selector.
         // It will detect if it's pointing to an unexisting named selector.
-        return $this->find('named',
+        return $this->find('named_partial',
             array(
                 $cleanname,
                 $this->getSession()->getSelectorsHandler()->xpathLiteral($arguments[0])
@@ -637,4 +651,66 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         }
         $this->getSession()->getDriver()->resizeWindow($width, $height);
     }
+
+    /**
+     * Waits for all the JS to be loaded.
+     *
+     * @throws \Exception
+     * @throws NoSuchWindow
+     * @throws UnknownError
+     * @return bool True or false depending whether all the JS is loaded or not.
+     */
+    public function wait_for_pending_js() {
+        // Waiting for JS is only valid for JS scenarios.
+        if (!$this->running_javascript()) {
+            return;
+        }
+
+        // We don't use behat_base::spin() here as we don't want to end up with an exception
+        // if the page & JSs don't finish loading properly.
+        for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) {
+            $pending = '';
+            try {
+                $jscode = '
+                    return function() {
+                        if (typeof M === "undefined") {
+                            if (document.readyState === "complete") {
+                                return "";
+                            } else {
+                                return "incomplete";
+                            }
+                        } else if (' . self::PAGE_READY_JS . ') {
+                            return "";
+                        } else {
+                            return M.util.pending_js.join(":");
+                        }
+                    }();';
+                $pending = $this->getSession()->evaluateScript($jscode);
+            } catch (NoSuchWindow $nsw) {
+                // We catch an exception here, in case we just closed the window we were interacting with.
+                // No javascript is running if there is no window right?
+                $pending = '';
+            } catch (UnknownError $e) {
+                // M is not defined when the window or the frame don't exist anymore.
+                if (strstr($e->getMessage(), 'M is not defined') != false) {
+                    $pending = '';
+                }
+            }
+
+            // If there are no pending JS we stop waiting.
+            if ($pending === '') {
+                return true;
+            }
+
+            // 0.1 seconds.
+            usleep(100000);
+        }
+
+        // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions().
+        // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds
+        // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the
+        // number of JS pending code and JS completed code will not match and we will reach this point.
+        throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . self::EXTENDED_TIMEOUT .
+            ' seconds. There is a Javascript error or the code is extremely slow.');
+    }
 }
index 0108064..54049f5 100644 (file)
@@ -42,6 +42,11 @@ require_once(__DIR__ . '/../../testing/classes/tests_finder.php');
  */
 class behat_config_manager {
 
+    /**
+     * @var bool Keep track of the automatic profile conversion. So we can notify user.
+     */
+    public static $autoprofileconversion = false;
+
     /**
      * Updates a config file
      *
@@ -397,47 +402,160 @@ class behat_config_manager {
             $CFG->behat_wwwroot = 'http://itwillnotbeused.com';
         }
 
-        $basedir = $CFG->dirroot . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'behat';
-
+        // Comments use black color, so failure path is not visible. Using color other then black/white is safer.
+        // https://github.com/Behat/Behat/pull/628.
         $config = array(
             'default' => array(
-                'paths' => array(
-                    'features' => $basedir . DIRECTORY_SEPARATOR . 'features',
-                    'bootstrap' => $basedir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'bootstrap',
+                'formatters' => array(
+                    'moodle_progress' => array(
+                        'output_styles' => array(
+                            'comment' => array('magenta'))
+                        )
                 ),
-                'context' => array(
-                    'class' => 'behat_init_context'
+                'suites' => array(
+                    'default' => array(
+                        'paths' => $features,
+                        'contexts' => array_keys($stepsdefinitions)
+                    )
                 ),
                 'extensions' => array(
-                    'Behat\MinkExtension\Extension' => array(
+                    'Behat\MinkExtension' => array(
                         'base_url' => $CFG->behat_wwwroot,
                         'goutte' => null,
                         'selenium2' => $selenium2wdhost
                     ),
-                    'Moodle\BehatExtension\Extension' => array(
-                        'formatters' => array(
-                            'moodle_progress' => 'Moodle\BehatExtension\Formatter\MoodleProgressFormatter',
-                            'moodle_list' => 'Moodle\BehatExtension\Formatter\MoodleListFormatter',
-                            'moodle_step_count' => 'Moodle\BehatExtension\Formatter\MoodleStepCountFormatter'
-                        ),
-                        'features' => $features,
+                    'Moodle\BehatExtension' => array(
+                        'moodledirroot' => $CFG->dirroot,
                         'steps_definitions' => $stepsdefinitions
                     )
-                ),
-                'formatter' => array(
-                    'name' => 'moodle_progress'
                 )
             )
         );
 
         // In case user defined overrides respect them over our default ones.
         if (!empty($CFG->behat_config)) {
-            $config = self::merge_config($config, $CFG->behat_config);
+            foreach ($CFG->behat_config as $profile => $values) {
+                $config = self::merge_config($config, self::merge_behat_config($profile, $values));
+            }
+        }
+        // Check for Moodle custom ones.
+        if (!empty($CFG->behat_profiles) && is_array($CFG->behat_profiles)) {
+            foreach ($CFG->behat_profiles as $profile => $values) {
+                $config = self::merge_config($config, self::get_behat_profile($profile, $values));
+            }
         }
 
         return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
     }
 
+    /**
+     * Parse $CFG->behat_config and return the array with required config structure for behat.yml
+     *
+     * @param string $profile profile name
+     * @param array $values values for profile
+     * @return array
+     */
+    protected static function merge_behat_config($profile, $values) {
+        // Only add profile which are compatible with Behat 3.x
+        // Just check if any of Bheat 2.5 config is set. Not checking for 3.x as it might have some other configs
+        // Like : rerun_cache etc.
+        if (!isset($values['filters']['tags']) && !isset($values['extensions']['Behat\MinkExtension\Extension'])) {
+            return array($profile => $values);
+        }
+
+        // Parse 2.5 format and get related values.
+        $oldconfigvalues = array();
+        if (isset($values['extensions']['Behat\MinkExtension\Extension'])) {
+            $extensionvalues = $values['extensions']['Behat\MinkExtension\Extension'];
+            if (isset($extensionvalues['selenium2']['browser'])) {
+                $oldconfigvalues['browser'] = $extensionvalues['selenium2']['browser'];
+            }
+            if (isset($extensionvalues['selenium2']['wd_host'])) {
+                $oldconfigvalues['wd_host'] = $extensionvalues['selenium2']['wd_host'];
+            }
+            if (isset($extensionvalues['capabilities'])) {
+                $oldconfigvalues['capabilities'] = $extensionvalues['capabilities'];
+            }
+        }
+
+        if (isset($values['filters']['tags'])) {
+            $oldconfigvalues['tags'] = $values['filters']['tags'];
+        }
+
+        if (!empty($oldconfigvalues)) {
+            self::$autoprofileconversion = true;
+            return self::get_behat_profile($profile, $oldconfigvalues);
+        }
+
+        // If nothing set above then return empty array.
+        return array();
+    }
+
+    /**
+     * Parse $CFG->behat_profile and return the array with required config structure for behat.yml.
+     *
+     * $CFG->behat_profiles = array(
+     *     'profile' = array(
+     *         'browser' => 'firefox',
+     *         'tags' => '@javascript',
+     *         'wd_host' => 'http://127.0.0.1:4444/wd/hub',
+     *         'capabilities' => array(
+     *             'platform' => 'Linux',
+     *             'version' => 44
+     *         )
+     *     )
+     * );
+     *
+     * @param string $profile profile name
+     * @param array $values values for profile.
+     * @return array
+     */
+    protected static function get_behat_profile($profile, $values) {
+        // Values should be an array.
+        if (!is_array($values)) {
+            return array();
+        }
+
+        // Check suite values.
+        $behatprofilesuites = array();
+        // Fill tags information.
+        if (isset($values['tags'])) {
+            $behatprofilesuites = array(
+                'suites' => array(
+                    'default' => array(
+                        'filters' => array(
+                            'tags' => $values['tags'],
+                        )
+                    )
+                )
+            );
+        }
+
+        // Selenium2 config values.
+        $behatprofileextension = array();
+        $seleniumconfig = array();
+        if (isset($values['browser'])) {
+            $seleniumconfig['browser'] = $values['browser'];
+        }
+        if (isset($values['wd_host'])) {
+            $seleniumconfig['wd_host'] = $values['wd_host'];
+        }
+        if (isset($values['capabilities'])) {
+            $seleniumconfig['capabilities'] = $values['capabilities'];
+        }
+        if (!empty($seleniumconfig)) {
+            $behatprofileextension = array(
+                'extensions' => array(
+                    'Behat\MinkExtension' => array(
+                        'selenium2' => $seleniumconfig,
+                    )
+                )
+            );
+        }
+
+        return array($profile => array_merge($behatprofilesuites, $behatprofileextension));
+    }
+
     /**
      * Attempt to split feature list into fairish buckets using timing information, if available.
      * Simply add each one to lightest buckets until all files allocated.
index 0223495..2d68166 100644 (file)
@@ -25,7 +25,7 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-use \Behat\Behat\Context\BehatContext;
+use Behat\Testwork\Environment\Environment;
 
 /**
  * Helper to get behat contexts.
@@ -38,18 +38,20 @@ use \Behat\Behat\Context\BehatContext;
 class behat_context_helper {
 
     /**
-     * @var BehatContext main behat context.
+     * Behat environment.
+     *
+     * @var Environment
      */
-    protected static $maincontext = false;
+    protected static $environment = null;
 
     /**
-     * Save main behat context reference to be used for finding sub-contexts.
+     * Sets the browser session.
      *
-     * @param BehatContext $maincontext
+     * @param Environment $environment
      * @return void
      */
-    public static function set_main_context(BehatContext $maincontext) {
-        self::$maincontext = $maincontext;
+    public static function set_session(Environment $environment) {
+        self::$environment = $environment;
     }
 
     /**
@@ -65,7 +67,7 @@ class behat_context_helper {
      */
     public static function get($classname) {
 
-        if (!$subcontext = self::$maincontext->getSubcontextByClassName($classname)) {
+        if (!$subcontext = self::$environment->getContext($classname)) {
             throw coding_exception('The required "' . $classname . '" class does not exist');
         }
 
index 76921aa..c328f5d 100644 (file)
@@ -158,7 +158,7 @@ XPATH
         } else {
             // Named selectors uses arrays as locators including the type of named selector.
             $locator = array($selectortype, $session->getSelectorsHandler()->xpathLiteral($element));
-            $selector = 'named';
+            $selector = 'named_partial';
         }
 
         return array($selector, $locator);
@@ -173,7 +173,7 @@ XPATH
     public static function register_moodle_selectors(Behat\Mink\Session $session) {
 
         foreach (self::get_moodle_selectors() as $name => $xpath) {
-            $session->getSelectorsHandler()->getSelector('named')->registerNamedXpath($name, $xpath);
+            $session->getSelectorsHandler()->getSelector('named_partial')->registerNamedXpath($name, $xpath);
         }
     }
 
index 69a9cbc..dc568b0 100644 (file)
@@ -58,10 +58,7 @@ class behat_form_checkbox extends behat_form_field {
             $this->field->click();
 
             // Trigger the onchange event as triggered when 'checking' the checkbox.
-            $this->session->getDriver()->triggerSynScript(
-                $this->field->getXPath(),
-                "Syn.trigger('change', {}, {{ELEMENT}})"
-            );
+            $this->trigger_on_change();
 
         } else if (empty($value) && $this->field->isChecked()) {
 
@@ -74,10 +71,7 @@ class behat_form_checkbox extends behat_form_field {
             $this->field->click();
 
             // Trigger the onchange event as triggered when 'checking' the checkbox.
-            $this->session->getDriver()->triggerSynScript(
-                $this->field->getXPath(),
-                "Syn.trigger('change', {}, {{ELEMENT}})"
-            );
+            $this->trigger_on_change();
         }
     }
 
@@ -110,4 +104,13 @@ class behat_form_checkbox extends behat_form_field {
         return false;
     }
 
+    /**
+     * Trigger on change event.
+     */
+    protected function trigger_on_change() {
+        $this->session->getDriver()->triggerSynScript(
+            $this->field->getXPath(),
+            "Syn.trigger('change', {}, {{ELEMENT}})"
+        );
+    }
 }
index 8304133..22e064c 100644 (file)
@@ -59,7 +59,7 @@ class behat_form_radio extends behat_form_checkbox {
      * @return string The value attribute
      */
     public function get_value() {
-        return (bool)$this->field->getAttribute('checked');
+        return $this->field->isSelected();
     }
 
     /**
@@ -75,20 +75,17 @@ class behat_form_radio extends behat_form_checkbox {
     public function set_value($value) {
 
         if ($this->running_javascript()) {
-            parent::set_value($value);
+            // Check on radio button.
+            $this->field->click();
+
+            // Trigger the onchange event as triggered when 'selecting' the radio.
+            if (!empty($value) && !$this->field->isSelected()) {
+                $this->trigger_on_change();
+            }
         } else {
             // Goutte does not accept a check nor a click in an input[type=radio].
             $this->field->setValue($this->field->getAttribute('value'));
         }
     }
 
-    /**
-     * Returns whether the provided value matches the current value or not.
-     *
-     * @param string $expectedvalue
-     * @return bool
-     */
-    public function matches($expectedvalue = false) {
-        return $this->text_matches($expectedvalue);
-    }
 }
index 6bc66a8..880d061 100644 (file)
@@ -49,28 +49,14 @@ class behat_form_select extends behat_form_field {
      */
     public function set_value($value) {
 
-        // In some browsers we select an option and it triggers all the
-        // autosubmits and works as expected but not in all of them, so we
-        // try to catch all the possibilities to make this function work as
-        // expected.
-
-        // Get the internal id of the element we are going to click.
-        // This kind of internal IDs are only available in the selenium wire
-        // protocol, so only available using selenium drivers, phantomjs and family.
-        if ($this->running_javascript()) {
-            $currentelementid = $this->get_internal_field_id();
-        }
-
         // Is the select multiple?
         $multiple = $this->field->hasAttribute('multiple');
-
-        // By default, assume the passed value is a non-multiple option.
-        $options = array(trim($value));
+        $singleselect = ($this->field->hasClass('singleselect') || $this->field->hasClass('urlselect'));
 
         // Here we select the option(s).
         if ($multiple) {
             // Split and decode values. Comma separated list of values allowed. With valuable commas escaped with backslash.
-            $options = preg_replace('/\\\,/', ',',  preg_split('/(?<!\\\),/', $value));
+            $options = preg_replace('/\\\,/', ',',  preg_split('/(?<!\\\),/', trim($value)));
             // This is a multiple select, let's pass the multiple flag after first option.
             $afterfirstoption = false;
             foreach ($options as $option) {
@@ -78,126 +64,21 @@ class behat_form_select extends behat_form_field {
                 $afterfirstoption = true;
             }
         } else {
-            // If value is already set then don't set it again.
-            if ($this->field->getValue() == $value) {
-                return;
-            } else {
-                $opt = $this->field->find('named', array(
-                    'option', $this->field->getSession()->getSelectorsHandler()->xpathLiteral($value)
-                ));
-                if ($opt && ($this->field->getValue() == $opt->getValue())) {
-                    return;
-                }
-            }
-
-            // If not running JS or not a singleselect then use selectOption.
-            // For singleselect only click event is enough.
-            if (!$this->running_javascript() ||
-                !($this->field->hasClass('singleselect') || $this->field->hasClass('urlselect'))) {
-
-                // This is a single select, let's pass the last one specified.
-                $this->field->selectOption(end($options));
-            }
-        }
-
-        // With JS disabled this is enough and we finish here.
-        if (!$this->running_javascript()) {
-            return;
-        }
-
-        // With JS enabled we add more clicks as some selenium
-        // drivers requires it to fire JS events.
-
-        // In some browsers the selectOption actions can perform a form submit or reload page
-        // so we need to ensure the element is still available to continue interacting
-        // with it. We don't wait here.
-        // getXpath() does not send a query to selenium, so we don't need to wrap it in a try & catch.
-        $selectxpath = $this->field->getXpath();
-        if (!$this->session->getDriver()->find($selectxpath)) {
-            return;
-        }
-
-        // We also check the selenium internal element id, if it have changed
-        // we are dealing with an autosubmit that was already executed, and we don't to
-        // execute anything else as the action we wanted was already performed.
-        if ($currentelementid != $this->get_internal_field_id()) {
-            return;
-        }
+           // By default, assume the passed value is a non-multiple option.
+            $this->field->selectOption(trim($value));
+       }
 
         // Wait for all the possible AJAX requests that have been
         // already triggered by selectOption() to be finished.
-        $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
-
-        // Wrapped in try & catch as the element may disappear if an AJAX request was submitted.
-        try {
-            $multiple = $this->field->hasAttribute('multiple');
-        } catch (Exception $e) {
-            // We do not specify any specific Exception type as there are
-            // different exceptions that can be thrown by the driver and
-            // we can not control them all, also depending on the selenium
-            // version the exception type can change.
-            return;
-        }
-
-        // Single select sometimes needs an extra click in the option.
-        if (!$multiple) {
-
-            // Var $options only contains 1 option.
-            $optionxpath = $this->get_option_xpath(end($options), $selectxpath);
-
-            // Using the driver direcly because Element methods are messy when dealing
-            // with elements inside containers.
-            if ($optionnodes = $this->session->getDriver()->find($optionxpath)) {
-
-                // Wrapped in a try & catch as we can fall into race conditions
-                // and the element may not be there.
-                try {
-                    current($optionnodes)->click();
-                } catch (Exception $e) {
-                    // We continue and return as this means that the element is not there or it is not the same.
-                    return;
-                }
-            }
-
-        } else {
-
-            // Wrapped in a try & catch as we can fall into race conditions
-            // and the element may not be there.
-            try {
-                // Multiple ones needs the click in the select.
-                $this->field->click();
-            } catch (Exception $e) {
-                // We continue and return as this means that the element is not there or it is not the same.
-                return;
-            }
-
-            // We also check that the option(s) are still there. We neither wait.
-            foreach ($options as $option) {
-                $optionxpath = $this->get_option_xpath($option, $selectxpath);
-                if (!$this->session->getDriver()->find($optionxpath)) {
-                    return;
-                }
+        if ($this->running_javascript()) {
+            // Trigger change event as this is needed by some drivers (Phantomjs). Don't do it for
+            // Singleselect as this will cause multiple event fire and lead to race-around condition.
+            $browser = \Moodle\BehatExtension\Driver\MoodleSelenium2Driver::getBrowser();
+            if (!$singleselect && ($browser == 'phantomjs')) {
+                $script = "Syn.trigger('change', {}, {{ELEMENT}})";
+                $this->session->getDriver()->triggerSynScript($this->field->getXpath(), $script);
             }
-
-            // Wait for all the possible AJAX requests that have been
-            // already triggered by clicking on the field to be finished.
             $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
-
-            // Wrapped in a try & catch as we can fall into race conditions
-            // and the element may not be there.
-            try {
-
-                // Repeating the select(s) as some drivers (chrome that I know) are moving
-                // to another option after the general select field click above.
-                $afterfirstoption = false;
-                foreach ($options as $option) {
-                    $this->field->selectOption(trim($option), $afterfirstoption);
-                    $afterfirstoption = true;
-                }
-            } catch (Exception $e) {
-                // We continue and return as this means that the element is not there or it is not the same.
-                return;
-            }
         }
     }
 
@@ -233,9 +114,6 @@ class behat_form_select extends behat_form_field {
 
         // We are dealing with a multi-select.
 
-        // Can pass multiple comma separated, with valuable commas escaped with backslash.
-        $expectedarr = array(); // Array of passed text options to test.
-
         // Unescape + trim all options and flip it to have the expected values as keys.
         $expectedoptions = $this->get_unescaped_options($expectedvalue);
 
@@ -306,45 +184,23 @@ class behat_form_select extends behat_form_field {
 
         $selectedoptions = array(); // To accumulate found selected options.
 
-        // Selenium getValue() implementation breaks - separates - values having
-        // commas within them, so we'll be looking for options with the 'selected' attribute instead.
-        if ($this->running_javascript()) {
-            // Get all the options in the select and extract their value/text pairs.
-            $alloptions = $this->field->findAll('xpath', '//option');
-            foreach ($alloptions as $option) {
-                // Is it selected?
-                if ($option->hasAttribute('selected')) {
-                    if ($multiple) {
-                        // If the select is multiple, text commas must be encoded.
-                        $selectedoptions[] = trim(str_replace(',', '\,', $option->{$method}()));
-                    } else {
-                        $selectedoptions[] = trim($option->{$method}());
-                    }
-                }
-            }
-
-        } else {
-            // Goutte does not keep the 'selected' attribute updated, but its getValue() returns
-            // the selected elements correctly, also those having commas within them.
-
-            // Goutte returns the values as an array or as a string depending
-            // on whether multiple options are selected or not.
-            $values = $this->field->getValue();
-            if (!is_array($values)) {
-                $values = array($values);
-            }
+        // Driver returns the values as an array or as a string depending
+        // on whether multiple options are selected or not.
+        $values = $this->field->getValue();
+        if (!is_array($values)) {
+            $values = array($values);
+        }
 
-            // Get all the options in the select and extract their value/text pairs.
-            $alloptions = $this->field->findAll('xpath', '//option');
-            foreach ($alloptions as $option) {
-                // Is it selected?
-                if (in_array($option->getValue(), $values)) {
-                    if ($multiple) {
-                        // If the select is multiple, text commas must be encoded.
-                        $selectedoptions[] = trim(str_replace(',', '\,', $option->{$method}()));
-                    } else {
-                        $selectedoptions[] = trim($option->{$method}());
-                    }
+        // Get all the options in the select and extract their value/text pairs.
+        $alloptions = $this->field->findAll('xpath', '//option');
+        foreach ($alloptions as $option) {
+            // Is it selected?
+            if (in_array($option->getValue(), $values)) {
+                if ($multiple) {
+                    // If the select is multiple, text commas must be encoded.
+                    $selectedoptions[] = trim(str_replace(',', '\,', $option->{$method}()));
+                } else {
+                    $selectedoptions[] = trim($option->{$method}());
                 }
             }
         }
index 7e9ea69..3d560cf 100644 (file)
@@ -57,7 +57,7 @@ class recordset_walk implements \Iterator {
     protected $callback;
 
     /**
-     * @var array|false Extra params for the callback.
+     * @var mixed|null Extra param for the callback.
      */
     protected $callbackextra;
 
@@ -66,9 +66,9 @@ class recordset_walk implements \Iterator {
      *
      * @param \moodle_recordset $recordset Recordset to iterate.
      * @param callable $callback Apply this function to each record. If using a method, it should be public.
-     * @param array $callbackextra Array of arguments to pass to the callback.
+     * @param mixed $callbackextra An extra single parameter to pass to the callback. Use a container to pass multiple values.
      */
-    public function __construct(\moodle_recordset $recordset, callable $callback, $callbackextra = false) {
+    public function __construct(\moodle_recordset $recordset, callable $callback, $callbackextra = null) {
         $this->recordset = $recordset;
         $this->callback = $callback;
         $this->callbackextra = $callbackextra;
@@ -99,10 +99,10 @@ class recordset_walk implements \Iterator {
         }
 
         // Apply callback and return.
-        if ($this->callbackextra) {
-            return call_user_func($this->callback, $record);
-        } else {
+        if (!is_null($this->callbackextra)) {
             return call_user_func($this->callback, $record, $this->callbackextra);
+        } else {
+            return call_user_func($this->callback, $record);
         }
     }
 
index 7f4fc16..f9b0bd6 100644 (file)
@@ -271,4 +271,9 @@ $definitions = array(
         'staticaccelerationsize' => 3
     ),
 
+    // Grade categories. Stored at request level as invalidation is very aggressive.
+    'grade_categories' => array(
+        'mode' => cache_store::MODE_REQUEST,
+        'simplekeys' => true,
+    ),
 );
index bd9c804..bab7045 100644 (file)
@@ -45,11 +45,14 @@ class core_recordset_walk_testcase extends advanced_testcase {
 
         $recordset = $DB->get_recordset('assign');
         $walker = new \core\dml\recordset_walk($recordset, array($this, 'simple_callback'));
-        $this->assertEquals(0, iterator_count($walker));
         $this->assertFalse($walker->valid());
+
+        $count = 0;
         foreach ($walker as $data) {
             // No error here.
+            $count++;
         }
+        $this->assertEquals(0, $count);
         $walker->close();
     }
 
@@ -65,11 +68,14 @@ class core_recordset_walk_testcase extends advanced_testcase {
         // Simple iteration.
         $recordset = $DB->get_recordset('assign');
         $walker = new \core\dml\recordset_walk($recordset, array($this, 'simple_callback'));
-        $this->assertEquals(10, iterator_count($walker));
+
+        $count = 0;
         foreach ($walker as $data) {
             // Checking that the callback is being executed on each iteration.
             $this->assertEquals($data->id . ' potatoes', $data->newfield);
+            $count++;
         }
+        $this->assertEquals(10, $count);
         // No exception if we double-close.
         $walker->close();
     }
@@ -85,17 +91,22 @@ class core_recordset_walk_testcase extends advanced_testcase {
 
         // Iteration with extra callback arguments.
         $recordset = $DB->get_recordset('assign');
+
         $walker = new \core\dml\recordset_walk(
             $recordset,
             array($this, 'extra_callback'),
             array('brown' => 'onions')
         );
-        $this->assertEquals(10, iterator_count($walker));
+
+        $count = 0;
         foreach ($walker as $data) {
             // Checking that the callback is being executed on each
             // iteration and the param is being passed.
             $this->assertEquals('onions', $data->brown);
+            $count++;
         }
+        $this->assertEquals(10, $count);
+
         $walker->close();
     }
 
@@ -105,7 +116,9 @@ class core_recordset_walk_testcase extends advanced_testcase {
      * @param stdClass $data
      * @return \Traversable
      */
-    public function simple_callback($data) {
+    public function simple_callback($data, $nothing = 'notpassed') {
+        // Confirm nothing was passed.
+        $this->assertEquals('notpassed', $nothing);
         $data->newfield = $data->id . ' potatoes';
         return $data;
     }
index 8757b29..478f50e 100644 (file)
@@ -15,14 +15,14 @@ Feature: Atto accessibility checker
     And I wait "2" seconds
     And I click on "Image" "button"
     And the field "Enter URL" matches value "/broken-image"
-    And I set the field "Describe this image" to "No more warning!"
+    And I set the field "Describe this image for someone who cannot see it" to "No more warning!"
     And I press "Save image"
     And I press "Accessibility checker"
     And I should see "Congratulations, no accessibility problems found!"
     And I click on ".moodle-dialogue-focused .closebutton" "css_element"
     And I select the text in the "Description" Atto editor
     And I click on "Image" "button"
-    And I set the field "Describe this image" to ""
+    And I set the field "Describe this image for someone who cannot see it" to ""
     And I set the field "Description not necessary" to "1"
     And I press "Save image"
     And I press "Accessibility checker"
index b889136..744831d 100644 (file)
@@ -17,7 +17,7 @@ Feature: Add images to Atto
     And I click on "Private files" "link"
     And I click on "moodle-logo.png" "link"
     And I click on "Select this file" "button"
-    And I set the field "Describe this image" to "It's the Moodle"
+    And I set the field "Describe this image for someone who cannot see it" to "It's the Moodle"
     # Wait for the page to "settle".
     And I wait until the page is ready
     And the field "Width" matches value "204"
@@ -45,7 +45,7 @@ Feature: Add images to Atto
     And I follow "Edit profile"
     And I select the text in the "Description" Atto editor
     And I click on "Image" "button"
-    Then the field "Describe this image" matches value "It's the Moodle"
+    Then the field "Describe this image for someone who cannot see it" matches value "It's the Moodle"
     And the field "Width" matches value "123"
     And the field "Height" matches value "456"
 
@@ -58,7 +58,7 @@ Feature: Add images to Atto
     And I select the text in the "Description" Atto editor
     When I click on "Image" "button"
     Then the field "Enter URL" matches value "/nothing/here"
-    And I set the field "Describe this image" to "Something"
+    And I set the field "Describe this image for someone who cannot see it" to "Something"
     And I set the field "Enter URL" to ""
     And I press "Save image"
     And I set the field "Description" to "<p>Image: <img src='/nothing/again' width='123' height='456' alt='Awesome!'>.</p>"
index d680313..8218293 100644 (file)
@@ -64,26 +64,6 @@ Feature: Using the activity grade form element
     And I press "Save and display"
     And I should not see "You must choose whether to rescale existing grades or not"
 
-  Scenario: Attempting to change the grade type when grades already exist
-    Given I log in as "teacher1"
-    And I follow "Course 1"
-    And I turn editing mode on
-    And I add a "Assignment" to section "1" and I fill the form with:
-      | Assignment name | Test assignment name |
-      | Description | Test assignment description |
-    And I follow "Test assignment name"
-    And I follow "View/grade all submissions"
-    And I click on "Grade Student 1" "link" in the "Student 1" "table_row"
-    And I set the field "Grade out of 100" to "50"
-    And I press "Save changes"
-    And I press "Continue"
-    And I click on "Edit settings" "link" in the "Administration" "block"
-    When I expand all fieldsets
-    Then I should see "Some grades have already been awarded, so the grade type cannot be changed. If you wish to change the maximum grade, you must first choose whether or not to rescale existing grades."
-    And I set the field "grade[modgrade_type]" to "Scale"
-    And I press "Save and display"
-    And I should see "You cannot change the type, as grades already exist for this item"
-
   Scenario: Attempting to change the scale when grades already exist
     Given I log in as "admin"
     And I navigate to "Scales" node in "Site administration > Grades"
@@ -116,9 +96,6 @@ Feature: Using the activity grade form element
     And I click on "Edit settings" "link" in the "Administration" "block"
     When I expand all fieldsets
     Then I should see "Some grades have already been awarded, so the grade type and scale cannot be changed"
-    And I set the field "grade[modgrade_scale]" to "Letter scale"
-    And I press "Save and display"
-    And I should see "You cannot change the scale, as grades already exist for this item"
 
   Scenario: Attempting to change the maximum grade when ratings exist
     Given I log in as "teacher1"
index 672fc08..13d15c4 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once('grade_object.php');
+require_once(__DIR__ . '/grade_object.php');
 
 /**
  * grade_category is an object mapped to DB table {prefix}grade_categories
@@ -188,7 +188,22 @@ class grade_category extends grade_object {
      * @return grade_category The retrieved grade_category instance or false if none found.
      */
     public static function fetch($params) {
-        return grade_object::fetch_helper('grade_categories', 'grade_category', $params);
+        if ($records = self::retrieve_record_set($params)) {
+            return reset($records);
+        }
+
+        $record = grade_object::fetch_helper('grade_categories', 'grade_category', $params);
+
+        // We store it as an array to keep a key => result set interface in the cache, grade_object::fetch_helper is
+        // managing exceptions. We return only the first element though.
+        $records = false;
+        if ($record) {
+            $records = array($record->id => $record);
+        }
+
+        self::set_record_set($params, $records);
+
+        return $record;
     }
 
     /**
@@ -198,7 +213,14 @@ class grade_category extends grade_object {
      * @return array array of grade_category insatnces or false if none found.
      */
     public static function fetch_all($params) {
-        return grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
+        if ($records = self::retrieve_record_set($params)) {
+            return $records;
+        }
+
+        $records = grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
+        self::set_record_set($params, $records);
+
+        return $records;
     }
 
     /**
@@ -1596,19 +1618,20 @@ class grade_category extends grade_object {
             } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) {
                 // We will not aggregate the scales, so we can ignore upating their weights.
                 continue;
+            } else if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && $gradeitem->weightoverride) {
+                // For an item with extra credit ignore other weigths and overrides but do not change anything at all
+                // if it's weight was already overridden.
+                continue;
             }
 
-            if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0) {
+            // Store the previous value here, no need to update if it is the same value.
+            $prevaggregationcoef2 = $gradeitem->aggregationcoef2;
+
+            if (!$oldextracreditcalculation && $gradeitem->aggregationcoef > 0 && !$gradeitem->weightoverride) {
                 // For an item with extra credit ignore other weigths and overrides.
-                // Do not change anything at all if it's weight was already overridden.
-                if (!$gradeitem->weightoverride) {
-                    $gradeitem->aggregationcoef2 = $totalgrademax ? ($gradeitem->grademax / $totalgrademax) : 0;
-                    $gradeitem->update();
-                }
-                continue;
-            }
+                $gradeitem->aggregationcoef2 = $totalgrademax ? ($gradeitem->grademax / $totalgrademax) : 0;
 
-            if (!$gradeitem->weightoverride) {
+            } else if (!$gradeitem->weightoverride) {
                 // Calculations with a grade maximum of zero will cause problems. Just set the weight to zero.
                 if ($totaloverriddenweight >= 1 || $totalnonoverriddengrademax == 0 || $gradeitem->grademax == 0) {
                     // There is no more weight to distribute.
@@ -1619,7 +1642,7 @@ class grade_category extends grade_object {
                     $gradeitem->aggregationcoef2 = ($gradeitem->grademax/$totalnonoverriddengrademax) *
                             (1 - $totaloverriddenweight);
                 }
-                $gradeitem->update();
+
             } else if ((!$automaticgradeitemspresent && $normalisetotal != 1) || ($requiresnormalising)
                     || $overridearray[$gradeitem->id]['weight'] < 0) {
                 // Just divide the overriden weight for this item against the total weight override of all
@@ -1631,6 +1654,9 @@ class grade_category extends grade_object {
                 } else {
                     $gradeitem->aggregationcoef2 = $overridearray[$gradeitem->id]['weight'] / $normalisetotal;
                 }
+            }
+
+            if (grade_floatval($prevaggregationcoef2) !== grade_floatval($gradeitem->aggregationcoef2)) {
                 // Update the grade item to reflect these changes.
                 $gradeitem->update();
             }
@@ -2604,4 +2630,68 @@ class grade_category extends grade_object {
 
         return $defaultcoefficients;
     }
+
+    /**
+     * Cleans the cache.
+     *
+     * We invalidate them all so it can be completely reloaded.
+     *
+     * Being conservative here, if there is a new grade_category we purge them, the important part
+     * is that this is not purged when there are no changes in grade_categories.
+     *
+     * @param bool $deleted
+     * @return void
+     */
+    protected function notify_changed($deleted) {
+        self::clean_record_set();
+    }
+
+    /**
+     * Generates a unique key per query.
+     *
+     * Not unique between grade_object children. self::retrieve_record_set and self::set_record_set will be in charge of
+     * selecting the appropriate cache.
+     *
+     * @param array $params An array of conditions like $fieldname => $fieldvalue
+     * @return string
+     */
+    protected static function generate_record_set_key($params) {
+        return sha1(json_encode($params));
+    }
+
+    /**
+     * Tries to retrieve a record set from the cache.
+     *
+     * @param array $params The query params
+     * @return grade_object[]|bool An array of grade_objects or false if not found.
+     */
+    protected static function retrieve_record_set($params) {
+        $cache = cache::make('core', 'grade_categories');
+        return $cache->get(self::generate_record_set_key($params));
+    }
+
+    /**
+     * Sets a result to the records cache, even if there were no results.
+     *
+     * @param string $params The query params
+     * @param grade_object[]|bool $records An array of grade_objects or false if there are no records matching the $key filters
+     * @return void
+     */
+    protected static function set_record_set($params, $records) {
+        $cache = cache::make('core', 'grade_categories');
+        return $cache->set(self::generate_record_set_key($params), $records);
+    }
+
+    /**
+     * Cleans the cache.
+     *
+     * Aggressive deletion to be conservative given the gradebook design.
+     * The key is based on the requested params, not easy nor worth to purge selectively.
+     *
+     * @return void
+     */
+    public static function clean_record_set() {
+        $cache = cache::make('core', 'grade_categories');
+        $cache->purge();
+    }
 }
index 40df5cc..229571e 100644 (file)
@@ -1763,6 +1763,7 @@ class grade_item extends grade_object {
             $grade->feedbackformat = $feedbackformat;
         }
 
+        $gradechanged = false;
         if (empty($grade->id)) {
             $grade->timecreated  = null;   // hack alert - date submitted - no submission yet
             $grade->timemodified = time(); // hack alert - date graded
@@ -1772,12 +1773,23 @@ class grade_item extends grade_object {
             if ($result && !is_null($grade->finalgrade)) {
                 \core\event\user_graded::create_from_grade($grade)->trigger();
             }
-        } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
-                or $grade->feedback       !== $oldgrade->feedback
-                or $grade->feedbackformat != $oldgrade->feedbackformat
-                or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
-                or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
-                or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
+            $gradechanged = true;
+        } else {
+            // Existing grade_grades.
+
+            if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
+                    or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
+                    or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
+                    or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
+                $gradechanged = true;
+            }
+
+            if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
+                    $gradechanged === false) {
+                // No grade nor feedback changed.
+                return $result;
+            }
+
             $grade->timemodified = time(); // hack alert - date graded
             $result = $grade->update($source);
 
@@ -1785,21 +1797,26 @@ class grade_item extends grade_object {
             if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
                 \core\event\user_graded::create_from_grade($grade)->trigger();
             }
-        } else {
-            // no grade change
-            return $result;
         }
 
         if (!$result) {
-            // something went wrong - better force final grade recalculation
+            // Something went wrong - better force final grade recalculation.
             $this->force_regrading();
+            return $result;
+        }
+
+        // If we are not updating grades we don't need to recalculate the whole course.
+        if (!$gradechanged) {
+            return $result;
+        }
 
-        } else if ($this->is_course_item() and !$this->needsupdate) {
+        if ($this->is_course_item() and !$this->needsupdate) {
             if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
                 $this->force_regrading();
             }
 
         } else if (!$this->needsupdate) {
+
             $course_item = grade_item::fetch_course_item($this->courseid);
             if (!$course_item->needsupdate) {
                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
@@ -1927,6 +1944,7 @@ class grade_item extends grade_object {
         }
         // end of hack alert
 
+        $gradechanged = false;
         if (empty($grade->id)) {
             $result = (bool)$grade->insert($source);
 
@@ -1934,31 +1952,47 @@ class grade_item extends grade_object {
             if ($result && !is_null($grade->finalgrade)) {
                 \core\event\user_graded::create_from_grade($grade)->trigger();
             }
-        } else if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
-                or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
-                or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
-                or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
-                or $grade->rawscaleid     != $oldgrade->rawscaleid
-                or $grade->feedback       !== $oldgrade->feedback
-                or $grade->feedbackformat != $oldgrade->feedbackformat
-                or $grade->timecreated    != $oldgrade->timecreated  // part of hack above
-                or $grade->timemodified   != $oldgrade->timemodified // part of hack above
-                ) {
+            $gradechanged = true;
+        } else {
+            // Existing grade_grades.
+
+            if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
+                    or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
+                    or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
+                    or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
+                    or $grade->rawscaleid != $oldgrade->rawscaleid) {
+                $gradechanged = true;
+            }
+
+            // The timecreated and timemodified checking is part of the hack above.
+            if ($gradechanged === false and
+                    $grade->feedback === $oldgrade->feedback and
+                    $grade->feedbackformat == $oldgrade->feedbackformat and
+                    $grade->timecreated == $oldgrade->timecreated and
+                    $grade->timemodified == $oldgrade->timemodified) {
+                // No changes.
+                return $result;
+            }
             $result = $grade->update($source);
 
             // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
             if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
                 \core\event\user_graded::create_from_grade($grade)->trigger();
             }
-        } else {
-            return $result;
         }
 
         if (!$result) {
-            // something went wrong - better force final grade recalculation
+            // Something went wrong - better force final grade recalculation.
             $this->force_regrading();
+            return $result;
+        }
 
-        } else if (!$this->needsupdate) {
+        // If we are not updating grades we don't need to recalculate the whole course.
+        if (!$gradechanged) {
+            return $result;
+        }
+
+        if (!$this->needsupdate) {
             $course_item = grade_item::fetch_course_item($this->courseid);
             if (!$course_item->needsupdate) {
                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
index c50ecd9..8cd7da7 100644 (file)
@@ -1140,37 +1140,30 @@ function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null,
         }
     }
 
+    $progresstotal = 0;
+    $progresscurrent = 0;
+
     $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
     $depends_on = array();
 
-    // first mark all category and calculated items as needing regrading
-    // this is slower, but 100% accurate
     foreach ($grade_items as $gid=>$gitem) {
-        if (!empty($updated_item) and $updated_item->id == $gid) {
-            $grade_items[$gid]->needsupdate = 1;
-
-        } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
+        if ((!empty($updated_item) and $updated_item->id == $gid) ||
+                $gitem->is_course_item() || $gitem->is_category_item() || $gitem->is_calculated()) {
             $grade_items[$gid]->needsupdate = 1;
         }
 
-        // construct depends_on lookup array
-        $depends_on[$gid] = $grade_items[$gid]->depends_on();
-    }
-
-    $progresstotal = 0;
-    $progresscurrent = 0;
-
-    // This progress total might not be 100% accurate, because more things might get marked as needsupdate
-    // during the process.
-    foreach ($grade_items as $item) {
-        if ($item->needsupdate) {
+        // We load all dependencies of these items later we can discard some grade_items based on this.
+        if ($grade_items[$gid]->needsupdate) {
+            $depends_on[$gid] = $grade_items[$gid]->depends_on();
             $progresstotal++;
         }
     }
+
     $progress->start_progress('regrade_course', $progresstotal);
 
     $errors = array();
     $finalids = array();
+    $updatedids = array();
     $gids     = array_keys($grade_items);
     $failed = 0;
 
@@ -1196,28 +1189,66 @@ function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null,
             $progress->progress($thisprogress);
             $progresscurrent = $thisprogress;
 
-            $doupdate = true;
             foreach ($depends_on[$gid] as $did) {
                 if (!in_array($did, $finalids)) {
-                    $doupdate = false;
-                    continue; // this item depends on something that is not yet in finals array
+                    // This item depends on something that is not yet in finals array.
+                    continue 2;
                 }
             }
 
-            //oki - let's update, calculate or aggregate :-)
-            if ($doupdate) {
-                $result = $grade_items[$gid]->regrade_final_grades($userid);
+            // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
+
+            // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
+            // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
+            // but any dependant in the cascade) have not been updated.
+
+            // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
+            // depend on $updated_item.
+
+            // Here we check to see if the direct decendants are marked as updated.
+            if (!empty($updated_item) && $gid != $updated_item->id && !in_array($updated_item->id, $depends_on[$gid])) {
+
+                // We need to ensure that none of this item's dependencies have been updated.
+                // If we find that one of the direct decendants of this grade item is marked as updated then this
+                // grade item needs to be recalculated and marked as updated.
+                // Being marked as updated is done further down in the code.
+
+                $updateddependencies = false;
+                foreach ($depends_on[$gid] as $dependency) {
+                    if (in_array($dependency, $updatedids)) {
+                        $updateddependencies = true;
+                        break;
+                    }
+                }
+                if ($updateddependencies === false) {
+                    // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
+                    // as final.
 
-                if ($result === true) {
-                    $grade_items[$gid]->regrading_finished();
-                    $grade_items[$gid]->check_locktime(); // do the locktime item locking
-                    $count++;
                     $finalids[] = $gid;
+                    continue;
+                }
+            }
+
+            // Let's update, calculate or aggregate.
+            $result = $grade_items[$gid]->regrade_final_grades($userid);
 
+            if ($result === true) {
+
+                // We should only update the database if we regraded all users.
+                if (empty($userid)) {
+                    $grade_items[$gid]->regrading_finished();
+                    // Do the locktime item locking.
+                    $grade_items[$gid]->check_locktime();
                 } else {
-                    $grade_items[$gid]->force_regrading();
-                    $errors[$gid] = $result;
+                    $grade_items[$gid]->needsupdate = 0;
                 }
+                $count++;
+                $finalids[] = $gid;
+                $updatedids[] = $gid;
+
+            } else {
+                $grade_items[$gid]->force_regrading();
+                $errors[$gid] = $result;
             }
         }
 
index 4ce2921..3df1368 100644 (file)
@@ -5460,6 +5460,37 @@ function email_should_be_diverted($email) {
     return true;
 }
 
+/**
+ * Generate a unique email Message-ID using the moodle domain and install path
+ *
+ * @param string $localpart An optional unique message id prefix.
+ * @return string The formatted ID ready for appending to the email headers.
+ */
+function generate_email_messageid($localpart = null) {
+    global $CFG;
+
+    $urlinfo = parse_url($CFG->wwwroot);
+    $base = '@' . $urlinfo['host'];
+
+    // If multiple moodles are on the same domain we want to tell them
+    // apart so we add the install path to the local part. This means
+    // that the id local part should never contain a / character so
+    // we can correctly parse the id to reassemble the wwwroot.
+    if (isset($urlinfo['path'])) {
+        $base = $urlinfo['path'] . $base;
+    }
+
+    if (empty($localpart)) {
+        $localpart = uniqid('', true);
+    }
+
+    // Because we may have an option /installpath suffix to the local part
+    // of the id we need to escape any / chars which are in the $localpart.
+    $localpart = str_replace('/', '%2F', $localpart);
+
+    return '<' . $localpart . $base . '>';
+}
+
 /**
  * Send an email to a specified user
  *
@@ -5682,6 +5713,11 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
     $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
     $messagetext = $renderer->render_from_template('core/email_text', $context);
 
+    // Autogenerate a MessageID if it's missing.
+    if (empty($mail->MessageID)) {
+        $mail->MessageID = generate_email_messageid();
+    }
+
     if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
         // Don't ever send HTML to users who don't want it.
         $mail->isHTML(true);
index 4534dde..e3a2f99 100644 (file)
@@ -1443,6 +1443,16 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertArrayHasKey($user1->id, $users);
         $this->assertArrayHasKey($user3->id, $users);
 
+        $users = get_role_users($teacherrole->id, $coursecontext, false, 'u.id, u.email');
+        $this->assertDebuggingCalled('get_role_users() adding u.lastname, u.firstname to the query result because they were required by $sort but missing in $fields');
+        $this->assertCount(2, $users);
+        $this->assertArrayHasKey($user1->id, $users);
+        $this->assertObjectHasAttribute('lastname', $users[$user1->id]);
+        $this->assertObjectHasAttribute('firstname', $users[$user1->id]);
+        $this->assertArrayHasKey($user3->id, $users);
+        $this->assertObjectHasAttribute('lastname', $users[$user3->id]);
+        $this->assertObjectHasAttribute('firstname', $users[$user3->id]);
+
         $users = get_role_users($teacherrole->id, $coursecontext, false, 'u.id, u.email, u.idnumber', 'u.idnumber', null, $group->id);
         $this->assertCount(1, $users);
         $this->assertArrayHasKey($user3->id, $users);
index 495a486..1cc0672 100644 (file)
@@ -28,8 +28,8 @@
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
 use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
-    Behat\Behat\Context\Step\Given as Given,
-    Behat\Behat\Context\Step\Then as Then,
+    Moodle\BehatExtension\Context\Step\Given as Given,
+    Moodle\BehatExtension\Context\Step\Then as Then,
     Behat\Gherkin\Node\TableNode as TableNode;
 
 /**
index f36571d..d750a8f 100644 (file)
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 require_once(__DIR__ . '/../../../lib/behat/behat_field_manager.php');
 
-use Behat\Behat\Context\Step\Given as Given,
-    Behat\Behat\Context\Step\When as When,
-    Behat\Behat\Context\Step\Then as Then,
-    Behat\Gherkin\Node\TableNode as TableNode,
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Moodle\BehatExtension\Context\Step\Given as Given,
+    Moodle\BehatExtension\Context\Step\When as When,
+    Moodle\BehatExtension\Context\Step\Then as Then,
     Behat\Gherkin\Node\PyStringNode as PyStringNode,
-    Behat\Mink\Element\NodeElement as NodeElement,
     Behat\Mink\Exception\ExpectationException as ExpectationException,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
index 57c6355..6ac017c 100644 (file)
@@ -33,7 +33,7 @@ use Behat\Mink\Exception\ExpectationException as ExpectationException,
     WebDriver\Exception\NoSuchElement as NoSuchElement,
     WebDriver\Exception\StaleElementReference as StaleElementReference,
     Behat\Gherkin\Node\TableNode as TableNode,
-    Behat\Behat\Context\Step\Given as Given;
+    Moodle\BehatExtension\Context\Step\Given as Given;
 
 /**
  * Cross component steps definitions.
@@ -192,7 +192,7 @@ class behat_general extends behat_base {
         // unnamed window (presumably the main window) to some other named
         // window, then we first set the main window name to a conventional
         // value that we can later use this name to switch back.
-        $this->getSession()->evaluateScript(
+        $this->getSession()->executeScript(
                 'if (window.name == "") window.name = "' . self::MAIN_WINDOW_NAME . '"');
 
         $this->getSession()->switchToWindow($windowname);
@@ -1158,7 +1158,9 @@ class behat_general extends behat_base {
 
         // Check if value exists in specific row/column.
         // Get row xpath.
-        $rowxpath = $tablexpath."/tbody/tr[th[normalize-space(.)=" . $rowliteral . "] or td[normalize-space(.)=" . $rowliteral . "]]";
+        // GoutteDriver uses DomCrawler\Crawler and it is making XPath relative to the current context, so use descendant.
+        $rowxpath = $tablexpath."/tbody/tr[descendant::th[normalize-space(.)=" . $rowliteral .
+                    "] | descendant::td[normalize-space(.)=" . $rowliteral . "]]";
 
         $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]";
 
@@ -1383,7 +1385,7 @@ class behat_general extends behat_base {
 
         $this->pageloaddetectionrunning = true;
 
-        $session->evaluateScript(
+        $session->executeScript(
                 'var span = document.createElement("span");
                 span.setAttribute("data-rel", "' . self::PAGE_LOAD_DETECTION_STRING . '");
                 span.setAttribute("style", "display: none;");
index cddbe93..95a16ee 100644 (file)
 
 require_once(__DIR__ . '/../../behat/behat_base.php');
 
-use Behat\Behat\Event\SuiteEvent as SuiteEvent,
-    Behat\Behat\Event\ScenarioEvent as ScenarioEvent,
-    Behat\Behat\Event\FeatureEvent as FeatureEvent,
-    Behat\Behat\Event\OutlineExampleEvent as OutlineExampleEvent,
-    Behat\Behat\Event\StepEvent as StepEvent,
+use Behat\Testwork\Hook\Scope\BeforeSuiteScope,
+    Behat\Testwork\Hook\Scope\AfterSuiteScope,
+    Behat\Behat\Hook\Scope\BeforeFeatureScope,
+    Behat\Behat\Hook\Scope\AfterFeatureScope,
+    Behat\Behat\Hook\Scope\BeforeScenarioScope,
+    Behat\Behat\Hook\Scope\AfterScenarioScope,
+    Behat\Behat\Hook\Scope\BeforeStepScope,
+    Behat\Behat\Hook\Scope\AfterStepScope,
     Behat\Mink\Exception\DriverException as DriverException,
     WebDriver\Exception\NoSuchWindow as NoSuchWindow,
     WebDriver\Exception\UnexpectedAlertOpen as UnexpectedAlertOpen,
@@ -104,7 +107,7 @@ class behat_hooks extends behat_base {
      * @throws Exception
      * @BeforeSuite
      */
-    public static function before_suite(SuiteEvent $event) {
+    public static function before_suite(BeforeSuiteScope $scope) {
         global $CFG;
 
         // Defined only when the behat CLI command is running, the moodle init setup process will
@@ -113,7 +116,6 @@ class behat_hooks extends behat_base {
         define('BEHAT_TEST', 1);
 
         define('CLI_SCRIPT', 1);
-
         // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
         require_once(__DIR__ . '/../../../config.php');
 
@@ -169,7 +171,7 @@ class behat_hooks extends behat_base {
      * @param FeatureEvent $event event fired before feature.
      * @BeforeFeature
      */
-    public static function before_feature(FeatureEvent $event) {
+    public static function before_feature(BeforeFeatureScope $event) {
         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
             return;
         }
@@ -183,7 +185,7 @@ class behat_hooks extends behat_base {
      * @param FeatureEvent $event event fired after feature.
      * @AfterFeature
      */
-    public static function after_feature(FeatureEvent $event) {
+    public static function after_feature(AfterFeatureScope $event) {
         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
             return;
         }
@@ -201,7 +203,7 @@ class behat_hooks extends behat_base {
      * @param SuiteEvent $event event fired after suite.
      * @AfterSuite
      */
-    public static function after_suite(SuiteEvent $event) {
+    public static function after_suite(AfterSuiteScope $event) {
         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
             return;
         }
@@ -225,7 +227,7 @@ class behat_hooks extends behat_base {
      * @throws coding_exception If here we are not using the test database it should be because of a coding error
      * @BeforeScenario
      */
-    public function before_scenario($event) {
+    public function before_scenario(BeforeScenarioScope $scope) {
         global $DB, $SESSION, $CFG;
 
         // As many checks as we can.
@@ -244,19 +246,18 @@ class behat_hooks extends behat_base {
         } catch (CurlExec $e) {
             // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
             // behat_util::is_server_running() we already checked that the server is running.
-            throw new Exception($driverexceptionmsg);
+            $this->stop_execution($driverexceptionmsg);
         } catch (DriverException $e) {
-            throw new Exception($driverexceptionmsg);
+            $this->stop_execution($driverexceptionmsg);
         } catch (UnknownError $e) {
             // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
-            $this->throw_unknown_exception($e);
+            $this->stop_execution($e->getMessage());
         }
 
-
         // We need the Mink session to do it and we do it only before the first scenario.
         if (self::is_first_scenario()) {
             behat_selectors::register_moodle_selectors($session);
-            behat_context_helper::set_main_context($event->getContext()->getMainContext());
+            behat_context_helper::set_session($scope->getEnvironment());
         }
 
         // Reset mink session between the scenarios.
@@ -285,7 +286,7 @@ class behat_hooks extends behat_base {
             // Let's be conservative as we never know when new upstream issues will affect us.
             $session->visit($this->locate_path('/'));
         } catch (UnknownError $e) {
-            $this->throw_unknown_exception($e);
+            $this->stop_execution($e->getMessage());
         }
 
 
@@ -310,16 +311,18 @@ class behat_hooks extends behat_base {
      * default would be at framework level, which will stop the execution of
      * the run.
      *
-     * @param StepEvent $event event fired before step.
-     * @BeforeStep @javascript
+     * @BeforeStep
      */
-    public function before_step_javascript(StepEvent $event) {
+    public function before_step_javascript(BeforeStepScope $scope) {
+        self::$currentstepexception = null;
 
-        try {
-            $this->wait_for_pending_js();
-            self::$currentstepexception = null;
-        } catch (Exception $e) {
-            self::$currentstepexception = $e;
+        // Only run if JS.
+        if ($this->running_javascript()) {
+            try {
+                $this->wait_for_pending_js();
+            } catch (Exception $e) {
+                self::$currentstepexception = $e;
+            }
         }
     }
 
@@ -335,16 +338,36 @@ class behat_hooks extends behat_base {
      * default would be at framework level, which will stop the execution of
      * the run.
      *
-     * @param StepEvent $event event fired after step.
-     * @AfterStep @javascript
+     * @AfterStep
      */
-    public function after_step_javascript(StepEvent $event) {
-        global $CFG;
+    public function after_step_javascript(AfterStepScope $scope) {
+        global $CFG, $DB;
+
+        // Save the page content if the step failed.
+        if (!empty($CFG->behat_faildump_path) &&
+            $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
+            $this->take_contentdump($scope);
+        }
+
+        // Abort any open transactions to prevent subsequent tests hanging.
+        // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
+        // want to see a message in the behat output.
+        if (($scope->getTestResult() instanceof \Behat\Behat\Tester\Result\ExecutedStepResult) &&
+            $scope->getTestResult()->hasException()) {
+            if ($DB && $DB->is_transaction_started()) {
+                $DB->force_transaction_rollback();
+            }
+        }
+
+        // Only run if JS.
+        if (!$this->running_javascript()) {
+            return;
+        }
 
         // Save a screenshot if the step failed.
         if (!empty($CFG->behat_faildump_path) &&
-                $event->getResult() === StepEvent::FAILED) {
-            $this->take_screenshot($event);
+            $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
+            $this->take_screenshot($scope);
         }
 
         try {
@@ -366,43 +389,16 @@ class behat_hooks extends behat_base {
         }
     }
 
-    /**
-     * Execute any steps required after the step has finished.
-     *
-     * This includes creating an HTML dump of the content if there was a failure.
-     *
-     * @param StepEvent $event event fired after step.
-     * @AfterStep
-     */
-    public function after_step(StepEvent $event) {
-        global $CFG, $DB;
-
-        // Save the page content if the step failed.
-        if (!empty($CFG->behat_faildump_path) &&
-                $event->getResult() === StepEvent::FAILED) {
-            $this->take_contentdump($event);
-        }
-
-        // Abort any open transactions to prevent subsequent tests hanging.
-        // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
-        // want to see a message in the behat output.
-        if ($event->hasException()) {
-            if ($DB && $DB->is_transaction_started()) {
-                $DB->force_transaction_rollback();
-            }
-        }
-    }
-
     /**
      * Executed after scenario having switch window to restart session.
      * This is needed to close all extra browser windows and starting
      * one browser window.
      *
-     * @param ScenarioEvent $event event fired after scenario.
+     * @param AfterScenarioScope $event event fired after scenario.
      * @AfterScenario @_switch_window
      */
-    public function after_scenario_switchwindow(ScenarioEvent $event) {
-        for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count) {
+    public function after_scenario_switchwindow(AfterScenarioScope $event) {
+        for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count++) {
             try {
                 $this->getSession()->restart();
                 break;
@@ -428,15 +424,15 @@ class behat_hooks extends behat_base {
      * Take screenshot when a step fails.
      *
      * @throws Exception
-     * @param StepEvent $event
+     * @param AfterStepScope $scope
      */
-    protected function take_screenshot(StepEvent $event) {
+    protected function take_screenshot(AfterStepScope $scope) {
         // Goutte can't save screenshots.
         if (!$this->running_javascript()) {
             return false;
         }
 
-        list ($dir, $filename) = $this->get_faildump_filename($event, 'png');
+        list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
         $this->saveScreenshot($filename, $dir);
     }
 
@@ -444,10 +440,10 @@ class behat_hooks extends behat_base {
      * Take a dump of the page content when a step fails.
      *
      * @throws Exception
-     * @param StepEvent $event
+     * @param AfterStepScope $scope
      */
-    protected function take_contentdump(StepEvent $event) {
-        list ($dir, $filename) = $this->get_faildump_filename($event, 'html');
+    protected function take_contentdump(AfterStepScope $scope) {
+        list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
 
         $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w');
         fwrite($fh, $this->getSession()->getPage()->getContent());
@@ -459,10 +455,10 @@ class behat_hooks extends behat_base {
      *
      * This is used for content such as the DOM, and screenshots.
      *
-     * @param StepEvent $event
+     * @param AfterStepScope $scope
      * @param String $filetype The file suffix to use. Limited to 4 chars.
      */
-    protected function get_faildump_filename(StepEvent $event, $filetype) {
+    protected function get_faildump_filename(AfterStepScope $scope, $filetype) {
         global $CFG;
 
         // All the contentdumps should be in the same parent dir.
@@ -482,72 +478,16 @@ class behat_hooks extends behat_base {
 
         // The scenario title + the failed step text.
         // We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format.
-        $filename = $event->getStep()->getParent()->getTitle() . '_' . $event->getStep()->getText();
+        $filename = $scope->getFeature()->getTitle() . '_' . $scope->getStep()->getText();
         $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
 
-        // File name limited to 255 characters. Leaving 4 chars for the file
+        // File name limited to 255 characters. Leaving 5 chars for line number and 4 chars for the file.
         // extension as we allow .png for images and .html for DOM contents.
-        $filename = substr($filename, 0, 250) . '.' . $filetype;
+        $filename = substr($filename, 0, 245) . '_' . $scope->getStep()->getLine() . '.' . $filetype;
 
         return array($dir, $filename);
     }
 
-    /**
-     * Waits for all the JS to be loaded.
-     *
-     * @throws \Exception
-     * @throws NoSuchWindow
-     * @throws UnknownError
-     * @return bool True or false depending whether all the JS is loaded or not.
-     */
-    protected function wait_for_pending_js() {
-
-        // We don't use behat_base::spin() here as we don't want to end up with an exception
-        // if the page & JSs don't finish loading properly.
-        for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) {
-            $pending = '';
-            try {
-                $jscode =
-                    'if (typeof M === "undefined") {
-                        if (document.readyState === "complete") {
-                            return "";
-                        } else {
-                            return "incomplete";
-                        }
-                    } else if (' . self::PAGE_READY_JS . ') {
-                        return "";
-                    } else {
-                        return M.util.pending_js.join(":");
-                    }';
-                $pending = $this->getSession()->evaluateScript($jscode);
-            } catch (NoSuchWindow $nsw) {
-                // We catch an exception here, in case we just closed the window we were interacting with.
-                // No javascript is running if there is no window right?
-                $pending = '';
-            } catch (UnknownError $e) {
-                // M is not defined when the window or the frame don't exist anymore.
-                if (strstr($e->getMessage(), 'M is not defined') != false) {
-                    $pending = '';
-                }
-            }
-
-            // If there are no pending JS we stop waiting.
-            if ($pending === '') {
-                return true;
-            }
-
-            // 0.1 seconds.
-            usleep(100000);
-        }
-
-        // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions().
-        // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds
-        // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the
-        // number of JS pending code and JS completed code will not match and we will reach this point.
-        throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . self::EXTENDED_TIMEOUT .
-            ' seconds. There is a Javascript error or the code is extremely slow.');
-    }
-
     /**
      * Internal step definition to find exceptions, debugging() messages and PHP debug messages.
      *
@@ -666,15 +606,15 @@ class behat_hooks extends behat_base {
     }
 
     /**
-     * Throws an exception after appending an extra info text.
+     * Stops execution because of some exception.
      *
-     * @throws Exception
-     * @param UnknownError $exception
+     * @param string $exception
      * @return void
      */
-    protected function throw_unknown_exception(UnknownError $exception) {
+    protected function stop_execution($exception) {
         $text = get_string('unknownexceptioninfo', 'tool_behat');
-        throw new Exception($text . PHP_EOL . $exception->getMessage());
+        echo $text . PHP_EOL . $exception . PHP_EOL;
+        exit(1);
     }
 
 }
index 81392ea..995a510 100644 (file)
 
 require_once(__DIR__ . '/../../behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
+use Moodle\BehatExtension\Context\Step\Given;
+use Moodle\BehatExtension\Context\Step\When;
 use Behat\Mink\Exception\ExpectationException as ExpectationException;
 use Behat\Mink\Exception\DriverException as DriverException;
-use Behat\Behat\Context\Step\When as When;
 
 /**
  * Steps definitions to navigate through the navigation tree nodes.
@@ -245,7 +245,8 @@ class behat_navigation extends behat_base {
             if ($parentnodes[0] === $siteadminstr) {
                 // We don't know if there if Site admin is already expanded so
                 // don't wait, it is non-JS and we already waited for the DOM.
-                if ($siteadminlink = $this->getSession()->getPage()->find('named', array('link', "'" . $siteadminstr . "'"))) {
+                $siteadminlink = $this->getSession()->getPage()->find('named_exact', array('link', "'" . $siteadminstr . "'"));
+                if ($siteadminlink) {
                     $siteadminlink->click();
                 }
             }
@@ -383,4 +384,35 @@ class behat_navigation extends behat_base {
         }
         return $node;
     }
+
+    /**
+     * Step to open the navigation bar if it is needed.
+     *
+     * The top log in and log out links are hidden when middle or small
+     * size windows (or devices) are used. This step returns a step definition
+     * clicking to expand the navbar if it is hidden.
+     *
+     * @Given /^I expand navigation bar$/
+     */
+    public function get_expand_navbar_step() {
+
+        // Checking if we need to click the navbar button to show the navigation menu, it
+        // is hidden by default when using clean theme and a medium or small screen size.
+
+        // The DOM and the JS should be all ready and loaded. Running without spinning
+        // as this is a widely used step and we can not spend time here trying to see
+        // a DOM node that is not always there (at the moment clean is not even the
+        // default theme...).
+        $navbuttonjs = "return (
+            Y.one('.btn-navbar') &&
+            Y.one('.btn-navbar').getComputedStyle('display') !== 'none'
+        )";
+
+        // Adding an extra click we need to show the 'Log in' link.
+        if (!$this->getSession()->getDriver()->evaluateScript($navbuttonjs)) {
+            return false;
+        }
+
+        return new Given('I click on ".btn-navbar" "css_element"');
+    }
 }
index 4b932b6..a9f05e4 100644 (file)
@@ -28,7 +28,7 @@
 require_once(__DIR__ . '/../../behat/behat_base.php');
 
 use Behat\Mink\Exception\ExpectationException as ExpectationException,
-    Behat\Behat\Context\Step\Given as Given,
+    Moodle\BehatExtension\Context\Step\Given as Given,
     Behat\Gherkin\Node\TableNode as TableNode;
 
 /**
index 738b829..a8f7454 100644 (file)
@@ -82,12 +82,25 @@ class behat_transformations extends behat_base {
      * Transformations applicable to TableNode arguments should also
      * be applied, adding them in a different method for Behat API restrictions.
      *
-     * @Transform /^table:(.*)/
+     * @Transform table:Surname,My Surname $NASTYSTRING2
      * @param TableNode $tablenode
      * @return TableNode The transformed table
      */
-    public function tablenode_transformations(TableNode $tablenode) {
+    public function prefixed_tablenode_transformations(TableNode $tablenode) {
+        return $this->tablenode_transformations($tablenode);
+    }
 
+    /**
+     * Transformations for TableNode arguments.
+     *
+     * Transformations applicable to TableNode arguments should also
+     * be applied, adding them in a different method for Behat API restrictions.
+     *
+     * @Transform table:Surname,$NASTYSTRING1
+     * @param TableNode $tablenode
+     * @return TableNode The transformed table
+     */
+    public function tablenode_transformations(TableNode $tablenode) {
         // Walk through all values including the optional headers.
         $rows = $tablenode->getRows();
         foreach ($rows as $rowkey => $row) {
@@ -101,7 +114,9 @@ class behat_transformations extends behat_base {
         }
 
         // Return the transformed TableNode.
-        $tablenode->setRows($rows);
+        unset($tablenode);
+        $tablenode = new TableNode($rows);
+
         return $tablenode;
     }
 
index e337c76..fb9d5ae 100644 (file)
@@ -2644,7 +2644,50 @@ class core_moodlelib_testcase extends advanced_testcase {
         $this->assertEventContextNotUsed($event);
     }
 
+    /**
+     * A data provider for testing email messageid
+     */
+    public function generate_email_messageid_provider() {
+        return array(
+            'nopath' => array(
+                'wwwroot' => 'http://www.example.com',
+                'ids' => array(
+                    'a-custom-id' => '<a-custom-id@www.example.com>',
+                    'an-id-with-/-a-slash' => '<an-id-with-%2F-a-slash@www.example.com>',
+                ),
+            ),
+            'path' => array(
+                'wwwroot' => 'http://www.example.com/path/subdir',
+                'ids' => array(
+                    'a-custom-id' => '<a-custom-id/path/subdir@www.example.com>',
+                    'an-id-with-/-a-slash' => '<an-id-with-%2F-a-slash/path/subdir@www.example.com>',
+                ),
+            ),
+        );
+    }
+
+    /**
+     * Test email message id generation
+     *
+     * @dataProvider generate_email_messageid_provider
+     *
+     * @param string $wwwroot The wwwroot
+     * @param array $msgids An array of msgid local parts and the final result
+     */
+    public function test_generate_email_messageid($wwwroot, $msgids) {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->wwwroot = $wwwroot;
+
+        foreach ($msgids as $local => $final) {
+            $this->assertEquals($final, generate_email_messageid($local));
+        }
+    }
 
+    /**
+     * A data provider for testing email diversion
+     */
     public function diverted_emails_provider() {
         return array(
             'nodiverts' => array(
@@ -2697,7 +2740,14 @@ class core_moodlelib_testcase extends advanced_testcase {
     }
 
     /**
+     * Test email diversion
+     *
      * @dataProvider diverted_emails_provider
+     *
+     * @param string $divertallemailsto An optional email address
+     * @param string $divertallemailsexcept An optional exclusion list
+     * @param array $addresses An array of test addresses
+     * @param boolean $expected Expected result
      */
     public function test_email_should_be_diverted($divertallemailsto, $divertallemailsexcept, $addresses, $expected) {
         global $CFG;
index 72e47f2..a88c602 100644 (file)
@@ -3,6 +3,9 @@ information provided here is intended especially for developers.
 
 === 3.1 ===
 
+* The get_role_users() function will now add the $sort fields that are not part
+  of the requested fields to the query result and will throw a debugging message
+  with the added fields when that happens.
 * The core_user::fill_properties_cache() static method has been introduced to be a reference
   and allow standard user fields data validation. Right now only type validation is supported
   checking it against the parameter (PARAM_*) type of the target user field. MDL-52781 is
index b3c92a2..2d44ffa 100644 (file)
@@ -371,6 +371,9 @@ function upgrade_stale_php_files_present() {
     global $CFG;
 
     $someexamplesofremovedfiles = array(
+        // Removed in 3.1.
+        '/repository/boxnet/migrationv1.php',
+        '/repository/boxnet/cli/migrationv1.php',
         // Removed in 3.0.
         '/mod/lti/grade.php',
         '/tag/coursetagslib.php',
index 8578a8e..b227f05 100644 (file)
@@ -21,9 +21,6 @@ Our changes:  Look for "moodle" in code (commit #3 - always mimic from current m
  * xhprof_lib/utils/callgraph_utils.php: Modified to use $CFG->pathtodot
 
 TODO:
- * with the 3 reports (index, callgraph and typeahead), close seesion asap,
-       so user can continue working with moodle while the report (specially
-       the graph is being generated).
  * improvements to the listing mode: various commodity details like:
        - allow to filter by various criteria
        - inline (and ajax) editing of reference/comment and deleting
index a315dbc..308c8bc 100644 (file)
@@ -34,6 +34,7 @@ require_once(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php');
 require_once($CFG->libdir . '/xhprof/xhprof_moodle.php');
 require_login();
 require_capability('moodle/site:config', context_system::instance());
+\core\session\manager::write_close();
 // End moodle modification.
 
 // by default assume that xhprof_html & xhprof_lib directories
index 4748424..75bef73 100644 (file)
@@ -36,6 +36,7 @@ require_once(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php');
 require_once($CFG->libdir . '/xhprof/xhprof_moodle.php');
 require_login();
 require_capability('moodle/site:config', context_system::instance());
+\core\session\manager::write_close();
 // End moodle modification.
 
 // by default assume that xhprof_html & xhprof_lib directories
index 0cebba7..4b5a027 100644 (file)
@@ -26,6 +26,7 @@ require_once(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php');
 require_once($CFG->libdir . '/xhprof/xhprof_moodle.php');
 require_login();
 require_capability('moodle/site:config', context_system::instance());
+\core\session\manager::write_close();
 // End moodle modification.
 
 // by default assume that xhprof_html & xhprof_lib directories
index 144983a..56c2573 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given,
+use Moodle\BehatExtension\Context\Step\Given as Given,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
index 0e95f6c..745618d 100644 (file)
@@ -25,13 +25,13 @@ Feature: Check that messages can be deleted
     And "Delete" "link" should exist in the "#message_1" "css_element"
     And "Delete" "link" should exist in the "#message_2" "css_element"
     # Confirm that there is a confirmation box before deleting, and that when we cancel the messages remain.
-    And I hover "#message_2" "css_element"
+    And I click on "#message_2" "css_element"
     And I click on "Delete" "link" in the "#message_2" "css_element"
     And I press "Cancel"
     And I should see "Hey bud, what's happening?"
     And I should see "Whoops, forgot to mention that I drank all your beers. Lol."
     # Confirm we can delete a message and then can no longer see it.
-    And I hover "#message_2" "css_element"
+    And I click on "#message_2" "css_element"
     And I click on "Delete" "link" in the "#message_2" "css_element"
     And I press "Delete"
     And I should see "Hey bud, what's happening?"
@@ -69,10 +69,10 @@ Feature: Check that messages can be deleted
     And "Delete" "link" should exist in the "#message_3" "css_element"
     And "Delete" "link" should exist in the "#message_4" "css_element"
     # Now, delete one of the messages that User 1 sent and one by User 2.
-    And I hover "#message_1" "css_element"
+    And I click on "#message_1" "css_element"
     And I click on "Delete" "link" in the "#message_1" "css_element"
     And I press "Delete"
-    And I hover "#message_2" "css_element"
+    And I click on "#message_2" "css_element"
     And I click on "Delete" "link" in the "#message_2" "css_element"
     And I press "Delete"
     # Confirm that the messages are no longer listed.
@@ -111,7 +111,7 @@ Feature: Check that messages can be deleted
     # Send a message from the admin to User 1
     And I send "Hey there, this is the all-powerful administrator. Obey my commands." message to "User 1" user
     # Check the admin is still able to delete messages.
-    And I hover "#message_1" "css_element"
+    And I click on "#message_1" "css_element"
     And I click on "Delete" "link" in the "#message_1" "css_element"
     And I press "Delete"
     And I should not see "Hey there, this is the all-powerful administrator. Obey my commands."
index 4900960..b54baff 100644 (file)
@@ -51,6 +51,7 @@ Feature: In an assignment, teacher can annotate PDF files during grading
     And I follow "Course 1"
     And I follow "Test assignment name"
     And I follow "View/grade all submissions"
+    And I click on "Edit" "link" in the "Submitted for grading" "table_row"
     And I click on "Grade" "link" in the "Submitted for grading" "table_row"
     And I follow "Launch PDF editor..."
     And I change window size to "large"
@@ -129,6 +130,7 @@ Feature: In an assignment, teacher can annotate PDF files during grading
     And I follow "Course 1"
     And I follow "Test assignment name"
     And I follow "View/grade all submissions"
+    And I click on "Edit" "link" in the "Student 2" "table_row"
     And I click on "Grade" "link" in the "Student 2" "table_row"
     And I follow "Launch PDF editor..."
     And I change window size to "large"
index 41fab07..a35c867 100644 (file)
@@ -27,8 +27,6 @@
 
 require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
-
 /**
  * Steps definitions related with the editpdf.
  *
index ad93008..6c81714 100644 (file)
@@ -6325,6 +6325,8 @@ class assign {
         // Hidden params.
         $mform->addElement('hidden', 'id', $this->get_course_module()->id);
         $mform->setType('id', PARAM_INT);
+        $mform->addElement('hidden', 'userid', $userid);
+        $mform->setType('userid', PARAM_INT);
         $mform->addElement('hidden', 'rownum', $rownum);
         $mform->setType('rownum', PARAM_INT);
         $mform->setConstant('rownum', $rownum);
index d63d098..13bebcf 100644 (file)
@@ -26,8 +26,8 @@ Feature: In an assignment, teachers can edit a students submission inline
       | assignsubmission_onlinetext_enabled | 1 |
       | assignsubmission_file_enabled | 0 |
       | assignfeedback_comments_enabled | 1 |
-      | assignfeedback_comments_commentinline | 1 |
       | assignfeedback_file_enabled | 1 |
+      | assignfeedback_comments_commentinline | 1 |
     And I log out
     And I log in as "student1"
     And I follow "Course 1"
index ba012aa..e370228 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given;
+use Moodle\BehatExtension\Context\Step\Given as Given;
 
 /**
  * Choice activity definitions.
@@ -67,14 +67,24 @@ class behat_mod_choice extends behat_base {
      * @return array
      */
     public function I_choose_options_from_activity($option, $choiceactivity) {
-        // Escaping again the strings as backslashes have been removed by the automatic transformation.
-        $return = array(new Given('I follow "' . $this->escape($choiceactivity) . '"'));
+        // Get Behat general and forms contexts.
+        $behatgeneral = behat_context_helper::get('behat_general');
+        $behatforms = behat_context_helper::get('behat_forms');
+
+        // Go to choice activity.
+        $behatgeneral->click_link($this->escape($choiceactivity));
+
+        // Wait for page to be loaded.
+        $this->wait_for_pending_js();
+
+        // Set all options.
         $options = explode('","', trim($option, '"'));
         foreach ($options as $option) {
-            $return[] = new Given('I set the field "' . $this->escape($option) . '" to "1"');
+            $behatforms->i_set_the_field_to($this->escape($option), '1');
         }
-        $return[] = new Given('I press "' . get_string('savemychoice', 'choice') . '"');
-        return $return;
+
+        // Save choice.
+        $behatforms->press_button(get_string('savemychoice', 'choice'));
     }
 
 }
index a190eb5..fd6eb52 100644 (file)
@@ -45,7 +45,7 @@ class backup_data_activity_structure_step extends backup_activity_structure_step
             'addtemplate', 'rsstemplate', 'rsstitletemplate', 'csstemplate',
             'jstemplate', 'asearchtemplate', 'approval', 'manageapproved', 'scale',
             'assessed', 'assesstimestart', 'assesstimefinish', 'defaultsort',
-            'defaultsortdir', 'editany', 'notification'));
+            'defaultsortdir', 'editany', 'notification', 'timemodified'));
 
         $fields = new backup_nested_element('fields');
 
index 7e6d4e2..dd71cf2 100644 (file)
@@ -61,6 +61,8 @@ class restore_data_activity_structure_step extends restore_activity_structure_st
         $data->timeviewto = $this->apply_date_offset($data->timeviewto);
         $data->assesstimestart = $this->apply_date_offset($data->assesstimestart);
         $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish);
+        // Added in 3.1, hence conditional.
+        $data->timemodified = isset($data->timemodified) ? $this->apply_date_offset($data->timemodified) : time();
 
         if ($data->scale < 0) { // scale found, get mapping
             $data->scale = -($this->get_mappingid('scale', abs($data->scale)));
index c097e70..3ac5e82 100644 (file)
@@ -128,7 +128,7 @@ class mod_data_external extends external_api {
                     $additionalfields = array('maxentries', 'rssarticles', 'singletemplate', 'listtemplate',
                         'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate',
                         'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'manageapproved', 'scale', 'assessed', 'assesstimestart',
-                        'assesstimefinish', 'defaultsort', 'defaultsortdir', 'editany', 'notification');
+                        'assesstimefinish', 'defaultsort', 'defaultsortdir', 'editany', 'notification', 'timemodified');
 
                     // This is for avoid a long repetitive list.
                     foreach ($additionalfields as $field) {
@@ -195,7 +195,8 @@ class mod_data_external extends external_api {
                             'defaultsort' => new external_value(PARAM_INT, 'defaultsort field', VALUE_OPTIONAL),
                             'defaultsortdir' => new external_value(PARAM_INT, 'defaultsortdir field', VALUE_OPTIONAL),
                             'editany' => new external_value(PARAM_BOOL, 'editany field', VALUE_OPTIONAL),
-                            'notification' => new external_value(PARAM_INT, 'notification field', VALUE_OPTIONAL)
+                            'notification' => new external_value(PARAM_INT, 'notification field', VALUE_OPTIONAL),
+                            'timemodified' => new external_value(PARAM_INT, 'Time modified', VALUE_OPTIONAL)
                         ), 'Database'
                     )
                 ),
index 3eea246..e069bff 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/data/db" VERSION="20150309" COMMENT="XMLDB file for Moodle mod/data"
+<XMLDB PATH="mod/data/db" VERSION="20160303" COMMENT="XMLDB file for Moodle mod/data"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
@@ -40,6 +40,7 @@
         <FIELD NAME="defaultsortdir" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="editany" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="notification" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Notify people when things change"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time the settings for this database module instance were last modified."/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
     </TABLE>
   </TABLES>
-</XMLDB>
+</XMLDB>
\ No newline at end of file
index bd68fcd..7ecf150 100644 (file)
@@ -63,5 +63,21 @@ function xmldb_data_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2016030300) {
+
+        // Define field timemodified to be added to data.
+        $table = new xmldb_table('data');
+        $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'notification');
+
+        // Conditionally launch add field timemodified.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Data savepoint reached.
+        upgrade_mod_savepoint(true, 2016030300, 'data');
+    }
+
+
     return true;
 }
index 3dc04c2..3fcc8d0 100644 (file)
@@ -27,8 +27,8 @@
 
 require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given,
-    Behat\Behat\Context\Step\When as When,
+use Moodle\BehatExtension\Context\Step\Given as Given,
+    Moodle\BehatExtension\Context\Step\When as When,
     Behat\Gherkin\Node\TableNode as TableNode;
 /**
  * Database-related steps definitions.
index d5ef8e5..eab816c 100644 (file)
@@ -31,8 +31,7 @@ Feature: Users can be required to specify certain fields when adding entries to
       | Required | yes |
       | Options | Required Checkbox Option 1 |
     And I follow "Fields"
-    And I set the field "newtype" to "Checkbox"
-    And I click on "Go" "button" in the ".fieldadd" "css_element"
+    And I select "Checkbox" from the "newtype" singleselect
     And I set the following fields to these values:
       | Field name | Required Two-Option Checkbox |
       | Field description | Required Two-Option Checkbox |
@@ -79,8 +78,7 @@ Feature: Users can be required to specify certain fields when adding entries to
       | Required | yes |
       | Options | Option 1 |
     And I follow "Fields"
-    And I set the field "newtype" to "Multimenu"
-    And I click on "Go" "button" in the ".fieldadd" "css_element"
+    And I select "Multimenu" from the "newtype" singleselect
     And I set the following fields to these values:
       | Field name | Required Two-Option Multimenu |
       | Field description | Required Two-Option Multimenu |
index a0ecd29..831f0c9 100644 (file)
@@ -141,7 +141,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         // Now, try as a teacher for getting all the additional fields.
         self::setUser($teacher);
 
-        $additionalfields = array('maxentries', 'rssarticles', 'singletemplate', 'listtemplate',
+        $additionalfields = array('maxentries', 'rssarticles', 'singletemplate', 'listtemplate', 'timemodified',
                                 'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate',
                                 'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'scale', 'assessed', 'assesstimestart',
                                 'assesstimefinish', 'defaultsort', 'defaultsortdir', 'editany', 'notification', 'manageapproved');
index 955567d..001a33c 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111601;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016030300;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;       // Requires this Moodle version
 $plugin->component = 'mod_data';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index d6c4b52..abf47af 100644 (file)
@@ -54,14 +54,16 @@ class behat_mod_feedback extends behat_base {
         $additem = $this->escape(get_string('add_item', 'feedback'));
         $rv[] = new Given("I select \"{$questiontype}\" from the \"{$additem}\" singleselect");
 
-        $newdata = new TableNode();
         $rows = $questiondata->getRows();
+        $modifiedrows = array();
         foreach ($rows as $row) {
             foreach ($row as $key => $value) {
                 $row[$key] = preg_replace('|\\\\n|', "\n", $value);
             }
-            $newdata->addRow($row);
+            $modifiedrows[] = $row;
         }
+        $newdata = new TableNode($modifiedrows);
+
         $rv[] = new Given('I set the following fields to these values:', $newdata);
 
         $saveitem = $this->escape(get_string('save_item', 'feedback'));
index 585e993..d6dcbd6 100644 (file)
@@ -112,8 +112,6 @@ Feature: Test creating different types of feedback questions
     And I should see "C1" in the "(info)" "table"
     And I should see "my long answer" in the "(longertext)" "table"
     And I should see "lots of feedbacks" in the "(longertext)" "table"
-    #And I should see "1 (50.00 %)" in the "option a:" "table_row"  // TODO: MDL-46891
-    #And I should see "1 (50.00 %)" in the "option b:" "table_row"  // TODO: MDL-46891
     And I should see "2 (100.00 %)" in the "option d:" "table_row"
     And I should see "1 (50.00 %)" in the "option e:" "table_row"
     And I should see "1 (50.00 %)" in the "option f:" "table_row"
@@ -131,9 +129,12 @@ Feature: Test creating different types of feedback questions
     And I should see "Average: 53.00" in the "(numeric)" "table"
     And I should see "no way" in the "(shorttext)" "table"
     And I should see "hello" in the "(shorttext)" "table"
-    And I log out
 
   Scenario: Create different types of questions in feedback with javascript disabled
+    And I log out
 
   @javascript
   Scenario: Create different types of questions in feedback with javascript enabled
+    And I should see "1 (50.00 %)" in the "option a:" "table_row"
+    And I should see "1 (50.00 %)" in the "option b:" "table_row"
+    And I log out
index 07044d3..1331c02 100644 (file)
@@ -70,21 +70,26 @@ class mod_folder_renderer extends plugin_renderer_base {
             $downloadable = folder_archive_available($folder, $cm);
 
             if ($downloadable) {
-                $containercontents .= $this->output->single_button(
+                $downloadbutton = $this->output->single_button(
                     new moodle_url('/mod/folder/download_folder.php', array('id' => $cm->id)),
                     get_string('downloadfolder', 'folder')
                 );
+
+                $output .= $this->output->container(
+                    $downloadbutton,
+                    'mdl-align folder-download-button');
             }
 
             if (has_capability('mod/folder:managefiles', $context)) {
-                $containercontents .= $this->output->single_button(
+                $editbutton = $this->output->single_button(
                     new moodle_url('/mod/folder/edit.php', array('id' => $cm->id)),
                     get_string('edit')
                 );
+
+                $output .= $this->output->container(
+                    $editbutton,
+                    'mdl-align folder-edit-button');
             }
-            $output .= $this->output->container(
-                $containercontents,
-                'mdl-align folder-edit-button');
         }
         return $output;
     }
index 39d976a..a1e9069 100644 (file)
@@ -410,11 +410,10 @@ WHERE
  *
  * @param int $postid The ID of the forum post we are notifying the user about
  * @param int $usertoid The ID of the user being notified
- * @param string $hostname The server's hostname
  * @return string A unique message-id
  */
-function forum_get_email_message_id($postid, $usertoid, $hostname) {
-    return '<'.hash('sha256',$postid.'to'.$usertoid).'@'.$hostname.'>';
+function forum_get_email_message_id($postid, $usertoid) {
+    return generate_email_messageid(hash('sha256', $postid . 'to' . $usertoid));
 }
 
 /**
@@ -596,9 +595,6 @@ function forum_cron() {
 
     if ($users && $posts) {
 
-        $urlinfo = parse_url($CFG->wwwroot);
-        $hostname = $urlinfo['host'];
-
         foreach ($users as $userto) {
             // Terminate if processing of any account takes longer than 2 minutes.
             core_php_time_limit::raise(120);
@@ -751,9 +747,9 @@ function forum_cron() {
 
                 $userfrom->customheaders = array (
                     // Headers to make emails easier to track.
-                    'List-Id: "'        . $cleanforumname . '" <moodleforum' . $forum->id . '@' . $hostname.'>',
+                    'List-Id: "'        . $cleanforumname . '" ' . generate_email_messageid('moodleforum' . $forum->id),
                     'List-Help: '       . $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id,
-                    'Message-ID: '      . forum_get_email_message_id($post->id, $userto->id, $hostname),
+                    'Message-ID: '      . forum_get_email_message_id($post->id, $userto->id),
                     'X-Course-Id: '     . $course->id,
                     'X-Course-Name: '   . format_string($course->fullname, true),
 
@@ -810,11 +806,11 @@ function forum_cron() {
                 $a->courseshortname = $data->get_coursename();
                 $postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
 
-                $rootid = forum_get_email_message_id($discussion->firstpost, $userto->id, $hostname);
+                $rootid = forum_get_email_message_id($discussion->firstpost, $userto->id);
 
                 if ($post->parent) {
                     // This post is a reply, so add reply header (RFC 2822).
-                    $parentid = forum_get_email_message_id($post->parent, $userto->id, $hostname);
+                    $parentid = forum_get_email_message_id($post->parent, $userto->id);
                     $userfrom->customheaders[] = "In-Reply-To: $parentid";
 
                     // If the post is deeply nested we also reference the parent message id and
index bb56542..f132e1f 100644 (file)
@@ -27,7 +27,7 @@
 
 require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given,
+use Moodle\BehatExtension\Context\Step\Given as Given,
     Behat\Gherkin\Node\TableNode as TableNode;
 /**
  * Forum-related steps definitions.