Merge branch 'MDL-53234-support-email' of https://github.com/brendanheywood/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 15 Mar 2016 05:43:22 +0000 (13:43 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 15 Mar 2016 05:43:22 +0000 (13:43 +0800)
256 files changed:
admin/roles/classes/define_role_table_advanced.php
admin/roles/define.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
admin/tool/monitor/lib.php
auth/db/auth.php
auth/db/tests/db_test.php
auth/manual/tests/behat/auth_manual.feature [new file with mode: 0644]
auth/tests/behat/behat_auth.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/util/ui/tests/behat/behat_backup.php
behat.yml.dist
blocks/community/communitycourse.php
blocks/tests/behat/behat_blocks.php
calendar/externallib.php
calendar/tests/behat/behat_calendar.php
calendar/tests/externallib_test.php
calendar/view.php
cohort/tests/behat/behat_cohort.php
completion/tests/behat/behat_completion.php
composer.json
composer.lock
config-dist.php
course/externallib.php
course/lib.php
course/modedit.php
course/modlib.php
course/moodleform_mod.php
course/tests/behat/behat_course.php
course/tests/behat/category_resort.feature
course/tests/behat/course_resort.feature
course/tests/courselib_test.php
enrol/meta/lib.php
enrol/meta/tests/behat/enrol_meta.feature
enrol/tests/behat/behat_enrol.php
grade/edit/tree/category.php
grade/edit/tree/category_form.php
grade/edit/tree/index.php
grade/grading/form/guide/guideeditor.php
grade/grading/form/guide/tests/behat/behat_gradingform_guide.php
grade/grading/form/rubric/rubriceditor.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/report/singleview/classes/local/screen/screen.php
grade/report/singleview/classes/local/screen/tablelike.php
grade/report/singleview/classes/local/ui/exclude.php
grade/report/singleview/tests/behat/singleview.feature
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_category_validation.feature [new file with mode: 0644]
group/tests/behat/behat_groups.php
install/lang/tr/moodle.php
lang/en/admin.php
lang/en/cache.php
lang/en/grades.php
lang/en/plugin.php
lang/en/tag.php
lib/accesslib.php
lib/adminlib.php
lib/ajax/getnavbranch.php
lib/amd/build/ajax.min.js
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-course-selector.min.js [new file with mode: 0644]
lib/amd/src/ajax.js
lib/amd/src/form-autocomplete.js
lib/amd/src/form-course-selector.js [new file with mode: 0644]
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_autocomplete.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/task/search_index_task.php [moved from lib/classes/task/search_task.php with 90% similarity]
lib/classes/task/search_optimize_task.php [new file with mode: 0644]
lib/classes/text.php
lib/classes/user.php
lib/coursecatlib.php
lib/datalib.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/tag.php
lib/db/tasks.php
lib/db/upgrade.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
lib/editor/atto/plugins/image/tests/behat/image.feature
lib/form/course.php [new file with mode: 0644]
lib/form/modgrade.php
lib/form/tests/behat/modgrade_validation.feature
lib/formslib.php
lib/grade/grade_category.php
lib/grade/grade_item.php
lib/gradelib.php
lib/htmlpurifier/locallib.php
lib/moodlelib.php
lib/phpunit/classes/util.php
lib/testing/generator/module_generator.php
lib/testing/tests/generator_test.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/coursecatlib_test.php
lib/tests/htmlpurifier_test.php
lib/tests/moodlelib_test.php
lib/tests/text_test.php
lib/tests/user_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
login/index.php
login/index_form.html
message/tests/behat/behat_message.php
message/tests/behat/delete_messages.feature
mod/assign/adminmanageplugins.php
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/tests/behat/behat_assignfeedback_editpdf.php
mod/assign/tests/behat/comment_inline.feature
mod/assign/tests/externallib_test.php
mod/assign/version.php
mod/book/db/services.php
mod/book/version.php
mod/chat/db/services.php
mod/chat/version.php
mod/choice/db/services.php
mod/choice/tests/behat/behat_mod_choice.php
mod/choice/version.php
mod/data/db/services.php
mod/data/field/file/field.class.php
mod/data/field/number/field.class.php
mod/data/field/picture/field.class.php
mod/data/field/text/field.class.php
mod/data/field/textarea/field.class.php
mod/data/field/url/field.class.php
mod/data/lib.php
mod/data/tests/behat/behat_mod_data.php
mod/data/tests/behat/required_entries.feature
mod/data/version.php
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/feedback/tests/behat/behat_mod_feedback.php
mod/feedback/tests/behat/question_types.feature
mod/feedback/view.php
mod/folder/db/services.php
mod/folder/version.php
mod/forum/db/services.php
mod/forum/lib.php
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/upgrade.txt
mod/forum/version.php
mod/glossary/db/services.php
mod/glossary/tests/behat/behat_mod_glossary.php
mod/glossary/version.php
mod/imscp/db/services.php
mod/imscp/version.php
mod/lti/classes/service_exception_handler.php [new file with mode: 0644]
mod/lti/db/services.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/service.php
mod/lti/servicelib.php
mod/lti/tests/externallib_test.php
mod/lti/tests/lib_test.php
mod/lti/tests/service_exception_handler_test.php [new file with mode: 0644]
mod/lti/tests/servicelib_test.php [new file with mode: 0644]
mod/lti/upgrade.txt
mod/lti/version.php
mod/page/db/services.php
mod/page/version.php
mod/quiz/classes/external.php
mod/quiz/db/services.php
mod/quiz/locallib.php
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/settings_form_fields_disableif.feature
mod/quiz/tests/external_test.php
mod/quiz/version.php
mod/resource/db/services.php
mod/resource/version.php
mod/scorm/db/services.php
mod/scorm/version.php
mod/survey/db/services.php
mod/survey/version.php
mod/url/db/services.php
mod/url/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/styles.css
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/styles.css
question/type/ddwtos/tests/behat/preview.feature
question/type/ddwtos/tests/walkthrough_test.php
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js
question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js
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
report/search/index.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/repository_callback.php
repository/tests/behat/behat_filepicker.php
repository/upload/tests/behat/behat_repository_upload.php
search/classes/engine.php
search/classes/manager.php
search/cli/indexer.php [new file with mode: 0644]
search/engine/solr/classes/document.php
search/engine/solr/classes/engine.php
search/engine/solr/setup_schema.php
search/tests/fixtures/mock_search_engine.php
user/index.php
version.php

index d18c144..eb46c76 100644 (file)
@@ -413,7 +413,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     }
 
     public function save_changes() {
-        global $DB;
+        global $DB, $CFG;
 
         if (!$this->roleid) {
             // Creating role.
@@ -427,6 +427,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             // the UI. It would be better to do this only when we know that fields affected are
             // updated. But thats getting into the weeds of the coursecat cache and role edits
             // should not be that frequent, so here is the ugly brutal approach.
+            require_once($CFG->libdir . '/coursecatlib.php');
             coursecat::role_assignment_changed($this->role->id, context_system::instance());
         }
 
index d30ad3e..8e98399 100644 (file)
@@ -201,7 +201,7 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey() && $de
     $event = \core\event\role_capabilities_updated::create(
         array(
             'context' => $systemcontext,
-            'objectid' => $roleid
+            'objectid' => $tableroleid
         )
     );
     $event->set_legacy_logdata(array(SITEID, 'role', $action, 'admin/roles/define.php?action=view&roleid=' . $tableroleid,
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"
index 7e91f0b..52543d4 100644 (file)
@@ -103,10 +103,12 @@ function tool_monitor_get_user_courses() {
     if (has_capability('tool/monitor:subscribe', context_system::instance())) {
         $options[0] = get_string('site');
     }
-    if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname', $orderby)) {
+    if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname, visible', $orderby)) {
         foreach ($courses as $course) {
-            $options[$course->id] = format_string($course->fullname, true,
-                array('context' => context_course::instance($course->id)));
+            $coursectx = context_course::instance($course->id);
+            if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursectx)) {
+                $options[$course->id] = format_string($course->fullname, true, array('context' => $coursectx));
+            }
         }
     }
     // If there are no courses and there is no site permission then return false.
index 147ef60..8c2428d 100644 (file)
@@ -328,6 +328,7 @@ class auth_plugin_db extends auth_plugin_base {
                         $updateuser = new stdClass();
                         $updateuser->id   = $user->id;
                         $updateuser->suspended = 1;
+                        $updateuser = $this->clean_data($updateuser);
                         user_update_user($updateuser, false);
                         $trace->output(get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1);
                     }
@@ -414,6 +415,7 @@ class auth_plugin_db extends auth_plugin_base {
                         $updateuser = new stdClass();
                         $updateuser->id = $olduser->id;
                         $updateuser->suspended = 0;
+                        $updateuser = $this->clean_data($updateuser);
                         user_update_user($updateuser);
                         $trace->output(get_string('auth_dbreviveduser', 'auth_db', array('name' => $username,
                             'id' => $olduser->id)), 1);
@@ -436,6 +438,7 @@ class auth_plugin_db extends auth_plugin_base {
                     $trace->output(get_string('auth_dbinsertuserduplicate', 'auth_db', array('username'=>$user->username, 'auth'=>$collision->auth)), 1);
                     continue;
                 }
+                $user = $this->clean_data($user);
                 try {
                     $id = user_create_user($user, false); // It is truly a new user.
                     $trace->output(get_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)), 1);
@@ -577,6 +580,7 @@ class auth_plugin_db extends auth_plugin_base {
         }
         if ($needsupdate) {
             require_once($CFG->dirroot . '/user/lib.php');
+            $updateuser = $this->clean_data($updateuser);
             user_update_user($updateuser);
         }
         return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0));
