Merge branch 'MDL-38455-master-int' of git://github.com/FMCorz/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 2 Apr 2013 06:53:28 +0000 (14:53 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 2 Apr 2013 06:53:28 +0000 (14:53 +0800)
144 files changed:
admin/settings/courses.php
admin/tests/behat/behat_admin.php [new file with mode: 0644]
admin/tests/behat/display_short_names.feature [new file with mode: 0644]
admin/tests/behat/upload_users.feature [new file with mode: 0644]
admin/tool/behat/cli/init.php
admin/tool/behat/renderer.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_stepslib.php
backup/util/ui/renderer.php
backup/util/ui/restore_ui_components.php
backup/util/ui/tests/behat/backup_courses.feature [new file with mode: 0644]
backup/util/ui/tests/behat/behat_backup.php [new file with mode: 0644]
backup/util/ui/tests/behat/duplicate_activities.feature [new file with mode: 0644]
backup/util/ui/tests/behat/restore_moodle2_courses.feature [new file with mode: 0644]
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-coverage.js [new file with mode: 0644]
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js [new file with mode: 0644]
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js [new file with mode: 0644]
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js [new file with mode: 0644]
blocks/navigation/yui/src/navigation/build.json [new file with mode: 0644]
blocks/navigation/yui/src/navigation/js/navigation.js [moved from blocks/navigation/yui/navigation/navigation.js with 93% similarity]
blocks/navigation/yui/src/navigation/meta/navigation.json [new file with mode: 0644]
blocks/tests/behat/add_blocks.feature
blocks/tests/behat/behat_blocks.php
completion/tests/behat/behat_completion.php [new file with mode: 0644]
completion/tests/behat/enable_manual_complete_mark.feature [new file with mode: 0644]
completion/tests/behat/restrict_section_availability.feature [new file with mode: 0644]
course/lib.php
course/modlib.php
course/tests/behat/add_activities.feature
course/tests/behat/behat_course.php
course/tests/behat/force_group_mode.feature [new file with mode: 0644]
course/tests/behat/section_highlighting.feature [new file with mode: 0644]
course/tests/behat/section_visibility.feature [new file with mode: 0644]
course/tests/courselib_test.php
lang/en/backup.php
lib/behat/behat_base.php
lib/behat/behat_field_manager.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_modvisible.php [new file with mode: 0644]
lib/coursecatlib.php
lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-coverage.js [new file with mode: 0644]
lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-debug.js [new file with mode: 0644]
lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-min.js [new file with mode: 0644]
lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse.js [new file with mode: 0644]
lib/editor/tinymce/yui/collapse/collapse.js [deleted file]
lib/editor/tinymce/yui/src/collapse/build.json [new file with mode: 0644]
lib/editor/tinymce/yui/src/collapse/js/collapse.js [new file with mode: 0644]
lib/editor/tinymce/yui/src/collapse/meta/collapse.json [new file with mode: 0644]
lib/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/README [changed mode: 0755->0644]
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/coursecatlib_test.php
lib/tests/fixtures/upload_users.csv
lib/yui/blocks/blocks.js [deleted file]
lib/yui/build/moodle-core-blocks/moodle-core-blocks-coverage.js [new file with mode: 0644]
lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-blocks/moodle-core-blocks.js [new file with mode: 0644]
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-coverage.js [new file with mode: 0644]
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js [new file with mode: 0644]
lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-coverage.js [new file with mode: 0644]
lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit.js [new file with mode: 0644]
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-coverage.js [new file with mode: 0644]
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification/moodle-core-notification-coverage.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification/moodle-core-notification-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification/moodle-core-notification-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification/moodle-core-notification.js [new file with mode: 0644]
lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-coverage.js [new file with mode: 0644]
lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp.js [new file with mode: 0644]
lib/yui/chooserdialogue/chooserdialogue.js [deleted file]
lib/yui/formautosubmit/formautosubmit.js [deleted file]
lib/yui/formchangechecker/formchangechecker.js [deleted file]
lib/yui/popuphelp/popuphelp.js [deleted file]
lib/yui/src/blocks/build.json [new file with mode: 0644]
lib/yui/src/blocks/js/blocks.js [new file with mode: 0644]
lib/yui/src/blocks/meta/blocks.json [new file with mode: 0644]
lib/yui/src/chooserdialogue/build.json [new file with mode: 0644]
lib/yui/src/chooserdialogue/js/chooserdialogue.js [new file with mode: 0644]
lib/yui/src/chooserdialogue/meta/chooserdialogue.json [new file with mode: 0644]
lib/yui/src/formautosubmit/build.json [new file with mode: 0644]
lib/yui/src/formautosubmit/js/formautosubmit.js [new file with mode: 0644]
lib/yui/src/formautosubmit/meta/formautosubmit.json [new file with mode: 0644]
lib/yui/src/formchangechecker/build.json [new file with mode: 0644]
lib/yui/src/formchangechecker/js/formchangechecker.js [new file with mode: 0644]
lib/yui/src/formchangechecker/meta/formchangechecker.json [new file with mode: 0644]
lib/yui/src/notification/build.json [new file with mode: 0644]
lib/yui/src/notification/js/notification.js [moved from lib/yui/notification/notification.js with 81% similarity]
lib/yui/src/notification/meta/notification.json [new file with mode: 0644]
lib/yui/src/popuphelp/build.json [new file with mode: 0644]
lib/yui/src/popuphelp/js/popuphelp.js [new file with mode: 0644]
lib/yui/src/popuphelp/meta/popuphelp.json [new file with mode: 0644]
mod/forum/tests/behat/add_forum.feature [new file with mode: 0644]
mod/forum/tests/behat/behat_mod_forum.php [new file with mode: 0644]
mod/forum/tests/behat/completion_condition_number_discussions.feature [new file with mode: 0644]
mod/forum/tests/behat/discussion_display.feature [new file with mode: 0644]
mod/forum/tests/behat/edit_post_student.feature [new file with mode: 0644]
mod/forum/tests/behat/edit_post_teacher.feature [new file with mode: 0644]
mod/forum/tests/behat/single_forum_discussion.feature [new file with mode: 0644]
mod/forum/tests/behat/track_read_posts.feature [new file with mode: 0644]
mod/quiz/attempt.php
mod/quiz/attemptlib.php
mod/quiz/autosave.ajax.php [new file with mode: 0644]
mod/quiz/lang/en/quiz.php
mod/quiz/settings.php
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-coverage.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js [new file with mode: 0644]
mod/quiz/yui/src/autosave/build.json [new file with mode: 0644]
mod/quiz/yui/src/autosave/js/autosave.js [new file with mode: 0644]
mod/quiz/yui/src/autosave/meta/autosave.json [new file with mode: 0644]
mod/survey/tests/behat/survey_types.feature [new file with mode: 0644]
question/behaviour/behaviourbase.php
question/engine/datalib.php
question/engine/lib.php
question/engine/questionattempt.php
question/engine/questionusage.php
question/engine/tests/helpers.php
question/engine/tests/questionattempt_db_test.php [new file with mode: 0644]
question/engine/tests/questionattempt_test.php
question/engine/tests/questionattempt_with_steps_test.php [new file with mode: 0644]
question/engine/tests/questionusage_autosave_test.php [new file with mode: 0644]
question/engine/tests/unitofwork_test.php
question/tests/generator/lib.php
question/type/match/tests/walkthrough_test.php
question/type/multianswer/tests/walkthrough_test.php
question/type/numerical/tests/walkthrough_test.php
question/type/shortanswer/tests/helper.php
question/type/upgrade.txt
repository/tests/behat/behat_filepicker.php [moved from lib/tests/behat/behat_filepicker.php with 99% similarity]
repository/tests/behat/create_folders.feature [new file with mode: 0644]
repository/tests/behat/zip_and_unzip.feature [moved from admin/tool/behat/tests/behat/manipulate_filepicker.feature with 66% similarity]
repository/upload/tests/behat/upload_file.feature
theme/mymobile/config.php
version.php

index 75b3246..8be5652 100644 (file)
@@ -135,6 +135,11 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), array('value'=>0, 'locked'=>0)));
     $ADMIN->add('backups', $temp);
 
+    // Create a page for general import configuration and defaults.
+    $temp = new admin_settingpage('importgeneralsettings', new lang_string('importgeneralsettings', 'backup'), 'moodle/backup:backupcourse');
+    $temp->add(new admin_setting_configtext('backup/import_general_maxresults', new lang_string('importgeneralmaxresults', 'backup'), new lang_string('importgeneralmaxresults_desc', 'backup'), 10));
+    $ADMIN->add('backups', $temp);
+
     // Create a page for automated backups configuration and defaults.
     $temp = new admin_settingpage('automated', new lang_string('automatedsetup','backup'), 'moodle/backup:backupcourse');
 