@@ -906,6 +910,30 @@ class auth_plugin_db extends auth_plugin_base {
         error_reporting($CFG->debug);
         ob_end_flush();
     }
+
+    /**
+     * Clean the user data that comes from an external database.
+     *
+     * @param array $user the user data to be validated against properties definition.
+     * @return stdClass $user the cleaned user data.
+     */
+    public function clean_data($user) {
+        if (empty($user)) {
+            return $user;
+        }
+
+        foreach ($user as $field => $value) {
+            // Get the property parameter type and do the cleaning.
+            try {
+                $property = core_user::get_property_definition($field);
+                $user->$field = clean_param($value, $property['type']);
+            } catch (coding_exception $e) {
+                debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
+            }
+        }
+
+        return $user;
+    }
 }
 
 
index 03c186d..e0d68d3 100644 (file)
@@ -399,4 +399,77 @@ class auth_db_testcase extends advanced_testcase {
         $this->assertEquals("select * from table WHERE column=? AND anothercolumn > ?", $sqlout);
         $this->assertEquals(array(1, 'b'), $arrout);
     }
+
+    /**
+     * Testing the clean_data() method.
+     */
+    public function test_clean_data() {
+        global $DB;
+
+        $this->resetAfterTest(false);
+        $this->preventResetByRollback();
+        $this->init_auth_database();
+        $auth = get_auth_plugin('db');
+        $auth->db_init();
+
+        // Create users on external table.
+        $extdbuser1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com');
+        $extdbuser1->id = $DB->insert_record('auth_db_users', $extdbuser1);
+
+        // User with malicious data on the name.
+        $extdbuser2 = (object)array('name'=>'user<script>alert(1);</script>xss', 'pass'=>'heslo', 'email'=>'xssuser@example.com');
+        $extdbuser2->id = $DB->insert_record('auth_db_users', $extdbuser2);
+
+        $trace = new null_progress_trace();
+
+        // Let's test user sync make sure still works as expected..
+        $auth->sync_users($trace, true);
+
+        // Get the user on moodle user table.
+        $user2 = $DB->get_record('user', array('email'=> $extdbuser2->email, 'auth'=>'db'));
+
+        // The malicious code should be sanitized.
+        $this->assertEquals($user2->username, 'userscriptalert1scriptxss');
+        $this->assertNotEquals($user2->username, $extdbuser2->name);
+
+        // User with correct data, should be equal to external db.
+        $user1 = $DB->get_record('user', array('email'=> $extdbuser1->email, 'auth'=>'db'));
+        $this->assertEquals($extdbuser1->name, $user1->username);
+        $this->assertEquals($extdbuser1->email, $user1->email);
+
+        // Now, let's update the name.
+        $extdbuser2->name = 'user no xss anymore';
+        $DB->update_record('auth_db_users', $extdbuser2);
+
+        // Run sync again to update the user data.
+        $auth->sync_users($trace, true);
+
+        // The user information should be updated.
+        $user2 = $DB->get_record('user', array('username' => 'usernoxssanymore', 'auth' => 'db'));
+        // The spaces should be removed, as it's the username.
+        $this->assertEquals($user2->username, 'usernoxssanymore');
+
+        // Now let's test just the clean_data() method isolated.
+        // Testing PARAM_USERNAME, PARAM_NOTAGS, PARAM_RAW_TRIMMED and others.
+        $user3 = new stdClass();
+        $user3->firstname = 'John <script>alert(1)</script> Doe';
+        $user3->username = 'john%#&~%*_doe';
+        $user3->email = ' john@testing.com ';
+        $user3->deleted = 'no';
+        $user3->description = '<b>A description <script>alert(123)</script>about myself.</b>';
+        $user3cleaned = $auth->clean_data($user3);
+
+        // Expected results.
+        $this->assertEquals($user3cleaned->firstname, 'John alert(1) Doe');
+        $this->assertEquals($user3cleaned->email, 'john@testing.com');
+        $this->assertEquals($user3cleaned->deleted, 0);
+        $this->assertEquals($user3->description, '<b>A description about myself.</b>');
+        $this->assertEquals($user3->username, 'john_doe');
+
+        // Try to clean an invalid property (fullname).
+        $user3->fullname = 'John Doe';
+        $auth->clean_data($user3);
+        $this->assertDebuggingCalled("The property 'fullname' could not be cleaned.");
+        $this->cleanup_auth_database();
+    }
 }
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 436f19f..b088b1a 100644 (file)
@@ -269,6 +269,9 @@ class backup_module_structure_step extends backup_structure_step {
             'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected',
             'availability', 'showdescription'));
 
+        $tags = new backup_nested_element('tags');
+        $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname'));
+
         // attach format plugin structure to $module element, only one allowed
         $this->add_plugin_structure('format', $module, false);
 
@@ -279,6 +282,9 @@ class backup_module_structure_step extends backup_structure_step {
         // attach local plugin structure to $module, multiple allowed
         $this->add_plugin_structure('local', $module, true);
 
+        $module->add_child($tags);
+        $tags->add_child($tag);
+
         // Set the sources
         $concat = $DB->sql_concat("'mod_'", 'm.name');
         $module->set_source_sql("
@@ -289,6 +295,13 @@ class backup_module_structure_step extends backup_structure_step {
               JOIN {course_sections} s ON s.id = cm.section
              WHERE cm.id = ?", array(backup::VAR_MODID));
 
+        $tag->set_source_sql("SELECT t.id, t.name, t.rawname
+                                FROM {tag} t
+                                JOIN {tag_instance} ti ON ti.tagid = t.id
+                               WHERE ti.itemtype = 'course_modules'
+                                 AND ti.component = 'core'
+                                 AND ti.itemid = ?", array(backup::VAR_MODID));
+
         // Define annotations
         $module->annotate_ids('grouping', 'groupingid');
 
index caf5f07..ec316b6 100644 (file)
@@ -3584,6 +3584,8 @@ class restore_module_structure_step extends restore_structure_step {
             $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
         }
 
+        $paths[] = new restore_path_element('tag', '/module/tags/tag');
+
         // Apply for 'format' plugins optional paths at module level
         $this->add_plugin_structure('format', $module);
 
@@ -3691,6 +3693,25 @@ class restore_module_structure_step extends restore_structure_step {
         }
     }
 
+    /**
+     * Fetch all the existing because tag_set() deletes them
+     * so everything must be reinserted on each call.
+     *
+     * @param stdClass $data Record data
+     */
+    protected function process_tag($data) {
+        global $CFG;
+
+        $data = (object)$data;
+
+        if (core_tag_tag::is_enabled('core', 'course_modules')) {
+            $modcontext = context::instance_by_id($this->task->get_contextid());
+            $instanceid = $this->task->get_moduleid();
+
+            core_tag_tag::add_item_tag('core', 'course_modules', $instanceid, $modcontext, $data->rawname);
+        }
+    }
+
     /**
      * Process the legacy availability table record. This table does not exist
      * in Moodle 2.7+ but we still support restore.
@@ -3759,6 +3780,20 @@ class restore_module_structure_step extends restore_structure_step {
                     array('id' => $availfield->coursemoduleid));
         }
     }
+    /**
+     * This method will be executed after the rest of the restore has been processed.
+     *
+     * Update old tag instance itemid(s).
+     */
+    protected function after_restore() {
+        global $DB;
+
+        $contextid = $this->task->get_contextid();
+        $instanceid = $this->task->get_activityid();
+        $olditemid = $this->task->get_old_activityid();
+
+        $DB->set_field('tag_instance', 'itemid', $instanceid, array('contextid' => $contextid, 'itemid' => $olditemid));
+    }
 }
 
 /**
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 9eb544b..d27cb07 100644 (file)
@@ -198,7 +198,7 @@ if (optional_param('executesearch', 0, PARAM_INT) and confirm_sesskey()) {
     require_once($CFG->dirroot . "/webservice/xmlrpc/lib.php");
     $xmlrpcclient = new webservice_xmlrpc_client($serverurl, $token);
     try {
-        $result = $xmlrpcclient->call($function, $params);
+        $result = $xmlrpcclient->call($function, array_values($params));
         $courses = $result['courses'];
         $coursetotal = $result['coursetotal'];
     } catch (Exception $e) {
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 c2053fa..da95699 100644 (file)
@@ -227,21 +227,21 @@ class core_calendar_external extends external_api {
             $params['options']['timeend'] = PHP_INT_MAX;
         }
 
+        // Event list does not check visibility and permissions, we'll check that later.
         $eventlist = calendar_get_events($params['options']['timestart'], $params['options']['timeend'], $funcparam['users'], $funcparam['groups'],
                 $funcparam['courses'], true, $params['options']['ignorehidden']);
+
         // WS expects arrays.
         $events = array();
-        foreach ($eventlist as $id => $event) {
-            $events[$id] = (array) $event;
-        }
 
         // We need to get events asked for eventids.
-        $eventsbyid = calendar_get_events_by_id($params['events']['eventids']);
-        foreach ($eventsbyid as $eventid => $eventobj) {
+        if ($eventsbyid = calendar_get_events_by_id($params['events']['eventids'])) {
+            $eventlist += $eventsbyid;
+        }
+
+        foreach ($eventlist as $eventid => $eventobj) {
             $event = (array) $eventobj;
-            if (isset($events[$eventid])) {
-                   continue;
-            }
+
             if ($hassystemcap) {
                 // User can see everything, no further check is needed.
                 $events[$eventid] = $event;
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 8a5b444..7619b2a 100644 (file)
@@ -370,6 +370,30 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
         $this->assertEquals(1, count($events['events']));
         $this->assertEquals(0, count($events['warnings']));
+
+        // Now, create an activity event.
+        $this->setAdminUser();
+        $nexttime = time() + DAYSECS;
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id, 'duedate' => $nexttime));
+
+        $this->setUser($user);
+        $paramevents = array ('courseids' => array($course->id));
+        $options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + WEEKSECS);
+        $events = core_calendar_external::get_calendar_events($paramevents, $options);
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+
+        $this->assertCount(5, $events['events']);
+
+        // Hide the assignment.
+        set_coursemodule_visible($assign->cmid, 0);
+        // Empty all the caches that may be affected  by this change.
+        accesslib_clear_all_caches_for_unit_testing();
+        course_modinfo::clear_instance_cache();
+
+        $events = core_calendar_external::get_calendar_events($paramevents, $options);
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+        // Expect one less.
+        $this->assertCount(4, $events['events']);
     }
 
     /**
index 5121a98..7902359 100644 (file)
@@ -68,10 +68,10 @@ if (!empty($day) && !empty($mon) && !empty($year)) {
     if (checkdate($mon, $day, $year)) {
         $time = make_timestamp($year, $mon, $day);
     } else {
-        $time = time();
+        $time = usergetmidnight(time());
     }
 } else if (empty($time)) {
-    $time = time();
+    $time = usergetmidnight(time());
 }
 
 $url->param('time', $time);
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 da480bb..6214828 100644 (file)
@@ -2135,7 +2135,11 @@ class core_course_external extends external_api {
                                                         (search, modulelist (only admins), blocklist (only admins), tagid)'),
                 'criteriavalue' => new external_value(PARAM_RAW, 'criteria value'),
                 'page'          => new external_value(PARAM_INT, 'page number (0 based)', VALUE_DEFAULT, 0),
-                'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0)
+                'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
+                'requiredcapabilities' => new external_multiple_structure(
+                    new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
+                    VALUE_OPTIONAL
+                )
             )
         );
     }
@@ -2147,11 +2151,16 @@ class core_course_external extends external_api {
      * @param string $criteriavalue Criteria value
      * @param int $page             Page number (for pagination)
      * @param int $perpage          Items per page
+     * @param array $requiredcapabilities Optional list of required capabilities (used to filter the list).
      * @return array of course objects and warnings
      * @since Moodle 3.0
      * @throws moodle_exception
      */
-    public static function search_courses($criterianame, $criteriavalue, $page=0, $perpage=0) {
+    public static function search_courses($criterianame,
+                                          $criteriavalue,
+                                          $page=0,
+                                          $perpage=0,
+                                          $requiredcapabilities=array()) {
         global $CFG;
         require_once($CFG->libdir . '/coursecatlib.php');
 
@@ -2161,7 +2170,8 @@ class core_course_external extends external_api {
             'criterianame'  => $criterianame,
             'criteriavalue' => $criteriavalue,
             'page'          => $page,
-            'perpage'       => $perpage
+            'perpage'       => $perpage,
+            'requiredcapabilities' => $requiredcapabilities
         );
         $params = self::validate_parameters(self::search_courses_parameters(), $parameters);
 
@@ -2194,8 +2204,8 @@ class core_course_external extends external_api {
         }
 
         // Search the courses.
-        $courses = coursecat::search_courses($searchcriteria, $options);
-        $totalcount = coursecat::search_courses_count($searchcriteria);
+        $courses = coursecat::search_courses($searchcriteria, $options, $params['requiredcapabilities']);
+        $totalcount = coursecat::search_courses_count($searchcriteria, $options, $params['requiredcapabilities']);
 
         $finalcourses = array();
         $categoriescache = array();
@@ -2244,10 +2254,12 @@ class core_course_external extends external_api {
             list($summary, $summaryformat) =
                 external_format_text($course->summary, $course->summaryformat, $coursecontext->id, 'course', 'summary', null);
 
+            $displayname = get_course_display_name_for_list($course);
             $coursereturns = array();
             $coursereturns['id']                = $course->id;
-            $coursereturns['fullname']          = $course->get_formatted_fullname();
-            $coursereturns['shortname']         = $course->get_formatted_shortname();
+            $coursereturns['fullname']          = external_format_string($course->fullname, $coursecontext->id);
+            $coursereturns['displayname']       = external_format_string($displayname, $coursecontext->id);
+            $coursereturns['shortname']         = external_format_string($course->shortname, $coursecontext->id);
             $coursereturns['categoryid']        = $course->category;
             $coursereturns['categoryname']      = $category->name;
             $coursereturns['summary']           = $summary;
@@ -2281,6 +2293,7 @@ class core_course_external extends external_api {
                         array(
                             'id' => new external_value(PARAM_INT, 'course id'),
                             'fullname' => new external_value(PARAM_TEXT, 'course full name'),
+                            'displayname' => new external_value(PARAM_TEXT, 'course display name'),
                             'shortname' => new external_value(PARAM_TEXT, 'course short name'),
                             'categoryid' => new external_value(PARAM_INT, 'category id'),
                             'categoryname' => new external_value(PARAM_TEXT, 'category name'),
index 4cb1e52..6afaae0 100644 (file)
@@ -1690,6 +1690,7 @@ function course_delete_module($cmid) {
 
     // Delete all tag instances associated with the instance of this module.
     core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
+    core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
 
     // Delete the context.
     context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
index 65880f9..27b982b 100644 (file)
@@ -156,6 +156,7 @@ if (!empty($add)) {
     $data->completionexpected = $cm->completionexpected;
     $data->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1;
     $data->showdescription    = $cm->showdescription;
+    $data->tags               = core_tag_tag::get_item_tags_array('core', 'course_modules', $cm->id);
     if (!empty($CFG->enableavailability)) {
         $data->availabilityconditionsjson = $cm->availability;
     }
index b878ca1..56814ad 100644 (file)
@@ -151,6 +151,11 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) {
         $DB->set_field($moduleinfo->modulename, 'intro', $moduleinfo->intro, array('id'=>$moduleinfo->instance));
     }
 
+    // Add module tags.
+    if (core_tag_tag::is_enabled('core', 'course_modules') && isset($moduleinfo->tags)) {
+        core_tag_tag::set_item_tags('core', 'course_modules', $moduleinfo->coursemodule, $modcontext, $moduleinfo->tags);
+    }
+
     // Course_modules and course_sections each contain a reference to each other.
     // So we have to update one of them twice.
     $sectionid = course_add_cm_to_section($course, $moduleinfo->coursemodule, $moduleinfo->section);
@@ -578,6 +583,11 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) {
         set_coursemodule_idnumber($moduleinfo->coursemodule, $moduleinfo->cmidnumber);
     }
 
+    // Update module tags.
+    if (core_tag_tag::is_enabled('core', 'course_modules') && isset($moduleinfo->tags)) {
+        core_tag_tag::set_item_tags('core', 'course_modules', $moduleinfo->coursemodule, $modcontext, $moduleinfo->tags);
+    }
+
     // Now that module is fully updated, also update completion data if required.
     // (this will wipe all user completion data and recalculate it)
     if ($completion->is_enabled() && !empty($moduleinfo->completionunlocked)) {
index 1f493d0..841a261 100644 (file)
@@ -597,6 +597,16 @@ abstract class moodleform_mod extends moodleform {
             $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
         }
 
+        // Populate module tags.
+        if (core_tag_tag::is_enabled('core', 'course_modules')) {
+            $mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
+            $mform->addElement('tags', 'tags', get_string('tags'), array('itemtype' => 'course_modules', 'component' => 'core'));
+            if ($this->_cm) {
+                $tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $this->_cm->id);
+                $mform->setDefault('tags', $tags);
+            }
+        }
+
         $this->standard_hidden_coursemodule_elements();
     }
 
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 de74065..0e4e9de 100644 (file)
@@ -1522,9 +1522,12 @@ class core_course_courselib_testcase extends advanced_testcase {
             case 'assign':
                 // Add some tags to this assignment.
                 core_tag_tag::set_item_tags('mod_assign', 'assign', $module->id, $modcontext, array('Tag 1', 'Tag 2', 'Tag 3'));
+                core_tag_tag::set_item_tags('core', 'course_modules', $module->cmid, $modcontext, array('Tag 3', 'Tag 4', 'Tag 5'));
 
                 // Confirm the tag instances were added.
-                $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
+                $criteria = array('component' => 'mod_assign', 'itemtype' => 'assign', 'contextid' => $modcontext->id);
+                $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
+                $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
                 $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
 
                 // Verify event assignment event has been generated.
@@ -1565,6 +1568,9 @@ class core_course_courselib_testcase extends advanced_testcase {
                 // Verify the tag instances were deleted.
                 $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
                 $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
+
+                $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
+                $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
                 break;
             case 'quiz':
                 // Verify category deleted.
index 5659d9f..e563b60 100644 (file)
@@ -132,21 +132,30 @@ class enrol_meta_plugin extends enrol_plugin {
      * Add new instance of enrol plugin.
      * @param object $course
      * @param array $fields instance fields
-     * @return int id of new instance, null if can not be created
+     * @return int id of last instance, null if can not be created
      */
     public function add_instance($course, array $fields = null) {
         global $CFG;
 
         require_once("$CFG->dirroot/enrol/meta/locallib.php");
 
-        if (!empty($fields['customint2']) && $fields['customint2'] == ENROL_META_CREATE_GROUP) {
-            $context = context_course::instance($course->id);
-            require_capability('moodle/course:managegroups', $context);
-            $groupid = enrol_meta_create_new_group($course->id, $fields['customint1']);
-            $fields['customint2'] = $groupid;
+        // Support creating multiple at once.
+        if (is_array($fields['customint1'])) {
+            $courses = array_unique($fields['customint1']);
+        } else {
+            $courses = array($fields['customint1']);
         }
+        foreach ($courses as $courseid) {
+            if (!empty($fields['customint2']) && $fields['customint2'] == ENROL_META_CREATE_GROUP) {
+                $context = context_course::instance($course->id);
+                require_capability('moodle/course:managegroups', $context);
+                $groupid = enrol_meta_create_new_group($course->id, $courseid);
+                $fields['customint2'] = $groupid;
+            }
 
-        $result = parent::add_instance($course, $fields);
+            $fields['customint1'] = $courseid;
+            $result = parent::add_instance($course, $fields);
+        }
 
         enrol_meta_sync($course->id);
 
@@ -258,7 +267,7 @@ class enrol_meta_plugin extends enrol_plugin {
         }
 
         // TODO: this has to be done via ajax or else it will fail very badly on large sites!
-        $courses = array('' => get_string('choosedots'));
+        $courses = array();
         $select = ', ' . context_helper::get_preload_record_columns_sql('ctx');
         $join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
 
@@ -313,10 +322,20 @@ class enrol_meta_plugin extends enrol_plugin {
     public function edit_instance_form($instance, MoodleQuickForm $mform, $coursecontext) {
         global $DB;
 
-        $courses = $this->get_course_options($instance, $coursecontext);
         $groups = $this->get_group_options($coursecontext);
+        $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $coursecontext->instanceid), '', 'customint1, id');
 
-        $mform->addElement('select', 'customint1', get_string('linkedcourse', 'enrol_meta'), $courses);
+        $excludelist = array($coursecontext->instanceid);
+        foreach ($existing as $existinginstance) {
+            $excludelist[] = $existinginstance->customint1;
+        }
+
+        $options = array(
+            'requiredcapabilities' => array('enrol/meta:selectaslinked'),
+            'multiple' => true,
+            'exclude' => $excludelist
+        );
+        $mform->addElement('course', 'customint1', get_string('linkedcourse', 'enrol_meta'), $options);
         $mform->addRule('customint1', get_string('required'), 'required', null, 'client');
         if (!empty($instance->id)) {
             $mform->freeze('customint1');
@@ -343,28 +362,25 @@ class enrol_meta_plugin extends enrol_plugin {
         $c = false;
 
         if (!empty($data['customint1'])) {
-            $c = $DB->get_record('course', array('id' => $data['customint1']));
-        }
-
-        if (!$c) {
-            $errors['customint1'] = get_string('required');
-        } else {
-            $coursecontext = context_course::instance($c->id);
-            $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $thiscourseid), '', 'customint1, id');
-            if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
-                $errors['customint1'] = get_string('error');
-            } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
-                $errors['customint1'] = get_string('error');
-            } else if ($c->id == SITEID or $c->id == $thiscourseid or isset($existing[$c->id])) {
-                $errors['customint1'] = get_string('error');
+            foreach ($data['customint1'] as $courseid) {
+                $c = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+                $coursecontext = context_course::instance($c->id);
+                $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $thiscourseid), '', 'customint1, id');
+                if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+                    $errors['customint1'] = get_string('error');
+                } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
+                    $errors['customint1'] = get_string('error');
+                } else if ($c->id == SITEID or $c->id == $thiscourseid or isset($existing[$c->id])) {
+                    $errors['customint1'] = get_string('error');
+                }
             }
+        } else {
+            $errors['customint1'] = get_string('required');
         }
 
-        $validcourses = array_keys($this->get_course_options($instance, $context));
         $validgroups = array_keys($this->get_group_options($context));
 
         $tovalidate = array(
-            'customint1' => $validcourses,
             'customint2' => $validgroups
         );
         $typeerrors = $this->validate_param_types($data, $tovalidate);
index 9a07371..c5df107 100644 (file)
@@ -1,4 +1,4 @@
-@enrol @enrol_meta
+@enrol @enrol_meta @javascript
 Feature: Enrolments are synchronised with meta courses
   In order to simplify enrolments in parent courses
   As a teacher
@@ -13,21 +13,21 @@ Feature: Enrolments are synchronised with meta courses
       | student4 | Student | 4 | student4@asd.com |
     And the following "courses" exist:
       | fullname | shortname |
-      | Course 1 | C1 |
-      | Course 2 | C2 |
-      | Course 3 | C3 |
+      | Course 1 | C1C1 |
+      | Course 2 | C2C2 |
+      | Course 3 | C3C3 |
     And the following "groups" exist:
       | name | course | idnumber |
-      | Groupcourse 1 | C3 | G1 |
-      | Groupcourse 2 | C3 | G2 |
+      | Groupcourse 1 | C3C3 | G1 |
+      | Groupcourse 2 | C3C3 | G2 |
     And the following "course enrolments" exist:
       | user | course | role |
-      | student1 | C1 | student |
-      | student2 | C1 | student |
-      | student3 | C1 | student |
-      | student4 | C1 | student |
-      | student1 | C2 | student |
-      | student2 | C2 | student |
+      | student1 | C1C1 | student |
+      | student2 | C1C1 | student |
+      | student3 | C1C1 | student |
+      | student4 | C1C1 | student |
+      | student1 | C2C2 | student |
+      | student2 | C2C2 | student |
     And I log in as "admin"
     And I navigate to "Manage enrol plugins" node in "Site administration > Plugins > Enrolments"
     And I click on "Enable" "link" in the "Course meta link" "table_row"
@@ -37,7 +37,7 @@ Feature: Enrolments are synchronised with meta courses
   Scenario: Add meta enrolment instance without groups
     When I follow "Course 3"
     And I add "Course meta link" enrolment method with:
-      | Link course  | Course 1 |
+      | Link course  | C1C1 |
     And I navigate to "Enrolled users" node in "Course administration > Users"
     Then I should see "Student 1"
     And I should see "Student 4"
@@ -45,18 +45,13 @@ Feature: Enrolments are synchronised with meta courses
 
   Scenario: Add meta enrolment instance with groups
     When I follow "Course 3"
-    And I navigate to "Enrolment methods" node in "Course administration > Users"
-    And I select "Course meta link" from the "Add method" singleselect
-    And I set the following fields to these values:
-      | Link course  | Course 1      |
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C1C1      |
       | Add to group | Groupcourse 1 |
-    And I press "Add method"
-    And I set the field "Add method" to "Course meta link"
-    And I press "Go"
-    And I set the following fields to these values:
-      | Link course  | Course 2      |
+    And I follow "Course 3"
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C2C2      |
       | Add to group | Groupcourse 2 |
-    And I press "Add method"
     And I navigate to "Enrolled users" node in "Course administration > Users"
     Then I should see "Groupcourse 1" in the "Student 1" "table_row"
     And I should see "Groupcourse 1" in the "Student 2" "table_row"
@@ -69,13 +64,9 @@ Feature: Enrolments are synchronised with meta courses
 
   Scenario: Add meta enrolment instance with auto-created groups
     When I follow "Course 3"
-    And I navigate to "Enrolment methods" node in "Course administration > Users"
-    And I set the field "Add method" to "Course meta link"
-    And I press "Go"
-    And I set the following fields to these values:
-      | Link course  | Course 1      |
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C1C1      |
       | Add to group | Create new group |
-    And I press "Add method"
     And I navigate to "Enrolled users" node in "Course administration > Users"
     Then I should see "Course 1 course" in the "Student 1" "table_row"
     And I should see "Course 1 course" in the "Student 2" "table_row"
@@ -86,17 +77,12 @@ Feature: Enrolments are synchronised with meta courses
 
   Scenario: Backup and restore of meta enrolment instance
     When I follow "Course 3"
-    And I navigate to "Enrolment methods" node in "Course administration > Users"
-    And I set the field "Add method" to "Course meta link"
-    And I press "Go"
-    And I set the following fields to these values:
-      | Link course  | Course 1      |
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C1C1      |
       | Add to group | Groupcourse 1 |
-    And I press "Add method"
-    And I select "Course meta link" from the "Add method" singleselect
-    And I set the following fields to these values:
-      | Link course  | Course 2      |
-    And I press "Add method"
+    And I follow "Course 3"
+    And I add "Course meta link" enrolment method with:
+      | Link course  | C2C2      |
     When I backup "Course 3" course using this options:
       | Confirmation | Filename | test_backup.mbz |
     And I click on "Restore" "link" in the "test_backup.mbz" "table_row"
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 9695c9c..9886fe2 100644 (file)
@@ -237,6 +237,11 @@ if ($mform->is_cancelled()) {
 
     $grade_item->outcomeid = null;
 
+    if (!empty($data->grade_item_rescalegrades) && $data->grade_item_rescalegrades == 'yes') {
+        $grade_item->rescale_grades_keep_percentage($grade_item_copy->grademin, $grade_item_copy->grademax, $grade_item->grademin,
+                $grade_item->grademax, 'gradebook');
+    }
+
     // update hiding flag
     if ($hiddenuntil) {
         $grade_item->set_hidden($hiddenuntil, false);
index b090740..98a18a4 100644 (file)
@@ -32,7 +32,7 @@ class edit_category_form extends moodleform {
     private $aggregation_options = array();
 
     function definition() {
-        global $CFG, $COURSE, $DB;
+        global $CFG, $COURSE, $DB, $OUTPUT;
         $mform =& $this->_form;
 
         $category = $this->_customdata['current'];
@@ -104,6 +104,25 @@ class edit_category_form extends moodleform {
         $mform->addHelpButton('grade_item_idnumber', 'idnumbermod');
         $mform->setType('grade_item_idnumber', PARAM_RAW);
 
+        if (!empty($category->id)) {
+            $gradecategory = grade_category::fetch(array('id' => $category->id));
+            $gradeitem = $gradecategory->load_grade_item();
+
+            // If grades exist set a message so the user knows why they can not alter the grade type or scale.
+            // We could never change the grade type for external items, so only need to show this for manual grade items.
+            if ($gradeitem->has_overridden_grades()) {
+                // Set a message so the user knows why the can not alter the grade type or scale.
+                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+                    $gradesexistmsg = get_string('modgradecategorycantchangegradetyporscalemsg', 'grades');
+                } else {
+                    $gradesexistmsg = get_string('modgradecategorycantchangegradetypemsg', 'grades');
+                }
+                $notification = new \core\output\notification($gradesexistmsg, \core\output\notification::NOTIFY_INFO);
+                $notification->set_show_closebutton(false);
+                $mform->addElement('static', 'gradesexistmsg', '', $OUTPUT->render($notification));
+            }
+        }
+
         $options = array(GRADE_TYPE_NONE=>get_string('typenone', 'grades'),
                          GRADE_TYPE_VALUE=>get_string('typevalue', 'grades'),
                          GRADE_TYPE_SCALE=>get_string('typescale', 'grades'),
@@ -140,6 +159,14 @@ class edit_category_form extends moodleform {
         $mform->disabledIf('grade_item_scaleid', 'grade_item_gradetype', 'noteq', GRADE_TYPE_SCALE);
         $mform->disabledIf('grade_item_scaleid', 'aggregation', 'eq', GRADE_AGGREGATE_SUM);
 
+        $choices = array();
+        $choices[''] = get_string('choose');
+        $choices['no'] = get_string('no');
+        $choices['yes'] = get_string('yes');
+        $mform->addElement('select', 'grade_item_rescalegrades', get_string('modgradecategoryrescalegrades', 'grades'), $choices);
+        $mform->addHelpButton('grade_item_rescalegrades', 'modgradecategoryrescalegrades', 'grades');
+        $mform->disabledIf('grade_item_rescalegrades', 'grade_item_gradetype', 'noteq', GRADE_TYPE_VALUE);
+
         $mform->addElement('text', 'grade_item_grademax', get_string('grademax', 'grades'));
         $mform->setType('grade_item_grademax', PARAM_RAW);
         $mform->addHelpButton('grade_item_grademax', 'grademax', 'grades');
@@ -398,6 +425,8 @@ class edit_category_form extends moodleform {
                     }
                 }
             }
+
+            $mform->removeElement('grade_item_rescalegrades');
         }
 
 
@@ -423,6 +452,25 @@ class edit_category_form extends moodleform {
                 $mform->removeElement('grade_item_display');
                 $mform->removeElement('grade_item_decimals');
                 $mform->hardFreeze('grade_item_scaleid');
+            // Only show the option to rescale grades on a category if its corresponding grade_item has overridden grade_grades.
+            } else if ($grade_item->has_overridden_grades()) {
+                // Can't change the grade type or the scale if there are grades.
+                $mform->hardFreeze('grade_item_gradetype, grade_item_scaleid');
+
+                // If we are using scles then remove the unnecessary rescale and grade fields.
+                if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
+                    $mform->removeElement('grade_item_rescalegrades');
+                    $mform->removeElement('grade_item_grademax');
+                    if ($mform->elementExists('grade_item_grademin')) {
+                        $mform->removeElement('grade_item_grademin');
+                    }
+                } else { // Not using scale, so remove it.
+                    $mform->removeElement('grade_item_scaleid');
+                    $mform->disabledIf('grade_item_grademax', 'grade_item_rescalegrades', 'eq', '');
+                    $mform->disabledIf('grade_item_grademin', 'grade_item_rescalegrades', 'eq', '');
+                }
+            } else { // Remove the rescale element if there are no grades.
+                $mform->removeElement('grade_item_rescalegrades');
             }
 
             //remove the aggregation coef element if not needed
@@ -484,6 +532,11 @@ class edit_category_form extends moodleform {
 /// perform extra validation before submission
     function validation($data, $files) {
         global $COURSE;
+        $gradeitem = false;
+        if ($data['id']) {
+            $gradecategory = grade_category::fetch(array('id' => $data['id']));
+            $gradeitem = $gradecategory->load_grade_item();
+        }
 
         $errors = parent::validation($data, $files);
 
@@ -501,6 +554,16 @@ class edit_category_form extends moodleform {
              }
         }
 
+        if ($data['id'] && $gradeitem->has_overridden_grades()) {
+            if ($gradeitem->gradetype == GRADE_TYPE_VALUE) {
+                if (grade_floats_different($data['grade_item_grademin'], $gradeitem->grademin) ||
+                    grade_floats_different($data['grade_item_grademax'], $gradeitem->grademax)) {
+                    if (empty($data['grade_item_rescalegrades'])) {
+                        $errors['grade_item_rescalegrades'] = get_string('mustchooserescaleyesorno', 'grades');
+                    }
+                }
+            }
+        }
         return $errors;
     }
 }
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 153a770..88763ec 100644 (file)
@@ -137,7 +137,7 @@ class moodlequickform_guideeditor extends HTML_QuickForm_input {
             $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
         }
         if ($this->validationerrors) {
-            $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
+            $html .= html_writer::div($renderer->notification($this->validationerrors));
         }
         $html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
         return $html;
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 533e81e..54601b5 100644 (file)
@@ -141,7 +141,7 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
             $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
         }
         if ($this->validationerrors) {
-            $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
+            $html .= html_writer::div($renderer->notification($this->validationerrors));
         }
         $html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName());
         return $html;
@@ -380,4 +380,4 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
         $value =  $this->prepare_data($this->_findValue($submitValues));
         return $this->_prepareValue($value, $assoc);
     }
-}
\ No newline at end of file
+}
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 8a43bd5..7c97600 100644 (file)
@@ -329,6 +329,13 @@ abstract class screen {
                 continue;
             }
 
+            // If the user submits Exclude grade elements without the proper.
+            // permissions then we should refuse to update.
+            if ($matches[1] === 'exclude' && !has_capability('moodle/grade:manage', $this->context)){
+                $warnings[] = get_string('nopermissions', 'error', get_string('grade:manage', 'role'));
+                continue;
+            }
+
             $msg = $element->set($posted);
 
             // Optional type.
index ecb6f90..5e5054f 100644 (file)
@@ -141,6 +141,12 @@ abstract class tablelike extends screen {
                 $html .= $this->structure->get_grade_analysis_icon($grade);
             }
 
+            // Singleview users without proper permissions should be presented
+            // disabled checkboxes for the Exclude grade attribute.
+            if ($field == 'exclude' && !has_capability('moodle/grade:manage', $this->context)){
+                $html->disabled = true;
+            }
+
             $line[] = $html;
         }
         return $line;
index caddb1e..62f7780 100644 (file)
@@ -35,11 +35,14 @@ use grade_grade;
  * @copyright 2014 Moodle Pty Ltd (http://moodle.com)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class exclude extends grade_attribute_format implements be_checked {
+class exclude extends grade_attribute_format implements be_checked, be_disabled {
 
     /** @var string $name The name of the input */
     public $name = 'exclude';
 
+    /** @var bool $disabled Is the checkbox disabled? */
+    public $disabled = false;
+
     /**
      * Is it checked?
      *
@@ -49,6 +52,15 @@ class exclude extends grade_attribute_format implements be_checked {
         return $this->grade->is_excluded();
     }
 
+    /**
+     * Is it disabled?
+     *
+     * @return bool
+     */
+    public function is_disabled() {
+        return $this->disabled;
+    }
+
     /**
      * Generate the element used to render the UI
      *
@@ -58,7 +70,8 @@ class exclude extends grade_attribute_format implements be_checked {
         return new checkbox_attribute(
             $this->get_name(),
             $this->get_label(),
-            $this->is_checked()
+            $this->is_checked(),
+            $this->is_disabled()
         );
     }
 
index f12404d..22aba02 100644 (file)
@@ -11,6 +11,7 @@ Feature: We can use Single view
     And the following "users" exist:
       | username | firstname | lastname | email | idnumber | alternatename |
       | teacher1 | Teacher | 1 | teacher1@example.com | t1 | fred |
+      | teacher2 | No edit | 1 | teacher2@example.com | t2 | nick |
       | student1 | Student | 1 | student1@example.com | s1 | james |
       | student2 | Student | 2 | student1@example.com | s2 | holly |
       | student3 | Student | 3 | student1@example.com | s3 | anna |
@@ -27,6 +28,7 @@ Feature: We can use Single view
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
+      | teacher2 | C1 | teacher |
       | student1 | C1 | student |
       | student2 | C1 | student |
       | student3 | C1 | student |
@@ -44,6 +46,10 @@ Feature: We can use Single view
     And the following "grade items" exist:
       | itemname | course | gradetype |
       | Test grade item | C1 | Scale |
+    And the following "permission overrides" exist:
+      | capability                  | permission | role     | contextlevel  | reference |
+      | moodle/grade:edit           | Allow      | teacher  | Course        | C1        |
+      | gradereport/singleview:view | Allow      | teacher  | Course        | C1        |
     And I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
@@ -88,6 +94,14 @@ Feature: We can use Single view
     And the following should exist in the "generaltable" table:
         | First name (Alternate name) Surname | Grade |
         | james (Student) 1 | Very good |
+    And I log out
+    And I log in as "teacher2"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I click on "Single view" "option"
+    And I click on "Student 4" "option"
+    And the "Exclude for Test assignment one" "checkbox" should be disabled
+    And the "Override for Test assignment one" "checkbox" should be enabled
 
   Scenario: Single view links work on grade report.
     Given I follow "Single view for Test assignment one"
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"
diff --git a/grade/tests/behat/grade_category_validation.feature b/grade/tests/behat/grade_category_validation.feature
new file mode 100644 (file)
index 0000000..0388b88
--- /dev/null
@@ -0,0 +1,108 @@
+@core_grades
+Feature: Editing a grade item
+  In order to ensure validation is provided to the teacher
+  As a teacher
+  I need to know why I can not add/edit values on the grade category form
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I navigate to "Scales" node in "Site administration > Grades"
+    And I press "Add a new scale"
+    And I set the following fields to these values:
+      | Name  | ABCDEF |
+      | Scale | F,E,D,C,B,A |
+    And I press "Save changes"
+    And I press "Add a new scale"
+    And I set the following fields to these values:
+      | Name  | Letter scale |
+      | Scale | Disappointing, Good, Very good, Excellent |
+    And I press "Save changes"
+    And I set the following administration settings values:
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+    And I log out
+    And I log in as "teacher1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I navigate to "Gradebook setup" node in "Course administration"
+    And I press "Add category"
+    And I set the following fields to these values:
+      | Category name | Cat 1 |
+      | Aggregation   | Highest grade |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Item 1 |
+      | Grade category | Cat 1 |
+    And I press "Save changes"
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | Item 2 |
+      | Grade category | Cat 1 |
+    And I press "Save changes"
+
+  Scenario: Being able to change the grade type, scale and maximum grade for a grade category when there are no overridden grades
+    Given I click on "Edit" "link" in the "Cat 1" "table_row"
+    When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    Then I should not see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded"
+    And I set the field "Grade type" to "Scale"
+    And I press "Save changes"
+    And I should see "Scale must be selected"
+    And I set the field "Scale" to "ABCDEF"
+    And I press "Save changes"
+    And I should not see "You cannot change the type, as grades already exist for this item"
+    And I click on "Edit" "link" in the "Cat 1" "table_row"
+    And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    And I should not see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded"
+    And I set the field "Scale" to "Letter scale"
+    And I press "Save changes"
+    And I should not see "You cannot change the scale, as grades already exist for this item"
+
+  Scenario: Attempting to change a category item's grade type when overridden grades already exist
+    Given I navigate to "Grader report" node in "Grade administration"
+    And I turn editing mode on
+    And I give the grade "20.00" to the user "Student 1" for the grade item "Cat 1 total"
+    And I press "Save changes"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+    And I click on "Edit" "link" in the "Cat 1" "table_row"
+    When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    Then I should see "This category has associated grade items which have been overridden. Therefore 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 "//div[contains(concat(' ', normalize-space(@class), ' '), 'fstatic') and contains(text(), 'Value')]" "xpath_element" should exist
+
+  Scenario: Attempting to change a category item's scale when overridden grades already exist
+    Given I click on "Edit" "link" in the "Cat 1" "table_row"
+    And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    And I set the field "Grade type" to "Scale"
+    And I set the field "Scale" to "ABCDEF"
+    And I press "Save changes"
+    And I navigate to "Grader report" node in "Grade administration"
+    And I turn editing mode on
+    And I give the grade "C" to the user "Student 1" for the grade item "Cat 1 total"
+    And I press "Save changes"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+    And I click on "Edit" "link" in the "Cat 1" "table_row"
+    When I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    Then I should see "This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type and scale cannot be changed."
+    And "//div[contains(concat(' ', normalize-space(@class), ' '), 'fstatic') and contains(text(), 'ABCDEF')]" "xpath_element" should exist
+
+  Scenario: Attempting to change a category item's maximum grade when no rescaling option has been chosen
+    Given I navigate to "Grader report" node in "Grade administration"
+    And I turn editing mode on
+    And I give the grade "20.00" to the user "Student 1" for the grade item "Cat 1 total"
+    And I press "Save changes"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
+    And I click on "Edit" "link" in the "Cat 1" "table_row"
+    And I click on "Edit settings" "link" in the "Cat 1" "table_row"
+    And I set the field "Maximum grade" to "50"
+    When I press "Save changes"
+    Then I should see "You must choose whether to rescale existing grades or not."
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 068fe75..3e5c6f4 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Dil';
+$string['moodlelogo'] = 'Moodle logo';
 $string['next'] = 'Sonraki';
 $string['previous'] = 'Önceki';
 $string['reload'] = 'Tekrar yükle';
index a38048b..1ed26d1 100644 (file)
@@ -1028,7 +1028,8 @@ $string['taskdeleteincompleteusers'] = 'Delete incomplete users';
 $string['taskdeleteunconfirmedusers'] = 'Delete unconfirmed users';
 $string['taskeventscron'] = 'Background processing for events';
 $string['taskfiletrashcleanup'] = 'Cleanup files in trash';
-$string['taskglobalsearch'] = 'Global search indexing';
+$string['taskglobalsearchindex'] = 'Global search indexing';
+$string['taskglobalsearchoptimize'] = 'Global search index optimization';
 $string['taskgradecron'] = 'Background processing for gradebook';
 $string['tasklegacycron'] = 'Legacy cron processing for plugins';
 $string['taskmessagingcleanup'] = 'Background processing for messaging';
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 f37a5fe..c5e9dab 100644 (file)
@@ -477,6 +477,8 @@ $string['modgrade_help'] = 'Select the type of grading used for this activity. I
 $string['modgradecantchangegradetype'] = 'You cannot change the type, as grades already exist for this item.';
 $string['modgradecantchangegradetypemsg'] = '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.';
 $string['modgradecantchangegradetyporscalemsg'] = 'Some grades have already been awarded, so the grade type and scale cannot be changed.';
+$string['modgradecategorycantchangegradetypemsg'] = 'This category has associated grade items which have been overridden. Therefore 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.';
+$string['modgradecategorycantchangegradetyporscalemsg'] = 'This category has associated grade items which have been overridden. Therefore some grades have already been awarded, so the grade type and scale cannot be changed.';
 $string['modgradecantchangescale'] = 'You cannot change the scale, as grades already exist for this item.';
 $string['modgradecantchangeratingmaxgrade'] = 'You cannot change the maximum grade when grades already exist for an activity with ratings.';
 $string['modgradedonotmodify'] = 'Do not modify existing grades';
@@ -489,6 +491,12 @@ $string['modgraderescalegrades_help'] = 'When changing the maximum grades on a g
 If this is set to \'Yes\', any existing grades will be rescaled so that the percentage grade remains the same.
 
 For example, if this option is set to \'Yes\', changing the maximum grade on an item from 10 to 20 would cause a grade of 6/10 (60%) to be rescaled to 12/20 (60%). With this option set to \'No\', the grade would change from 6/10 (60%) to 6/20 (30%), requiring manual adjustment of the grade items to ensure correct scores.';
+$string['modgradecategoryrescalegrades'] = 'Rescale overridden grades';
+$string['modgradecategoryrescalegrades_help'] = 'When changing the maximum grades on a gradebook item you need to specify whether or not this will cause existing percentage grades to change as well.
+
+If this is set to \'Yes\', any existing overridden grades will be rescaled so that the percentage grade remains the same.
+
+For example, if this option is set to \'Yes\', changing the maximum grade on an item from 10 to 20 would cause a grade of 6/10 (60%) to be rescaled to 12/20 (60%). With this option set to \'No\', the grade will remain unchanged, requiring manual adjustment of the grade items to ensure correct scores.';
 $string['modgradetype'] = 'Type';
 $string['modgradetypenone'] = 'None';
 $string['modgradetypepoint'] = 'Point';
index 335cc75..b3d0a5d 100644 (file)
@@ -161,6 +161,8 @@ $string['type_report'] = 'Site report';
 $string['type_report_plural'] = 'Reports';
 $string['type_repository'] = 'Repository';
 $string['type_repository_plural'] = 'Repositories';
+$string['type_search'] = 'Search engine';
+$string['type_search_plural'] = 'Search engines';
 $string['type_theme'] = 'Theme';
 $string['type_theme_plural'] = 'Themes';
 $string['type_tool'] = 'Admin tool';
index 552cdcc..af4bd3f 100644 (file)
@@ -116,6 +116,7 @@ $string['tagarea_blog_external'] = 'External blog posts';
 $string['tagarea_post'] = 'Blog posts';
 $string['tagarea_user'] = 'User interests';
 $string['tagarea_course'] = 'Courses';
+$string['tagarea_course_modules'] = 'Course modules';
 $string['tagareaenabled'] = 'Enabled';
 $string['tagareaname'] = 'Name';
 $string['tagareas'] = 'Tag areas';
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 5deb194..3df2cc3 100644 (file)
@@ -9538,22 +9538,10 @@ class admin_setting_searchsetupinfo extends admin_setting {
 
         $return .= $brtag . get_string('searchsetupdescription', 'search') . $brtag . $brtag;
 
-        // Enable global search.
-        $row = array();
-        $url = new moodle_url("/admin/search.php?query=enableglobalsearch");
-        $row[0] = '1. ' . html_writer::tag('a', get_string('enableglobalsearch', 'admin'),
-                        array('href' => $url));
-        $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
-        if (\core_search\manager::is_global_search_enabled()) {
-            $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
-        }
-        $row[1] = $status;
-        $table->data[] = $row;
-
         // Select a search engine.
         $row = array();
         $url = new moodle_url('/admin/settings.php?section=manageglobalsearch#admin-searchengine');
-        $row[0] = '2. ' . html_writer::tag('a', get_string('selectsearchengine', 'admin'),
+        $row[0] = '1. ' . html_writer::tag('a', get_string('selectsearchengine', 'admin'),
                         array('href' => $url));
 
         $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
@@ -9568,7 +9556,7 @@ class admin_setting_searchsetupinfo extends admin_setting {
         // Available areas.
         $row = array();
         $url = new moodle_url('/admin/settings.php?section=manageglobalsearch#admin-searchengine');
-        $row[0] = '3. ' . html_writer::tag('a', get_string('enablesearchareas', 'admin'),
+        $row[0] = '2. ' . html_writer::tag('a', get_string('enablesearchareas', 'admin'),
                         array('href' => $url));
 
         $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
@@ -9582,11 +9570,11 @@ class admin_setting_searchsetupinfo extends admin_setting {
         // Setup search engine.
         $row = array();
         if (empty($CFG->searchengine)) {
-            $row[0] = '4. ' . get_string('setupsearchengine', 'admin');
+            $row[0] = '3. ' . get_string('setupsearchengine', 'admin');
             $row[1] = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
         } else {
             $url = new moodle_url('/admin/settings.php?section=search' . $CFG->searchengine);
-            $row[0] = '4. ' . html_writer::tag('a', get_string('setupsearchengine', 'admin'),
+            $row[0] = '3. ' . html_writer::tag('a', get_string('setupsearchengine', 'admin'),
                             array('href' => $url));
             // Check the engine status.
             $searchengine = \core_search\manager::search_engine_instance();
@@ -9603,7 +9591,7 @@ class admin_setting_searchsetupinfo extends admin_setting {
         // Indexed data.
         $row = array();
         $url = new moodle_url('/report/search/index.php#searchindexform');
-        $row[0] = '5. ' . html_writer::tag('a', get_string('indexdata', 'admin'), array('href' => $url));
+        $row[0] = '4. ' . html_writer::tag('a', get_string('indexdata', 'admin'), array('href' => $url));
         if ($anyindexed) {
             $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
         } else {
@@ -9612,6 +9600,18 @@ class admin_setting_searchsetupinfo extends admin_setting {
         $row[1] = $status;
         $table->data[] = $row;
 
+        // Enable global search.
+        $row = array();
+        $url = new moodle_url("/admin/search.php?query=enableglobalsearch");
+        $row[0] = '5. ' . html_writer::tag('a', get_string('enableglobalsearch', 'admin'),
+                        array('href' => $url));
+        $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
+        if (\core_search\manager::is_global_search_enabled()) {
+            $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
+        }
+        $row[1] = $status;
+        $table->data[] = $row;
+
         $return .= html_writer::table($table);
 
         return highlight($query, $return);
index e2e319a..d87f4ea 100644 (file)
@@ -32,6 +32,10 @@ require_once(dirname(__FILE__) . '/../../config.php');
 /** Include course lib for its functions */
 require_once($CFG->dirroot.'/course/lib.php');
 
+if (!empty($CFG->forcelogin)) {
+    require_login();
+}
+
 try {
     // Start buffer capture so that we can `remove` any errors
     ob_start();
index c7c80e1..3af51d6 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index 1adc2b0..d024ddb 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
diff --git a/lib/amd/build/form-course-selector.min.js b/lib/amd/build/form-course-selector.min.js
new file mode 100644 (file)
index 0000000..6eaaee6
Binary files /dev/null and b/lib/amd/build/form-course-selector.min.js differ
index 38a393b..0ba0d5d 100644 (file)
@@ -148,7 +148,8 @@ define(['jquery', 'core/config'], function($, config) {
                 context: requests,
                 dataType: 'json',
                 processData: false,
-                async: async
+                async: async,
+                contentType: "application/json"
             };
 
             var script = config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey;
index 24d32c4..474708e 100644 (file)
@@ -469,12 +469,20 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 if (!option.prop('selected')) {
                     option.remove();
                 } else {
-                    existingValues.push(option.attr('value'));
+                    existingValues.push(String(option.attr('value')));
                 }
             });
+
+            if (!options.multiple && originalSelect.children('option').length === 0) {
+                // If this is a single select - and there are no current options
+                // the first option added will be selected by the browser. This causes a bug!
+                // We need to insert an empty option so that none of the real options are selected.
+                var option = $('<option>');
+                originalSelect.append(option);
+            }
             // And add all the new ones returned from ajax.
             $.each(processedResults, function(resultIndex, result) {
-                if (existingValues.indexOf(result.value) === -1) {
+                if (existingValues.indexOf(String(result.value)) === -1) {
                     var option = $('<option>');
                     option.append(result.label);
                     option.attr('value', result.value);
@@ -562,7 +570,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         });
         // Handler used to force set the value from behat.
         inputElement.on('behat:set-value', function() {
-            if (options.tags) {
+            var suggestionsElement = $(document.getElementById(state.suggestionsId));
+            if ((inputElement.attr('aria-expanded') === "true") &&
+                    (suggestionsElement.children('[aria-selected=true]').length > 0)) {
+                // If the suggestion list has an active item, select it.
+                selectCurrentItem(options, state, originalSelect);
+            } else if (options.tags) {
+                // If tags are enabled, create a tag.
                 createItem(options, state, originalSelect);
             }
         });
@@ -747,11 +761,22 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 // If this field uses ajax, set it up.
                 if (options.ajax) {
                     require([options.ajax], function(ajaxHandler) {
+                        var throttleTimeout = null;
                         var handler = function(e) {
                             updateAjax(e, options, state, originalSelect, ajaxHandler);
                         };
+
+                        // For input events, we do not want to trigger many, many updates.
+                        var throttledHandler = function(e) {
+                            if (throttleTimeout !== null) {
+                                window.clearTimeout(throttleTimeout);
+                                throttleTimeout = null;
+                            }
+                            throttleTimeout = window.setTimeout(handler.bind(this, e), 300);
+                        };
                         // Trigger an ajax update after the text field value changes.
-                        inputElement.on("input keypress", handler);
+                        inputElement.on("input keypress", throttledHandler);
+
                         var arrowElement = $(document.getElementById(state.downArrowId));
                         arrowElement.on("click", handler);
                     });
diff --git a/lib/amd/src/form-course-selector.js b/lib/amd/src/form-course-selector.js
new file mode 100644 (file)
index 0000000..2bf7aed
--- /dev/null
@@ -0,0 +1,76 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course selector adaptor for auto-complete form element.
+ *
+ * @module     core/form-course-selector
+ * @class      form-course-selector
+ * @package    core
+ * @copyright  2016 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      3.1
+ */
+define(['core/ajax', 'jquery'], function(ajax, $) {
+
+    return /** @alias module:core/form-course-selector */ {
+        // Public variables and functions.
+        processResults: function(selector, data) {
+            // Mangle the results into an array of objects.
+            var results = [], i = 0;
+            var excludelist = String($(selector).data('exclude')).split(',');
+
+            for (i = 0; i < data.courses.length; i++) {
+                if (excludelist.indexOf(String(data.courses[i].id)) === -1) {
+                    results.push({ value: data.courses[i].id, label: data.courses[i].displayname });
+                }
+            }
+            return results;
+        },
+
+        transport: function(selector, query, success, failure) {
+            // Parse some data-attributes from the form element.
+            var requiredcapabilities = $(selector).data('requiredcapabilities');
+            if (requiredcapabilities.trim() !== "") {
+                requiredcapabilities = requiredcapabilities.split(',');
+            } else {
+                requiredcapabilities = [];
+            }
+            // Build the query.
+            var promise = null;
+
+            if (typeof query === "undefined") {
+                query = '';
+            }
+
+            var searchargs = {
+                criterianame: 'search',
+                criteriavalue: query,
+                page: 0,
+                perpage: 100,
+                requiredcapabilities: requiredcapabilities
+            };
+            // Go go go!
+            promise = ajax.call([{
+                methodname: 'core_course_search_courses', args: searchargs
+            }]);
+
+            promise[0].done(success);
+            promise[0].fail(failure);
+
+            return promise;
+        }
+    };
+});
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 3bd7249..aadfbc1 100644 (file)
@@ -48,6 +48,9 @@ class behat_form_autocomplete extends behat_form_text {
             throw new coding_exception('Setting the valid of an autocomplete field requires javascript.');
         }
         $this->field->setValue($value);
+        // After the value is set, there is a 400ms throttle and then search. So adding 2 sec. delay to ensure both
+        // throttle + search finishes.
+        sleep(2);
         $id = $this->field->getAttribute('id');
         $js = ' require(["jquery"], function($) { $(document.getElementById("'.$id.'")).trigger("behat:set-value"); }); ';
         $this->session->executeScript($js);
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);
-    }
 }
diff --git