diff --git a/admin/tests/behat/behat_admin.php b/admin/tests/behat/behat_admin.php
new file mode 100644 (file)
index 0000000..cdca881
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Steps definitions related with administration.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../lib/behat/behat_field_manager.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
+/**
+ * Site administration level steps definitions.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_admin extends behat_base {
+
+    /**
+     * Sets the specified site settings. A table with | Setting label | value | is expected.
+     *
+     * @Given /^I set the following administration settings values:$/
+     * @param TableNode $table
+     */
+    public function i_set_the_following_administration_settings_values(TableNode $table) {
+
+        if (!$data = $table->getRowsHash()) {
+            return;
+        }
+
+        foreach ($data as $label => $value) {
+
+            // We expect admin block to be visible, otherwise go to homepage.
+            if (!$this->getSession()->getPage()->find('css', '.block_settings')) {
+                $this->getSession()->visit($this->locate_path('/'));
+                $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+            }
+
+            // Search by label.
+            $searchbox = $this->find_field('Search in settings');
+            $searchbox->setValue($label);
+            $submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
+            $submitsearch->press();
+
+            $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+
+            // Admin settings does not use the same DOM structure than other moodle forms
+            // but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
+            $exception = new ElementNotFoundException($this->getSession(), '"' . $label . '" administration setting ');
+            $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]
+[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]";
+            $fieldnode = $this->find('xpath', $fieldxpath, $exception);
+            $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']
+/child::div[contains(concat(' ', @class, ' '),  ' form-')]/child::*/parent::div");
+
+            // Getting the class which contains the field type.
+            $classes = explode(' ', $formfieldtypenode->getAttribute('class'));
+            foreach ($classes as $class) {
+                if (substr($class, 0, 5) == 'form-') {
+                    $type = substr($class, 5);
+                }
+            }
+
+            // Instantiating the appropiate field type.
+            $field = behat_field_manager::get_field_instance($type, $fieldnode, $this->getSession());
+            $field->set_value($value);
+
+            $this->find_button('Save changes')->press();
+        }
+    }
+
+}
diff --git a/admin/tests/behat/display_short_names.feature b/admin/tests/behat/display_short_names.feature
new file mode 100644 (file)
index 0000000..836d753
--- /dev/null
@@ -0,0 +1,22 @@
+@admin
+Feature: Display extended course names
+  In order to display more info about the courses
+  As a moodle admin
+  I need to display courses short names along with courses full names
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course fullname | C_shortname | 0 |
+    And I log in as "admin"
+
+  Scenario: Courses list without extended course names (default value)
+    Then I should see "Course fullname"
+    And I should not see "C_shortname Course fullname"
+
+  Scenario: Courses list with extended course names
+    Given I click on "Courses" "link" in the "//div[@id='settingsnav']//descendant::li[contains(concat(' ', @class, ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
+    And I check "Display extended course names"
+    When I press "Save changes"
+    And I am on homepage
+    Then I should see "C_shortname Course fullname"
diff --git a/admin/tests/behat/upload_users.feature b/admin/tests/behat/upload_users.feature
new file mode 100644 (file)
index 0000000..e61208e
--- /dev/null
@@ -0,0 +1,46 @@
+@admin @_only_local
+Feature: Upload users
+  In order to add users to the system
+  As a moodle admin
+  I need to upload files containing the users data
+
+  @javascript
+  Scenario: Upload users enrolling them on courses and groups
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Maths | math102 | 0 |
+    And the following "groups" exists:
+      | name | course | idnumber |
+      | Section 1 | math102 | S1 |
+      | Section 3 | math102 | S3 |
+    And I log in as "admin"
+    And I expand "Front page settings" node
+    And I expand "Site administration" node
+    And I expand "Users" node
+    And I expand "Accounts" node
+    And I follow "Upload users"
+    When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filepicker
+    And I press "Upload users"
+    Then I should see "Upload users preview"
+    And I should see "Tom"
+    And I should see "Jones"
+    And I should see "verysecret"
+    And I should see "jonest@someplace.edu"
+    And I should see "Reznor"
+    And I should see "course1"
+    And I should see "math102"
+    And I should see "group1"
+    And I should see "Section 1"
+    And I press "Upload users"
+    And I press "Continue"
+    And I follow "Browse list of users"
+    And I should see "Tom Jones"
+    And I should see "Trent Reznor"
+    And I should see "reznor@someplace.edu"
+    And I am on homepage
+    And I follow "Maths"
+    And I expand "Users" node
+    And I follow "Groups"
+    And I select "Section 1 (1)" from "groups"
+    And I wait "4" seconds
+    And the "members" select box should contain "Tom Jones"
index 1916367..ca68c16 100644 (file)
@@ -65,7 +65,7 @@ if ($code == 0) {
     // Changing to moodle dirroot to run composer related commands at project level.
     chdir(__DIR__ . '/../../../..');
     if (!file_exists(__DIR__ . '/../../../../composer.phar')) {
-        passthru("curl http://getcomposer.org/install | php", $code);
+        passthru("curl http://getcomposer.org/installer | php", $code);
         if ($code != 0) {
             exit($code);
         }
index 0df7e3a..503bc09 100644 (file)
@@ -89,7 +89,7 @@ class tool_behat_renderer extends plugin_renderer_base {
             $stepsdefinitions = implode('', $stepsdefinitions);
 
             // Replace text selector type arguments with a user-friendly select.
-            $stepsdefinitions = preg_replace_callback('/(TEXT_SELECTOR_STRING)/',
+            $stepsdefinitions = preg_replace_callback('/(TEXT_SELECTOR\d?_STRING)/',
                 function ($matches) {
                     return html_writer::select(behat_command::$allowedtextselectors, uniqid());
                 },
@@ -97,7 +97,7 @@ class tool_behat_renderer extends plugin_renderer_base {
             );
 
             // Replace selector type arguments with a user-friendly select.
-            $stepsdefinitions = preg_replace_callback('/(SELECTOR_STRING)/',
+            $stepsdefinitions = preg_replace_callback('/(SELECTOR\d?_STRING)/',
                 function ($matches) {
                     return html_writer::select(behat_command::$allowedselectors, uniqid());
                 },
index 8a8713a..5f231e9 100644 (file)
@@ -71,6 +71,8 @@ class restore_course_task extends restore_task {
             $this->add_step(new restore_course_structure_step('course_info', 'course.xml'));
         }
 
+        $this->add_step(new restore_course_legacy_files_step('legacy_files'));
+
         // Restore course enrolments (plugins and membership). Conditionally prevented for any IMPORT/HUB operation
         if ($this->plan->get_mode() != backup::MODE_IMPORT && $this->plan->get_mode() != backup::MODE_HUB) {
             $this->add_step(new restore_enrolments_structure_step('course_enrolments', 'enrolments.xml'));
index f254475..5afd99e 100644 (file)
@@ -1459,7 +1459,6 @@ class restore_course_structure_step extends restore_structure_step {
 
         // Add course related files, without itemid to match
         $this->add_related_files('course', 'summary', null);
-        $this->add_related_files('course', 'legacy', null);
 
         // Deal with legacy allowed modules.
         if ($this->legacyrestrictmodules) {
@@ -1486,6 +1485,29 @@ class restore_course_structure_step extends restore_structure_step {
     }
 }
 
+/**
+ * Execution step that will migrate legacy files if present.
+ */
+class restore_course_legacy_files_step extends restore_execution_step {
+    public function define_execution() {
+        global $DB;
+
+        // Do a check for legacy files and skip if there are none.
+        $sql = 'SELECT count(*)
+                  FROM {backup_files_temp}
+                 WHERE backupid = ?
+                   AND contextid = ?
+                   AND component = ?
+                   AND filearea  = ?';
+        $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
+
+        if ($DB->count_records_sql($sql, $params)) {
+            $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
+            restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
+                'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
+        }
+    }
+}
 
 /*
  * Structure step that will read the roles.xml file (at course/activity/block levels)
index 7b8dfe2..b9a2277 100644 (file)
@@ -590,8 +590,14 @@ class core_backup_renderer extends plugin_renderer_base {
             return $output;
         }
 
-        $output .= html_writer::tag('div', get_string('totalcoursesearchresults', 'backup', $component->get_count()), array('class'=>'ics-totalresults'));
+        $countstr = '';
+        if ($component->has_more_results()) {
+            $countstr = get_string('morecoursesearchresults', 'backup', $component->get_count());
+        } else {
+            $countstr = get_string('totalcoursesearchresults', 'backup', $component->get_count());
+        }
 
+        $output .= html_writer::tag('div', $countstr, array('class'=>'ics-totalresults'));
         $output .= html_writer::start_tag('div', array('class' => 'ics-results'));
 
         $table = new html_table();
@@ -610,6 +616,14 @@ class core_backup_renderer extends plugin_renderer_base {
             );
             $table->data[] = $row;
         }
+        if ($component->has_more_results()) {
+            $cell = new html_table_cell(get_string('moreresults', 'backup'));
+            $cell->colspan = 3;
+            $cell->attributes['class'] = 'notifyproblem';
+            $row = new html_table_row(array($cell));
+            $row->attributes['class'] = 'rcs-course';
+            $table->data[] = $row;
+        }
         $output .= html_writer::table($table);
         $output .= html_writer::end_tag('div');
 
index 7ebaea8..e1aca51 100644 (file)
@@ -39,7 +39,6 @@ abstract class restore_search_base implements renderable {
      */
     static $VAR_SEARCH = 'search';
 
-    static $MAXRESULTS = 10;
     /**
      * The current search string
      * @var string|null
@@ -65,6 +64,16 @@ abstract class restore_search_base implements renderable {
      * @var array
      */
     private $requiredcapabilities = array();
+    /**
+     * Max number of courses to return in a search.
+     * @var int
+     */
+    private $maxresults = null;
+    /**
+     * Indicates if we have more than maxresults found.
+     * @var boolean
+     */
+    private $hasmoreresults = false;
 
     /**
      * Constructor
@@ -73,6 +82,7 @@ abstract class restore_search_base implements renderable {
     public function __construct(array $config=array()) {
 
         $this->search = optional_param($this->get_varsearch(), self::DEFAULT_SEARCH, PARAM_NOTAGS);
+        $this->maxresults = get_config('backup', 'import_general_maxresults');
 
         foreach ($config as $name=>$value) {
             $method = 'set_'.$name;
@@ -177,8 +187,8 @@ abstract class restore_search_base implements renderable {
         foreach ($this->requiredcapabilities as $cap) {
             $requiredcaps[] = $cap['capability'];
         }
-        // Iterate while we have records and haven't reached MAXRESULTS
-        while ($totalcourses > $offs and $this->totalcount < self::$MAXRESULTS) {
+        // Iterate while we have records and haven't reached $this->maxresults.
+        while ($totalcourses > $offs and $this->totalcount < $this->maxresults) {
             $resultset = $DB->get_records_sql($sql, $params, $offs, $blocksz);
             foreach ($resultset as $result) {
                 context_instance_preload($result);
@@ -189,11 +199,14 @@ abstract class restore_search_base implements renderable {
                         continue;
                     }
                 }
-                $this->results[$result->id] = $result;
-                $this->totalcount++;
-                if ($this->totalcount >= self::$MAXRESULTS) {
+                // Check if we are over the limit.
+                if ($this->totalcount+1 > $this->maxresults) {
+                    $this->hasmoreresults = true;
                     break;
                 }
+                // If not, then continue.
+                $this->totalcount++;
+                $this->results[$result->id] = $result;
             }
             $offs += $blocksz;
         }
@@ -202,7 +215,10 @@ abstract class restore_search_base implements renderable {
     }
 
     final public function has_more_results() {
-        return $this->get_count() >= self::$MAXRESULTS;
+        if ($this->results === null) {
+            $this->search();
+        }
+        return $this->hasmoreresults;
     }
 
     /**
diff --git a/backup/util/ui/tests/behat/backup_courses.feature b/backup/util/ui/tests/behat/backup_courses.feature
new file mode 100644 (file)
index 0000000..584354e
--- /dev/null
@@ -0,0 +1,37 @@
+@backup
+Feature: Backup Moodle courses
+  In order to save and store course contents
+  As a moodle admin
+  I need to create backups of courses
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And I log in as "admin"
+
+  @javascript
+  Scenario: Backup a course providing options
+    When I backup "Course 1" course using this options:
+    Then I should see "Restore"
+    And I click on "Restore" "link" in the ".backup-files-table" "css_element"
+    And I should see "URL of backup"
+    And I should see "Anonymize user information"
+
+  @javascript
+  Scenario: Backup a course with default options
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+      | Include calendar events | 0 |
+      | Include course logs | 1 |
+      | setting_section_section_5_userinfo | 0 |
+      | setting_section_section_5_included | 0 |
+    Then I should see "Restore"
+    And I click on "Restore" "link" in the ".backup-files-table" "css_element"
+    And I should not see "Section 3"
+    And I press "Continue"
+    And I click on "Continue" "button" in the ".bcs-current-course" "css_element"
+    And I click on "//div[contains(concat(' ', @class, ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element"
+    And I click on "setting_root_logs" "checkbox" in the "//div[contains(@class, 'fitem')][contains(., 'Include course logs')]" "xpath_element"
+    And I press "Cancel"
+    And I click on "Cancel" "button" in the ".confirmation-dialogue" "css_element"
diff --git a/backup/util/ui/tests/behat/behat_backup.php b/backup/util/ui/tests/behat/behat_backup.php
new file mode 100644 (file)
index 0000000..8f02603
--- /dev/null
@@ -0,0 +1,288 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Backup and restore actions to help behat feature files writting.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+require_once(__DIR__ . '/../../../../../lib/behat/behat_field_manager.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
+    Behat\Mink\Exception\ExpectationException as ExpectationException;
+
+/**
+ * Backup-related steps definitions.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_backup extends behat_base {
+
+    /**
+     * Backups the specified course using the provided options. If you are interested in restoring this backup would be useful to provide a 'Filename' option.
+     *
+     * @Given /^I backup "(?P<course_fullname_string>(?:[^"]|\\")*)" course using this options:$/
+     * @param string $backupcourse
+     * @param TableNode $options Backup options or false if no options provided
+     */
+    public function i_backup_course_using_this_options($backupcourse, $options = false) {
+
+        // We can not use other steps here as we don't know where the provided data
+        // table elements are used, and we need to catch exceptions contantly.
+
+        // Go to homepage.
+        $this->getSession()->visit($this->locate_path('/'));
+
+        // Click the course link.
+        $this->find_link($backupcourse)->click();
+
+        // Click the backup link.
+        $this->find_link('Backup')->click();
+
+        // Initial settings.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Schema settings.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Confirmation and review, backup filename can also be specified.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Perform backup')->press();
+
+        // Waiting for it to finish.
+        $this->wait(10);
+
+        // Last backup continue button.
+        $this->find_button('Continue')->press();
+    }
+
+    /**
+     * Restores the backup into the specified course and the provided options. You should be in the 'Restore' page where the backup is.
+     *
+     * @Given /^I restore "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into "(?P<existing_course_fullname_string>(?:[^"]|\\")*)" course using this options:$/
+     * @param string $backupfilename
+     * @param string $existingcourse
+     * @param TableNode $options Restore forms options or false if no options provided
+     */
+    public function i_restore_backup_into_course_using_this_options($backupfilename, $existingcourse, $options = false) {
+
+        // Confirm restore.
+        $this->select_backup($backupfilename);
+
+        // Selecting the specified course (we can not call behat_forms::select_radio here as is in another behat subcontext).
+        $existingcourse = str_replace("'", "\'", $existingcourse);
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]
+/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.
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode->click();
+        $this->wait();
+
+        // Common restore process using provided key/value options.
+        $this->process_restore($options);
+    }
+
+    /**
+     * Restores the specified backup into a new course using the provided options. You should be in the 'Restore' page where the backup is.
+     *
+     * @Given /^I restore "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into a new course using this options:$/
+     * @param string $backupfilename
+     * @param TableNode $options Restore forms options or false if no options provided
+     */
+    public function i_restore_backup_into_a_new_course_using_this_options($backupfilename, $options = false) {
+
+        // Confirm restore.
+        $this->select_backup($backupfilename);
+
+        // The first category in the list.
+        $radionode = $this->find('xpath', "//div[contains(@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.
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode->click();
+        $this->wait();
+
+        // Common restore process using provided key/value options.
+        $this->process_restore($options);
+    }
+
+    /**
+     * Merges the backup into the current course using the provided restore options. You should be in the 'Restore' page where the backup is.
+     *
+     * @Given /^I merge "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into the current course using this options:$/
+     * @param string $backupfilename
+     * @param TableNode $options Restore forms options or false if no options provided
+     */
+    public function i_merge_backup_into_the_current_course($backupfilename, $options = false) {
+
+        // Confirm restore.
+        $this->select_backup($backupfilename);
+
+        // Merge without deleting radio option.
+        $radionode = $this->find('xpath', "//div[contains(@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.
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode->click();
+        $this->wait();
+
+        // Common restore process using provided key/value options.
+        $this->process_restore($options);
+    }
+
+    /**
+     * Merges the backup into the current course after deleting this contents, using the provided restore options. You should be in the 'Restore' page where the backup is.
+     *
+     * @Given /^I merge "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into the current course after deleting it's contents using this options:$/
+     * @param string $backupfilename
+     * @param TableNode $options Restore forms options or false if no options provided
+     */
+    public function i_merge_backup_into_current_course_deleting_its_contents($backupfilename, $options = false) {
+
+        // Confirm restore.
+        $this->select_backup($backupfilename);
+
+        // Delete contents radio option.
+        $radionode = $this->find('xpath', "//div[contains(@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.
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode->click();
+        $this->wait();
+
+        // Common restore process using provided key/value options.
+        $this->process_restore($options);
+    }
+
+    /**
+     * Selects the backup to restore.
+     *
+     * @throws ExpectationException
+     * @param string $backupfilename
+     * @return void
+     */
+    protected function select_backup($backupfilename) {
+
+        // Using xpath as there are other restore links before this one.
+        $exception = new ExpectationException('The "' . $backupfilename . '" backup file can not be found in this page', $this->getSession());
+        $xpath = "//tr[contains(., '" . $backupfilename . "')]/descendant::a[contains(., 'Restore')]";
+        $restorelink = $this->find('xpath', $xpath, $exception);
+        $restorelink->click();
+
+        // Confirm the backup contents.
+        $restore = $this->find_button('Continue')->press();
+    }
+
+    /**
+     * Executes the common steps of all restore processes.
+     *
+     * @param TableNode $options The backup and restore options or false if no options provided
+     * @return void
+     */
+    protected function process_restore($options) {
+
+        // Settings.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Schema.
+        $this->fill_backup_restore_form($options);
+        $this->find_button('Next')->press();
+
+        // Review, no options here.
+        $this->find_button('Perform restore')->press();
+        $this->wait(10);
+
+        // Last restore continue button, redirected to restore course after this.
+        $this->find_button('Continue')->press();
+    }
+
+    /**
+     * Tries to fill the current page form elements with the provided options.
+     *
+     * This step is slow as it spins over each provided option, we are
+     * not expected to have lots of provided options, anyways, is better
+     * to be conservative and wait for the elements to appear rather than
+     * to have false failures.
+     *
+     * @param TableNode $options The backup and restore options or false if no options provided
+     * @return void
+     */
+    protected function fill_backup_restore_form($options) {
+
+        // Nothing to fill if no options are provided.
+        if (!$options) {
+            return;
+        }
+
+        // If we find any of the provided options in the current form we should set the value.
+        $datahash = $options->getRowsHash();
+        foreach ($datahash as $locator => $value) {
+
+            try {
+                $fieldnode = $this->find_field($locator);
+                $field = behat_field_manager::get_field($fieldnode, $locator, $this->getSession());
+                $field->set_value($value);
+
+            } catch (ElementNotFoundException $e) {
+                // Next provided option then, this one should be part of another page's fields.
+            }
+        }
+    }
+
+    /**
+     * Waits until the DOM is ready.
+     *
+     * @param int To override the default timeout
+     * @return void
+     */
+    protected function wait($timeout = false) {
+
+        if (!$timeout) {
+            $timeout = self::TIMEOUT;
+        }
+        $this->getSession()->wait($timeout, '(document.readyState === "complete")');
+    }
+
+}
diff --git a/backup/util/ui/tests/behat/duplicate_activities.feature b/backup/util/ui/tests/behat/duplicate_activities.feature
new file mode 100644 (file)
index 0000000..f23efc4
--- /dev/null
@@ -0,0 +1,33 @@
+@backup
+Feature: Duplicate activities
+  In order to set up my course contents quickly
+  As a moodle teacher
+  I need to duplicate activities inside the same course
+
+  @javascript
+  Scenario: Duplicate an activity
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Database" to section "1" and I fill the form with:
+      | Name | Test database name |
+      | Description | Test database description |
+    When I click on "Duplicate" "link" in the "#section-1" "css_element"
+    And I press "Continue"
+    And I press "Edit the new copy"
+    And I fill the moodle form with:
+      | Name | Duplicated database name |
+      | Description | Duplicated database description |
+    And I press "Save and return to course"
+    Then I should see "Test database name" in the "#section-1" "css_element"
+    And I should see "Duplicated database name" in the "#section-1" "css_element"
+    And "Test database name" "link" should appear before "Duplicated database name" "link"
diff --git a/backup/util/ui/tests/behat/restore_moodle2_courses.feature b/backup/util/ui/tests/behat/restore_moodle2_courses.feature
new file mode 100644 (file)
index 0000000..2c4a57e
--- /dev/null
@@ -0,0 +1,100 @@
+@backup
+Feature: Restore Moodle 2 course backups
+  In order to continue using my stored course contents
+  As a moodle teacher and as a moodle admin
+  I need to restore them inside other Moodle courses or in new courses
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category | format |
+      | Course 1 | C1 | 0 | topics |
+      | Course 2 | C2 | 0 | topics |
+    And I log in as "admin"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    And I add the "Community finder" block
+
+  @javascript
+  Scenario: Restore a course in another existing course
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I restore "test_backup.mbz" backup into "Course 2" course using this options:
+    Then I should see "Course 2"
+    And I should see "Community finder"
+    And I should see "Test forum name"
+
+  @javascript
+  Scenario: Restore a course in a new course
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I restore "test_backup.mbz" backup into a new course using this options:
+      | Course name | Course 1 restored in a new course |
+    Then I should see "Course 1 restored in a new course"
+    And I should see "Community finder"
+    And I should see "Test forum name"
+
+  @javascript
+  Scenario: Restore a backup into the same course
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I merge "test_backup.mbz" backup into the current course using this options:
+      | setting_section_section_5_included | 0 |
+      | setting_section_section_5_userinfo | 0 |
+    Then I should see "Course 1"
+    And I should not see "Section 3"
+    And I should see "Community finder"
+    And I should see "Test forum name"
+
+  @javascript
+  Scenario: Restore a backup into the same course removing it's contents before that
+    When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    And I follow "Course 1"
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum post backup name |
+      | Description | Test forum post backup description |
+    And I follow "Restore"
+    And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
+      | setting_section_section_5_userinfo | 0 |
+      | setting_section_section_5_included | 0 |
+    Then I should see "Course 1"
+    And I should not see "Section 3"
+    And I should not see "Test forum post backup name"
+    And I should see "Community finder"
+    And I should see "Test forum name"
+
+  @javascript
+  Scenario: Restore a backup into a new course changing the course format afterwards
+    Given I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
+    When I restore "test_backup.mbz" backup into a new course using this options:
+    Then I should see "Topic 1"
+    And I should see "Test forum name"
+    And I follow "Edit settings"
+    And the "id_format" field should match "Topics format" value
+    And I fill the moodle form with:
+      | id_startdate_day | 1 |
+      | id_startdate_month | January |
+      | id_startdate_year | 2020 |
+      | id_format | Weekly format |
+    And I press "Save changes"
+    And I should see "1 January - 7 January"
+    And I should see "Test forum name"
+    And I follow "Edit settings"
+    And the "id_format" field should match "Weekly format" value
+    And I fill the moodle form with:
+      | id_format | Social format |
+    And I press "Save changes"
+    And I should see "An open forum for chatting about anything you want to"
+    And I follow "Edit settings"
+    And the "id_format" field should match "Social format" value
+    And I fill the moodle form with:
+      | id_format | SCORM format |
+    And I press "Save changes"
+    And I should see "Adding a new SCORM package"
+    And I follow "Edit settings"
+    And the "id_format" field should match "SCORM format" value
+    And I press "Cancel"
diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-coverage.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-coverage.js
new file mode 100644 (file)
index 0000000..5ec86de
Binary files /dev/null and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-coverage.js differ
diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js
new file mode 100644 (file)
index 0000000..9ba9df7
Binary files /dev/null and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js differ
diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js
new file mode 100644 (file)
index 0000000..2f09116
Binary files /dev/null and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js differ
diff --git a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js
new file mode 100644 (file)
index 0000000..9ba9df7
Binary files /dev/null and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js differ
diff --git a/blocks/navigation/yui/src/navigation/build.json b/blocks/navigation/yui/src/navigation/build.json
new file mode 100644 (file)
index 0000000..5e28a6c
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-block_navigation-navigation",
+  "builds": {
+    "moodle-block_navigation-navigation": {
+      "jsfiles": [
+        "navigation.js"
+      ]
+    }
+  }
+}
@@ -1,5 +1,3 @@
-YUI.add('moodle-block_navigation-navigation', function(Y){
-
 /**
  * A 'actionkey' Event to help with Y.delegate().
  * The event consists of the left arrow, right arrow, enter and space keys.
@@ -28,10 +26,11 @@ Y.Event.define("actionkey", {
     },
 
     _keyHandler: function (e, notifier, args) {
+        var actObj;
         if (!args.actions) {
-            var actObj = {collapse:true, expand:true, toggle:true, enter:true};
+            actObj = {collapse:true, expand:true, toggle:true, enter:true};
         } else {
-            var actObj = args.actions;
+            actObj = args.actions;
         }
         if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
             e.action = this._keys[e.keyCode];
@@ -41,7 +40,7 @@ Y.Event.define("actionkey", {
 
     on: function (node, sub, notifier) {
         // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
-        if (sub.args == null) {
+        if (sub.args === null) {
             //no actions given
             sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false});
         } else {
@@ -49,14 +48,14 @@ Y.Event.define("actionkey", {
         }
     },
 
-    detach: function (node, sub, notifier) {
+    detach: function (node, sub) {
         //detach our _detacher handle of the subscription made in on()
         sub._detacher.detach();
     },
 
     delegate: function (node, sub, notifier, filter) {
         // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
-        if (sub.args == null) {
+        if (sub.args === null) {
             //no actions given
             sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false});
         } else {
@@ -64,14 +63,14 @@ Y.Event.define("actionkey", {
         }
     },
 
-    detachDelegate: function (node, sub, notifier) {
+    detachDelegate: function (node, sub) {
         sub._delegateDetacher.detach();
     }
 });
 
 var EXPANSIONLIMIT_EVERYTHING = 0,
-    EXPANSIONLIMIT_COURSE     = 20,
-    EXPANSIONLIMIT_SECTION    = 30,
+    //EXPANSIONLIMIT_COURSE     = 20,
+    //EXPANSIONLIMIT_SECTION    = 30,
     EXPANSIONLIMIT_ACTIVITY   = 40;
 
 /**
@@ -104,7 +103,7 @@ var NODETYPE = {
     USER : 80,
     /** @type int Container = 90 */
     CONTAINER : 90
-}
+};
 
 /**
  * Navigation tree class.
@@ -112,9 +111,9 @@ var NODETYPE = {
  * This class establishes the tree initially, creating expandable branches as
  * required, and delegating the expand/collapse event.
  */
-var TREE = function(config) {
+var TREE = function() {
     TREE.superclass.constructor.apply(this, arguments);
-}
+};
 TREE.prototype = {
     /**
      * The tree's ID, normally its block instance id.
@@ -189,7 +188,7 @@ TREE.prototype = {
         // First check if they managed to click on the li iteslf, then find the closest
         // LI ancestor and use that
 
-        if (e.target.test('a') && (e.keyCode == 0 || e.keyCode == 13)) {
+        if (e.target.test('a') && (e.keyCode === 0 || e.keyCode === 13)) {
             // A link has been clicked (or keypress is 'enter') don't fire any more events just do the default.
             e.stopPropagation();
             return;
@@ -198,7 +197,7 @@ TREE.prototype = {
         // Makes sure we can get to the LI containing the branch.
         var target = e.target;
         if (!target.test('li')) {
-            target = target.ancestor('li')
+            target = target.ancestor('li');
         }
         if (!target) {
             return;
@@ -206,7 +205,7 @@ TREE.prototype = {
 
         // Toggle expand/collapse providing its not a root level branch.
         if (!target.hasClass('depth_1')) {
-            if (e.type == 'actionkey') {
+            if (e.type === 'actionkey') {
                 switch (e.action) {
                     case 'expand' :
                         target.removeClass('collapsed');
@@ -247,7 +246,7 @@ TREE.prototype = {
             }
         }
     }
-}
+};
 // The tree extends the YUI base foundation.
 Y.extend(TREE, Y.Base, TREE.prototype, {
     NAME : 'navigation-tree',
@@ -266,7 +265,7 @@ Y.extend(TREE, Y.Base, TREE.prototype, {
         expansionlimit : {
             value : 0,
             setter : function(val) {
-                return parseInt(val);
+                return parseInt(val, 10);
             }
         }
     }
@@ -280,9 +279,9 @@ if (M.core_dock && M.core_dock.genericblock) {
  * This class is used to manage a tree branch, in particular its ability to load
  * its contents by AJAX.
  */
-var BRANCH = function(config) {
+BRANCH = function() {
     BRANCH.superclass.constructor.apply(this, arguments);
-}
+};
 BRANCH.prototype = {
     /**
      * The node for this branch (p)
@@ -292,17 +291,19 @@ BRANCH.prototype = {
      * Initialises the branch when it is first created.
      */
     initializer : function(config) {
+        var i,
+            children;
         if (config.branchobj !== null) {
             // Construct from the provided xml
-            for (var i in config.branchobj) {
+            for (i in config.branchobj) {
                 this.set(i, config.branchobj[i]);
             }
-            var children = this.get('children');
+            children = this.get('children');
             this.set('haschildren', (children.length > 0));
         }
         if (config.overrides !== null) {
             // Construct from the provided xml
-            for (var i in config.overrides) {
+            for (i in config.overrides) {
                 this.set(i, config.overrides[i]);
             }
         }
@@ -311,7 +312,7 @@ BRANCH.prototype = {
         // Now check whether the branch is not expandable because of the expansionlimit
         var expansionlimit = this.get('tree').get('expansionlimit');
         var type = this.get('type');
-        if (expansionlimit != EXPANSIONLIMIT_EVERYTHING &&  type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) {
+        if (expansionlimit !== EXPANSIONLIMIT_EVERYTHING &&  type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) {
             this.set('expandable', false);
             this.set('haschildren', false);
         }
@@ -413,12 +414,12 @@ BRANCH.prototype = {
      * request made here.
      */
     ajaxLoad : function(e) {
-        if (e.type == 'actionkey' && e.action != 'enter') {
+        if (e.type === 'actionkey' && e.action !== 'enter') {
             e.halt();
         } else {
             e.stopPropagation();
         }
-        if (e.type = 'actionkey' && e.action == 'enter' && e.target.test('A')) {
+        if (e.type === 'actionkey' && e.action === 'enter' && e.target.test('A')) {
             // No ajaxLoad for enter.
             this.node.setAttribute('data-expandable', '0');
             this.node.setAttribute('data-loaded', '1');
@@ -466,7 +467,7 @@ BRANCH.prototype = {
             if (object.children && object.children.length > 0) {
                 var coursecount = 0;
                 for (var i in object.children) {
-                    if (typeof(object.children[i])=='object') {
+                    if (typeof(object.children[i])==='object') {
                         if (object.children[i].type == NODETYPE.COURSE) {
                             coursecount++;
                         }
@@ -502,7 +503,7 @@ BRANCH.prototype = {
                 if (children[i].type == NODETYPE.COURSE) {
                     count++;
                 }
-                if (typeof(children[i])=='object') {
+                if (typeof(children[i]) === 'object') {
                     branch.addChild(children[i]);
                 }
             }
@@ -536,7 +537,7 @@ BRANCH.prototype = {
             icon : {'pix':"i/navigationitem",'component':'moodle'}
         });
     }
-}
+};
 Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
     NAME : 'navigation-branch',
     ATTRS : {
@@ -558,7 +559,7 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
             value : '',
             validator : Y.Lang.isString,
             getter : function(val) {
-                if (val == '') {
+                if (val === '') {
                     val = 'expandable_branch_'+M.block_navigation.expandablebranchcount;
                     M.block_navigation.expandablebranchcount++;
                 }
@@ -620,5 +621,3 @@ M.block_navigation = M.block_navigation || {
         new TREE(properties);
     }
 };
-
-}, '@VERSION@', {requires:['base', 'core_dock', 'io-base', 'node', 'dom', 'event-custom', 'event-delegate', 'json-parse']});
diff --git a/blocks/navigation/yui/src/navigation/meta/navigation.json b/blocks/navigation/yui/src/navigation/meta/navigation.json
new file mode 100644 (file)
index 0000000..b4c3760
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "moodle-block_navigation-navigation": {
+    "requires": [
+        "base",
+        "core_dock",
+        "io-base",
+        "node",
+        "dom",
+        "event-custom",
+        "event-delegate",
+        "json-parse"
+    ]
+  }
+}
index 7ee5644..47caaaa 100644 (file)
@@ -4,8 +4,7 @@ Feature: Add blocks
   As a teacher
   I need to add blocks to pages
 
-  @javascript
-  Scenario: Add a block to a course
+  Background:
     Given the following "users" exists:
       | username | firstname | lastname | email |
       | student1 | Student | 1 | student1@asd.com |
@@ -21,4 +20,9 @@ Feature: Add blocks
     And I follow "Course 1"
     And I turn editing mode on
     When I add the "Blog menu" block
-    Then I should see "View all of my entries"
+    Then I should see "View my entries about this course"
+
+  @javascript
+  Scenario: Add a block to a course with Javascript enabled
+
+  Scenario: Add a block to a course with Javascript disabled
index c90f1df..1c8f70d 100644 (file)
@@ -46,7 +46,16 @@ class behat_blocks extends behat_base {
      * @param string $blockname
      */
     public function i_add_the_block($blockname) {
-        return new Given('I select "' . $blockname . '" from "bui_addblock"');
+        $steps = new Given('I select "' . $blockname . '" from "bui_addblock"');
+
+        // If we are running without javascript we need to submit the form.
+        if (!$this->running_javascript()) {
+            $steps = array(
+                $steps,
+                new Given('I click on "Go" "button" in the "#add_block" "css_element"')
+            );
+        }
+        return $steps;
     }
 
 }
diff --git a/completion/tests/behat/behat_completion.php b/completion/tests/behat/behat_completion.php
new file mode 100644 (file)
index 0000000..7bf57fb
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Completion steps definitions.
+ *
+ * @package    core_completion
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, 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,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
+/**
+ * Steps definitions to deal with course and activities completion.
+ *
+ * @package    core_completion
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_completion extends behat_base {
+
+    /**
+     * Checks that the specified user has completed the specified activity of the current course.
+     *
+     * @Then /^"(?P<user_fullname_string>(?:[^"]|\\")*)" user has completed "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
+     * @param string $userfullname
+     * @param string $activityname
+     */
+    public function user_has_completed_activity($userfullname, $activityname) {
+
+        // Will throw an exception if the element can not be hovered.
+        $xpath = "//table[@id='completion-progress']/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Completed')]";
+
+        return array(
+            new Given('I go to the current course activity completion report'),
+            new Given('I hover "' . $xpath . '" "xpath_element"')
+        );
+    }
+
+    /**
+     * Checks that the specified user has not completed the specified activity of the current course.
+     *
+     * @Then /^"(?P<user_fullname_string>(?:[^"]|\\")*)" user has not completed "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
+     * @param string $userfullname
+     * @param string $activityname
+     */
+    public function user_has_not_completed_activity($userfullname, $activityname) {
+
+        $xpath = "//table[@id='completion-progress']/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Not completed')]";
+        return array(
+            new Given('I go to the current course activity completion report'),
+            new Given('I hover "' . $xpath . '" "xpath_element"')
+        );
+
+        return $steps;
+    }
+
+    /**
+     * Goes to the current course activity completion report.
+     *
+     * @Given /^I go to the current course activity completion report$/
+     */
+    public function go_to_the_current_course_activity_completion_report() {
+
+        $steps = array();
+
+        // Expand reports node if we can't see the link.
+        try {
+            $this->find('xpath', "//*[@id='settingsnav']/descendant::li/descendant::li[not(contains(@class,'collapsed'))]/descendant::p[contains(., 'Activity completion')]");
+        } catch (ElementNotFoundException $e) {
+            $steps[] = new Given('I expand "Reports" node');
+        }
+
+        $steps[] = new Given('I follow "Activity completion"');
+
+        return $steps;
+    }
+
+}
diff --git a/completion/tests/behat/enable_manual_complete_mark.feature b/completion/tests/behat/enable_manual_complete_mark.feature
new file mode 100644 (file)
index 0000000..0afd58f
--- /dev/null
@@ -0,0 +1,47 @@
+@core_completion
+Feature: Allow students to manually mark an activity as complete
+  In order to let students decide when an activity is completed
+  As a moodle teacher
+  I need to allow students to mark activities as completed
+
+  @javascript
+  Scenario: Mark an activity as completed
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | Frist | teacher1@asd.com |
+      | student1 | Student | First | student1@asd.com |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | Enable completion tracking | 1 |
+      | Enable conditional access | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Edit settings"
+    And I fill the moodle form with:
+      | Completion tracking | Enabled, control via completion and activity settings |
+      | Completion tracking begins on enrolment | 1 |
+    And I press "Save changes"
+    When I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    Then "Student First" user has not completed "Test forum name" activity
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I press "Mark as complete: Test forum name"
+    And I wait "3" seconds
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I expand "Reports" node
+    And I follow "Activity completion"
+    And "Student First" user has completed "Test forum name" activity
diff --git a/completion/tests/behat/restrict_section_availability.feature b/completion/tests/behat/restrict_section_availability.feature
new file mode 100644 (file)
index 0000000..88866b1
--- /dev/null
@@ -0,0 +1,53 @@
+@core_completion
+Feature: Restrict sections availability through completion conditions
+  In order to control section's contents access through activities completion
+  As a moodle teacher
+  I need to restrict sections availability using different conditions
+
+  @javascript
+  Scenario: Show section greyed-out to students when completion conditions are not satisfied
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | Frist | teacher1@asd.com |
+      | student1 | Student | First | student1@asd.com |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | Enable completion tracking | 1 |
+      | Enable conditional access | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Edit settings"
+    And I fill the moodle form with:
+      | Completion tracking | Enabled, control via completion and activity settings |
+      | Completion tracking begins on enrolment | 1 |
+    And I press "Save changes"
+    And I add a "Label" to section "1" and I fill the form with:
+      | Label text | Test label |
+      | Completion tracking | Students can manually mark the activity as completed |
+    And I add a "Page" to section "2" and I fill the form with:
+      | Name | Test page name |
+      | Description | Test page description |
+      | Page content | Test page contents |
+    When I click on "Edit summary" "link" in the "#section-2" "css_element"
+    And I fill the moodle form with:
+      | id_conditioncompletiongroup_0_conditionsourcecmid | Test label |
+      | id_conditioncompletiongroup_0_conditionrequiredcompletion | must be marked complete |
+      | Before section can be accessed | Show section greyed-out, with restriction information |
+    And I press "Save changes"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "Not available until the activity Test label is marked complete."
+    And I should not see "Test page name"
+    And I press "Not completed: Test label. Select to mark as complete."
+    And I should see "Test page name"
+    And I should not see "Not available until the activity Test label is marked complete."
index f906949..9f76d76 100644 (file)
@@ -2340,17 +2340,19 @@ function moveto_module($mod, $section, $beforemod=NULL) {
     }
 
     // if moving to a hidden section then hide module
-    if (!$section->visible && $mod->visible) {
-        // Set this in the object because it is sent as a response to ajax calls.
-        $mod->visible = 0;
-        set_coursemodule_visible($mod->id, 0);
-        // Set visibleold to 1 so module will be visible when section is made visible.
-        $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id));
-    }
-    if ($section->visible && !$mod->visible) {
-        set_coursemodule_visible($mod->id, $mod->visibleold);
-        // Set this in the object because it is sent as a response to ajax calls.
-        $mod->visible = $mod->visibleold;
+    if ($mod->section != $section->id) {
+        if (!$section->visible && $mod->visible) {
+            // Set this in the object because it is sent as a response to ajax calls.
+            $mod->visible = 0;
+            set_coursemodule_visible($mod->id, 0);
+            // Set visibleold to 1 so module will be visible when section is made visible.
+            $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id));
+        }
+        if ($section->visible && !$mod->visible) {
+            set_coursemodule_visible($mod->id, $mod->visibleold);
+            // Set this in the object because it is sent as a response to ajax calls.
+            $mod->visible = $mod->visibleold;
+        }
     }
 
 /// Add the module into the new section
index 166c638..614ac8a 100644 (file)
@@ -61,6 +61,7 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) {
     $newcm->module           = $moduleinfo->module;
     $newcm->instance         = 0; // Not known yet, will be updated later (this is similar to restore code).
     $newcm->visible          = $moduleinfo->visible;
+    $newcm->visibleold       = $moduleinfo->visible;
     $newcm->groupmode        = $moduleinfo->groupmode;
     $newcm->groupingid       = $moduleinfo->groupingid;
     $newcm->groupmembersonly = $moduleinfo->groupmembersonly;
@@ -128,7 +129,6 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) {
     // Make sure visibility is set correctly (in particular in calendar).
     // Note: allow them to set it even without moodle/course:activityvisibility.
     set_coursemodule_visible($moduleinfo->coursemodule, $moduleinfo->visible);
-    $DB->set_field('course_modules', 'visibleold', 1, array('id' => $moduleinfo->coursemodule));
 
     if (isset($moduleinfo->cmidnumber)) { // Label.
         // Set cm idnumber - uniqueness is already verified by form validation.
index 1c77c44..932bca0 100644 (file)
@@ -4,8 +4,7 @@ Feature: Add activities to courses
   As a teacher
   I need to add activites to a course
 
-  @javascript
-  Scenario: Add an activity to the course
+  Background:
     Given the following "users" exists:
       | username | firstname | lastname | email |
       | student1 | Student | 1 | student1@asd.com |
@@ -19,8 +18,11 @@ Feature: Add activities to courses
       | student2 | C1 | student |
     And I log in as "admin"
     And I follow "Course 1"
-    When I turn editing mode on
-    And I add a "Database" to section "3" and I fill the form with:
+    And I turn editing mode on
+
+  @javascript
+  Scenario: Add an activity to a course
+    When I add a "Database" to section "3" and I fill the form with:
       | Name | Test name |
       | Description | Test database description |
       | Required entries | 9 |
@@ -35,21 +37,29 @@ Feature: Add activities to courses
 
   @javascript
   Scenario: Add an activity without the required fields
-    Given the following "users" exists:
-      | username | firstname | lastname | email |
-      | student1 | Student | 1 | student1@asd.com |
-      | student2 | Student | 2 | student2@asd.com |
-    And the following "courses" exists:
-      | fullname | shortname | format |
-      | Course 1 | C1 | topics |
-    And the following "course enrolments" exists:
-      | user | course | role |
-      | student1 | C1 | student |
-      | student2 | C1 | student |
-    And I log in as "admin"
-    And I follow "Course 1"
-    When I turn editing mode on
-    And I add a "Database" to section "3" and I fill the form with:
+    When I add a "Database" to section "3" and I fill the form with:
       | Name | Test name |
     Then I should see "Adding a new"
     And I should see "Required"
+
+  Scenario: Add an activity to a course with Javascript disabled
+    Then I should see "Add a resource to section 'Topic 1'"
+    And I should see "Add an activity to section 'Topic 1'"
+    And I should see "Add a resource to section 'Topic 2'"
+    And I should see "Add an activity to section 'Topic 2'"
+    And I should see "Add a resource to section 'Topic 3'"
+    And I should see "Add an activity to section 'Topic 3'"
+    And I add a "Label" to section "2"
+    And I should see "Adding a new Label to Topic 2"
+    And I fill the moodle form with:
+      | Label text | I'm a label |
+    And I press "Save and return to course"
+    And I add a "Database" to section "3"
+    And I should see "Adding a new Database to Topic 3"
+    And I fill the moodle form with:
+      | Name | Test database name |
+      | Description | Test database description |
+    And I press "Save and return to course"
+    And I should not see "Adding a new"
+    And I should see "Test database name"
+    And I should see "I'm a label"
index 0f1e0ca..c0622e2 100644 (file)
@@ -28,7 +28,9 @@
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
 use Behat\Behat\Context\Step\Given as Given,
-    Behat\Gherkin\Node\TableNode as TableNode;
+    Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
  * Course-related steps definitions.
@@ -83,15 +85,331 @@ class behat_course extends behat_base {
      */
     public function i_add_to_section($activity, $section) {
 
-        // Clicks add activity or resource section link.
-        $sectionxpath = "//*[@id='section-" . $section . "']/*/*/*/div[@class='section-modchooser']/span/a";
-        $sectionnode = $this->find('xpath', $sectionxpath);
-        $sectionnode->click();
+        $sectionxpath = "//*[@id='section-" . $section . "']";
 
-        // Clicks the selected activity if it exists.
-        $activityxpath = ".//label[contains(.,'" . $activity . "')]/input";
-        $activitynode = $this->find('xpath', $activityxpath);
-        $activitynode->doubleClick();
+        if ($this->running_javascript()) {
+
+            // Clicks add activity or resource section link.
+            $sectionxpath = $sectionxpath . "/descendant::div[@class='section-modchooser']/span/a";
+            $sectionnode = $this->find('xpath', $sectionxpath);
+            $sectionnode->click();
+
+            // Clicks the selected activity if it exists.
+            $activityxpath = ".//label[contains(.,'" . $activity . "')]/input";
+            $activitynode = $this->find('xpath', $activityxpath);
+            $activitynode->doubleClick();
+
+        } else {
+            // Without Javascript.
+
+            // Selecting the option from the select box which contains the option.
+            $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' section_add_menus ')]
+/descendant::select[contains(., '" . $activity . "')]";
+            $selectnode = $this->find('xpath', $selectxpath);
+            $selectnode->selectOption($activity);
+
+            // Go button.
+            $gobuttonxpath = $selectxpath . "/ancestor::form/descendant::input[@type='submit']";
+            $gobutton = $this->find('xpath', $gobuttonxpath);
+            $gobutton->click();
+        }
+
+    }
+
+    /**
+     * Turns course section highlighting on.
+     *
+     * @Given /^I turn section "(?P<section_number>\d+)" highlighting on$/
+     * @param int $sectionnumber The section number
+     */
+    public function i_turn_section_highlighting_on($sectionnumber) {
+
+        // Ensures the section exists.
+        $xpath = $this->section_exists($sectionnumber);
+
+        return array(
+            new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'),
+            new Given('I wait "2" seconds')
+        );
+    }
+
+    /**
+     * Turns course section highlighting off.
+     *
+     * @Given /^I turn section "(?P<section_number>\d+)" highlighting off$/
+     * @param int $sectionnumber The section number
+     */
+    public function i_turn_section_highlighting_off($sectionnumber) {
+
+        // Ensures the section exists.
+        $xpath = $this->section_exists($sectionnumber);
+
+        return array(
+            new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'),
+            new Given('I wait "2" seconds')
+        );
+    }
+
+    /**
+     * Shows the specified hidden section. You need to be in the course page and on editing mode.
+     *
+     * @Given /^I show section "(?P<section_number>\d+)"$/
+     * @param int $sectionnumber
+     */
+    public function i_show_section($sectionnumber) {
+        $showicon = $this->show_section_icon_exists($sectionnumber);
+        $showicon->click();
+
+        // It requires time.
+        $this->getSession()->wait(5000, false);
+    }
+
+    /**
+     * Hides the specified visible section. You need to be in the course page and on editing mode.
+     *
+     * @Given /^I hide section "(?P<section_number>\d+)"$/
+     * @param int $sectionnumber
+     */
+    public function i_hide_section($sectionnumber) {
+        $hideicon = $this->hide_section_icon_exists($sectionnumber);
+        $hideicon->click();
+
+        // It requires time.
+        $this->getSession()->wait(5000, false);
+    }
+
+    /**
+     * Checks if the specified course section hightlighting is turned on. You need to be in the course page on editing mode.
+     *
+     * @Then /^section "(?P<section_number>\d+)" should be highlighted$/
+     * @throws ExpectationException
+     * @param int $sectionnumber The section number
+     */
+    public function section_should_be_highlighted($sectionnumber) {
+
+        // Ensures the section exists.
+        $xpath = $this->section_exists($sectionnumber);
+
+        // The important checking, we can not check the img.
+        $xpath = $xpath . "/descendant::img[@alt='" . get_string('markedthistopic') . "'][contains(@src, 'marked')]";
+        $exception = new ExpectationException('The "' . $sectionnumber . '" section is not highlighted', $this->getSession());
+        $this->find('xpath', $xpath, $exception);
+    }
+
+    /**
+     * Checks if the specified course section highlighting is turned off. You need to be in the course page on editing mode.
+     *
+     * @Then /^section "(?P<section_number>\d+)" should not be highlighted$/
+     * @throws ExpectationException
+     * @param int $sectionnumber The section number
+     */
+    public function section_should_not_be_highlighted($sectionnumber) {
+
+        // We only catch ExpectationException, ElementNotFoundException should be thrown if the specified section does not exist.
+        try {
+            $this->section_should_be_highlighted($sectionnumber);
+        } catch (ExpectationException $e) {
+            // ExpectedException means that it is not highlighted.
+            return;
+        }
+
+        throw new ExpectationException('The "' . $sectionnumber . '" section is highlighted', $this->getSession());
+    }
+
+    /**
+     * Checks that the specified section is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
+     *
+     * @Then /^section "(?P<section_number>\d+)" should be hidden$/
+     * @throws ExpectationException
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param int $sectionnumber
+     */
+    public function section_should_be_hidden($sectionnumber) {
+
+        $sectionxpath = $this->section_exists($sectionnumber);
+
+        // Section should be hidden.
+        $exception = new ExpectationException('The section is not hidden', $this->getSession());
+        $this->find('xpath', $sectionxpath . "[contains(concat(' ', @class, ' '), ' hidden ')]", $exception);
+
+        // The checking are different depending on user permissions.
+        if ($this->is_course_editor()) {
+
+            // The section must be hidden.
+            $this->show_section_icon_exists($sectionnumber);
+
+            // If there are activities they should be hidden and the visibility icon should not be available.
+            if ($activities = $this->get_section_activities($sectionxpath)) {
+
+                $dimmedexception = new ExpectationException('There are activities that are not dimmed', $this->getSession());
+                $visibilityexception = new ExpectationException('There are activities which visibility icons are clickable', $this->getSession());
+                foreach ($activities as $activity) {
+
+                    // Dimmed.
+                    $this->find('xpath', "//div[contains(concat(' ', @class, ' '), ' activityinstance ')]
+/a[contains(concat(' ', @class, ' '), ' dimmed ')]", $dimmedexception, $activity);
+
+                    // To check that the visibility is not clickable we check the funcionality rather than the applied style.
+                    $visibilityiconnode = $this->find('css', 'a.editing_show img', false, $activity);
+                    $visibilityiconnode->click();
+
+                    // We ensure that we still see the show icon.
+                    $visibilityiconnode = $this->find('css', 'a.editing_show img', $visibilityexception, $activity);
+                }
+            }
+
+        } else {
+            // There shouldn't be activities.
+            if ($this->get_section_activities($sectionxpath)) {
+                throw new ExpectationException('There are activities in the section and they should be hidden', $this->getSession());
+            }
+        }
+    }
+
+    /**
+     * Checks that the specified section is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
+     *
+     * @Then /^section "(?P<section_number>\d+)" should be visible$/
+     * @throws ExpectationException
+     * @param int $sectionnumber
+     */
+    public function section_should_be_visible($sectionnumber) {
+
+        $sectionxpath = $this->section_exists($sectionnumber);
+
+        // Section should not be hidden.
+        if (!$this->getSession()->getPage()->find('xpath', $sectionxpath . "[not(contains(concat(' ', @class, ' '), ' hidden '))]")) {
+            throw new ExpectationException('The section is hidden', $this->getSession());
+        }
+
+        // Hide section button should be visible.
+        if ($this->is_course_editor()) {
+            $this->hide_section_icon_exists($sectionnumber);
+        }
+    }
+
+    /**
+     * Checks if the course section exists.
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param int $sectionnumber
+     * @return string The xpath of the section.
+     */
+    protected function section_exists($sectionnumber) {
+
+        // Just to give more info in case it does not exist.
+        $xpath = "//li[@id='section-" . $sectionnumber . "']";
+        $exception = new ElementNotFoundException($this->getSession(), "Section $sectionnumber ");
+        $this->find('xpath', $xpath, $exception);
+
+        return $xpath;
+    }
+
+    /**
+     * Returns the show section icon or throws an exception.
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param int $sectionnumber
+     * @return NodeElement
+     */
+    protected function show_section_icon_exists($sectionnumber) {
+
+        // Gets the section xpath and ensure it exists.
+        $xpath = $this->section_exists($sectionnumber);
+
+        // We need to know the course format as the text strings depends on them.
+        $courseformat = $this->get_course_format();
+
+        // Checking the show button alt text and show icon.
+        $xpath = $xpath . "/descendant::a/descendant::img[@alt='". get_string('showfromothers', $courseformat) ."'][contains(@src, 'show')]";
+
+        $exception = new ElementNotFoundException($this->getSession(), 'Show section icon ');
+        return $this->find('xpath', $xpath, $exception);
+    }
+
+    /**
+     * Returns the hide section icon link if it exists or throws exception.
+     *
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param int $sectionnumber
+     * @return NodeElement
+     */
+    protected function hide_section_icon_exists($sectionnumber) {
+
+        // Gets the section xpath and ensure it exists.
+        $xpath = $this->section_exists($sectionnumber);
+
+        // We need to know the course format as the text strings depends on them.
+        $courseformat = $this->get_course_format();
+
+        // Checking the hide button alt text and hide icon.
+        $xpath = $xpath . "/descendant::a/descendant::img[@alt='". get_string('hidefromothers', $courseformat) ."'][contains(@src, 'hide')]";
+
+        $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
+        return $this->find('xpath', $xpath, $exception);
+    }
+
+    /**
+     * Gets the current course format.
+     *
+     * @throws ExpectationException If we are not in the course view page.
+     * @return string The course format in a frankenstyled name.
+     */
+    protected function get_course_format() {
+
+        $exception = new ExpectationException('You are not in a course page', $this->getSession());
+
+        // The moodle body's id attribute contains the course format.
+        $node = $this->getSession()->getPage()->find('css', 'body');
+        if (!$node) {
+            throw $exception;
+        }
+
+        if (!$bodyid = $node->getAttribute('id')) {
+            throw $exception;
+        }
+
+        if (strstr($bodyid, 'page-course-view-') === false) {
+            throw $exception;
+        }
+
+        return 'format_' . str_replace('page-course-view-', '', $bodyid);
+    }
+
+    /**
+     * Gets the section's activites DOM nodes.
+     *
+     * @param string $sectionxpath
+     * @return array NodeElement instances
+     */
+    protected function get_section_activities($sectionxpath) {
+
+        $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', @class, ' '), ' activity ')]";
+
+        // We spin here, as activities usually require a lot of time to load.
+        try {
+            $activities = $this->find_all('xpath', $xpath);
+        } catch (ElementNotFoundException $e) {
+            return false;
+        }
+
+        return $activities;
+    }
+
+    /**
+     * Returns whether the user can edit the course contents or not.
+     *
+     * @return bool
+     */
+    protected function is_course_editor() {
+
+        // We don't need to behat_base::spin() here as all is already loaded.
+        if (!$this->getSession()->getPage()->findButton('Turn editing off') &&
+                !$this->getSession()->getPage()->findButton('Turn editing on')) {
+            return false;
+        }
+
+        return true;
     }
 
 }
diff --git a/course/tests/behat/force_group_mode.feature b/course/tests/behat/force_group_mode.feature
new file mode 100644 (file)
index 0000000..2ddccd5
--- /dev/null
@@ -0,0 +1,60 @@
+@core_course @_cross_browser
+Feature: Force group mode in a course
+  In order to use the same group mode all over the course
+  As a moodle teacher
+  I need to force the group mode of all course's activities
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "chat" to section "1" and I fill the form with:
+      | Name of this chat room | Chat room |
+      | Description | Chat description |
+    And I follow "Edit settings"
+
+  @javascript
+  Scenario: Forced group mode using separate groups
+    Given I fill the moodle form with:
+      | Group mode | Separate groups |
+      | Force group mode | Yes |
+    When I press "Save changes"
+    Then ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
+    And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
+    And I click on "//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+    And ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
+    And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
+
+  @javascript
+  Scenario: Forced group mode using visible groups
+    Given I fill the moodle form with:
+      | Group mode | Visible groups |
+      | Force group mode | Yes |
+    And I press "Save changes"
+    Then ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
+    And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
+    And I click on "//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+    And ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
+    And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
+
+  @javascript
+  Scenario: Forced group mode without groups
+    Given I fill the moodle form with:
+      | Group mode | No groups |
+      | Force group mode | Yes |
+    And I press "Save changes"
+    Then ".//a//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should not exists
+    And ".//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should exists
+    And I click on "//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+    And ".//a//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should not exists
+    And ".//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should exists
+
diff --git a/course/tests/behat/section_highlighting.feature b/course/tests/behat/section_highlighting.feature
new file mode 100644 (file)
index 0000000..dfb5ab5
--- /dev/null
@@ -0,0 +1,43 @@
+@core_course @_cross_browser
+Feature: Topic's course sections highlighting
+  In order to highlight parts of the course to students
+  As a teacher
+  I need to highlight one specific section
+
+  @javascript
+  Scenario: Highlight a topic's course section
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | student1 | Student | 1 | student1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I turn section "1" highlighting on
+    Then section "1" should be highlighted
+    And I turn section "2" highlighting on
+    And section "2" should be highlighted
+    And section "1" should not be highlighted
+    And I am on homepage
+    And I follow "Course 1"
+    And section "2" should be highlighted
+    And section "1" should not be highlighted
+    And I turn section "2" highlighting off
+    And section "2" should not be highlighted
+    And I reload the page
+    And section "2" should not be highlighted
+    And I am on homepage
+    And I follow "Course 1"
+    And section "2" should not be highlighted
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And section "1" should not be highlighted
+    And section "2" should not be highlighted
diff --git a/course/tests/behat/section_visibility.feature b/course/tests/behat/section_visibility.feature
new file mode 100644 (file)
index 0000000..dad5d8c
--- /dev/null
@@ -0,0 +1,69 @@
+@core_course @_cross_browser
+Feature: Show/hide course sections
+  In order to delay sections availability
+  As a moodle teacher
+  I need to show or hide sections
+
+  @javascript
+  Scenario: Show / hide section icon functions correctly
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | student1 | Student | 1 | student1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test hidden forum 11 name |
+      | Description | Test hidden forum 11 description |
+      | Visible | Hide |
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test hidden forum 12 name |
+      | Description | Test hidden forum 12 description |
+      | Visible | Show |
+    And I add a "forum" to section "2" and I fill the form with:
+      | Forum name | Test hidden forum 21 name |
+      | Description | Test hidden forum 21 description |
+      | Visible | Hide |
+    And I add a "forum" to section "2" and I fill the form with:
+      | Forum name | Test hidden forum 22 name |
+      | Description | Test hidden forum 22 description |
+      | Visible | Show |
+    And I add a "forum" to section "3" and I fill the form with:
+      | Forum name | Test hidden forum 31 name |
+      | Description | Test hidden forum 31 description |
+      | Visible | Hide |
+    And I add a "forum" to section "3" and I fill the form with:
+      | Forum name | Test hidden forum 32 name |
+      | Description | Test hidden forum 32 description |
+      | Visible | Show |
+    And I follow "Course 1"
+    When I hide section "1"
+    Then section "1" should be hidden
+    And section "2" should be visible
+    And section "3" should be visible
+    And I hide section "2"
+    And section "2" should be hidden
+    And I show section "2"
+    And section "2" should be visible
+    And I hide section "3"
+    And I show section "3"
+    And I hide section "3"
+    And section "3" should be hidden
+    And I reload the page
+    And section "1" should be hidden
+    And section "2" should be visible
+    And section "3" should be hidden
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And section "1" should be hidden
+    And section "2" should be visible
+    And section "3" should be hidden
index 9d08906..d688159 100644 (file)
@@ -1112,4 +1112,137 @@ class courselib_testcase extends advanced_testcase {
             $this->assertGreaterThanOrEqual($last, $activity->timestamp);
         }
     }
+
+    /**
+     * Tests moving a module between hidden/visible sections and
+     * verifies that the course/module visiblity seettings are
+     * retained.
+     */
+    public function test_moveto_module_between_hidden_sections() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course(array('numsections' => 4), array('createsections' => true));
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
+        $quiz= $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
+
+        // Set the page as hidden
+        set_coursemodule_visible($page->cmid, 0);
+
+        // Set sections 3 as hidden.
+        set_section_visible($course->id, 3, 0);
+
+        $modinfo = get_fast_modinfo($course);
+
+        $hiddensection = $modinfo->get_section_info(3);
+        // New section is definitely not visible:
+        $this->assertEquals($hiddensection->visible, 0);
+
+        $forumcm = $modinfo->cms[$forum->cmid];
+        $pagecm = $modinfo->cms[$page->cmid];
+
+        // Move the forum and the page to a hidden section.
+        moveto_module($forumcm, $hiddensection);
+        moveto_module($pagecm, $hiddensection);
+
+        // Reset modinfo cache.
+        get_fast_modinfo(0, 0, true);
+
+        $modinfo = get_fast_modinfo($course);
+
+        // Verify that forum and page have been moved to the hidden section and quiz has not.
+        $this->assertContains($forum->cmid, $modinfo->sections[3]);
+        $this->assertContains($page->cmid, $modinfo->sections[3]);
+        $this->assertNotContains($quiz->cmid, $modinfo->sections[3]);
+
+        // Verify that forum has been made invisible.
+        $forumcm = $modinfo->cms[$forum->cmid];
+        $this->assertEquals($forumcm->visible, 0);
+        // Verify that old state has been retained.
+        $this->assertEquals($forumcm->visibleold, 1);
+
+        // Verify that page has stayed invisible.
+        $pagecm = $modinfo->cms[$page->cmid];
+        $this->assertEquals($pagecm->visible, 0);
+        // Verify that old state has been retained.
+        $this->assertEquals($pagecm->visibleold, 0);
+
+        // Verify that quiz has been unaffected.
+        $quizcm = $modinfo->cms[$quiz->cmid];
+        $this->assertEquals($quizcm->visible, 1);
+
+        // Move forum and page back to visible section.
+        $visiblesection = $modinfo->get_section_info(2);
+        moveto_module($forumcm, $visiblesection);
+        moveto_module($pagecm, $visiblesection);
+
+        // Reset modinfo cache.
+        get_fast_modinfo(0, 0, true);
+        $modinfo = get_fast_modinfo($course);
+
+        // Verify that forum has been made visible.
+        $forumcm = $modinfo->cms[$forum->cmid];
+        $this->assertEquals($forumcm->visible, 1);
+
+        // Verify that page has stayed invisible.
+        $pagecm = $modinfo->cms[$page->cmid];
+        $this->assertEquals($pagecm->visible, 0);
+
+        // Move the page in the same section (this is what mod duplicate does_
+        moveto_module($pagecm, $visiblesection, $forumcm);
+
+        // Reset modinfo cache.
+        get_fast_modinfo(0, 0, true);
+
+        // Verify that the the page is still hidden
+        $modinfo = get_fast_modinfo($course);
+        $pagecm = $modinfo->cms[$page->cmid];
+        $this->assertEquals($pagecm->visible, 0);
+    }
+
+    /**
+     * Tests moving a module around in the same section. moveto_module()
+     * is called this way in modduplicate.
+     */
+    public function test_moveto_module_in_same_section() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course(array('numsections' => 3), array('createsections' => true));
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+
+        // Simulate inconsistent visible/visibleold values (MDL-38713).
+        $cm = $DB->get_record('course_modules', array('id' => $page->cmid), '*', MUST_EXIST);
+        $cm->visible = 0;
+        $cm->visibleold = 1;
+        $DB->update_record('course_modules', $cm);
+
+        $modinfo = get_fast_modinfo($course);
+        $forumcm = $modinfo->cms[$forum->cmid];
+        $pagecm = $modinfo->cms[$page->cmid];
+
+        // Verify that page is hidden.
+        $this->assertEquals($pagecm->visible, 0);
+
+        // Verify section 0 is where all mods added.
+        $section = $modinfo->get_section_info(0);
+        $this->assertEquals($section->id, $forumcm->section);
+        $this->assertEquals($section->id, $pagecm->section);
+
+
+        // Move the forum and the page to a hidden section.
+        moveto_module($pagecm, $section, $forumcm);
+
+        // Reset modinfo cache.
+        get_fast_modinfo(0, 0, true);
+
+        // Verify that the the page is still hidden
+        $modinfo = get_fast_modinfo($course);
+        $pagecm = $modinfo->cms[$page->cmid];
+        $this->assertEquals($pagecm->visible, 0);
+    }
 }
index 032f241..5e02f6b 100644 (file)
@@ -138,6 +138,9 @@ $string['generalroleassignments'] = 'Include role assignments';
 $string['generalsettings'] = 'General backup settings';
 $string['generaluserscompletion'] = 'Include user completion information';
 $string['generalusers'] = 'Include users';
+$string['importgeneralsettings'] = 'General import defaults';
+$string['importgeneralmaxresults'] = 'Maximum number of courses listed for import';
+$string['importgeneralmaxresults_desc'] = 'This controls the number of courses that are listed during the first step of the import process';
 $string['importfile'] = 'Import a backup file';
 $string['importbackupstage1action'] = 'Next';
 $string['importbackupstage2action'] = 'Next';
@@ -246,3 +249,4 @@ $string['skipmodifprev'] = 'Skip courses not modified since previous backup';
 $string['skipmodifprevhelp'] = 'Choose whether or not to skip courses that have not been modified since previous backup';
 $string['totalcategorysearchresults'] = 'Total categories: {$a}';
 $string['totalcoursesearchresults'] = 'Total courses: {$a}';
+$string['morecoursesearchresults'] = 'More than {$a} courses found, showing first {$a} results';
index 5917d88..ece712b 100644 (file)
@@ -104,7 +104,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
                 $exceptionlocator = $locator[1];
 
                 // If we are in a @javascript session all contents would be displayed as HTML characters.
-                if (get_class($this->getSession()->getDriver()) === 'Behat\Mink\Driver\Selenium2Driver') {
+                if ($this->running_javascript()) {
                     $locator[1] = html_entity_decode($locator[1], ENT_NOQUOTES);
                 }
 
@@ -405,4 +405,13 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         return $this->transform_selector($selectortype, $element);
     }
 
+    /**
+     * Returns whether the scenario is running in a browser that can run Javascript or not.
+     *
+     * @return boolean
+     */
+    protected function running_javascript() {
+        return get_class($this->getSession()->getDriver()) === 'Moodle\BehatExtension\Driver\MoodleSelenium2Driver';
+    }
+
 }
index 01cc965..a72e753 100644 (file)
@@ -53,7 +53,6 @@ class behat_field_manager {
      * @return behat_form_field
      */
     public static function get_field(NodeElement $fieldnode, $locator, Session $session) {
-        global $CFG;
 
         // Get the field type if is part of a moodleform.
         if (self::is_moodleform_field($fieldnode)) {
@@ -65,6 +64,23 @@ class behat_field_manager {
             $type = 'field';
         }
 
+        return self::get_field_instance($type, $fieldnode, $session);
+    }
+
+    /**
+     * Returns the appropiate behat_form_field according to the provided type.
+     *
+     * It defaults to behat_form_field.
+     *
+     * @param string $type The field type (checkbox, date_selector, text...)
+     * @param NodeElement $fieldnode
+     * @param Session $session The behat session
+     * @return behat_form_field
+     */
+    public static function get_field_instance($type, NodeElement $fieldnode, Session $session) {
+
+        global $CFG;
+
         $classname = 'behat_form_' . $type;
 
         // Fallsback on the default form field if nothing specific exists.
index 938bde6..8ba838a 100644 (file)
@@ -69,7 +69,14 @@ class behat_form_field {
      * @return void
      */
     public function set_value($value) {
-        $this->field->setValue($value);
+
+        // If we are not dealing with a text-based tag try to find the most appropiate
+        // behat_form_* class to deal with it.
+        if ($instance = $this->guess_type()) {
+            $instance->set_value($value);
+        } else {
+            $this->field->setValue($value);
+        }
     }
 
     /**
@@ -78,7 +85,62 @@ class behat_form_field {
      * @return string
      */
     public function get_value() {
-        return $this->field->getValue();
+
+        // If we are not dealing with a text-based tag try to find the most appropiate
+        // behat_form_* class to deal with it.
+        if ($instance = $this->guess_type()) {
+            return $instance->get_value();
+        } else {
+            return $this->field->getValue();
+        }
+    }
+
+    /**
+     * Guesses the element type we are dealing with in case is not a text-based element.
+     *
+     * This class is the generic field type, behat_field_manager::get_field()
+     * should be able to find the appropiate class for the field type, but
+     * in cases like moodle form group elements we can not find the type of
+     * the field through the DOM so we also need to take care of the
+     * different field types from here. If we need to deal with more complex
+     * moodle form elements we will need to refactor this simple HTML elements
+     * guess method.
+     *
+     * @return mixed False if no need for an special behat_form_*, otherwise the behat_form_*
+     */
+    private function guess_type() {
+        global $CFG;
+
+        // Textareas are considered text based elements.
+        $tagname = $this->field->getTagName();
+        if ($tagname == 'textarea') {
+            return false;
+        }
+
+        if ($tagname == 'input') {
+            $type = $this->field->getAttribute('type');
+            switch ($type) {
+                case 'text':
+                    return false;
+                case 'checkbox':
+                    $classname = 'behat_form_checkbox';
+                    break;
+                case 'radio':
+                    // Behaves the same way.
+                    $classname = 'behat_form_checkbox';
+                    break;
+                default:
+                    return false;
+            }
+        }
+
+        // Select tag.
+        if ($tagname == 'select') {
+            $classname = 'behat_form_select';
+        }
+
+        $classpath = $CFG->dirroot . '/lib/behat/form_field/' . $classname . '.php';
+        return new $classname($this->session, $this->field);
     }
 
 }
diff --git a/lib/behat/form_field/behat_form_modvisible.php b/lib/behat/form_field/behat_form_modvisible.php
new file mode 100644 (file)
index 0000000..04e65ff
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Silly behat_form_select extension.
+ *
+ * @package    core_form
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__  . '/behat_form_select.php');
+
+/**
+ * Allows interaction with fmodvisible form fields.
+ *
+ * Plain behat_form_select extension as is the same
+ * kind of field.
+ *
+ * @package    core_form
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_form_modvisible extends behat_form_select {}
index 30752ce..0faa0fb 100644 (file)
@@ -123,7 +123,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         if (array_key_exists($name, self::$coursecatfields)) {
             if ($this->$name === false) {
                 // property was not retrieved from DB, retrieve all not retrieved fields
-                $notretrievedfields = array_diff(self::$coursecatfields, array_filter(self::$coursecatfields));
+                $notretrievedfields = array_diff_key(self::$coursecatfields, array_filter(self::$coursecatfields));
                 $record = $DB->get_record('course_categories', array('id' => $this->id),
                         join(',', array_keys($notretrievedfields)), MUST_EXIST);
                 foreach ($record as $key => $value) {
@@ -737,7 +737,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
             $fields[] = 'c.summary';
             $fields[] = 'c.summaryformat';
         } else {
-            $fields[] = $DB->sql_length('c.summary'). ' hassummary';
+            $fields[] = $DB->sql_substr('c.summary', 1, 1). ' hassummary';
         }
         $sql = "SELECT ". join(',', $fields). ", $ctxselect
                 FROM {course} c
@@ -749,6 +749,9 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         if ($checkvisibility) {
             // Loop through all records and make sure we only return the courses accessible by user.
             foreach ($list as $course) {
+                if (isset($list[$course->id]->hassummary)) {
+                    $list[$course->id]->hassummary = strlen($list[$course->id]->hassummary) > 0;
+                }
                 if (empty($course->visible)) {
                     // load context only if we need to check capability
                     context_helper::preload_from_record($course);
diff --git a/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-coverage.js b/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-coverage.js
new file mode 100644 (file)
index 0000000..ad31e6c
Binary files /dev/null and b/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-coverage.js differ
diff --git a/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-debug.js b/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-debug.js
new file mode 100644 (file)
index 0000000..4a443b0
Binary files /dev/null and b/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-debug.js differ
diff --git a/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-min.js b/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-min.js
new file mode 100644 (file)
index 0000000..7b896f8
Binary files /dev/null and b/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse-min.js differ
diff --git a/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse.js b/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse.js
new file mode 100644 (file)
index 0000000..4a443b0
Binary files /dev/null and b/lib/editor/tinymce/yui/build/moodle-editor_tinymce-collapse/moodle-editor_tinymce-collapse.js differ
diff --git a/lib/editor/tinymce/yui/collapse/collapse.js b/lib/editor/tinymce/yui/collapse/collapse.js
deleted file mode 100644 (file)
index ea68c0d..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-YUI.add('moodle-editor_tinymce-collapse', function(Y) {
-
-    var COLLAPSE = function() {
-        COLLAPSE.superclass.constructor.apply(this, arguments);
-    };
-
-    Y.extend(COLLAPSE, Y.Base, {
-
-        toggleNodeTemplate : null,
-        /**
-         * Set up basic values for static access.
-         */
-        init : function() {
-            this.initialise_toggles(10);
-        },
-
-        /**
-         * Has TinyMCE been loaded and the editors been initialised?
-         * Designed mainly for IE
-         * @return bool
-         */
-        editors_initialised : function() {
-            return typeof tinyMCE !== 'undefined';
-        },
-
-        initialise_toggles : function(refreshes) {
-            var editors_initialised = this.editors_initialised(), self = this, editor;
-            if (!editors_initialised && refreshes) {
-                setTimeout(function() {
-                        self.initialise_toggles(refreshes - 1);
-                    }, 100);
-                return;
-            }
-
-            // Create the toggle template for use later
-            this.toggleNodeTemplate = Y.Node.create('<a class="toggle_editor_toolbar" />');
-            this.toggleNodeTemplate.setContent(M.util.get_string('showeditortoolbar', 'form'));
-
-            // Delegate clicks of the toggle_editor_toolbar
-            Y.one('body').delegate('click', this.toggle_collapse_from_event, 'a.toggle_editor_toolbar', this);
-
-            // Set up editors which have already been created
-            for (editor in tinyMCE.editors) {
-                this.setup_collapse(tinyMCE.editors[editor]);
-            }
-
-            // Set up for future editors.
-            // I haven't yet found a way of directly delegating the editor.onInit event. Instead we have to listen for the
-            // tinyMCE.onAddEditor event, and then add a further event listener to the editor's onInit event.
-            // onAddEditor is triggered before the editor has been created.
-            // We use Y.Bind to ensure that context is maintained.
-            tinyMCE.onAddEditor.add(Y.bind(this.add_setup_collapse_listener, this));
-
-        },
-
-        /**
-         * Setup a listener for a new editor which will actually set the editor up
-         * @param {Manager} mgr
-         * @param {Editor} ed
-         */
-        add_setup_collapse_listener : function (mgr, ed) {
-            // Bind the editor.onInit function to set this editor up. This ensures we maintain our context (this)
-            ed.onInit.add(Y.bind(this.setup_collapse, this));
-        },
-
-        /**
-         * Setup the toggle system for the provided editor
-         *
-         * @param {Editor} ed The TinyMCE editor instance
-         */
-        setup_collapse : function(ed) {
-            var textarea = Y.Node(ed.getElement()),
-                editortable = Y.Node(ed.getContainer()).one('> table'),
-                thisToggleNode;
-
-            // Does this text area support collapsing at all?
-            if (!textarea.hasClass('collapsible')) {
-                return;
-            }
-
-            // Did we find an appropriate table to work with
-            if (!editortable) {
-                return;
-            }
-
-            // Add toggle button.
-            thisToggleNode = this.toggleNodeTemplate.cloneNode(true);
-            editortable.get('parentNode').insert(thisToggleNode, editortable);
-
-            // Toggle the toolbars initially.
-            if (Y.Node(ed.getElement()).hasClass('collapsed')) {
-                this.toggle_collapse(thisToggleNode, editortable, 0);
-            } else {
-                this.toggle_collapse(thisToggleNode, editortable, 1);
-            }
-        },
-
-        /**
-         * Toggle the specified editor toolbars.
-         *
-         * @param {Node} button The toggle button which we have to change the text for
-         * @param {Node} editortable The table which the tinyMCE editor is in
-         * @param {Boolean} newstate The intended toggle state
-         */
-        toggle_collapse : function(button, editortable, newstate) {
-            var toolbar = editortable.one('td.mceToolbar').ancestor('tr'),
-                statusbar = editortable.one('.mceStatusbar').ancestor('tr'),
-                editor, iframe, size;
-
-            // Check whether we have a state already.
-            if (typeof newstate === 'undefined') {
-                if (toolbar.getStyle('display') === 'none') {
-                    newstate = 1;
-                } else {
-                    newstate = 0;
-                }
-            }
-
-            // Toggle the various states and update the button text to suit
-            if (newstate === 0) {
-                toolbar.hide();
-                statusbar.hide();
-                button.setContent(M.util.get_string('showeditortoolbar', 'form'));
-            } else {
-                toolbar.show();
-                statusbar.show();
-                button.setContent(M.util.get_string('hideeditortoolbar', 'form'));
-            }
-
-            // TinyMCE renders the toolbar and path bar as part of the textarea. So toggling these items
-            // changes the required size of the rendered textarea. Frustrating but it's the way it's built.
-            // So we get TinyMCE to resize itself for us. Clunky but it works.
-
-            // Get the tinyMCE editor object for this text area.
-            editorid = editortable.ancestor('div').one('textarea').get('id');
-            editor = tinyMCE.getInstanceById(editorid);
-
-            // Somehow, this editor did not exist.
-            if (!editor) {
-                return;
-            }
-
-            // Resize editor to reflect presence of toolbar and path bar..
-            iframe = editor.getBody();
-            if (iframe) {
-                size = tinymce.DOM.getSize(iframe);
-                // If objects exist resize editor.
-                if (size) {
-                    editor.theme.resizeTo(size.w, size.h);
-                }
-            }
-        },
-
-        toggle_collapse_from_event : function(thisevent) {
-            var button = thisevent.target.ancestor('a', true),
-                editortable = thisevent.target.ancestor('span', true).one('table.mceLayout');
-            this.toggle_collapse(button, editortable);
-        }
-    });
-
-    M.editor_collapse = M.editor_collapse || {};
-    M.editor_collapse.init = function(params) {
-        return new COLLAPSE(params);
-    };
-
-}, '@VERSION@', {requires:['base', 'node', 'dom']});
\ No newline at end of file
diff --git a/lib/editor/tinymce/yui/src/collapse/build.json b/lib/editor/tinymce/yui/src/collapse/build.json
new file mode 100644 (file)
index 0000000..996558e
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-editor_tinymce-collapse",
+  "builds": {
+    "moodle-editor_tinymce-collapse": {
+      "jsfiles": [
+        "collapse.js"
+      ]
+    }
+  }
+}
diff --git a/lib/editor/tinymce/yui/src/collapse/js/collapse.js b/lib/editor/tinymce/yui/src/collapse/js/collapse.js
new file mode 100644 (file)
index 0000000..1b5e581
--- /dev/null
@@ -0,0 +1,163 @@
+var COLLAPSE = function() {
+    COLLAPSE.superclass.constructor.apply(this, arguments);
+};
+
+Y.extend(COLLAPSE, Y.Base, {
+    // A location to store the node toggling template so that we do not have to create it each time.
+    toggleNodeTemplate : null,
+
+    /**
+      * Set up basic values for static access.
+      */
+    init : function() {
+        this.initialise_toggles(10);
+    },
+
+    /**
+     * Has TinyMCE been loaded and the editors been initialised?
+     * Designed mainly for IE
+     * @return bool
+     */
+    editors_initialised : function() {
+        return typeof tinyMCE !== 'undefined';
+    },
+
+    initialise_toggles : function(refreshes) {
+        var editors_initialised = this.editors_initialised(), self = this, editor;
+        if (!editors_initialised && refreshes) {
+            setTimeout(function() {
+                    self.initialise_toggles(refreshes - 1);
+                }, 100);
+            return;
+        }
+
+        // Create the toggle template for use later
+        this.toggleNodeTemplate = Y.Node.create('<a class="toggle_editor_toolbar" />');
+        this.toggleNodeTemplate.setContent(M.util.get_string('showeditortoolbar', 'form'));
+
+        // Delegate clicks of the toggle_editor_toolbar
+        Y.one('body').delegate('click', this.toggle_collapse_from_event, 'a.toggle_editor_toolbar', this);
+
+        // Set up editors which have already been created
+        for (editor in tinyMCE.editors) {
+            this.setup_collapse(tinyMCE.editors[editor]);
+        }
+
+        // Set up for future editors.
+        // I haven't yet found a way of directly delegating the editor.onInit event. Instead we have to listen for the
+        // tinyMCE.onAddEditor event, and then add a further event listener to the editor's onInit event.
+        // onAddEditor is triggered before the editor has been created.
+        // We use Y.Bind to ensure that context is maintained.
+        tinyMCE.onAddEditor.add(Y.bind(this.add_setup_collapse_listener, this));
+
+    },
+
+    /**
+      * Setup a listener for a new editor which will actually set the editor up
+      * @param {Manager} mgr
+      * @param {Editor} ed
+      */
+    add_setup_collapse_listener : function (mgr, ed) {
+        // Bind the editor.onInit function to set this editor up. This ensures we maintain our context (this)
+        ed.onInit.add(Y.bind(this.setup_collapse, this));
+    },
+
+    /**
+      * Setup the toggle system for the provided editor
+      *
+      * @param {Editor} ed The TinyMCE editor instance
+      */
+    setup_collapse : function(ed) {
+        var textarea = Y.Node(ed.getElement()),
+            editortable = Y.Node(ed.getContainer()).one('> table'),
+            thisToggleNode;
+
+        // Does this text area support collapsing at all?
+        if (!textarea.hasClass('collapsible')) {
+            return;
+        }
+
+        // Did we find an appropriate table to work with
+        if (!editortable) {
+            return;
+        }
+
+        // Add toggle button.
+        thisToggleNode = this.toggleNodeTemplate.cloneNode(true);
+        editortable.get('parentNode').insert(thisToggleNode, editortable);
+
+        // Toggle the toolbars initially.
+        if (Y.Node(ed.getElement()).hasClass('collapsed')) {
+            this.toggle_collapse(thisToggleNode, editortable, 0);
+        } else {
+            this.toggle_collapse(thisToggleNode, editortable, 1);
+        }
+    },
+
+    /**
+      * Toggle the specified editor toolbars.
+      *
+      * @param {Node} button The toggle button which we have to change the text for
+      * @param {Node} editortable The table which the tinyMCE editor is in
+      * @param {Boolean} newstate The intended toggle state
+      */
+    toggle_collapse : function(button, editortable, newstate) {
+        var toolbar = editortable.one('td.mceToolbar').ancestor('tr'),
+            statusbar = editortable.one('.mceStatusbar').ancestor('tr'),
+            editor, iframe, size;
+
+        // Check whether we have a state already.
+        if (typeof newstate === 'undefined') {
+            if (toolbar.getStyle('display') === 'none') {
+                newstate = 1;
+            } else {
+                newstate = 0;
+            }
+        }
+
+        // Toggle the various states and update the button text to suit
+        if (newstate === 0) {
+            toolbar.hide();
+            statusbar.hide();
+            button.setContent(M.util.get_string('showeditortoolbar', 'form'));
+        } else {
+            toolbar.show();
+            statusbar.show();
+            button.setContent(M.util.get_string('hideeditortoolbar', 'form'));
+        }
+
+        // TinyMCE renders the toolbar and path bar as part of the textarea. So toggling these items
+        // changes the required size of the rendered textarea. Frustrating but it's the way it's built.
+        // So we get TinyMCE to resize itself for us. Clunky but it works.
+
+        // Get the tinyMCE editor object for this text area.
+        editorid = editortable.ancestor('div').one('textarea').get('id');
+        editor = tinyMCE.getInstanceById(editorid);
+
+        // Somehow, this editor did not exist.
+        if (!editor) {
+            return;
+        }
+
+        // Resize editor to reflect presence of toolbar and path bar..
+        iframe = editor.getBody();
+        if (iframe) {
+            size = tinymce.DOM.getSize(iframe);
+            // If objects exist resize editor.
+            if (size) {
+                editor.theme.resizeTo(size.w, size.h);
+            }
+        }
+    },
+
+    toggle_collapse_from_event : function(thisevent) {
+        var button = thisevent.target.ancestor('a', true),
+            editortable = thisevent.target.ancestor('span', true).one('table.mceLayout');
+        this.toggle_collapse(button, editortable);
+    }
+});
+
+M.editor_collapse = M.editor_collapse || {};
+M.editor_collapse.init = function(params) {
+    return new COLLAPSE(params);
+};
diff --git a/lib/editor/tinymce/yui/src/collapse/meta/collapse.json b/lib/editor/tinymce/yui/src/collapse/meta/collapse.json
new file mode 100644 (file)
index 0000000..cd81377
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "moodle-editor_tinymce-collapse": {
+    "requires": [
+        "base",
+        "node",
+        "dom"
+    ]
+  }
+}
index fd82d95..b40cdb0 100644 (file)
@@ -176,7 +176,9 @@ class behat_forms extends behat_base {
         $selectnode->selectOption($option);
 
         // Adding a click as Selenium requires it to fire some JS events.
-        $selectnode->click();
+        if ($this->running_javascript()) {
+            $selectnode->click();
+        }
     }
 
     /**
@@ -192,7 +194,9 @@ class behat_forms extends behat_base {
         $radionode->check();
 
         // Adding a click as Selenium requires it to fire some JS events.
-        $radionode->click();
+        if ($this->running_javascript()) {
+            $radionode->click();
+        }
     }
 
     /**
index 67d773e..ea6b623 100644 (file)
@@ -27,7 +27,8 @@
 
 require_once(__DIR__ . '/../../behat/behat_base.php');
 
-use Behat\Mink\Exception\ExpectationException as ExpectationException;
+use Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
  * Cross component steps definitions.
@@ -53,6 +54,15 @@ class behat_general extends behat_base {
         $this->getSession()->visit($this->locate_path('/'));
     }
 
+    /**
+     * Reloads the current page.
+     *
+     * @Given /^I reload the page$/
+     */
+    public function reload() {
+        $this->getSession()->reload();
+    }
+
     /**
      * Clicks link with specified id|title|alt|text.
      *
@@ -180,6 +190,60 @@ class behat_general extends behat_base {
         $this->assertSession()->elementTextNotContains($selector, $locator, $text);
     }
 
+    /**
+     * Checks, that the first specified element appears before the second one.
+     *
+     * @Given /^"(?P<preceding_element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" should appear before "(?P<following_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException
+     * @param string $preelement The locator of the preceding element
+     * @param string $preselectortype The locator of the preceding element
+     * @param string $postelement The locator of the latest element
+     * @param string $postselectortype The selector type of the latest element
+     */
+    public function should_appear_before($preelement, $preselectortype, $postelement, $postselectortype) {
+
+        // We allow postselectortype as a non-text based selector.
+        list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
+        list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
+
+        $prexpath = $this->find($preselector, $prelocator)->getXpath();
+        $postxpath = $this->find($postselector, $postlocator)->getXpath();
+
+        // Using following xpath axe to find it.
+        $msg = '"'.$preelement.'" "'.$preselectortype.'" does not appear before "'.$postelement.'" "'.$postselectortype.'"';
+        $xpath = $prexpath.'/following::*[contains(., '.$postxpath.')]';
+        if (!$this->getSession()->getDriver()->find($xpath)) {
+            throw new ExpectationException($msg, $this->getSession());
+        }
+    }
+
+    /**
+     * Checks, that the first specified element appears after the second one.
+     *
+     * @Given /^"(?P<following_element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" should appear after "(?P<preceding_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
+     * @throws ExpectationException
+     * @param string $postelement The locator of the latest element
+     * @param string $postselectortype The selector type of the latest element
+     * @param string $preelement The locator of the preceding element
+     * @param string $preselectortype The locator of the preceding element
+     */
+    public function should_appear_after($postelement, $postselectortype, $preelement, $preselectortype) {
+
+        // We allow postselectortype as a non-text based selector.
+        list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
+        list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
+
+        $postxpath = $this->find($postselector, $postlocator)->getXpath();
+        $prexpath = $this->find($preselector, $prelocator)->getXpath();
+
+        // Using preceding xpath axe to find it.
+        $msg = '"'.$postelement.'" "'.$postselectortype.'" does not appear after "'.$preelement.'" "'.$preselectortype.'"';
+        $xpath = $postxpath.'/preceding::*[contains(., '.$prexpath.')]';
+        if (!$this->getSession()->getDriver()->find($xpath)) {
+            throw new ExpectationException($msg, $this->getSession());
+        }
+    }
+
     /**
      * Checks, that element of specified type is disabled.
      *
@@ -216,4 +280,44 @@ class behat_general extends behat_base {
         }
     }
 
+    /**
+     * Checks the provided element and selector type exists in the current page.
+     *
+     * This step is for advanced users, use it if you don't find anything else suitable for what you need.
+     *
+     * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exists$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
+     * @param string $element The locator of the specified selector
+     * @param string $selectortype The selector type
+     */
+    public function should_exists($element, $selectortype) {
+
+        // Getting Mink selector and locator.
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+
+        // Will throw an ElementNotFoundException if it does not exist.
+        $this->find($selector, $locator);
+    }
+
+    /**
+     * Checks that the provided element and selector type not exists in the current page.
+     *
+     * This step is for advanced users, use it if you don't find anything else suitable for what you need.
+     *
+     * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exists$/
+     * @throws ExpectationException
+     * @param string $element The locator of the specified selector
+     * @param string $selectortype The selector type
+     */
+    public function should_not_exists($element, $selectortype) {
+
+        try {
+            $this->should_exists($element, $selectortype);
+            throw new ExpectationException('The "' . $element . '" "' . $selectortype . '" exists in the current page', $this->getSession());
+        } catch (ElementNotFoundException $e) {
+            // It passes.
+            return;
+        }
+    }
+
 }
index c071edc..baa5ecd 100644 (file)
@@ -405,17 +405,25 @@ class coursecatlib_testcase extends advanced_testcase {
         // search courses
 
         // search by text
+        $res = coursecat::search_courses(array('search' => 'Test'));
+        $this->assertEquals(array($c4->id, $c3->id, $c1->id, $c8->id, $c5->id), array_keys($res));
+        $this->assertEquals(5, coursecat::search_courses_count(array('search' => 'Test')));
+
+        // search by text with specified offset and limit
+        $options = array('sort' => array('fullname' => 1), 'offset' => 1, 'limit' => 2);
+        $res = coursecat::search_courses(array('search' => 'Test'), $options);
+        $this->assertEquals(array($c4->id, $c5->id), array_keys($res));
+        $this->assertEquals(5, coursecat::search_courses_count(array('search' => 'Test'), $options));
+
+        // IMPORTANT: the tests below may fail on some databases
+        // case-insensitive search
         $res = coursecat::search_courses(array('search' => 'test'));
         $this->assertEquals(array($c4->id, $c3->id, $c1->id, $c8->id, $c5->id), array_keys($res));
         $this->assertEquals(5, coursecat::search_courses_count(array('search' => 'test')));
 
+        // non-latin language search
         $res = coursecat::search_courses(array('search' => 'Математика'));
         $this->assertEquals(array($c3->id, $c6->id), array_keys($res));
         $this->assertEquals(2, coursecat::search_courses_count(array('search' => 'Математика'), array()));
-
-        $options = array('sort' => array('fullname' => 1), 'offset' => 1, 'limit' => 2);
-        $res = coursecat::search_courses(array('search' => 'test'), $options);
-        $this->assertEquals(array($c4->id, $c5->id), array_keys($res));
-        $this->assertEquals(5, coursecat::search_courses_count(array('search' => 'test'), $options));
     }
 }
\ No newline at end of file
index 6257131..f228098 100644 (file)
@@ -1,2 +1,3 @@
-username,password,email,firstname,lastname
-teacher1,moodle,teacher1@teacher1.com,Teacher,1
+username,password,firstname,lastname,email,course1,group1
+jonest,verysecret,Tom,Jones,jonest@someplace.edu,math102,Section 1
+reznor,somesecret,Trent,Reznor,reznor@someplace.edu,math102,Section 3
diff --git a/lib/yui/blocks/blocks.js b/lib/yui/blocks/blocks.js
deleted file mode 100644 (file)
index 29616e5..0000000
+++ /dev/null
@@ -1,309 +0,0 @@
-YUI.add('moodle-core-blocks', function(Y) {
-
-    var AJAXURL = '/lib/ajax/blocks.php',
-    CSS = {
-        BLOCK : 'block',
-        BLOCKREGION : 'block-region',
-        BLOCKADMINBLOCK : 'block_adminblock',
-        EDITINGMOVE : 'editing_move',
-        HEADER : 'header',
-        LIGHTBOX : 'lightbox',
-        REGIONCONTENT : 'region-content',
-        SKIPBLOCK : 'skip-block',
-        SKIPBLOCKTO : 'skip-block-to',
-        MYINDEX : 'page-my-index',
-        REGIONMAIN : 'region-main'
-    }
-
-    var DRAGBLOCK = function() {
-        DRAGBLOCK.superclass.constructor.apply(this, arguments);
-    };
-    Y.extend(DRAGBLOCK, M.core.dragdrop, {
-        skipnodetop : null,
-        skipnodebottom : null,
-        dragsourceregion : null,
-        initializer : function(params) {
-            // Set group for parent class
-            this.groups = ['block'];
-            this.samenodeclass = CSS.BLOCK;
-            this.parentnodeclass = CSS.REGIONCONTENT;
-
-            // Add relevant classes and ID to 'content' block region on My Home page.
-            var myhomecontent = Y.Node.all('body#'+CSS.MYINDEX+' #'+CSS.REGIONMAIN+' > .'+CSS.REGIONCONTENT);
-            if (myhomecontent.size() > 0) {
-                var contentregion = myhomecontent.item(0);
-                contentregion.addClass(CSS.BLOCKREGION);
-                contentregion.set('id', CSS.REGIONCONTENT);
-                contentregion.one('div').addClass(CSS.REGIONCONTENT);
-            }
-
-            // Initialise blocks dragging
-            // Find all block regions on the page
-            var blockregionlist = Y.Node.all('div.'+CSS.BLOCKREGION);
-
-            if (blockregionlist.size() === 0) {
-                return false;
-            }
-
-            // See if we are missing either of block regions,
-            // if yes we need to add an empty one to use as target
-            if (blockregionlist.size() != this.get('regions').length) {
-                var blockregion = Y.Node.create('<div></div>')
-                    .addClass(CSS.BLOCKREGION);
-                var regioncontent = Y.Node.create('<div></div>')
-                    .addClass(CSS.REGIONCONTENT);
-                blockregion.appendChild(regioncontent);
-                var pre = blockregionlist.filter('#region-pre');
-                var post = blockregionlist.filter('#region-post');
-
-                if (pre.size() === 0 && post.size() === 1) {
-                    // pre block is missing, instert it before post
-                    blockregion.setAttrs({id : 'region-pre'});
-                    post.item(0).insert(blockregion, 'before');
-                    blockregionlist.unshift(blockregion);
-                } else if (post.size() === 0 && pre.size() === 1) {
-                    // post block is missing, instert it after pre
-                    blockregion.setAttrs({id : 'region-post'});
-                    pre.item(0).insert(blockregion, 'after');
-                    blockregionlist.push(blockregion);
-                }
-            }
-
-            blockregionlist.each(function(blockregionnode) {
-
-                // Setting blockregion as droptarget (the case when it is empty)
-                // The region-post (the right one)
-                // is very narrow, so add extra padding on the left to drop block on it.
-                var tar = new Y.DD.Drop({
-                    node: blockregionnode.one('div.'+CSS.REGIONCONTENT),
-                    groups: this.groups,
-                    padding: '40 240 40 240'
-                });
-
-                // Make each div element in the list of blocks draggable
-                var del = new Y.DD.Delegate({
-                    container: blockregionnode,
-                    nodes: '.'+CSS.BLOCK,
-                    target: true,
-                    handles: ['.'+CSS.HEADER],
-                    invalid: '.block-hider-hide, .block-hider-show, .moveto',
-                    dragConfig: {groups: this.groups}
-                });
-                del.dd.plug(Y.Plugin.DDProxy, {
-                    // Don't move the node at the end of the drag
-                    moveOnEnd: false
-                });
-                del.dd.plug(Y.Plugin.DDWinScroll);
-
-                var blocklist = blockregionnode.all('.'+CSS.BLOCK);
-                blocklist.each(function(blocknode) {
-                    var move = blocknode.one('a.'+CSS.EDITINGMOVE);
-                    if (move) {
-                        move.remove();
-                        blocknode.one('.'+CSS.HEADER).setStyle('cursor', 'move');
-                    }
-                }, this);
-            }, this);
-        },
-
-        get_block_id : function(node) {
-            return Number(node.get('id').replace(/inst/i, ''));
-        },
-
-        get_block_region : function(node) {
-            var region = node.ancestor('div.'+CSS.BLOCKREGION).get('id').replace(/region-/i, '');
-            if (Y.Array.indexOf(this.get('regions'), region) === -1) {
-                // Must be standard side-X
-                if (right_to_left()) {
-                    if (region == 'post') {
-                        region = 'pre';
-                    } else if (region == 'pre') {
-                        region = 'post';
-                    }
-                }
-                return 'side-' + region;
-            }
-            // Perhaps custom region
-            return region;
-        },
-
-        get_region_id : function(node) {
-            return node.get('id').replace(/region-/i, '');
-        },
-
-        drag_start : function(e) {
-            // Get our drag object
-            var drag = e.target;
-
-            // Store the parent node of original drag node (block)
-            // we will need it later for show/hide empty regions
-            this.dragsourceregion = drag.get('node').ancestor('div.'+CSS.BLOCKREGION);
-
-            // Determine skipnodes and store them
-            if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) {
-                this.skipnodetop = drag.get('node').previous();
-            }
-            if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) {
-                this.skipnodebottom = drag.get('node').next();
-            }
-        },
-
-        drop_over : function(e) {
-            // Get a reference to our drag and drop nodes
-            var drag = e.drag.get('node');
-            var drop = e.drop.get('node');
-
-            // We need to fix the case when parent drop over event has determined
-            // 'goingup' and appended the drag node after admin-block.
-            if (drop.hasClass(this.parentnodeclass) && drop.one('.'+CSS.BLOCKADMINBLOCK) && drop.one('.'+CSS.BLOCKADMINBLOCK).next('.'+CSS.BLOCK)) {
-                drop.prepend(drag);
-            }
-
-            // Block is moved within the same region
-            // stop here, no need to modify anything.
-            if (this.dragsourceregion.contains(drop)) {
-                return false;
-            }
-
-            // TODO: Hiding-displaying block region only works for base theme blocks
-            // (region-pre, region-post) at the moment. It should be improved
-            // to work with custom block regions as well.
-
-            // TODO: Fix this for the case when user drag block towards empty section,
-            // then the section appears, then user chnages his mind and moving back to
-            // original section. The opposite section remains opened and empty.
-
-            var documentbody = Y.one('body');
-            // Moving block towards hidden region-content, display it
-            var regionname = this.get_region_id(this.dragsourceregion);
-            if (documentbody.hasClass('side-'+regionname+'-only')) {
-                documentbody.removeClass('side-'+regionname+'-only');
-            }
-
-            // Moving from empty region-content towards the opposite one,
-            // hide empty one (only for region-pre, region-post areas at the moment).
-            regionname = this.get_region_id(drop.ancestor('div.'+CSS.BLOCKREGION));
-            if (this.dragsourceregion.all('.'+CSS.BLOCK).size() == 0 && this.dragsourceregion.get('id').match(/(region-pre|region-post)/i)) {
-                if (!documentbody.hasClass('side-'+regionname+'-only')) {
-                    documentbody.addClass('side-'+regionname+'-only');
-                }
-            }
-        },
-
-        drop_end : function(e) {
-            // clear variables
-            this.skipnodetop = null;
-            this.skipnodebottom = null;
-            this.dragsourceregion = null;
-        },
-
-        drag_dropmiss : function(e) {
-            // Missed the target, but we assume the user intended to drop it
-            // on the last last ghost node location, e.drag and e.drop should be
-            // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
-            this.drop_hit(e);
-        },
-
-        drop_hit : function(e) {
-            var drag = e.drag;
-            // Get a reference to our drag node
-            var dragnode = drag.get('node');
-            var dropnode = e.drop.get('node');
-
-            // Amend existing skipnodes
-            if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) {
-                // the one that belongs to block below move below
-                dragnode.insert(dragnode.previous(), 'after');
-            }
-            // Move original skipnodes
-            if (this.skipnodetop) {
-                dragnode.insert(this.skipnodetop, 'before');
-            }
-            if (this.skipnodebottom) {
-                dragnode.insert(this.skipnodebottom, 'after');
-            }
-
-            // Add lightbox if it not there
-            var lightbox = M.util.add_lightbox(Y, dragnode);
-
-            // Prepare request parameters
-            var params = {
-                sesskey : M.cfg.sesskey,
-                courseid : this.get('courseid'),
-                pagelayout : this.get('pagelayout'),
-                pagetype : this.get('pagetype'),
-                subpage : this.get('subpage'),
-                contextid : this.get('contextid'),
-                action : 'move',
-                bui_moveid : this.get_block_id(dragnode),
-                bui_newregion : this.get_block_region(dropnode)
-            };
-
-            if (this.get('cmid')) {
-                params.cmid = this.get('cmid');
-            }
-
-            if (dragnode.next('.'+this.samenodeclass) && !dragnode.next('.'+this.samenodeclass).hasClass(CSS.BLOCKADMINBLOCK)) {
-                params.bui_beforeid = this.get_block_id(dragnode.next('.'+this.samenodeclass));
-            }
-
-            // Do AJAX request
-            Y.io(M.cfg.wwwroot+AJAXURL, {
-                method: 'POST',
-                data: params,
-                on: {
-                    start : function(tid) {
-                        lightbox.show();
-                    },
-                    success: function(tid, response) {
-                        window.setTimeout(function(e) {
-                            lightbox.hide();
-                        }, 250);
-                        try {
-                            var responsetext = Y.JSON.parse(response.responseText);
-                            if (responsetext.error) {
-                                new M.core.ajaxException(responsetext);
-                            }
-                        } catch (e) {}
-                    },
-                    failure: function(tid, response) {
-                        this.ajax_failure(response);
-                        lightbox.hide();
-                    }
-                },
-                context:this
-            });
-        }
-    }, {
-        NAME : 'core-blocks-dragdrop',
-        ATTRS : {
-            courseid : {
-                value : null
-            },
-            cmid : {
-                value : null
-            },
-            contextid : {
-                value : null
-            },
-            pagelayout : {
-                value : null
-            },
-            pagetype : {
-                value : null
-            },
-            subpage : {
-                value : null
-            },
-            regions : {
-                value : null
-            }
-        }
-    });
-
-    M.core_blocks = M.core_blocks || {};
-    M.core_blocks.init_dragdrop = function(params) {
-        new DRAGBLOCK(params);
-    }
-}, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'dd-scroll', 'moodle-core-dragdrop', 'moodle-core-notification']});
-
diff --git a/lib/yui/build/moodle-core-blocks/moodle-core-blocks-coverage.js b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-coverage.js
new file mode 100644 (file)
index 0000000..f6735bb
Binary files /dev/null and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-coverage.js differ
diff --git a/lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js
new file mode 100644 (file)
index 0000000..8430a9b
Binary files /dev/null and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js differ
diff --git a/lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js
new file mode 100644 (file)
index 0000000..24f8901
Binary files /dev/null and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js differ
diff --git a/lib/yui/build/moodle-core-blocks/moodle-core-blocks.js b/lib/yui/build/moodle-core-blocks/moodle-core-blocks.js
new file mode 100644 (file)
index 0000000..8430a9b
Binary files /dev/null and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks.js differ
diff --git a/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-coverage.js b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-coverage.js
new file mode 100644 (file)
index 0000000..f753b30
Binary files /dev/null and b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-coverage.js differ
diff --git a/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js
new file mode 100644 (file)
index 0000000..97d948c
Binary files /dev/null and b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js differ
diff --git a/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js
new file mode 100644 (file)
index 0000000..97c602e
Binary files /dev/null and b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js differ
diff --git a/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js
new file mode 100644 (file)
index 0000000..97d948c
Binary files /dev/null and b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js differ
diff --git a/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-coverage.js b/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-coverage.js
new file mode 100644 (file)
index 0000000..40e3824
Binary files /dev/null and b/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-coverage.js differ
diff --git a/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-debug.js b/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-debug.js
new file mode 100644 (file)
index 0000000..00f2bcf
Binary files /dev/null and b/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-debug.js differ
diff --git a/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-min.js b/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-min.js
new file mode 100644 (file)
index 0000000..5c7b88a
Binary files /dev/null and b/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit-min.js differ
diff --git a/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit.js b/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit.js
new file mode 100644 (file)
index 0000000..c812697
Binary files /dev/null and b/lib/yui/build/moodle-core-formautosubmit/moodle-core-formautosubmit.js differ
diff --git a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-coverage.js b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-coverage.js
new file mode 100644 (file)
index 0000000..0f675b3
Binary files /dev/null and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-coverage.js differ
diff --git a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js
new file mode 100644 (file)
index 0000000..5dffba2
Binary files /dev/null and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js differ
diff --git a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
new file mode 100644 (file)
index 0000000..c928e91
Binary files /dev/null and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js differ
diff --git a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js
new file mode 100644 (file)
index 0000000..5dffba2
Binary files /dev/null and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js differ
diff --git a/lib/yui/build/moodle-core-notification/moodle-core-notification-coverage.js b/lib/yui/build/moodle-core-notification/moodle-core-notification-coverage.js
new file mode 100644 (file)
index 0000000..862ff70
Binary files /dev/null and b/lib/yui/build/moodle-core-notification/moodle-core-notification-coverage.js differ
diff --git a/lib/yui/build/moodle-core-notification/moodle-core-notification-debug.js b/lib/yui/build/moodle-core-notification/moodle-core-notification-debug.js
new file mode 100644 (file)
index 0000000..d12a077
Binary files /dev/null and b/lib/yui/build/moodle-core-notification/moodle-core-notification-debug.js differ
diff --git a/lib/yui/build/moodle-core-notification/moodle-core-notification-min.js b/lib/yui/build/moodle-core-notification/moodle-core-notification-min.js
new file mode 100644 (file)
index 0000000..738909e
Binary files /dev/null and b/lib/yui/build/moodle-core-notification/moodle-core-notification-min.js differ
diff --git a/lib/yui/build/moodle-core-notification/moodle-core-notification.js b/lib/yui/build/moodle-core-notification/moodle-core-notification.js
new file mode 100644 (file)
index 0000000..d12a077
Binary files /dev/null and b/lib/yui/build/moodle-core-notification/moodle-core-notification.js differ
diff --git a/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-coverage.js b/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-coverage.js
new file mode 100644 (file)
index 0000000..796e492
Binary files /dev/null and b/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-coverage.js differ
diff --git a/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-debug.js b/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-debug.js
new file mode 100644 (file)
index 0000000..acefacb
Binary files /dev/null and b/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-debug.js differ
diff --git a/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-min.js b/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-min.js
new file mode 100644 (file)
index 0000000..0619adf
Binary files /dev/null and b/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp-min.js differ
diff --git a/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp.js b/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp.js
new file mode 100644 (file)
index 0000000..acefacb
Binary files /dev/null and b/lib/yui/build/moodle-core-popuphelp/moodle-core-popuphelp.js differ
diff --git a/lib/yui/chooserdialogue/chooserdialogue.js b/lib/yui/chooserdialogue/chooserdialogue.js
deleted file mode 100644 (file)
index 5de6281..0000000
+++ /dev/null
@@ -1,348 +0,0 @@
-YUI.add('moodle-core-chooserdialogue', function(Y) {
-
-    var CHOOSERDIALOGUE = function() {
-        CHOOSERDIALOGUE.superclass.constructor.apply(this, arguments);
-    };
-
-    Y.extend(CHOOSERDIALOGUE, Y.Base, {
-        // The panel widget
-        panel: null,
-
-        // The submit button - we disable this until an element is set
-        submitbutton : null,
-
-        // The chooserdialogue container
-        container : null,
-
-        // Any event listeners we may need to cancel later
-        listenevents : [],
-
-        // The initial overflow setting
-        initialoverflow : '',
-
-        bodycontent : null,
-        headercontent : null,
-        instanceconfig : null,
-
-        setup_chooser_dialogue : function(bodycontent, headercontent, config) {
-            this.bodycontent = bodycontent;
-            this.headercontent = headercontent;
-            this.instanceconfig = config;
-        },
-
-        prepare_chooser : function () {
-            if (this.panel) {
-                return;
-            }
-
-            // Set Default options
-            var paramkey,
-                params = {
-                bodyContent : this.bodycontent.get('innerHTML'),
-                headerContent : this.headercontent.get('innerHTML'),
-                width : '540px',
-                draggable : true,
-                visible : false, // Hide by default
-                zindex : 100, // Display in front of other items
-                lightbox : true, // This dialogue should be modal
-                shim : true,
-                closeButtonTitle : this.get('closeButtonTitle')
-            };
-
-            // Override with additional options
-            for (paramkey in this.instanceconfig) {
-              params[paramkey] = this.instanceconfig[paramkey];
-            }
-
-            // Create the panel
-            this.panel = new M.core.dialogue(params);
-
-            // Remove the template for the chooser
-            this.bodycontent.remove();
-            this.headercontent.remove();
-
-            // Hide and then render the panel
-            this.panel.hide();
-            this.panel.render();
-
-            // Set useful links
-            this.container = this.panel.get('boundingBox').one('.choosercontainer');
-            this.options = this.container.all('.option input[type=radio]');
-
-            // Add the chooserdialogue class to the container for styling
-            this.panel.get('boundingBox').addClass('chooserdialogue');
-        },
-
-        /**
-         * Display the module chooser
-         *
-         * @param e Event Triggering Event
-         * @return void
-         */
-        display_chooser : function (e) {
-            var bb, dialogue, thisevent;
-            this.prepare_chooser();
-
-            // Stop the default event actions before we proceed
-            e.preventDefault();
-
-            bb = this.panel.get('boundingBox');
-            dialogue = this.container.one('.alloptions');
-
-            // Get the overflow setting when the chooser was opened - we
-            // may need this later
-            if (Y.UA.ie > 0) {
-                this.initialoverflow = Y.one('html').getStyle('overflow');
-            } else {
-                this.initialoverflow = Y.one('body').getStyle('overflow');
-            }
-
-            // This will detect a change in orientation and retrigger centering
-            thisevent = Y.one('document').on('orientationchange', function() {
-                this.center_dialogue(dialogue);
-            }, this);
-            this.listenevents.push(thisevent);
-
-            // Detect window resizes (most browsers)
-            thisevent = Y.one('window').on('resize', function() {
-                this.center_dialogue(dialogue);
-            }, this);
-            this.listenevents.push(thisevent);
-
-            // These will trigger a check_options call to display the correct help
-            thisevent = this.container.on('click', this.check_options, this);
-            this.listenevents.push(thisevent);
-            thisevent = this.container.on('key_up', this.check_options, this);
-            this.listenevents.push(thisevent);
-            thisevent = this.container.on('dblclick', function(e) {
-                if (e.target.ancestor('div.option')) {
-                    this.check_options();
-
-                    // Prevent duplicate submissions
-                    this.submitbutton.setAttribute('disabled', 'disabled');
-                    this.options.setAttribute('disabled', 'disabled');
-                    this.cancel_listenevents();
-
-                    this.container.one('form').submit();
-                }
-            }, this);
-            this.listenevents.push(thisevent);
-
-            this.container.one('form').on('submit', function() {
-                // Prevent duplicate submissions on submit
-                this.submitbutton.setAttribute('disabled', 'disabled');
-                this.options.setAttribute('disabled', 'disabled');
-                this.cancel_listenevents();
-            }, this);
-
-            // Hook onto the cancel button to hide the form
-            thisevent = this.container.one('.addcancel').on('click', this.cancel_popup, this);
-            this.listenevents.push(thisevent);
-
-            // Hide will be managed by cancel_popup after restoring the body overflow
-            thisevent = bb.one('button.closebutton').on('click', this.cancel_popup, this);
-            this.listenevents.push(thisevent);
-
-            // Grab global keyup events and handle them
-            thisevent = Y.one('document').on('keydown', this.handle_key_press, this);
-            this.listenevents.push(thisevent);
-
-            // Add references to various elements we adjust
-            this.jumplink     = this.container.one('.jump');
-            this.submitbutton = this.container.one('.submitbutton');
-
-            // Disable the submit element until the user makes a selection
-            this.submitbutton.set('disabled', 'true');
-
-            // Ensure that the options are shown
-            this.options.removeAttribute('disabled');
-
-            // Display the panel
-            this.panel.show();
-
-            // Re-centre the dialogue after we've shown it.
-            this.center_dialogue(dialogue);
-
-            // Finally, focus the first radio element - this enables form selection via the keyboard
-            this.container.one('.option input[type=radio]').focus();
-
-            // Trigger check_options to set the initial jumpurl
-            this.check_options();
-        },
-
-        /**
-         * Cancel any listen events in the listenevents queue
-         *
-         * Several locations add event handlers which should only be called before the form is submitted. This provides
-         * a way of cancelling those events.
-         *
-         * @return void
-         */
-        cancel_listenevents : function () {
-            // Detach all listen events to prevent duplicate triggers
-            var thisevent;
-            while (this.listenevents.length) {
-                thisevent = this.listenevents.shift();
-                thisevent.detach();
-            }
-        },
-
-        /**
-         * Calculate the optimum height of the chooser dialogue
-         *
-         * This tries to set a sensible maximum and minimum to ensure that some options are always shown, and preferably
-         * all, whilst fitting the box within the current viewport.
-         *
-         * @param dialogue Y.Node The dialogue
-         * @return void
-         */
-        center_dialogue : function(dialogue) {
-            var bb = this.panel.get('boundingBox'),
-                winheight = bb.get('winHeight'),
-                winwidth = bb.get('winWidth'),
-                offsettop = 0,
-                newheight, totalheight, dialoguetop, dialoguewidth, dialogueleft;
-
-            // Try and set a sensible max-height -- this must be done before setting the top
-            // Set a default height of 640px
-            newheight = this.get('maxheight');
-            if (winheight <= newheight) {
-                // Deal with smaller window sizes
-                if (winheight <= this.get('minheight')) {
-                    newheight = this.get('minheight');
-                } else {
-                    newheight = winheight;
-                }
-            }
-
-            // Set a fixed position if the window is large enough
-            if (newheight > this.get('minheight')) {
-                bb.setStyle('position', 'fixed');
-                // Disable the page scrollbars
-                if (Y.UA.ie > 0) {
-                    Y.one('html').setStyle('overflow', 'hidden');
-                } else {
-                    Y.one('body').setStyle('overflow', 'hidden');
-                }
-            } else {
-                bb.setStyle('position', 'absolute');
-                offsettop = Y.one('window').get('scrollTop');
-                // Ensure that the page scrollbars are enabled
-                if (Y.UA.ie > 0) {
-                    Y.one('html').setStyle('overflow', this.initialoverflow);
-                } else {
-                    Y.one('body').setStyle('overflow', this.initialoverflow);
-                }
-            }
-
-            // Take off 15px top and bottom for borders, plus 40px each for the title and button area before setting the
-            // new max-height
-            totalheight = newheight;
-            newheight = newheight - (15 + 15 + 40 + 40);
-            dialogue.setStyle('maxHeight', newheight + 'px');
-
-            dialogueheight = bb.getStyle('height');
-            if (dialogueheight.match(/.*px$/)) {
-                dialogueheight = dialogueheight.replace(/px$/, '');
-            } else {
-                dialogueheight = totalheight;
-            }
-
-            if (dialogueheight < this.get('baseheight')) {
-                dialogueheight = this.get('baseheight');
-                dialogue.setStyle('height', dialogueheight + 'px');
-            }
-
-
-            // Re-calculate the location now that we've changed the size
-            dialoguetop = Math.max(12, ((winheight - dialogueheight) / 2)) + offsettop;
-
-            // We need to set the height for the yui3-widget - can't work
-            // out what we're setting at present -- shoud be the boudingBox
-            bb.setStyle('top', dialoguetop + 'px');
-
-            // Calculate the left location of the chooser
-            // We don't set a minimum width in the same way as we do height as the width would be far lower than the
-            // optimal width for moodle anyway.
-            dialoguewidth = bb.get('offsetWidth');
-            dialogueleft = (winwidth - dialoguewidth) / 2;
-            bb.setStyle('left', dialogueleft + 'px');
-        },
-
-        handle_key_press : function(e) {
-            if (e.keyCode === 27) {
-                this.cancel_popup(e);
-            }
-        },
-
-        cancel_popup : function (e) {
-            // Prevent normal form submission before hiding
-            e.preventDefault();
-            this.hide();
-        },
-
-        hide : function() {
-            // Cancel all listen events
-            this.cancel_listenevents();
-
-            // Re-enable the page scrollbars
-            if (Y.UA.ie > 0) {
-                Y.one('html').setStyle('overflow', this.initialoverflow);
-            } else {
-                Y.one('body').setStyle('overflow', this.initialoverflow);
-            }
-
-            this.container.detachAll();
-            this.panel.hide();
-        },
-
-        check_options : function() {
-            // Check which options are set, and change the parent class
-            // to show/hide help as required
-            this.options.each(function(thisoption) {
-                var optiondiv = thisoption.get('parentNode').get('parentNode');
-                if (thisoption.get('checked')) {
-                    optiondiv.addClass('selected');
-
-                    // Trigger any events for this option
-                    this.option_selected(thisoption);
-
-                    // Ensure that the form may be submitted
-                    this.submitbutton.removeAttribute('disabled');
-
-                    // Ensure that the radio remains focus so that keyboard navigation is still possible
-                    thisoption.focus();
-                } else {
-                    optiondiv.removeClass('selected');
-                }
-            }, this);
-        },
-
-        option_selected : function() {
-        }
-    },
-    {
-        NAME : 'moodle-core-chooserdialogue',
-        ATTRS : {
-            minheight : {
-                value : 300
-            },
-            baseheight: {
-                value : 400
-            },
-            maxheight : {
-                value : 660
-            },
-            closeButtonTitle : {
-                validator : Y.Lang.isString,
-                value : 'Close'
-            }
-        }
-    });
-    M.core = M.core || {};
-    M.core.chooserdialogue = CHOOSERDIALOGUE;
-},
-'@VERSION@', {
-    requires:['base', 'panel', 'moodle-core-notification']
-}
-);
diff --git a/lib/yui/formautosubmit/formautosubmit.js b/lib/yui/formautosubmit/formautosubmit.js
deleted file mode 100644 (file)
index 3c349f4..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-YUI.add('moodle-core-formautosubmit',
-    function(Y) {
-        var CSS,
-            FORMAUTOSUBMITNAME = 'core-formautosubmit',
-            FORMAUTOSUBMIT,
-            INITIALIZED = false;
-
-        // The CSS selectors we use
-        CSS = {
-            AUTOSUBMIT : 'autosubmit'
-        };
-
-        FORMAUTOSUBMIT = function() {
-            FORMAUTOSUBMIT.superclass.constructor.apply(this, arguments);
-        };
-
-        Y.extend(FORMAUTOSUBMIT, Y.Base, {
-
-            /**
-              * Initialize the module
-              */
-            initializer : function() {
-                // Set up local variables
-                var applyto,
-                    thisselect;
-                // We only apply the delegation once
-                if (!INITIALIZED) {
-                    INITIALIZED = true;
-                    applyto = Y.one('body');
-
-                    // We don't listen for change events by default as using the keyboard triggers these too.
-                    applyto.delegate('key', this.process_changes, 'press:13', 'select.' + CSS.AUTOSUBMIT, this);
-                    applyto.delegate('click', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
-
-                    if (Y.UA.os === 'macintosh' && Y.UA.webkit) {
-                        // Macintosh webkit browsers like change events, but non-macintosh webkit browsers don't.
-                        applyto.delegate('change', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
-                    }
-                    if (Y.UA.ios) {
-                        // IOS doesn't trigger click events because it's touch-based.
-                        applyto.delegate('change', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
-                    }
-                }
-
-                // Assign this select items 'nothing' value and lastindex (current value)
-                if (this.get('selectid')) {
-                    thisselect = Y.one('select#' + this.get('selectid'));
-                    if (thisselect) {
-                        if (this.get('nothing')) {
-                            thisselect.setData('nothing', this.get('nothing'));
-                        }
-                        thisselect.setData('startindex', thisselect.get('selectedIndex'));
-                    } else {
-                        Y.log("Warning: A single_select element was renderered, but the output is not displayed on the page.");
-                    }
-                }
-            },
-
-            /**
-             * Check whether the select element was changed
-             */
-            check_changed : function(e) {
-                var select,
-                    nothing,
-                    startindex,
-                    currentindex,
-                    previousindex;
-                select = e.target.ancestor('select.' + CSS.AUTOSUBMIT, true);
-                if (!select) {
-                    return false;
-                }
-
-                nothing = select.getData('nothing');
-                startindex = select.getData('startindex');
-                currentindex = select.get('selectedIndex');
-
-                previousindex = select.getAttribute('data-previousindex');
-                select.setAttribute('data-previousindex', currentindex);
-                if (!previousindex) {
-                    previousindex = startindex;
-                }
-
-                // Check whether the field has changed, and is not the 'nothing' value
-                if ((nothing===false || select.get('value') !== nothing)
-                        && startindex !== select.get('selectedIndex') && currentindex !== previousindex) {
-                    return select;
-                }
-                return false;
-            },
-
-            /**
-             * Process any changes
-             */
-            process_changes : function(e) {
-                var select = this.check_changed(e),
-                    form;
-                if (select) {
-                    form = select.ancestor('form', true);
-                    form.submit();
-                }
-            }
-        },
-        {
-            NAME : FORMAUTOSUBMITNAME,
-            ATTRS : {
-                selectid : {
-                    'value' : ''
-                },
-                nothing : {
-                    'value' : ''
-                },
-                ignorechangeevent : {
-                    'value' : false
-                }
-            }
-        });
-
-        M.core = M.core || {};
-        M.core.init_formautosubmit = M.core.init_formautosubmit || function(config) {
-            return new FORMAUTOSUBMIT(config);
-        };
-    },
-    '@VERSION@', {
-        requires : ['base', 'event-key']
-    }
-);
diff --git a/lib/yui/formchangechecker/formchangechecker.js b/lib/yui/formchangechecker/formchangechecker.js
deleted file mode 100644 (file)
index 0351585..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-YUI.add('moodle-core-formchangechecker',
-    function(Y) {
-        var FORMCHANGECHECKERNAME = 'core-formchangechecker',
-
-            FORMCHANGECHECKER = function() {
-                FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
-            };
-
-        Y.extend(FORMCHANGECHECKER, Y.Base, {
-
-                // The delegated listeners we need to detach after the initial value has been stored once
-                initialvaluelisteners : [],
-
-                /**
-                 * Initialize the module
-                 */
-                initializer : function() {
-                    var formid = 'form#' + this.get('formid'),
-                        currentform = Y.one(formid);
-
-                    if (!currentform) {
-                        // If the form was not found, then we can't check for changes.
-                        return;
-                    }
-
-                    // Add change events to the form elements
-                    currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'input', this);
-                    currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'textarea', this);
-                    currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'select', this);
-
-                    // Add a focus event to check for changes which are made without triggering a change event
-                    this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'input', this));
-                    this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'textarea', this));
-                    this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'select', this));
-
-                    // We need any submit buttons on the form to set the submitted flag
-                    Y.one(formid).on('submit', M.core_formchangechecker.set_form_submitted, this);
-
-                    // YUI doesn't support onbeforeunload properly so we must use the DOM to set the onbeforeunload. As
-                    // a result, the has_changed must stay in the DOM too
-                    window.onbeforeunload = M.core_formchangechecker.report_form_dirty_state;
-                },
-
-                /**
-                 * Store the initial value of the currently focussed element
-                 *
-                 * If an element has been focussed and changed but not yet blurred, the on change
-                 * event won't be fired. We need to store it's initial value to compare it in the
-                 * get_form_dirty_state function later.
-                 */
-                store_initial_value : function(e) {
-                    var thisevent;
-                    if (e.target.hasClass('ignoredirty')) {
-                        // Don't warn on elements with the ignoredirty class
-                        return;
-                    }
-                    if (M.core_formchangechecker.get_form_dirty_state()) {
-                        // Detach all listen events to prevent duplicate initial value setting
-                        while (this.initialvaluelisteners.length) {
-                            thisevent = this.initialvaluelisteners.shift();
-                            thisevent.detach();
-                        }
-
-                        return;
-                    }
-
-                    // Make a note of the current element so that it can be interrogated and
-                    // compared in the get_form_dirty_state function
-                    M.core_formchangechecker.stateinformation.focused_element = {
-                        element : e.target,
-                        initial_value : e.target.get('value')
-                    };
-                }
-            },
-            {
-                NAME : FORMCHANGECHECKERNAME,
-                ATTRS : {
-                    formid : {
-                        'value' : ''
-                    }
-                }
-            }
-        );
-
-        M.core_formchangechecker = M.core_formchangechecker || {};
-
-        // We might have multiple instances of the form change protector
-        M.core_formchangechecker.instances = M.core_formchangechecker.instances || [];
-        M.core_formchangechecker.init = function(config) {
-            var formchangechecker = new FORMCHANGECHECKER(config);
-            M.core_formchangechecker.instances.push(formchangechecker);
-            return formchangechecker;
-        };
-
-        // Store state information
-        M.core_formchangechecker.stateinformation = [];
-
-        /**
-         * Set the form changed state to true
-         */
-        M.core_formchangechecker.set_form_changed = function(e) {
-            if (e && e.target && e.target.hasClass('ignoredirty')) {
-                // Don't warn on elements with the ignoredirty class
-                return;
-            }
-            M.core_formchangechecker.stateinformation.formchanged = 1;
-
-            // Once the form has been marked as dirty, we no longer need to keep track of form elements
-            // which haven't yet blurred
-            delete M.core_formchangechecker.stateinformation.focused_element;
-        };
-
-        /**
-         * Set the form submitted state to true
-         */
-        M.core_formchangechecker.set_form_submitted = function() {
-            M.core_formchangechecker.stateinformation.formsubmitted = 1;
-        };
-
-        /**
-         * Attempt to determine whether the form has been modified in any way and
-         * is thus 'dirty'
-         *
-         * @return Integer 1 is the form is dirty; 0 if not
-         */
-        M.core_formchangechecker.get_form_dirty_state = function() {
-            var state = M.core_formchangechecker.stateinformation,
-                editor;
-
-            // If the form was submitted, then return a non-dirty state
-            if (state.formsubmitted) {
-                return 0;
-            }
-
-            // If any fields have been marked dirty, return a dirty state
-            if (state.formchanged) {
-                return 1;
-            }
-
-            // If a field has been focused and changed, but still has focus then the browser won't fire the
-            // onChange event. We check for this eventuality here
-            if (state.focused_element) {
-                if (state.focused_element.element.get('value') !== state.focused_element.initial_value) {
-                    return 1;
-                }
-            }
-
-            // Handle TinyMCE editor instances
-            // We can't add a listener in the initializer as the editors may not have been created by that point
-            // so we do so here instead
-            if (typeof tinyMCE !== 'undefined') {
-                for (editor in tinyMCE.editors) {
-                    if (tinyMCE.editors[editor].isDirty()) {
-                        return 1;
-                    }
-                }
-            }
-
-            // If we reached here, then the form hasn't met any of the dirty conditions
-            return 0;
-        };
-
-        /**
-         * Return a suitable message if changes have been made to a form
-         */
-        M.core_formchangechecker.report_form_dirty_state = function(e) {
-            if (!M.core_formchangechecker.get_form_dirty_state()) {
-                // the form is not dirty, so don't display any message
-                return;
-            }
-
-            // This is the error message that we'll show to browsers which support it
-            var warningmessage = M.util.get_string('changesmadereallygoaway', 'moodle');
-
-            // Most browsers are happy with the returnValue being set on the event
-            // But some browsers do not consistently pass the event
-            if (e) {
-                e.returnValue = warningmessage;
-            }
-
-            // But some require it to be returned instead
-            return warningmessage;
-        };
-    },
-    '@VERSION@', {
-        requires : ['base', 'event-focus']
-    }
-);
diff --git a/lib/yui/popuphelp/popuphelp.js b/lib/yui/popuphelp/popuphelp.js
deleted file mode 100644 (file)
index 6ef2710..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-YUI.add('moodle-core-popuphelp', function(Y) {
-    function POPUPHELP() {
-        POPUPHELP.superclass.constructor.apply(this, arguments);
-    }
-
-    var SELECTORS = {
-            CLICKABLELINKS: 'span.helptooltip > a',
-            FOOTER: 'div.moodle-dialogue-ft'
-        },
-
-        CSS = {
-            ICON: 'icon',
-            ICONPRE: 'icon-pre'
-        },
-        ATTRS = {};
-
-    // Set the modules base properties.
-    POPUPHELP.NAME = 'moodle-core-popuphelp';
-    POPUPHELP.ATTRS = ATTRS;
-
-    Y.extend(POPUPHELP, Y.Base, {
-        panel: null,
-
-        initializer: function() {
-            Y.one('body').delegate('click', this.display_panel, SELECTORS.CLICKABLELINKS, this);
-        },
-
-        display_panel: function(e) {
-            if (!this.panel) {
-                this.panel = new M.core.tooltip({
-                    bodyhandler: this.set_body_content,
-                    footerhandler: this.set_footer,
-                    initialheadertext: M.util.get_string('loadinghelp', 'moodle'),
-                    initialfootertext: ''
-                });
-            }
-
-            // Call the tooltip setup.
-            this.panel.display_panel(e);
-        },
-
-        /**
-         * Override the footer handler to add a 'More help' link where relevant.
-         *
-         * @param {Object} helpobject The object returned from the AJAX call.
-         */
-        set_footer: function(helpobject) {
-            // Check for an optional link to documentation on moodle.org.
-            if (helpobject.doclink) {
-                // Wrap a help icon and the morehelp text in an anchor. The class of the anchor should
-                // determine whether it's opened in a new window or not.
-                doclink = Y.Node.create('<a />')
-                    .setAttrs({
-                        'href': helpobject.doclink.link
-                    })
-                    .addClass(helpobject.doclink['class']);
-                helpicon = Y.Node.create('<img />')
-                    .setAttrs({
-                        'src': M.util.image_url('docs', 'core')
-                    })
-                    .addClass(CSS.ICON)
-                    .addClass(CSS.ICONPRE);
-                doclink.appendChild(helpicon);
-                doclink.appendChild(helpobject.doclink.linktext);
-
-                // Set the footerContent to the contents of the doclink.
-                this.set('footerContent', doclink);
-                this.bb.one(SELECTORS.FOOTER).show();
-            } else {
-                this.bb.one(SELECTORS.FOOTER).hide();
-            }
-        }
-    });
-    M.core = M.core || {};
-    M.core.popuphelp = M.core.popuphelp || null;
-    M.core.init_popuphelp = M.core.init_popuphelp || function(config) {
-        // Only set up a single instance of the popuphelp.
-        if (!M.core.popuphelp) {
-            M.core.popuphelp = new POPUPHELP(config);
-        }
-        return M.core.popuphelp;
-    };
-},
-'@VERSION@', {
-    requires: ['moodle-core-tooltip']
-});
diff --git a/lib/yui/src/blocks/build.json b/lib/yui/src/blocks/build.json
new file mode 100644 (file)
index 0000000..41689c9
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-core-blocks",
+  "builds": {
+    "moodle-core-blocks": {
+      "jsfiles": [
+        "blocks.js"
+      ]
+    }
+  }
+}
diff --git a/lib/yui/src/blocks/js/blocks.js b/lib/yui/src/blocks/js/blocks.js
new file mode 100644 (file)
index 0000000..ff2809d
--- /dev/null
@@ -0,0 +1,305 @@
+var AJAXURL = '/lib/ajax/blocks.php',
+CSS = {
+    BLOCK : 'block',
+    BLOCKREGION : 'block-region',
+    BLOCKADMINBLOCK : 'block_adminblock',
+    EDITINGMOVE : 'editing_move',
+    HEADER : 'header',
+    LIGHTBOX : 'lightbox',
+    REGIONCONTENT : 'region-content',
+    SKIPBLOCK : 'skip-block',
+    SKIPBLOCKTO : 'skip-block-to',
+    MYINDEX : 'page-my-index',
+    REGIONMAIN : 'region-main'
+};
+
+var DRAGBLOCK = function() {
+    DRAGBLOCK.superclass.constructor.apply(this, arguments);
+};
+Y.extend(DRAGBLOCK, M.core.dragdrop, {
+    skipnodetop : null,
+    skipnodebottom : null,
+    dragsourceregion : null,
+    initializer : function() {
+        // Set group for parent class
+        this.groups = ['block'];
+        this.samenodeclass = CSS.BLOCK;
+        this.parentnodeclass = CSS.REGIONCONTENT;
+
+        // Add relevant classes and ID to 'content' block region on My Home page.
+        var myhomecontent = Y.Node.all('body#'+CSS.MYINDEX+' #'+CSS.REGIONMAIN+' > .'+CSS.REGIONCONTENT);
+        if (myhomecontent.size() > 0) {
+            var contentregion = myhomecontent.item(0);
+            contentregion.addClass(CSS.BLOCKREGION);
+            contentregion.set('id', CSS.REGIONCONTENT);
+            contentregion.one('div').addClass(CSS.REGIONCONTENT);
+        }
+
+        // Initialise blocks dragging
+        // Find all block regions on the page
+        var blockregionlist = Y.Node.all('div.'+CSS.BLOCKREGION);
+
+        if (blockregionlist.size() === 0) {
+            return false;
+        }
+
+        // See if we are missing either of block regions,
+        // if yes we need to add an empty one to use as target
+        if (blockregionlist.size() != this.get('regions').length) {
+            var blockregion = Y.Node.create('<div></div>')
+                .addClass(CSS.BLOCKREGION);
+            var regioncontent = Y.Node.create('<div></div>')
+                .addClass(CSS.REGIONCONTENT);
+            blockregion.appendChild(regioncontent);
+            var pre = blockregionlist.filter('#region-pre');
+            var post = blockregionlist.filter('#region-post');
+
+            if (pre.size() === 0 && post.size() === 1) {
+                // pre block is missing, instert it before post
+                blockregion.setAttrs({id : 'region-pre'});
+                post.item(0).insert(blockregion, 'before');
+                blockregionlist.unshift(blockregion);
+            } else if (post.size() === 0 && pre.size() === 1) {
+                // post block is missing, instert it after pre
+                blockregion.setAttrs({id : 'region-post'});
+                pre.item(0).insert(blockregion, 'after');
+                blockregionlist.push(blockregion);
+            }
+        }
+
+        blockregionlist.each(function(blockregionnode) {
+
+            // Setting blockregion as droptarget (the case when it is empty)
+            // The region-post (the right one)
+            // is very narrow, so add extra padding on the left to drop block on it.
+            var tar = new Y.DD.Drop({
+                node: blockregionnode.one('div.'+CSS.REGIONCONTENT),
+                groups: this.groups,
+                padding: '40 240 40 240'
+            });
+
+            // Make each div element in the list of blocks draggable
+            var del = new Y.DD.Delegate({
+                container: blockregionnode,
+                nodes: '.'+CSS.BLOCK,
+                target: true,
+                handles: ['.'+CSS.HEADER],
+                invalid: '.block-hider-hide, .block-hider-show, .moveto',
+                dragConfig: {groups: this.groups}
+            });
+            del.dd.plug(Y.Plugin.DDProxy, {
+                // Don't move the node at the end of the drag
+                moveOnEnd: false
+            });
+            del.dd.plug(Y.Plugin.DDWinScroll);
+
+            var blocklist = blockregionnode.all('.'+CSS.BLOCK);
+            blocklist.each(function(blocknode) {
+                var move = blocknode.one('a.'+CSS.EDITINGMOVE);
+                if (move) {
+                    move.remove();
+                    blocknode.one('.'+CSS.HEADER).setStyle('cursor', 'move');
+                }
+            }, this);
+        }, this);
+    },
+
+    get_block_id : function(node) {
+        return Number(node.get('id').replace(/inst/i, ''));
+    },
+
+    get_block_region : function(node) {
+        var region = node.ancestor('div.'+CSS.BLOCKREGION).get('id').replace(/region-/i, '');
+        if (Y.Array.indexOf(this.get('regions'), region) === -1) {
+            // Must be standard side-X
+            if (right_to_left()) {
+                if (region === 'post') {
+                    region = 'pre';
+                } else if (region === 'pre') {
+                    region = 'post';
+                }
+            }
+            return 'side-' + region;
+        }
+        // Perhaps custom region
+        return region;
+    },
+
+    get_region_id : function(node) {
+        return node.get('id').replace(/region-/i, '');
+    },
+
+    drag_start : function(e) {
+        // Get our drag object
+        var drag = e.target;
+
+        // Store the parent node of original drag node (block)
+        // we will need it later for show/hide empty regions
+        this.dragsourceregion = drag.get('node').ancestor('div.'+CSS.BLOCKREGION);
+
+        // Determine skipnodes and store them
+        if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) {
+            this.skipnodetop = drag.get('node').previous();
+        }
+        if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) {
+            this.skipnodebottom = drag.get('node').next();
+        }
+    },
+
+    drop_over : function(e) {
+        // Get a reference to our drag and drop nodes
+        var drag = e.drag.get('node');
+        var drop = e.drop.get('node');
+
+        // We need to fix the case when parent drop over event has determined
+        // 'goingup' and appended the drag node after admin-block.
+        if (drop.hasClass(this.parentnodeclass) && drop.one('.'+CSS.BLOCKADMINBLOCK) && drop.one('.'+CSS.BLOCKADMINBLOCK).next('.'+CSS.BLOCK)) {
+            drop.prepend(drag);
+        }
+
+        // Block is moved within the same region
+        // stop here, no need to modify anything.
+        if (this.dragsourceregion.contains(drop)) {
+            return false;
+        }
+
+        // TODO: Hiding-displaying block region only works for base theme blocks
+        // (region-pre, region-post) at the moment. It should be improved
+        // to work with custom block regions as well.
+
+        // TODO: Fix this for the case when user drag block towards empty section,
+        // then the section appears, then user chnages his mind and moving back to
+        // original section. The opposite section remains opened and empty.
+
+        var documentbody = Y.one('body');
+        // Moving block towards hidden region-content, display it
+        var regionname = this.get_region_id(this.dragsourceregion);
+        if (documentbody.hasClass('side-'+regionname+'-only')) {
+            documentbody.removeClass('side-'+regionname+'-only');
+        }
+
+        // Moving from empty region-content towards the opposite one,
+        // hide empty one (only for region-pre, region-post areas at the moment).
+        regionname = this.get_region_id(drop.ancestor('div.'+CSS.BLOCKREGION));
+        if (this.dragsourceregion.all('.'+CSS.BLOCK).size() == 0 && this.dragsourceregion.get('id').match(/(region-pre|region-post)/i)) {
+            if (!documentbody.hasClass('side-'+regionname+'-only')) {
+                documentbody.addClass('side-'+regionname+'-only');
+            }
+        }
+    },
+
+    drop_end : function() {
+        // clear variables
+        this.skipnodetop = null;
+        this.skipnodebottom = null;
+        this.dragsourceregion = null;
+    },
+
+    drag_dropmiss : function(e) {
+        // Missed the target, but we assume the user intended to drop it
+        // on the last last ghost node location, e.drag and e.drop should be
+        // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
+        this.drop_hit(e);
+    },
+
+    drop_hit : function(e) {
+        var drag = e.drag;
+        // Get a reference to our drag node
+        var dragnode = drag.get('node');
+        var dropnode = e.drop.get('node');
+
+        // Amend existing skipnodes
+        if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) {
+            // the one that belongs to block below move below
+            dragnode.insert(dragnode.previous(), 'after');
+        }
+        // Move original skipnodes
+        if (this.skipnodetop) {
+            dragnode.insert(this.skipnodetop, 'before');
+        }
+        if (this.skipnodebottom) {
+            dragnode.insert(this.skipnodebottom, 'after');
+        }
+
+        // Add lightbox if it not there
+        var lightbox = M.util.add_lightbox(Y, dragnode);
+
+        // Prepare request parameters
+        var params = {
+            sesskey : M.cfg.sesskey,
+            courseid : this.get('courseid'),
+            pagelayout : this.get('pagelayout'),
+            pagetype : this.get('pagetype'),
+            subpage : this.get('subpage'),
+            contextid : this.get('contextid'),
+            action : 'move',
+            bui_moveid : this.get_block_id(dragnode),
+            bui_newregion : this.get_block_region(dropnode)
+        };
+
+        if (this.get('cmid')) {
+            params.cmid = this.get('cmid');
+        }
+
+        if (dragnode.next('.'+this.samenodeclass) && !dragnode.next('.'+this.samenodeclass).hasClass(CSS.BLOCKADMINBLOCK)) {
+            params.bui_beforeid = this.get_block_id(dragnode.next('.'+this.samenodeclass));
+        }
+
+        // Do AJAX request
+        Y.io(M.cfg.wwwroot+AJAXURL, {
+            method: 'POST',
+            data: params,
+            on: {
+                start : function() {
+                    lightbox.show();
+                },
+                success: function(tid, response) {
+                    window.setTimeout(function() {
+                        lightbox.hide();
+                    }, 250);
+                    try {
+                        var responsetext = Y.JSON.parse(response.responseText);
+                        if (responsetext.error) {
+                            new M.core.ajaxException(responsetext);
+                        }
+                    } catch (e) {}
+                },
+                failure: function(tid, response) {
+                    this.ajax_failure(response);
+                    lightbox.hide();
+                }
+            },
+            context:this
+        });
+    }
+}, {
+    NAME : 'core-blocks-dragdrop',
+    ATTRS : {
+        courseid : {
+            value : null
+        },
+        cmid : {
+            value : null
+        },
+        contextid : {
+            value : null
+        },
+        pagelayout : {
+            value : null
+        },
+        pagetype : {
+            value : null
+        },
+        subpage : {
+            value : null
+        },
+        regions : {
+            value : null
+        }
+    }
+});
+
+M.core_blocks = M.core_blocks || {};
+M.core_blocks.init_dragdrop = function(params) {
+    new DRAGBLOCK(params);
+};
diff --git a/lib/yui/src/blocks/meta/blocks.json b/lib/yui/src/blocks/meta/blocks.json
new file mode 100644 (file)
index 0000000..c28b82b
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "moodle-core-blocks": {
+    "requires": [
+        "base",
+        "node",
+        "io",
+        "dom",
+        "dd",
+        "dd-scroll",
+        "moodle-core-dragdrop",
+        "moodle-core-notification"
+    ]
+  }
+}
diff --git a/lib/yui/src/chooserdialogue/build.json b/lib/yui/src/chooserdialogue/build.json
new file mode 100644 (file)
index 0000000..dbf1884
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-core-chooserdialogue",
+  "builds": {
+    "moodle-core-chooserdialogue": {
+      "jsfiles": [
+        "chooserdialogue.js"
+      ]
+    }
+  }
+}
diff --git a/lib/yui/src/chooserdialogue/js/chooserdialogue.js b/lib/yui/src/chooserdialogue/js/chooserdialogue.js
new file mode 100644 (file)
index 0000000..7247084
--- /dev/null
@@ -0,0 +1,341 @@
+var CHOOSERDIALOGUE = function() {
+    CHOOSERDIALOGUE.superclass.constructor.apply(this, arguments);
+};
+
+Y.extend(CHOOSERDIALOGUE, Y.Base, {
+    // The panel widget
+    panel: null,
+
+    // The submit button - we disable this until an element is set
+    submitbutton : null,
+
+    // The chooserdialogue container
+    container : null,
+
+    // Any event listeners we may need to cancel later
+    listenevents : [],
+
+    // The initial overflow setting
+    initialoverflow : '',
+
+    bodycontent : null,
+    headercontent : null,
+    instanceconfig : null,
+
+    setup_chooser_dialogue : function(bodycontent, headercontent, config) {
+        this.bodycontent = bodycontent;
+        this.headercontent = headercontent;
+        this.instanceconfig = config;
+    },
+
+    prepare_chooser : function () {
+        if (this.panel) {
+            return;
+        }
+
+        // Set Default options
+        var paramkey,
+            params = {
+            bodyContent : this.bodycontent.get('innerHTML'),
+            headerContent : this.headercontent.get('innerHTML'),
+            width : '540px',
+            draggable : true,
+            visible : false, // Hide by default
+            zindex : 100, // Display in front of other items
+            lightbox : true, // This dialogue should be modal
+            shim : true,
+            closeButtonTitle : this.get('closeButtonTitle')
+        };
+
+        // Override with additional options
+        for (paramkey in this.instanceconfig) {
+          params[paramkey] = this.instanceconfig[paramkey];
+        }
+
+        // Create the panel
+        this.panel = new M.core.dialogue(params);
+
+        // Remove the template for the chooser
+        this.bodycontent.remove();
+        this.headercontent.remove();
+
+        // Hide and then render the panel
+        this.panel.hide();
+        this.panel.render();
+
+        // Set useful links
+        this.container = this.panel.get('boundingBox').one('.choosercontainer');
+        this.options = this.container.all('.option input[type=radio]');
+
+        // Add the chooserdialogue class to the container for styling
+        this.panel.get('boundingBox').addClass('chooserdialogue');
+    },
+
+    /**
+      * Display the module chooser
+      *
+      * @param e Event Triggering Event
+      * @return void
+      */
+    display_chooser : function (e) {
+        var bb, dialogue, thisevent;
+        this.prepare_chooser();
+
+        // Stop the default event actions before we proceed
+        e.preventDefault();
+
+        bb = this.panel.get('boundingBox');
+        dialogue = this.container.one('.alloptions');
+
+        // Get the overflow setting when the chooser was opened - we
+        // may need this later
+        if (Y.UA.ie > 0) {
+            this.initialoverflow = Y.one('html').getStyle('overflow');
+        } else {
+            this.initialoverflow = Y.one('body').getStyle('overflow');
+        }
+
+        // This will detect a change in orientation and retrigger centering
+        thisevent = Y.one('document').on('orientationchange', function() {
+            this.center_dialogue(dialogue);
+        }, this);
+        this.listenevents.push(thisevent);
+
+        // Detect window resizes (most browsers)
+        thisevent = Y.one('window').on('resize', function() {
+            this.center_dialogue(dialogue);
+        }, this);
+        this.listenevents.push(thisevent);
+
+        // These will trigger a check_options call to display the correct help
+        thisevent = this.container.on('click', this.check_options, this);
+        this.listenevents.push(thisevent);
+        thisevent = this.container.on('key_up', this.check_options, this);
+        this.listenevents.push(thisevent);
+        thisevent = this.container.on('dblclick', function(e) {
+            if (e.target.ancestor('div.option')) {
+                this.check_options();
+
+                // Prevent duplicate submissions
+                this.submitbutton.setAttribute('disabled', 'disabled');
+                this.options.setAttribute('disabled', 'disabled');
+                this.cancel_listenevents();
+
+                this.container.one('form').submit();
+            }
+        }, this);
+        this.listenevents.push(thisevent);
+
+        this.container.one('form').on('submit', function() {
+            // Prevent duplicate submissions on submit
+            this.submitbutton.setAttribute('disabled', 'disabled');
+            this.options.setAttribute('disabled', 'disabled');
+            this.cancel_listenevents();
+        }, this);
+
+        // Hook onto the cancel button to hide the form
+        thisevent = this.container.one('.addcancel').on('click', this.cancel_popup, this);
+        this.listenevents.push(thisevent);
+
+        // Hide will be managed by cancel_popup after restoring the body overflow
+        thisevent = bb.one('button.closebutton').on('click', this.cancel_popup, this);
+        this.listenevents.push(thisevent);
+
+        // Grab global keyup events and handle them
+        thisevent = Y.one('document').on('keydown', this.handle_key_press, this);
+        this.listenevents.push(thisevent);
+
+        // Add references to various elements we adjust
+        this.jumplink     = this.container.one('.jump');
+        this.submitbutton = this.container.one('.submitbutton');
+
+        // Disable the submit element until the user makes a selection
+        this.submitbutton.set('disabled', 'true');
+
+        // Ensure that the options are shown
+        this.options.removeAttribute('disabled');
+
+        // Display the panel
+        this.panel.show();
+
+        // Re-centre the dialogue after we've shown it.
+        this.center_dialogue(dialogue);
+
+        // Finally, focus the first radio element - this enables form selection via the keyboard
+        this.container.one('.option input[type=radio]').focus();
+
+        // Trigger check_options to set the initial jumpurl
+        this.check_options();
+    },
+
+    /**
+      * Cancel any listen events in the listenevents queue
+      *
+      * Several locations add event handlers which should only be called before the form is submitted. This provides
+      * a way of cancelling those events.
+      *
+      * @return void
+      */
+    cancel_listenevents : function () {
+        // Detach all listen events to prevent duplicate triggers
+        var thisevent;
+        while (this.listenevents.length) {
+            thisevent = this.listenevents.shift();
+            thisevent.detach();
+        }
+    },
+
+    /**
+      * Calculate the optimum height of the chooser dialogue
+      *
+      * This tries to set a sensible maximum and minimum to ensure that some options are always shown, and preferably
+      * all, whilst fitting the box within the current viewport.
+      *
+      * @param dialogue Y.Node The dialogue
+      * @return void
+      */
+    center_dialogue : function(dialogue) {
+        var bb = this.panel.get('boundingBox'),
+            winheight = bb.get('winHeight'),
+            winwidth = bb.get('winWidth'),
+            offsettop = 0,
+            newheight, totalheight, dialoguetop, dialoguewidth, dialogueleft;
+
+        // Try and set a sensible max-height -- this must be done before setting the top
+        // Set a default height of 640px
+        newheight = this.get('maxheight');
+        if (winheight <= newheight) {
+            // Deal with smaller window sizes
+            if (winheight <= this.get('minheight')) {
+                newheight = this.get('minheight');
+            } else {
+                newheight = winheight;
+            }
+        }
+
+        // Set a fixed position if the window is large enough
+        if (newheight > this.get('minheight')) {
+            bb.setStyle('position', 'fixed');
+            // Disable the page scrollbars
+            if (Y.UA.ie > 0) {
+                Y.one('html').setStyle('overflow', 'hidden');
+            } else {
+                Y.one('body').setStyle('overflow', 'hidden');
+            }
+        } else {
+            bb.setStyle('position', 'absolute');
+            offsettop = Y.one('window').get('scrollTop');
+            // Ensure that the page scrollbars are enabled
+            if (Y.UA.ie > 0) {
+                Y.one('html').setStyle('overflow', this.initialoverflow);
+            } else {
+                Y.one('body').setStyle('overflow', this.initialoverflow);
+            }
+        }
+
+        // Take off 15px top and bottom for borders, plus 40px each for the title and button area before setting the
+        // new max-height
+        totalheight = newheight;
+        newheight = newheight - (15 + 15 + 40 + 40);
+        dialogue.setStyle('maxHeight', newheight + 'px');
+
+        dialogueheight = bb.getStyle('height');
+        if (dialogueheight.match(/.*px$/)) {
+            dialogueheight = dialogueheight.replace(/px$/, '');
+        } else {
+            dialogueheight = totalheight;
+        }
+
+        if (dialogueheight < this.get('baseheight')) {
+            dialogueheight = this.get('baseheight');
+            dialogue.setStyle('height', dialogueheight + 'px');
+        }
+
+
+        // Re-calculate the location now that we've changed the size
+        dialoguetop = Math.max(12, ((winheight - dialogueheight) / 2)) + offsettop;
+
+        // We need to set the height for the yui3-widget - can't work
+        // out what we're setting at present -- shoud be the boudingBox
+        bb.setStyle('top', dialoguetop + 'px');
+
+        // Calculate the left location of the chooser
+        // We don't set a minimum width in the same way as we do height as the width would be far lower than the
+        // optimal width for moodle anyway.
+        dialoguewidth = bb.get('offsetWidth');
+        dialogueleft = (winwidth - dialoguewidth) / 2;
+        bb.setStyle('left', dialogueleft + 'px');
+    },
+
+    handle_key_press : function(e) {
+        if (e.keyCode === 27) {
+            this.cancel_popup(e);
+        }
+    },
+
+    cancel_popup : function (e) {
+        // Prevent normal form submission before hiding
+        e.preventDefault();
+        this.hide();
+    },
+
+    hide : function() {
+        // Cancel all listen events
+        this.cancel_listenevents();
+
+        // Re-enable the page scrollbars
+        if (Y.UA.ie > 0) {
+            Y.one('html').setStyle('overflow', this.initialoverflow);
+        } else {
+            Y.one('body').setStyle('overflow', this.initialoverflow);
+        }
+
+        this.container.detachAll();
+        this.panel.hide();
+    },
+
+    check_options : function() {
+        // Check which options are set, and change the parent class
+        // to show/hide help as required
+        this.options.each(function(thisoption) {
+            var optiondiv = thisoption.get('parentNode').get('parentNode');
+            if (thisoption.get('checked')) {
+                optiondiv.addClass('selected');
+
+                // Trigger any events for this option
+                this.option_selected(thisoption);
+
+                // Ensure that the form may be submitted
+                this.submitbutton.removeAttribute('disabled');
+
+                // Ensure that the radio remains focus so that keyboard navigation is still possible
+                thisoption.focus();
+            } else {
+                optiondiv.removeClass('selected');
+            }
+        }, this);
+    },
+
+    option_selected : function() {
+    }
+},
+{
+    NAME : 'moodle-core-chooserdialogue',
+    ATTRS : {
+        minheight : {
+            value : 300
+        },
+        baseheight: {
+            value : 400
+        },
+        maxheight : {
+            value : 660
+        },
+        closeButtonTitle : {
+            validator : Y.Lang.isString,
+            value : 'Close'
+        }
+    }
+});
+M.core = M.core || {};
+M.core.chooserdialogue = CHOOSERDIALOGUE;
diff --git a/lib/yui/src/chooserdialogue/meta/chooserdialogue.json b/lib/yui/src/chooserdialogue/meta/chooserdialogue.json
new file mode 100644 (file)
index 0000000..07ebea3
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "moodle-core-chooserdialogue": {
+    "requires": [
+        "base",
+        "panel",
+        "moodle-core-notification"
+    ]
+  }
+}
diff --git a/lib/yui/src/formautosubmit/build.json b/lib/yui/src/formautosubmit/build.json
new file mode 100644 (file)
index 0000000..2bfd035
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-core-formautosubmit",
+  "builds": {
+    "moodle-core-formautosubmit": {
+      "jsfiles": [
+        "formautosubmit.js"
+      ]
+    }
+  }
+}
diff --git a/lib/yui/src/formautosubmit/js/formautosubmit.js b/lib/yui/src/formautosubmit/js/formautosubmit.js
new file mode 100644 (file)
index 0000000..e78d50b
--- /dev/null
@@ -0,0 +1,119 @@
+var CSS,
+    FORMAUTOSUBMITNAME = 'core-formautosubmit',
+    FORMAUTOSUBMIT,
+    INITIALIZED = false;
+
+// The CSS selectors we use
+CSS = {
+    AUTOSUBMIT : 'autosubmit'
+};
+
+FORMAUTOSUBMIT = function() {
+    FORMAUTOSUBMIT.superclass.constructor.apply(this, arguments);
+};
+
+Y.extend(FORMAUTOSUBMIT, Y.Base, {
+
+    /**
+      * Initialize the module
+      */
+    initializer : function() {
+        // Set up local variables
+        var applyto,
+            thisselect;
+        // We only apply the delegation once
+        if (!INITIALIZED) {
+            INITIALIZED = true;
+            applyto = Y.one('body');
+
+            // We don't listen for change events by default as using the keyboard triggers these too.
+            applyto.delegate('key', this.process_changes, 'press:13', 'select.' + CSS.AUTOSUBMIT, this);
+            applyto.delegate('click', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
+
+            if (Y.UA.os === 'macintosh' && Y.UA.webkit) {
+                // Macintosh webkit browsers like change events, but non-macintosh webkit browsers don't.
+                applyto.delegate('change', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
+            }
+            if (Y.UA.ios) {
+                // IOS doesn't trigger click events because it's touch-based.
+                applyto.delegate('change', this.process_changes, 'select.' + CSS.AUTOSUBMIT, this);
+            }
+        }
+
+        // Assign this select items 'nothing' value and lastindex (current value)
+        if (this.get('selectid')) {
+            thisselect = Y.one('select#' + this.get('selectid'));
+            if (thisselect) {
+                if (this.get('nothing')) {
+                    thisselect.setData('nothing', this.get('nothing'));
+                }
+                thisselect.setData('startindex', thisselect.get('selectedIndex'));
+            } else {
+                Y.log("Warning: A single_select element was renderered, but the output is not displayed on the page.");
+            }
+        }
+    },
+
+    /**
+      * Check whether the select element was changed
+      */
+    check_changed : function(e) {
+        var select,
+            nothing,
+            startindex,
+            currentindex,
+            previousindex;
+        select = e.target.ancestor('select.' + CSS.AUTOSUBMIT, true);
+        if (!select) {
+            return false;
+        }
+
+        nothing = select.getData('nothing');
+        startindex = select.getData('startindex');
+        currentindex = select.get('selectedIndex');
+
+        previousindex = select.getAttribute('data-previousindex');
+        select.setAttribute('data-previousindex', currentindex);
+        if (!previousindex) {
+            previousindex = startindex;
+        }
+
+        // Check whether the field has changed, and is not the 'nothing' value
+        if ((nothing===false || select.get('value') !== nothing)
+                && startindex !== select.get('selectedIndex') && currentindex !== previousindex) {
+            return select;
+        }
+        return false;
+    },
+
+    /**
+      * Process any changes
+      */
+    process_changes : function(e) {
+        var select = this.check_changed(e),
+            form;
+        if (select) {
+            form = select.ancestor('form', true);
+            form.submit();
+        }
+    }
+},
+{
+    NAME : FORMAUTOSUBMITNAME,
+    ATTRS : {
+        selectid : {
+            'value' : ''
+        },
+        nothing : {
+            'value' : ''
+        },
+        ignorechangeevent : {
+            'value' : false
+        }
+    }
+});
+
+M.core = M.core || {};
+M.core.init_formautosubmit = M.core.init_formautosubmit || function(config) {
+    return new FORMAUTOSUBMIT(config);
+};
diff --git a/lib/yui/src/formautosubmit/meta/formautosubmit.json b/lib/yui/src/formautosubmit/meta/formautosubmit.json
new file mode 100644 (file)
index 0000000..f034605
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "moodle-core-formautosubmit": {
+    "requires": [
+      "base",
+      "event-key"
+    ]
+  }
+}
diff --git a/lib/yui/src/formchangechecker/build.json b/lib/yui/src/formchangechecker/build.json
new file mode 100644 (file)
index 0000000..6fa6ccf
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-core-formchangechecker",
+  "builds": {
+    "moodle-core-formchangechecker": {
+      "jsfiles": [
+        "formchangechecker.js"
+      ]
+    }
+  }
+}
diff --git a/lib/yui/src/formchangechecker/js/formchangechecker.js b/lib/yui/src/formchangechecker/js/formchangechecker.js
new file mode 100644 (file)
index 0000000..0aec7ec
--- /dev/null
@@ -0,0 +1,181 @@
+var FORMCHANGECHECKERNAME = 'core-formchangechecker',
+
+    FORMCHANGECHECKER = function() {
+        FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
+    };
+
+Y.extend(FORMCHANGECHECKER, Y.Base, {
+
+        // The delegated listeners we need to detach after the initial value has been stored once
+        initialvaluelisteners : [],
+
+        /**
+          * Initialize the module
+          */
+        initializer : function() {
+            var formid = 'form#' + this.get('formid'),
+                currentform = Y.one(formid);
+
+            if (!currentform) {
+                // If the form was not found, then we can't check for changes.
+                return;
+            }
+
+            // Add change events to the form elements
+            currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'input', this);
+            currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'textarea', this);
+            currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'select', this);
+
+            // Add a focus event to check for changes which are made without triggering a change event
+            this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'input', this));
+            this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'textarea', this));
+            this.initialvaluelisteners.push(currentform.delegate('focus', this.store_initial_value, 'select', this));
+
+            // We need any submit buttons on the form to set the submitted flag
+            Y.one(formid).on('submit', M.core_formchangechecker.set_form_submitted, this);
+
+            // YUI doesn't support onbeforeunload properly so we must use the DOM to set the onbeforeunload. As
+            // a result, the has_changed must stay in the DOM too
+            window.onbeforeunload = M.core_formchangechecker.report_form_dirty_state;
+        },
+
+        /**
+          * Store the initial value of the currently focussed element
+          *
+          * If an element has been focussed and changed but not yet blurred, the on change
+          * event won't be fired. We need to store it's initial value to compare it in the
+          * get_form_dirty_state function later.
+          */
+        store_initial_value : function(e) {
+            var thisevent;
+            if (e.target.hasClass('ignoredirty')) {
+                // Don't warn on elements with the ignoredirty class
+                return;
+            }
+            if (M.core_formchangechecker.get_form_dirty_state()) {
+                // Detach all listen events to prevent duplicate initial value setting
+                while (this.initialvaluelisteners.length) {
+                    thisevent = this.initialvaluelisteners.shift();
+                    thisevent.detach();
+                }
+
+                return;
+            }
+
+            // Make a note of the current element so that it can be interrogated and
+            // compared in the get_form_dirty_state function
+            M.core_formchangechecker.stateinformation.focused_element = {
+                element : e.target,
+                initial_value : e.target.get('value')
+            };
+        }
+    },
+    {
+        NAME : FORMCHANGECHECKERNAME,
+        ATTRS : {
+            formid : {
+                'value' : ''
+            }
+        }
+    }
+);
+
+M.core_formchangechecker = M.core_formchangechecker || {};
+
+// We might have multiple instances of the form change protector
+M.core_formchangechecker.instances = M.core_formchangechecker.instances || [];
+M.core_formchangechecker.init = function(config) {
+    var formchangechecker = new FORMCHANGECHECKER(config);
+    M.core_formchangechecker.instances.push(formchangechecker);
+    return formchangechecker;
+};
+
+// Store state information
+M.core_formchangechecker.stateinformation = [];
+
+/**
+  * Set the form changed state to true
+  */
+M.core_formchangechecker.set_form_changed = function(e) {
+    if (e && e.target && e.target.hasClass('ignoredirty')) {
+        // Don't warn on elements with the ignoredirty class
+        return;
+    }
+    M.core_formchangechecker.stateinformation.formchanged = 1;
+
+    // Once the form has been marked as dirty, we no longer need to keep track of form elements
+    // which haven't yet blurred
+    delete M.core_formchangechecker.stateinformation.focused_element;
+};
+
+/**
+  * Set the form submitted state to true
+  */
+M.core_formchangechecker.set_form_submitted = function() {
+    M.core_formchangechecker.stateinformation.formsubmitted = 1;
+};
+
+/**
+  * Attempt to determine whether the form has been modified in any way and
+  * is thus 'dirty'
+  *
+  * @return Integer 1 is the form is dirty; 0 if not
+  */
+M.core_formchangechecker.get_form_dirty_state = function() {
+    var state = M.core_formchangechecker.stateinformation,
+        editor;
+
+    // If the form was submitted, then return a non-dirty state
+    if (state.formsubmitted) {
+        return 0;
+    }
+
+    // If any fields have been marked dirty, return a dirty state
+    if (state.formchanged) {
+        return 1;
+    }
+
+    // If a field has been focused and changed, but still has focus then the browser won't fire the
+    // onChange event. We check for this eventuality here
+    if (state.focused_element) {
+        if (state.focused_element.element.get('value') !== state.focused_element.initial_value) {
+            return 1;
+        }
+    }
+
+    // Handle TinyMCE editor instances
+    // We can't add a listener in the initializer as the editors may not have been created by that point
+    // so we do so here instead
+    if (typeof tinyMCE !== 'undefined') {
+        for (editor in tinyMCE.editors) {
+            if (tinyMCE.editors[editor].isDirty()) {
+                return 1;
+            }
+        }
+    }
+
+    // If we reached here, then the form hasn't met any of the dirty conditions
+    return 0;
+};
+
+/**
+  * Return a suitable message if changes have been made to a form
+  */
+M.core_formchangechecker.report_form_dirty_state = function(e) {
+    if (!M.core_formchangechecker.get_form_dirty_state()) {
+        // the form is not dirty, so don't display any message
+        return;
+    }
+
+    // This is the error message that we'll show to browsers which support it
+    var warningmessage = M.util.get_string('changesmadereallygoaway', 'moodle');
+
+    // Most browsers are happy with the returnValue being set on the event
+    // But some browsers do not consistently pass the event
+    if (e) {
+        e.returnValue = warningmessage;
+    }
+
+    // But some require it to be returned instead
+    return warningmessage;
+};
diff --git a/lib/yui/src/formchangechecker/meta/formchangechecker.json b/lib/yui/src/formchangechecker/meta/formchangechecker.json
new file mode 100644 (file)
index 0000000..ca81382
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "moodle-core-formchangechecker": {
+    "requires": [
+      "base",
+      "event-focus"
+    ]
+  }
+}
diff --git a/lib/yui/src/notification/build.json b/lib/yui/src/notification/build.json
new file mode 100644 (file)
index 0000000..dfc25ad
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-core-notification",
+  "builds": {
+    "moodle-core-notification": {
+      "jsfiles": [
+        "notification.js"
+      ]
+    }
+  }
+}
similarity index 81%
rename from lib/yui/notification/notification.js
rename to lib/yui/src/notification/js/notification.js
index f7f3000..e15286d 100644 (file)
@@ -1,12 +1,9 @@
-YUI.add('moodle-core-notification', function(Y) {
-
 var DIALOGUE_NAME = 'Moodle dialogue',
     DIALOGUE_PREFIX = 'moodle-dialogue',
     CONFIRM_NAME = 'Moodle confirmation dialogue',
     EXCEPTION_NAME = 'Moodle exception',
     AJAXEXCEPTION_NAME = 'Moodle AJAX exception',
     ALERT_NAME = 'Moodle alert',
-    C = Y.Node.create,
     BASE = 'notificationBase',
     COUNT = 0,
     CONFIRMYES = 'yesLabel',
@@ -33,11 +30,11 @@ DIALOGUE = function(config) {
     COUNT++;
     var id = 'moodle-dialogue-'+COUNT;
     config.notificationBase =
-        new C('<div class="'+CSS.BASE+'">')
-              .append(new C('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
-              .append(new C('<div class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
-              .append(new C('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
-              .append(new C('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
+        Y.Node.create('<div class="'+CSS.BASE+'">')
+              .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
+              .append(Y.Node.create('<div class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
+              .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
+              .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
     Y.one(document.body).append(config.notificationBase);
     config.srcNode =    '#'+id;
     config.width =      config.width || '400px';
@@ -59,7 +56,7 @@ DIALOGUE = function(config) {
             {
                 section: Y.WidgetStdMod.HEADER,
                 classNames: 'closebutton',
-                action: function (e) {
+                action: function () {
                     this.hide();
                 }
             }
@@ -73,7 +70,7 @@ DIALOGUE = function(config) {
     }
 };
 Y.extend(DIALOGUE, Y.Panel, {
-    initializer : function(config) {
+    initializer : function() {
         this.after('visibleChange', this.visibilityChanged, this);
         this.render();
         this.show();
@@ -149,10 +146,10 @@ Y.extend(ALERT, DIALOGUE, {
     _enterKeypress : null,
     initializer : function() {
         this.publish('complete');
-        var yes = C('<input type="button" id="id_yuialertconfirm-' + this.COUNT + '" value="'+this.get(CONFIRMYES)+'" />'),
-            content = C('<div class="confirmation-dialogue"></div>')
-                    .append(C('<div class="confirmation-message">'+this.get('message')+'</div>'))
-                    .append(C('<div class="confirmation-buttons"></div>')
+        var yes = Y.Node.create('<input type="button" id="id_yuialertconfirm-' + this.COUNT + '" value="'+this.get(CONFIRMYES)+'" />'),
+            content = Y.Node.create('<div class="confirmation-dialogue"></div>')
+                    .append(Y.Node.create('<div class="confirmation-message">'+this.get('message')+'</div>'))
+                    .append(Y.Node.create('<div class="confirmation-buttons"></div>')
                             .append(yes));
         this.get(BASE).addClass('moodle-dialogue-confirm');
         this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
@@ -202,11 +199,11 @@ Y.extend(CONFIRM, DIALOGUE, {
         this.publish('complete');
         this.publish('complete-yes');
         this.publish('complete-no');
-        var yes = C('<input type="button" id="id_yuiconfirmyes-' + this.COUNT + '" value="'+this.get(CONFIRMYES)+'" />'),
-            no = C('<input type="button" id="id_yuiconfirmno-' + this.COUNT + '" value="'+this.get(CONFIRMNO)+'" />'),
-            content = C('<div class="confirmation-dialogue"></div>')
-                        .append(C('<div class="confirmation-message">'+this.get(QUESTION)+'</div>'))
-                        .append(C('<div class="confirmation-buttons"></div>')
+        var yes = Y.Node.create('<input type="button" id="id_yuiconfirmyes-' + this.COUNT + '" value="'+this.get(CONFIRMYES)+'" />'),
+            no = Y.Node.create('<input type="button" id="id_yuiconfirmno-' + this.COUNT + '" value="'+this.get(CONFIRMNO)+'" />'),
+            content = Y.Node.create('<div class="confirmation-dialogue"></div>')
+                        .append(Y.Node.create('<div class="confirmation-message">'+this.get(QUESTION)+'</div>'))
+                        .append(Y.Node.create('<div class="confirmation-buttons"></div>')
                             .append(yes)
                             .append(no));
         this.get(BASE).addClass('moodle-dialogue-confirm');
@@ -268,11 +265,11 @@ Y.extend(EXCEPTION, DIALOGUE, {
             delay = this.get('hideTimeoutDelay');
         this.get(BASE).addClass('moodle-dialogue-exception');
         this.setStdModContent(Y.WidgetStdMod.HEADER, '<h1 id="moodle-dialogue-'+COUNT+'-header-text">' + config.name + '</h1>', Y.WidgetStdMod.REPLACE);
-        content = C('<div class="moodle-exception"></div>')
-                .append(C('<div class="moodle-exception-message">'+this.get('message')+'</div>'))
-                .append(C('<div class="moodle-exception-param hidden param-filename"><label>File:</label> '+this.get('fileName')+'</div>'))
-                .append(C('<div class="moodle-exception-param hidden param-linenumber"><label>Line:</label> '+this.get('lineNumber')+'</div>'))
-                .append(C('<div class="moodle-exception-param hidden param-stacktrace"><label>Stack trace:</label> <pre>'+this.get('stack')+'</pre></div>'));
+        content = Y.Node.create('<div class="moodle-exception"></div>')
+                .append(Y.Node.create('<div class="moodle-exception-message">'+this.get('message')+'</div>'))
+                .append(Y.Node.create('<div class="moodle-exception-param hidden param-filename"><label>File:</label> '+this.get('fileName')+'</div>'))
+                .append(Y.Node.create('<div class="moodle-exception-param hidden param-linenumber"><label>Line:</label> '+this.get('lineNumber')+'</div>'))
+                .append(Y.Node.create('<div class="moodle-exception-param hidden param-stacktrace"><label>Stack trace:</label> <pre>'+this.get('stack')+'</pre></div>'));
         if (M.cfg.developerdebug) {
             content.all('.moodle-exception-param').removeClass('hidden');
         }
@@ -344,11 +341,11 @@ Y.extend(AJAXEXCEPTION, DIALOGUE, {
             delay = this.get('hideTimeoutDelay');
         this.get(BASE).addClass('moodle-dialogue-exception');
         this.setStdModContent(Y.WidgetStdMod.HEADER, '<h1 id="moodle-dialogue-'+COUNT+'-header-text">' + config.name + '</h1>', Y.WidgetStdMod.REPLACE);
-        content = C('<div class="moodle-ajaxexception"></div>')
-                .append(C('<div class="moodle-exception-message">'+this.get('error')+'</div>'))
-                .append(C('<div class="moodle-exception-param hidden param-debuginfo"><label>URL:</label> '+this.get('reproductionlink')+'</div>'))
-                .append(C('<div class="moodle-exception-param hidden param-debuginfo"><label>Debug info:</label> '+this.get('debuginfo')+'</div>'))
-                .append(C('<div class="moodle-exception-param hidden param-stacktrace"><label>Stack trace:</label> <pre>'+this.get('stacktrace')+'</pre></div>'));
+        content = Y.Node.create('<div class="moodle-ajaxexception"></div>')
+                .append(Y.Node.create('<div class="moodle-exception-message">'+this.get('error')+'</div>'))
+                .append(Y.Node.create('<div class="moodle-exception-param hidden param-debuginfo"><label>URL:</label> '+this.get('reproductionlink')+'</div>'))
+                .append(Y.Node.create('<div class="moodle-exception-param hidden param-debuginfo"><label>Debug info:</label> '+this.get('debuginfo')+'</div>'))
+                .append(Y.Node.create('<div class="moodle-exception-param hidden param-stacktrace"><label>Stack trace:</label> <pre>'+this.get('stacktrace')+'</pre></div>'));
         if (M.cfg.developerdebug) {
             content.all('.moodle-exception-param').removeClass('hidden');
         }
@@ -404,5 +401,3 @@ M.core.alert = ALERT;
 M.core.confirm = CONFIRM;
 M.core.exception = EXCEPTION;
 M.core.ajaxException = AJAXEXCEPTION;
-
-}, '@VERSION@', {requires:['base','node','panel','event-key', 'dd-plugin']});
diff --git a/lib/yui/src/notification/meta/notification.json b/lib/yui/src/notification/meta/notification.json
new file mode 100644 (file)
index 0000000..739c827
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "moodle-core-notification": {
+    "requires": [
+        "base",
+        "node",
+        "panel",
+        "event-key",
+        "dd-plugin"
+    ]
+  }
+}
diff --git a/lib/yui/src/popuphelp/build.json b/lib/yui/src/popuphelp/build.json
new file mode 100644 (file)
index 0000000..4be9e69
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-core-popuphelp",
+  "builds": {
+    "moodle-core-popuphelp": {
+      "jsfiles": [
+        "popuphelp.js"
+      ]
+    }
+  }
+}
diff --git a/lib/yui/src/popuphelp/js/popuphelp.js b/lib/yui/src/popuphelp/js/popuphelp.js
new file mode 100644 (file)
index 0000000..0be3efe
--- /dev/null
@@ -0,0 +1,81 @@
+function POPUPHELP() {
+    POPUPHELP.superclass.constructor.apply(this, arguments);
+}
+
+var SELECTORS = {
+        CLICKABLELINKS: 'span.helptooltip > a',
+        FOOTER: 'div.moodle-dialogue-ft'
+    },
+
+    CSS = {
+        ICON: 'icon',
+        ICONPRE: 'icon-pre'
+    },
+    ATTRS = {};
+
+// Set the modules base properties.
+POPUPHELP.NAME = 'moodle-core-popuphelp';
+POPUPHELP.ATTRS = ATTRS;
+
+Y.extend(POPUPHELP, Y.Base, {
+    panel: null,
+
+    initializer: function() {
+        Y.one('body').delegate('click', this.display_panel, SELECTORS.CLICKABLELINKS, this);
+    },
+
+    display_panel: function(e) {
+        if (!this.panel) {
+            this.panel = new M.core.tooltip({
+                bodyhandler: this.set_body_content,
+                footerhandler: this.set_footer,
+                initialheadertext: M.util.get_string('loadinghelp', 'moodle'),
+                initialfootertext: ''
+            });
+        }
+
+        // Call the tooltip setup.
+        this.panel.display_panel(e);
+    },
+
+    /**
+      * Override the footer handler to add a 'More help' link where relevant.
+      *
+      * @param {Object} helpobject The object returned from the AJAX call.
+      */
+    set_footer: function(helpobject) {
+        // Check for an optional link to documentation on moodle.org.
+        if (helpobject.doclink) {
+            // Wrap a help icon and the morehelp text in an anchor. The class of the anchor should
+            // determine whether it's opened in a new window or not.
+            doclink = Y.Node.create('<a />')
+                .setAttrs({
+                    'href': helpobject.doclink.link
+                })
+                .addClass(helpobject.doclink['class']);
+            helpicon = Y.Node.create('<img />')
+                .setAttrs({
+                    'src': M.util.image_url('docs', 'core')
+                })
+                .addClass(CSS.ICON)
+                .addClass(CSS.ICONPRE);
+            doclink.appendChild(helpicon);
+            doclink.appendChild(helpobject.doclink.linktext);
+
+            // Set the footerContent to the contents of the doclink.
+            this.set('footerContent', doclink);
+            this.bb.one(SELECTORS.FOOTER).show();
+        } else {
+            this.bb.one(SELECTORS.FOOTER).hide();
+        }
+    }
+});
+M.core = M.core || {};
+M.core.popuphelp = M.core.popuphelp || null;
+M.core.init_popuphelp = M.core.init_popuphelp || function(config) {
+    // Only set up a single instance of the popuphelp.
+    if (!M.core.popuphelp) {
+        M.core.popuphelp = new POPUPHELP(config);
+    }
+    return M.core.popuphelp;
+};
diff --git a/lib/yui/src/popuphelp/meta/popuphelp.json b/lib/yui/src/popuphelp/meta/popuphelp.json
new file mode 100644 (file)
index 0000000..6df06f9
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "moodle-core-popuphelp": {
+    "requires": [
+        "moodle-core-tooltip"
+    ]
+  }
+}
diff --git a/mod/forum/tests/behat/add_forum.feature b/mod/forum/tests/behat/add_forum.feature
new file mode 100644 (file)
index 0000000..80466ea
--- /dev/null
@@ -0,0 +1,29 @@
+@mod_forum
+Feature: Add forum activities and discussions
+  In order to discuss topics with other users
+  As a moodle teacher
+  I need to add forum activities to moodle courses
+
+  @javascript
+  Scenario: Add a forum and a discussion
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Forum type | Standard forum for general use |
+      | Description | Test forum description |
+    When I add a new discussion to "Test forum name" forum with:
+      | Subject | Forum post 1 |
+      | Message | This is the body |
+    And I wait "6" seconds
+    Then I should see "Test forum name"
diff --git a/mod/forum/tests/behat/behat_mod_forum.php b/mod/forum/tests/behat/behat_mod_forum.php
new file mode 100644 (file)
index 0000000..8ddf100
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Steps definitions related with the forum activity.
+ *
+ * @package    mod_forum
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, 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,
+    Behat\Gherkin\Node\TableNode as TableNode;
+/**
+ * Forum-related steps definitions.
+ *
+ * @package    mod_forum
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_mod_forum extends behat_base {
+
+    /**
+     * Adds a discussion to the forum specified by it's name with the provided table data (usually Subject and Message). The step begins from the forum's course page.
+     *
+     * @Given /^I add a new discussion to "(?P<forum_name_string>(?:[^"]|\\")*)" forum with:$/
+     * @param string $forumname
+     * @param TableNode $table
+     */
+    public function i_add_a_forum_discussion_to_forum_with($forumname, TableNode $table) {
+
+        // Escaping $forumname as it has been stripped automatically by the transformer.
+        return array(
+            new Given('I follow "' . $this->escape($forumname) . '"'),
+            new Given('I press "Add a new discussion topic"'),
+            new Given('I fill the moodle form with:', $table),
+            new Given('I press "Post to forum"'),
+            new Given('I wait "5" seconds')
+        );
+    }
+
+    /**
+     * Adds a reply to the specified post of the specified forum. The step begins from the forum's page or from the forum's course page.
+     *
+     * @Given /^I reply "(?P<post_subject_string>(?:[^"]|\\")*)" post from "(?P<forum_name_string>(?:[^"]|\\")*)" forum with:$/
+     * @param mixed $postname The subject of the post
+     * @param mixed $forumname The forum name
+     * @param TableNode $table
+     */
+    public function i_reply_post_from_forum_with($postsubject, $forumname, TableNode $table) {
+
+        return array(
+            new Given('I follow "' . $this->escape($forumname) . '"'),
+            new Given('I follow "' . $this->escape($postsubject) . '"'),
+            new Given('I follow "Reply"'),
+            new Given('I fill the moodle form with:', $table),
+            new Given('I press "Post to forum"'),
+            new Given('I wait "5" seconds')
+        );
+    }
+}
diff --git a/mod/forum/tests/behat/completion_condition_number_discussions.feature b/mod/forum/tests/behat/completion_condition_number_discussions.feature
new file mode 100644 (file)
index 0000000..eb0c914
--- /dev/null
@@ -0,0 +1,54 @@
+@mod_forum
+Feature: Set a certain number of discussions as a completion condition for a forum
+  In order to ensure students are participating on forums
+  As a moodle teacher
+  I need to set a minimum number of discussions to mark the forum activity as completed
+
+  @javascript
+  Scenario: Set X number of discussions as a condition
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@asd.com |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | Enable completion tracking | 1 |
+      | Enable conditional access | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Edit settings"
+    And I fill the moodle form with:
+      | Completion tracking | Enabled, control via completion and activity settings |
+      | Completion tracking begins on enrolment | 1 |
+    And I press "Save changes"
+    When I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+      | Completion tracking | Show activity as complete when conditions are met |
+      | completiondiscussionsenabled | 1 |
+      | completiondiscussions | 2 |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    Then I hover "//li[contains(concat(' ', @class, ' '), ' modtype_forum ')]/descendant::img[@alt='Not completed: Test forum name']" "xpath_element"
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Post 1 subject |
+      | Message | Body 1 content |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Post 2 subject |
+      | Message | Body 2 content |
+    And I follow "Course 1"
+    And I hover "//li[contains(concat(' ', @class, ' '), ' modtype_forum ')]/descendant::img[contains(@alt, 'Completed: Test forum name')]" "xpath_element"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And "Student 1" user has completed "Test forum name" activity
diff --git a/mod/forum/tests/behat/discussion_display.feature b/mod/forum/tests/behat/discussion_display.feature
new file mode 100644 (file)
index 0000000..5d5d77e
--- /dev/null
@@ -0,0 +1,93 @@
+@mod_forum
+Feature: Students can choose from 4 discussion display options and their choice is remembered
+  In order to read forum posts in a suitable view
+  As a moodle user
+  I need to select which display method I want to use
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 1 |
+      | Message | Discussion contents 1, first message |
+    And I reply "Discussion 1" post from "Test forum name" forum with:
+      | Subject | Reply 1 to discussion 1 |
+      | Message | Discussion contents 1, second message |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 2 |
+      | Message | Discussion contents 2, first message |
+    And I reply "Discussion 2" post from "Test forum name" forum with:
+      | Subject | Reply 1 to discussion 2 |
+      | Message | Discussion contents 2, second message |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+
+  @javascript
+  Scenario: Display replies flat, with oldest first
+    Given I reply "Discussion 1" post from "Test forum name" forum with:
+      | Subject | Reply 2 to discussion 1 |
+      | Message | Discussion contents 1, third message |
+    When I select "Display replies flat, with oldest first" from "mode"
+    Then I should see "Discussion contents 1, first message" in the "div.firstpost.starter" "css_element"
+    And I should see "Discussion contents 1, second message" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ') and not(contains(@class, 'starter'))]" "xpath_element"
+    And I reply "Discussion 2" post from "Test forum name" forum with:
+      | Subject | Reply 2 to discussion 2 |
+      | Message | Discussion contents 2, third message |
+    And the "Display mode" field should match "1" value
+    And I should see "Discussion contents 2, first message" in the "div.firstpost.starter" "css_element"
+    And I should see "Discussion contents 2, second message" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ') and not(contains(@class, 'starter'))]" "xpath_element"
+
+  @javascript
+  Scenario: Display replies flat, with newest first
+    Given I reply "Discussion 1" post from "Test forum name" forum with:
+      | Subject | Reply 2 to discussion 1 |
+      | Message | Discussion contents 1, third message |
+    When I select "Display replies flat, with newest first" from "mode"
+    Then I should see "Discussion contents 1, first message" in the "div.firstpost.starter" "css_element"
+    And I should see "Discussion contents 1, third message" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ') and not(contains(@class, 'starter'))]" "xpath_element"
+    And I reply "Discussion 2" post from "Test forum name" forum with:
+      | Subject | Reply 2 to discussion 2 |
+      | Message | Discussion contents 2, third message |
+    And the "Display mode" field should match "-1" value
+    And I should see "Discussion contents 2, first message" in the "div.firstpost.starter" "css_element"
+    And I should see "Discussion contents 2, third message" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ') and not(contains(@class, 'starter'))]" "xpath_element"
+
+  @javascript
+  Scenario: Display replies in threaded form
+    Given I follow "Test forum name"
+    And I follow "Discussion 1"
+    When I select "Display replies in threaded form" from "mode"
+    Then I should see "Discussion contents 1, first message"
+    And I should see "Reply 1 to discussion 1" in the "span.forumthread" "css_element"
+    And I follow "Test forum name"
+    And I follow "Discussion 2"
+    And the "Display mode" field should match "2" value
+    And I should see "Discussion contents 2, first message"
+    And I should see "Reply 1 to discussion 2" in the "span.forumthread" "css_element"
+
+  @javascript
+  Scenario: Display replies in nested form
+    Given I follow "Test forum name"
+    And I follow "Discussion 1"
+    When I select "Display replies in nested form" from "mode"
+    Then I should see "Discussion contents 1, first message" in the "div.firstpost.starter" "css_element"
+    And I should see "Discussion contents 1, second message" in the "div.indent div.forumpost" "css_element"
+    And I follow "Test forum name"
+    And I follow "Discussion 2"
+    And the "Display mode" field should match "3" value
+    And I should see "Discussion contents 2, first message" in the "div.firstpost.starter" "css_element"
+    And I should see "Discussion contents 2, second message" in the "div.indent div.forumpost" "css_element"
diff --git a/mod/forum/tests/behat/edit_post_student.feature b/mod/forum/tests/behat/edit_post_student.feature
new file mode 100644 (file)
index 0000000..e693af6
--- /dev/null
@@ -0,0 +1,62 @@
+@mod_forum
+Feature: Students can edit or delete their forum posts within a set time limit
+  In order to refine forum posts
+  As a moodle user
+  I need to edit or delete my forum posts within a certain period of time after posting
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I expand "Site administration" node
+    And I expand "Security" node
+    And I follow "Site policies"
+    And I select "1 minutes" from "Maximum time to edit posts"
+    And I press "Save changes"
+    And I am on homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Forum type | Standard forum for general use |
+      | Description | Test forum description |
+    And I log out
+    And I follow "Course 1"
+    And I log in as "student1"
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Forum post subject |
+      | Message | This is the body |
+    And I wait "6" seconds
+
+  @javascript
+  Scenario: Edit forum post
+    When I follow "Forum post subject"
+    And I follow "Edit"
+    And I fill the moodle form with:
+      | Subject | Edited post subject |
+      | Message | Edited post body |
+    And I press "Save changes"
+    And I wait "6" seconds
+    Then I should see "Edited post subject"
+    And I should see "Edited post body"
+
+  @javascript
+  Scenario: Delete forum post
+    When I follow "Forum post subject"
+    And I follow "Delete"
+    And I press "Continue"
+    Then I should not see "Forum post subject"
+
+  @javascript
+  Scenario: Time limit expires
+    When I wait "70" seconds
+    And I follow "Forum post subject"
+    Then I should not see "Edit"
+    And I should not see "Delete"
diff --git a/mod/forum/tests/behat/edit_post_teacher.feature b/mod/forum/tests/behat/edit_post_teacher.feature
new file mode 100644 (file)
index 0000000..6d81fbd
--- /dev/null
@@ -0,0 +1,67 @@
+@mod_forum
+Feature: Teachers can edit or delete any forum post
+  In order to refine the forum contents
+  As a moodle teacher
+  I need to edit or delete any user's forum posts
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | student1 | Student | 1 | student1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Teacher post subject |
+      | Message | Teacher post message |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I reply "Teacher post subject" post from "Test forum name" forum with:
+      | Subject | Student post subject |
+      | Message | Student post message |
+
+  @javascript
+  Scenario: A teacher can delete another user's posts
+    Given I log out
+    And I log in as "teacher1"
+    When I follow "Course 1"
+    And I follow "Test forum name"
+    And I follow "Teacher post subject"
+    And I click on "Delete" "link" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Student post subject')]" "xpath_element"
+    And I press "Continue"
+    And I wait "4" seconds
+    Then I should not see "Student post subject"
+    And I should not see "Student post message"
+
+  @javascript
+  Scenario: A teacher can edit another user's posts
+    Given I log out
+    And I log in as "teacher1"
+    When I follow "Course 1"
+    And I follow "Test forum name"
+    And I follow "Teacher post subject"
+    And I click on "Edit" "link" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Student post subject')]" "xpath_element"
+    And I fill the moodle form with:
+      | Subject | Edited student subject |
+    And I press "Save changes"
+    And I wait "4" seconds
+    Then I should see "Edited student subject"
+    And I should see "Edited by Teacher 1 - original submission"
+
+  @javascript
+  Scenario: A student can't edit or delete another user's posts
+    When I follow "Teacher post subject"
+    Then I should not see "Edit" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Teacher post subject')]" "xpath_element"
+    And I should not see "Delete" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Teacher post subject')]" "xpath_element"
diff --git a/mod/forum/tests/behat/single_forum_discussion.feature b/mod/forum/tests/behat/single_forum_discussion.feature
new file mode 100644 (file)
index 0000000..9bf6526
--- /dev/null
@@ -0,0 +1,43 @@
+@mod_forum
+Feature: Single simple forum discussion type
+  In order to restrict the discussion topic to one
+  As a moodle teacher
+  I need to create a forum with a single simple discussion
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | student1 | Student | 1 | student1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Single discussion forum name |
+      | Forum type | A single simple discussion |
+      | Description | Single discussion forum description |
+
+  @javascript
+  Scenario: Teacher can start the single simple discussion
+    When I follow "Single discussion forum name"
+    Then I should see "Single discussion forum description" in the "div.firstpost.starter" "css_element"
+    And I should not see "Add a new discussion topic"
+
+  @javascript
+  Scenario: Student can not add more discussions
+    Given I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    When I reply "Single discussion forum name" post from "Single discussion forum name" forum with:
+      | Subject | Reply to single discussion subject |
+      | Message | Reply to single discussion message |
+    Then I should not see "Add a new discussion topic"
+    And I should see "Reply" in the "div.firstpost.starter" "css_element"
+    And I should see "Reply to single discussion message"
diff --git a/mod/forum/tests/behat/track_read_posts.feature b/mod/forum/tests/behat/track_read_posts.feature
new file mode 100644 (file)
index 0000000..0e8fe0d
--- /dev/null
@@ -0,0 +1,85 @@
+@mod_forum
+Feature: A teacher can set one of 3 possible options for tracking read forum posts
+  In order to ease the forum posts follow up
+  As a moodle user
+  I need to distinct the unread posts from the read ones
+
+  Background:
+    Given the following "users" exists:
+      | username | firstname | lastname | email | trackforums |
+      | student1 | Student | 1 | student1@asd.com | 1 |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | student1 | C1 | student |
+    And I log in as "admin"
+    And I follow "Course 1"
+    And I turn editing mode on
+
+  @javascript
+  Scenario: Tracking forum posts on
+    Given I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Forum type | Standard forum for general use |
+      | Description | Test forum description |
+      | Read tracking for this forum | On |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Test post subject |
+      | Message | Test post message |
+    And I wait "6" seconds
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "1 unread post"
+    And I follow "1 unread post"
+    And I should not see "Don't track unread posts"
+    And I follow "Test post subject"
+    And I follow "Course 1"
+    And I should not see "1 unread post"
+
+  @javascript
+  Scenario: Tracking forum posts off
+    Given I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Forum type | Standard forum for general use |
+      | Description | Test forum description |
+      | Read tracking for this forum | Off |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Test post subject |
+      | Message | Test post message |
+    And I wait "6" seconds
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    Then I should not see "1 unread post"
+    And I follow "Test forum name"
+    And I should not see "Track unread posts"
+
+  @javascript
+  Scenario: Tracking forum posts optional
+    Given I add a "forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Forum type | Standard forum for general use |
+      | Description | Test forum description |
+      | Read tracking for this forum | Optional |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Test post subject |
+      | Message | Test post message |
+    And I wait "6" seconds
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "1 unread post"
+    And I follow "Test forum name"
+    And I follow "Don't track unread posts"
+    And I wait "4" seconds
+    And I follow "Course 1"
+    And I should not see "1 unread post"
+    And I follow "Test forum name"
+    And I follow "Track unread posts"
+    And I wait "4" seconds
+    And I follow "1"
+    And I follow "Course 1"
+    And I should not see "1 unread post"
index d3f350f..6ed12fe 100644 (file)
@@ -87,6 +87,14 @@ if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
     redirect($attemptobj->start_attempt_url(null, $page));
 }
 
+// Set up auto-save if required.
+$autosaveperiod = get_config('quiz', 'autosaveperiod');
+if ($autosaveperiod) {
+    $PAGE->requires->yui_module('moodle-mod_quiz-autosave',
+            'M.mod_quiz.autosave.init', array($autosaveperiod));
+}
+
+// Log this page view.
 add_to_log($attemptobj->get_courseid(), 'quiz', 'continue attempt',
         'review.php?attempt=' . $attemptobj->get_attemptid(),
         $attemptobj->get_quizid(), $attemptobj->get_cmid());
index eef2f89..0585941 100644 (file)
@@ -1358,6 +1358,23 @@ class quiz_attempt {
         $transaction->allow_commit();
     }
 
+    /**
+     * Process all the autosaved data that was part of the current request.
+     *
+     * @param int $timestamp the timestamp that should be stored as the modifed
+     * time in the database for these actions. If null, will use the current time.
+     */
+    public function process_auto_save($timestamp) {
+        global $DB;
+
+        $transaction = $DB->start_delegated_transaction();
+
+        $this->quba->process_all_autosaves($timestamp);
+        question_engine::save_questions_usage_by_activity($this->quba);
+
+        $transaction->allow_commit();
+    }
+
     /**
      * Update the flagged state for all question_attempts in this usage, if their
      * flagged state was changed in the request.
diff --git a/mod/quiz/autosave.ajax.php b/mod/quiz/autosave.ajax.php
new file mode 100644 (file)
index 0000000..9b6f132
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Thisscript processes ajax auto-save requests during the quiz.
+ *
+ * @package    mod_quiz
+ * @copyright  2013 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('AJAX_SCRIPT', true);
+
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+// Remember the current time as the time any responses were submitted
+// (so as to make sure students don't get penalized for slow processing on this page).
+$timenow = time();
+require_sesskey();
+
+// Get submitted parameters.
+$attemptid = required_param('attempt',  PARAM_INT);
+$thispage  = optional_param('thispage', 0, PARAM_INT);
+
+$transaction = $DB->start_delegated_transaction();
+$attemptobj = quiz_attempt::create($attemptid);
+
+// Check login.
+require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
+
+// Check that this attempt belongs to this user.
+if ($attemptobj->get_userid() != $USER->id) {
+    throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
+}
+
+// Check capabilities.
+if (!$attemptobj->is_preview_user()) {
+    $attemptobj->require_capability('mod/quiz:attempt');
+}
+
+// If the attempt is already closed, send them to the review page.
+if ($attemptobj->is_finished()) {
+    throw new moodle_quiz_exception($attemptobj->get_quizobj(),
+            'attemptalreadyclosed', null, $attemptobj->review_url());
+}
+
+$attemptobj->process_auto_save($timenow);
+$transaction->allow_commit();
index 47dabba..1c1ccf5 100644 (file)
@@ -97,6 +97,8 @@ $string['attemptsonly'] = 'Show only students with attempts';
 $string['attemptstate'] = 'State';
 $string['attemptstillinprogress'] = 'Attempt still in progress';
 $string['attemptsunlimited'] = 'Unlimited attempts';
+$string['autosaveperiod'] = 'Auto-save period';
+$string['autosaveperiod_desc'] = 'If you turn on this option, then the students will be automatically saved during quiz attempts. The trade-off here is that doing this will increase the load on your server, but it makes it much less likely that students will lose their work.';
 $string['back'] = 'Back to preview question';
 $string['backtocourse'] = 'Back to the course';
 $string['backtoquestionlist'] = 'Back to question list';
@@ -231,6 +233,7 @@ $string['deletingquestionattempts'] = 'Deleting question attempts';
 $string['description'] = 'Description';
 $string['disabled'] = 'Disabled';
 $string['displayoptions'] = 'Display options';
+$string['donotuseautosave'] = 'Do not use auto-save';
 $string['download'] = 'Click to download the exported category file';
 $string['downloadextra'] = '(file is also stored in the course files in the /backupdata/quiz folder)';
 $string['duplicateresponse'] = 'This submission has been ignored because you gave an equivalent answer earlier.';
@@ -504,6 +507,7 @@ $string['numattemptsmade'] = '{$a} attempts made on this quiz';
 $string['numberabbr'] = '#';
 $string['numerical'] = 'Numerical';
 $string['numquestionsx'] = 'Questions: {$a}';
+$string['oneminute'] = '1 minute';
 $string['onlyteachersexport'] = 'Only teachers can export questions';
 $string['onlyteachersimport'] = 'Only teachers with editing rights can import questions';
 $string['onthispage'] = 'This page';
index 8cb0854..ec07c63 100644 (file)
@@ -200,6 +200,16 @@ if (!empty($CFG->enableoutcomes)) {
         '0'));
 }
 
+// Autosave frequency.
+$options = array(
+      0 => get_string('donotuseautosave', 'quiz'),
+     60 => get_string('oneminute', 'quiz'),
+    120 => get_string('numminutes', 'moodle', 2),
+    300 => get_string('numminutes', 'moodle', 5),
+);
+$quizsettings->add(new admin_setting_configselect('quiz/autosaveperiod',
+        get_string('autosaveperiod', 'quiz'), get_string('autosaveperiod_desc', 'quiz'), 0, $options));
+
 // Now, depending on whether any reports have their own settings page, add
 // the quiz setting page to the appropriate place in the tree.
 if (empty($reportsbyname)) {
diff --git a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-coverage.js b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-coverage.js
new file mode 100644 (file)
index 0000000..983a753
Binary files /dev/null and b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-coverage.js differ
diff --git a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js
new file mode 100644 (file)
index 0000000..7d54780
Binary files /dev/null and b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js differ
diff --git a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js
new file mode 100644 (file)
index 0000000..005bc0a
Binary files /dev/null and b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js differ
diff --git a/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js
new file mode 100644 (file)
index 0000000..44eb073
Binary files /dev/null and b/mod/quiz/yui/build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js differ
diff --git a/mod/quiz/yui/src/autosave/build.json b/mod/quiz/yui/src/autosave/build.json
new file mode 100644 (file)
index 0000000..31f970c
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "name": "moodle-mod_quiz-autosave",
+    "builds": {
+        "moodle-mod_quiz-autosave": {
+            "jsfiles": [
+                "autosave.js"
+            ]
+        }
+    }
+}
diff --git a/mod/quiz/yui/src/autosave/js/autosave.js b/mod/quiz/yui/src/autosave/js/autosave.js
new file mode 100644 (file)
index 0000000..14c039c
--- /dev/null
@@ -0,0 +1,224 @@
+// 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/>.
+
+
+/**
+ * Auto-save functionality for during quiz attempts.
+ *
+ * @package   mod_quiz
+ * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+M.mod_quiz = M.mod_quiz || {};
+M.mod_quiz.autosave = {
+    /** Delays and repeat counts. */
+    TINYMCE_DETECTION_DELAY:  500,
+    TINYMCE_DETECTION_REPEATS: 20,
+    WATCH_HIDDEN_DELAY:      1000,
+
+    /** Selectors. */
+    SELECTORS: {
+        QUIZ_FORM:             '#responseform',
+        VALUE_CHANGE_ELEMENTS: 'input, textarea',
+        CHANGE_ELEMENTS:       'input, select',
+        HIDDEN_INPUTS:         'input[type=hidden]'
+    },
+
+    /** Script that handles the auto-saves. */
+    AUTOSAVE_HANDLER: M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php',
+
+    /** The delay between a change being made, and it being auto-saved. */
+    delay: 120000,
+
+    /** The form we are monitoring. */
+    form: null,
+
+    /** Whether the form has been modified since the last save started. */
+    dirty: false,
+
+    /** Timer object for the delay between form modifaction and the save starting. */
+    delay_timer: null,
+
+    /** Y.io transaction for the save ajax request. */
+    save_transaction: null,
+
+    /** Properly bound key change handler. */
+    editor_change_handler: null,
+
+    hidden_field_values: {},
+
+    /**
+     * Initialise the autosave code.
+     * @param delay the delay, in seconds, between a change being detected, and
+     * a save happening.
+     */
+    init: function(delay) {
+        this.form = Y.one(this.SELECTORS.QUIZ_FORM);
+        if (!this.form) {
+            Y.log('No response form found. Why did you try to set up autosave?');
+            return;
+        }
+
+        this.delay = delay * 1000;
+
+        this.form.delegate('valuechange', this.value_changed, this.SELECTORS.VALUE_CHANGE_ELEMENTS, this);
+        this.form.delegate('change',      this.value_changed, this.SELECTORS.CHANGE_ELEMENTS,       this);
+        this.form.on('submit', this.stop_autosaving, this);
+
+        this.init_tinymce(this.TINYMCE_DETECTION_REPEATS);
+
+        this.save_hidden_field_values();
+        this.watch_hidden_fields();
+    },
+
+    save_hidden_field_values: function() {
+        this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
+            var name  = hidden.get('name');
+            if (!name) {
+                return;
+            }
+            this.hidden_field_values[name] = hidden.get('value');
+        }, this);
+    },
+
+    watch_hidden_fields: function() {
+        this.detect_hidden_field_changes();
+        Y.later(this.WATCH_HIDDEN_DELAY, this, this.watch_hidden_fields);
+    },
+
+    detect_hidden_field_changes: function() {
+        this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
+            var name  = hidden.get('name'),
+                value = hidden.get('value');
+            if (!name) {
+                return;
+            }
+            if (!(name in this.hidden_field_values) || value !== this.hidden_field_values[name]) {
+                this.hidden_field_values[name] = value;
+                this.value_changed({target: hidden});
+            }
+        }, this);
+    },
+
+    /**
+     * @param repeatcount Because TinyMCE might load slowly, after us, we need
+     * to keep trying every 10 seconds or so, until we detect TinyMCE is there,
+     * or enough time has passed.
+     */
+    init_tinymce: function(repeatcount) {
+        if (typeof tinymce === 'undefined') {
+            if (repeatcount > 0) {
+                Y.later(this.TINYMCE_DETECTION_DELAY, this, self.init_tinymce,repeatcount - 1);
+            }
+            return;
+        }
+
+        Y.log('Found TinyMCE.');
+        this.editor_change_handler = Y.bind(this.editor_changed, this);
+        tinyMCE.onAddEditor.add(Y.bind(this.init_tinymce_editor, this));
+    },
+
+    /**
+     * @param repeatcount Because TinyMCE might load slowly, after us, we need
+     * to keep trying every 10 seconds or so, until we detect TinyMCE is there,
+     * or enough time has passed.
+     */
+    init_tinymce_editor: function(notused, editor) {
+        Y.log('Found TinyMCE editor ' + editor.id + '.');
+        editor.onChange.add(this.editor_change_handler);
+        editor.onRedo.add(this.editor_change_handler);
+        editor.onUndo.add(this.editor_change_handler);
+        editor.onKeyDown.add(this.editor_change_handler);
+    },
+
+    value_changed: function(e) {
+        if (e.target.get('name') === 'thispage') {
+            return; // Not interesting.
+        }
+        Y.log('Detected a value change in element ' + e.target.get('name') + '.');
+        this.start_save_timer_if_necessary();
+    },
+
+    editor_changed: function(editor) {
+        Y.log('Detected a value change in editor ' + editor.id + '.');
+        this.start_save_timer_if_necessary();
+    },
+
+    start_save_timer_if_necessary: function() {
+        this.dirty = true;
+
+        if (this.delay_timer || this.save_transaction) {
+            // Already counting down or daving.
+            return;
+        }
+
+        this.start_save_timer();
+    },
+
+    start_save_timer: function() {
+        this.cancel_delay();
+        this.delay_timer = Y.later(this.delay, this, this.save_changes);
+    },
+
+    cancel_delay: function() {
+        if (this.delay_timer) {
+            this.delay_timer.cancel();
+        }
+        this.delay_timer = null;
+    },
+
+    save_changes: function() {
+        this.cancel_delay();
+        this.dirty = false;
+
+        if (this.is_time_nearly_over()) {
+            Y.log('No more saving, time is nearly over.');
+            this.stop_autosaving();
+            return;
+        }
+
+        Y.log('Doing a save.');
+        this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, {
+            method:  'POST',
+            form:    {id: this.form},
+            on:      {complete: this.save_done},
+            context: this
+        });
+    },
+
+    save_done: function() {
+        Y.log('Save completed.');
+        this.save_transaction = null;
+
+        if (this.dirty) {
+            Y.log('Dirty after save.');
+            this.start_save_timer();
+        }
+    },
+
+    is_time_nearly_over: function() {
+        return M.mod_quiz.timer && M.mod_quiz.timer.endtime &&
+                (new Date().getTime() + 2*this.delay) > M.mod_quiz.timer.endtime;
+    },
+
+    stop_autosaving: function() {
+        this.cancel_delay();
+        this.delay_timer = true;
+        if (this.save_transaction) {
+            this.save_transaction.abort();
+        }
+    }
+};
diff --git a/mod/quiz/yui/src/autosave/meta/autosave.json b/mod/quiz/yui/src/autosave/meta/autosave.json
new file mode 100644 (file)
index 0000000..0b265c5
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "moodle-mod_quiz-autosave": {
+        "requires": [
+            "base",
+            "node",
+            "event",
+            "event-valuechange",
+            "node-event-delegate",
+            "io-form"
+        ]
+    }
+}
diff --git a/mod/survey/tests/behat/survey_types.feature b/mod/survey/tests/behat/survey_types.feature
new file mode 100644 (file)
index 0000000..60d302a
--- /dev/null