Merge branch 'wip-MDL-36363-m25' of https://github.com/samhemelryk/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 29 Jan 2013 11:06:33 +0000 (12:06 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 29 Jan 2013 11:06:33 +0000 (12:06 +0100)
177 files changed:
.gitignore
admin/cli/maintenance.php
admin/roles/lib.php
admin/tool/behat/cli/util.php [new file with mode: 0644]
admin/tool/behat/index.php [new file with mode: 0644]
admin/tool/behat/lang/en/tool_behat.php [new file with mode: 0644]
admin/tool/behat/locallib.php [new file with mode: 0644]
admin/tool/behat/renderer.php [new file with mode: 0644]
admin/tool/behat/settings.php [new file with mode: 0644]
admin/tool/behat/steps_definitions_form.php [new file with mode: 0644]
admin/tool/behat/styles.css [new file with mode: 0644]
admin/tool/behat/tests/behat/data_generators.feature [new file with mode: 0644]
admin/tool/behat/tests/behat/list_steps.feature [new file with mode: 0644]
admin/tool/behat/tests/behat/test_environment.feature [new file with mode: 0644]
admin/tool/behat/tests/tool_behat_test.php [new file with mode: 0644]
admin/tool/behat/version.php [new file with mode: 0644]
auth/ldap/tests/ldap_test.php [new file with mode: 0644]
auth/tests/behat/behat_auth.php [new file with mode: 0644]
auth/tests/behat/login.feature [new file with mode: 0644]
behat.yml.dist [new file with mode: 0644]
blocks/completionstatus/details.php
blocks/course_overview/block_course_overview.php
blocks/course_overview/lang/en/block_course_overview.php
blocks/course_overview/module.js
blocks/course_overview/move.php
blocks/course_overview/renderer.php
blocks/course_overview/styles.css
blocks/course_overview/version.php
blocks/glossary_random/block_glossary_random.php
blocks/glossary_random/db/access.php
blocks/glossary_random/db/upgrade.php [deleted file]
blocks/glossary_random/lang/en/block_glossary_random.php
blocks/glossary_random/version.php
blocks/mentees/block_mentees.php
blocks/mentees/db/access.php
blocks/mentees/db/upgrade.php [deleted file]
blocks/mentees/lang/en/block_mentees.php
blocks/mentees/version.php
blocks/news_items/block_news_items.php
blocks/news_items/db/access.php
blocks/news_items/lang/en/block_news_items.php
blocks/news_items/version.php
blocks/online_users/block_online_users.php
blocks/online_users/db/access.php
blocks/online_users/db/upgrade.php [deleted file]
blocks/online_users/lang/en/block_online_users.php
blocks/online_users/version.php
blocks/rss_client/editfeed.php
blocks/rss_client/managefeeds.php
blocks/rss_client/viewfeed.php
blocks/section_links/block_section_links.php
blocks/section_links/config_instance.html [deleted file]
blocks/section_links/db/upgrade.php [moved from blocks/news_items/db/upgrade.php with 51% similarity]
blocks/section_links/edit_form.php [new file with mode: 0644]
blocks/section_links/lang/en/block_section_links.php
blocks/section_links/renderer.php [new file with mode: 0644]
blocks/section_links/settings.php
blocks/section_links/version.php
blocks/tests/behat/add_blocks.feature [new file with mode: 0644]
blocks/tests/behat/behat_blocks.php [new file with mode: 0644]
cache/admin.php
cache/classes/factory.php
cache/locallib.php
composer.json
config-dist.php
course/category.php
course/lib.php
course/reset_form.php
course/search.php
course/tests/behat/add_activities.feature [new file with mode: 0644]
course/tests/behat/behat_course.php [new file with mode: 0644]
course/tests/externallib_test.php
course/yui/toolboxes/toolboxes.js
enrol/flatfile/lib.php
enrol/ldap/tests/ldap_test.php
enrol/paypal/lib.php
enrol/self/lib.php
enrol/yui/rolemanager/rolemanager.js
index.php
lang/en/admin.php
lang/en/cache.php
lang/en/form.php
lang/en/message.php
lang/en/question.php
lang/en/role.php
lib/adminlib.php
lib/behat/behat_base.php [new file with mode: 0644]
lib/behat/classes/behat_command.php [new file with mode: 0644]
lib/behat/classes/behat_config_manager.php [new file with mode: 0644]
lib/behat/classes/util.php [new file with mode: 0644]
lib/behat/features/bootstrap/behat_init_context.php [new file with mode: 0644]
lib/behat/form_field/behat_form_editor.php [new file with mode: 0644]
lib/behat/form_field/behat_form_field.php [new file with mode: 0644]
lib/behat/form_field/behat_form_select.php [new file with mode: 0644]
lib/behat/lib.php [new file with mode: 0644]
lib/csslib.php
lib/db/caches.php
lib/dml/mssql_native_moodle_recordset.php
lib/dml/mysqli_native_moodle_recordset.php
lib/dml/oci_native_moodle_recordset.php
lib/dml/pgsql_native_moodle_recordset.php
lib/dml/sqlsrv_native_moodle_recordset.php
lib/editor/tinymce/editor_styles.css [deleted file]
lib/editor/tinymce/lib.php
lib/editor/tinymce/styles.css [new file with mode: 0644]
lib/editor/tinymce/yui/collapse/collapse.js [new file with mode: 0644]
lib/enrollib.php
lib/filestorage/tests/zip_packer_test.php
lib/filestorage/zip_archive.php
lib/form/editor.php
lib/javascript-static.js
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/phpunit/bootstrap.php
lib/pluginlib.php
lib/sessionlib.php
lib/setup.php
lib/setuplib.php
lib/testing/classes/tests_finder.php
lib/testing/generator/data_generator.php
lib/tests/behat/behat_data_generators.php [new file with mode: 0644]
lib/tests/behat/behat_forms.php [new file with mode: 0644]
lib/tests/behat/behat_general.php [new file with mode: 0644]
lib/tests/behat/behat_hooks.php [new file with mode: 0644]
lib/tests/behat/behat_navigation.php [new file with mode: 0644]
lib/yui/chooserdialogue/chooserdialogue.js
message/index.php
message/lib.php
message/tests/externallib_test.php
mod/feedback/analysis.php
mod/feedback/analysis_course.php
mod/feedback/lib.php
mod/forum/lib.php
mod/glossary/editcategories.html
mod/glossary/editcategories.php
mod/glossary/formats.php
mod/lesson/format.php
mod/quiz/edit.php
mod/quiz/styles.css
mod/quiz/tests/reportlib_test.php [moved from mod/quiz/report/tests/reportlib_test.php with 100% similarity]
mod/scorm/player.php
mod/wiki/pagelib.php
phpunit.xml.dist
question/format/blackboard_six/formatqti.php
question/format/blackboard_six/lang/en/qformat_blackboard_six.php
question/format/examview/format.php
question/format/learnwise/format.php
question/format/missingword/format.php
question/format/missingword/lang/en/qformat_missingword.php
question/format/missingword/tests/fixtures/question.missingword1.txt [new file with mode: 0644]
question/format/missingword/tests/fixtures/question.missingword2.txt [new file with mode: 0644]
question/format/missingword/tests/fixtures/question.missingword3.txt [new file with mode: 0644]
question/format/xhtml/format.php
question/type/edit_question_form.php
question/type/match/backup/moodle1/lib.php
question/type/match/backup/moodle2/backup_qtype_match_plugin.class.php
question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php
question/type/match/db/install.xml
question/type/match/db/upgrade.php
question/type/match/db/upgradelib.php
question/type/match/edit_match_form.php
question/type/match/lang/en/qtype_match.php
question/type/match/question.php
question/type/match/questiontype.php
question/type/match/renderer.php
question/type/match/tests/question_test.php
question/type/match/tests/questiontype_test.php
question/type/match/tests/upgradelibnewqe_test.php
question/type/match/tests/walkthrough_test.php
question/type/match/version.php
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/renderer.php
question/type/numerical/question.php
question/type/numerical/questiontype.php
question/type/numerical/tests/answerprocessor_test.php
report/completion/user.php

index e27bed2..e1cc233 100644 (file)
@@ -30,3 +30,4 @@ phpunit.xml
 composer.phar
 composer.lock
 /vendor/
+/behat.yml
index 01bfcc6..b267a03 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -16,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Enable or disable maintenance mode
+ * Enable or disable maintenance mode.
  *
  * @package    core
  * @subpackage cli
 
 define('CLI_SCRIPT', true);
 
-require(dirname(dirname(dirname(__FILE__))).'/config.php');
-require_once($CFG->libdir.'/clilib.php');      // cli only functions
+require(__DIR__.'/../../config.php');
+require_once("$CFG->libdir/clilib.php");
+require_once("$CFG->libdir/adminlib.php");
 
 
-// now get cli options
-list($options, $unrecognized) = cli_get_params(array('enable'=>false, 'disable'=>false, 'help'=>false),
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(array('enable'=>false, 'enablelater'=>0, 'enableold'=>false, 'disable'=>false, 'help'=>false),
                                                array('h'=>'help'));
 
 if ($unrecognized) {
@@ -45,12 +45,14 @@ if ($options['help']) {
 Current status displayed if not option specified.
 
 Options:
---enable              Enable maintenance mode
+--enable              Enable CLI maintenance mode
+--enablelater=MINUTES Number of minutes before entering CLI maintenance mode
+--enableold           Enable legacy half-maintenance mode
 --disable             Disable maintenance mode
 -h, --help            Print out this help
 
 Example:
-\$sudo -u www-data /usr/bin/php admin/cli/maintenance.php
+\$ sudo -u www-data /usr/bin/php admin/cli/maintenance.php
 "; //TODO: localize - to be translated later when everything is finished
 
     echo $help;
@@ -59,18 +61,52 @@ Example:
 
 cli_heading(get_string('sitemaintenancemode', 'admin')." ($CFG->wwwroot)");
 
-if ($options['enable']) {
+if ($options['enablelater']) {
+    if (file_exists("$CFG->dataroot/climaintenance.html")) {
+        // Already enabled, sorry.
+        echo get_string('clistatusenabled', 'admin')."\n";
+        return 1;
+    }
+
+    $time = time() + ($options['enablelater']*60);
+    set_config('maintenance_later', $time);
+
+    echo get_string('clistatusenabledlater', 'admin', userdate($time))."\n";
+    return 0;
+
+} else if ($options['enable']) {
+    if (file_exists("$CFG->dataroot/climaintenance.html")) {
+        // The maintenance is already enabled, nothing to do.
+    } else {
+        enable_cli_maintenance_mode();
+    }
+    set_config('maintenance_enabled', 0);
+    unset_config('maintenance_later');
+    echo get_string('sitemaintenanceoncli', 'admin')."\n";
+    exit(0);
+
+} else if ($options['enableold']) {
     set_config('maintenance_enabled', 1);
+    unset_config('maintenance_later');
     echo get_string('sitemaintenanceon', 'admin')."\n";
     exit(0);
+
 } else if ($options['disable']) {
     set_config('maintenance_enabled', 0);
+    unset_config('maintenance_later');
+    if (file_exists("$CFG->dataroot/climaintenance.html")) {
+        unlink("$CFG->dataroot/climaintenance.html");
+    }
     echo get_string('sitemaintenanceoff', 'admin')."\n";
     exit(0);
 }
 
-if (!empty($CFG->maintenance_enabled)) {
+if (!empty($CFG->maintenance_enabled) or file_exists("$CFG->dataroot/climaintenance.html")) {
     echo get_string('clistatusenabled', 'admin')."\n";
+
+} else if (isset($CFG->maintenance_later)) {
+    echo get_string('clistatusenabledlater', 'admin', userdate($CFG->maintenance_later))."\n";
+
 } else {
     echo get_string('clistatusdisabled', 'admin')."\n";
 }
index 60acdf9..45dccf6 100644 (file)
@@ -661,7 +661,7 @@ class define_role_table_advanced extends capability_table_with_risks {
     public function make_copy() {
         $this->roleid = 0;
         unset($this->role->id);
-        $this->role->name .= ' ' . get_string('copyasnoun');
+        $this->role->name = role_get_name($this->role, null, ROLENAME_ORIGINAL) . ' ' . get_string('copyasnoun');
         $this->role->shortname .= 'copy';
     }
 
@@ -738,6 +738,56 @@ class define_role_table_advanced extends capability_table_with_risks {
         return $output;
     }
 
+    /**
+     * Returns an array of roles of the allowed type.
+     *
+     * @param string $type Must be one of: assign, switch, or override.
+     * @return array
+     */
+    protected function get_allow_roles_list($type) {
+        global $DB;
+
+        if ($type !== 'assign' and $type !== 'switch' and $type !== 'override') {
+            debugging('Invalid role allowed type specified', DEBUG_DEVELOPER);
+            return array();
+        }
+
+        if (empty($this->roleid)) {
+            return array();
+        }
+
+        $sql = "SELECT r.*
+                  FROM {role} r
+                  JOIN {role_allow_{$type}} a ON a.allow{$type} = r.id
+                 WHERE a.roleid = :roleid
+              ORDER BY r.sortorder ASC";
+        return $DB->get_records_sql($sql, array('roleid'=>$this->roleid));
+    }
+
+    /**
+     * Returns an array of roles with the allowed type.
+     *
+     * @param string $type Must be one of: assign, switch, or override.
+     * @return array Am array of role names with the allowed type
+     */
+    protected function get_allow_role_control($type) {
+        if ($roles = $this->get_allow_roles_list($type)) {
+            $roles = role_fix_names($roles, null, ROLENAME_ORIGINAL, true);
+            return implode(', ', $roles);
+        } else {
+            return get_string('none');
+        }
+    }
+
+    /**
+     * Returns information about the risks associated with a role.
+     *
+     * @return string
+     */
+    protected function get_role_risks_info() {
+        return '';
+    }
+
     protected function print_field($name, $caption, $field) {
         global $OUTPUT;
         // Attempt to generate HTML like formslib.
@@ -781,6 +831,12 @@ class define_role_table_advanced extends capability_table_with_risks {
         $this->print_field('edit-description', get_string('customroledescription', 'role').'&nbsp;'.$OUTPUT->help_icon('customroledescription', 'role'), $this->get_description_field('description'));
         $this->print_field('menuarchetype', get_string('archetype', 'role').'&nbsp;'.$OUTPUT->help_icon('archetype', 'role'), $this->get_archetype_field('archetype'));
         $this->print_field('', get_string('maybeassignedin', 'role'), $this->get_assignable_levels_control());
+        $this->print_field('', get_string('allowassign', 'role'), $this->get_allow_role_control('assign'));
+        $this->print_field('', get_string('allowoverride', 'role'), $this->get_allow_role_control('override'));
+        $this->print_field('', get_string('allowswitch', 'role'), $this->get_allow_role_control('switch'));
+        if ($risks = $this->get_role_risks_info()) {
+            $this->print_field('', get_string('rolerisks', 'role'), $risks);
+        }
         echo "</div>";
 
         $this->print_show_hide_advanced_button();
@@ -882,6 +938,57 @@ class view_role_definition_table extends define_role_table_advanced {
         // Do nothing.
     }
 
+    /**
+     * Returns HTML risk icons.
+     *
+     * @return string
+     */
+    protected function get_role_risks_info() {
+        global $OUTPUT;
+
+        if (empty($this->roleid)) {
+            return '';
+        }
+
+        $risks = array();
+        $allrisks = get_all_risks();
+        foreach ($this->capabilities as $capability) {
+            $perm = $this->permissions[$capability->name];
+            if ($perm != CAP_ALLOW) {
+                continue;
+            }
+            foreach ($allrisks as $type=>$risk) {
+                if ($risk & (int)$capability->riskbitmask) {
+                    $risks[$type] = $risk;
+                }
+            }
+        }
+
+        $risksurl = new moodle_url(get_docs_url(s(get_string('risks', 'role'))));
+        foreach ($risks as $type=>$risk) {
+            $pixicon = new pix_icon('/i/' . str_replace('risk', 'risk_', $type), get_string($type . 'short', 'admin'));
+            $risks[$type] = $OUTPUT->action_icon($risksurl, $pixicon, new popup_action('click', $risksurl));
+        }
+
+        return implode(' ', $risks);
+    }
+
+    /**
+     * Returns true if the row should be skipped.
+     *
+     * @param string $capability
+     * @return bool
+     */
+    protected function skip_row($capability) {
+        $perm = $this->permissions[$capability->name];
+        if ($perm == CAP_INHERIT) {
+            // Do not print empty rows in role overview, admins need to know quickly what is allowed and prohibited,
+            // if they want to see the list of all capabilities they can go to edit role page.
+            return true;
+        }
+        parent::skip_row($capability);
+    }
+
     protected function add_permission_cells($capability) {
         $perm = $this->permissions[$capability->name];
         $permname = $this->allpermissions[$perm];
diff --git a/admin/tool/behat/cli/util.php b/admin/tool/behat/cli/util.php
new file mode 100644 (file)
index 0000000..4fd147f
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * CLI tool with utilities to manage Behat integration in Moodle
+ *
+ * All CLI utilities uses $CFG->behat_dataroot and $CFG->prefix_dataroot as
+ * $CFG->dataroot and $CFG->prefix
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+if (isset($_SERVER['REMOTE_ADDR'])) {
+    die(); // No access from web!.
+}
+
+// Basic functions.
+require_once(__DIR__ . '/../../../../lib/clilib.php');
+require_once(__DIR__ . '/../../../../lib/behat/lib.php');
+
+
+// CLI options.
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'help'    => false,
+        'install' => false,
+        'drop'    => false,
+        'enable'  => false,
+        'disable' => false,
+    ),
+    array(
+        'h' => 'help'
+    )
+);
+
+
+// Checking util.php CLI script usage.
+$help = "
+Behat utilities to manage the test environment
+
+Options:
+--install  Installs the test environment for acceptance tests
+--drop     Drops the database tables and the dataroot contents
+--enable   Enables test environment and updates tests list
+--disable  Disables test environment
+
+-h, --help     Print out this help
+
+Example from Moodle root directory:
+\$ php admin/tool/behat/cli/util.php --enable
+
+More info in http://docs.moodle.org/dev/Acceptance_testing#Running_tests
+";
+
+if (!empty($options['help'])) {
+    echo $help;
+    exit(0);
+}
+
+
+// Checking $CFG->behat_* vars and values.
+define('BEHAT_UTIL', true);
+define('CLI_SCRIPT', true);
+define('ABORT_AFTER_CONFIG', true);
+define('NO_OUTPUT_BUFFERING', true);
+
+error_reporting(E_ALL | E_STRICT);
+ini_set('display_errors', '1');
+ini_set('log_errors', '1');
+
+require_once(__DIR__ . '/../../../../config.php');
+
+// CFG->behat_prefix must be set and with value different than CFG->prefix and phpunit_prefix.
+if (!isset($CFG->behat_prefix) ||
+   (isset($CFG->behat_prefix) &&
+       ($CFG->behat_prefix == $CFG->prefix ||
+       $CFG->behat_prefix == $CFG->phpunit_prefix))) {
+    behat_error(BEHAT_EXITCODE_CONFIG,
+        'Define $CFG->behat_prefix in config.php with a value different than $CFG->prefix and $CFG->phpunit_prefix');
+}
+
+// CFG->behat_dataroot must be set and with value different than CFG->dataroot and phpunit_dataroot.
+if (!isset($CFG->behat_dataroot) ||
+   (isset($CFG->behat_dataroot) &&
+       ($CFG->behat_dataroot == $CFG->dataroot ||
+       $CFG->behat_dataroot == $CFG->phpunit_dataroot))) {
+    behat_error(BEHAT_EXITCODE_CONFIG,
+        'Define $CFG->behat_dataroot in config.php with a value different than $CFG->dataroot and $CFG->phpunit_dataroot');
+}
+
+// Create behat_dataroot if it doesn't exists.
+if (!file_exists($CFG->behat_dataroot)) {
+    if (!mkdir($CFG->behat_dataroot, $CFG->directorypermissions)) {
+        behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
+    }
+}
+if (!is_dir($CFG->behat_dataroot) || !is_writable($CFG->behat_dataroot)) {
+    behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory has no permissions or is not a directory');
+}
+
+// Check that the directory does not contains other things.
+if (!file_exists("$CFG->behat_dataroot/behattestdir.txt")) {
+    if ($dh = opendir($CFG->behat_dataroot)) {
+        while (($file = readdir($dh)) !== false) {
+            if ($file === 'behat' or $file === '.' or $file === '..' or $file === '.DS_Store') {
+                continue;
+            }
+            behat_error(BEHAT_EXITCODE_CONFIG, '$CFG->behat_dataroot directory is not empty, ensure this is the directory where you want to install behat test dataroot');
+        }
+        closedir($dh);
+        unset($dh);
+        unset($file);
+    }
+
+    // Now we create dataroot directory structure for behat tests.
+    testing_initdataroot($CFG->behat_dataroot, 'behat');
+}
+
+// Overrides vars with behat-test ones.
+$vars = array('wwwroot', 'prefix', 'dataroot');
+foreach ($vars as $var) {
+    $CFG->{$var} = $CFG->{'behat_' . $var};
+}
+
+$CFG->noemailever = true;
+$CFG->passwordsaltmain = 'moodle';
+
+// Continues setup.
+define('ABORT_AFTER_CONFIG_CANCEL', true);
+require("$CFG->dirroot/lib/setup.php");
+
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/upgradelib.php');
+require_once($CFG->libdir.'/clilib.php');
+require_once($CFG->libdir.'/pluginlib.php');
+require_once($CFG->libdir.'/installlib.php');
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+// Behat utilities.
+require_once($CFG->libdir . '/behat/classes/util.php');
+require_once($CFG->libdir . '/behat/classes/behat_command.php');
+
+// Run command (only one per time).
+if ($options['install']) {
+    behat_util::install_site();
+    mtrace("Acceptance tests site installed");
+} else if ($options['drop']) {
+    behat_util::drop_site();
+    mtrace("Acceptance tests site dropped");
+} else if ($options['enable']) {
+    behat_util::start_test_mode();
+    $runtestscommand = behat_command::get_behat_command() . ' --config '
+        . $CFG->behat_dataroot . DIRECTORY_SEPARATOR . 'behat' . DIRECTORY_SEPARATOR . 'behat.yml';
+    mtrace("Acceptance tests environment enabled, to run the tests use:\n " . $runtestscommand);
+} else if ($options['disable']) {
+    behat_util::stop_test_mode();
+    mtrace("Acceptance tests environment disabled");
+} else {
+    echo $help;
+}
+
+exit(0);
diff --git a/admin/tool/behat/index.php b/admin/tool/behat/index.php
new file mode 100644 (file)
index 0000000..f68ec13
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * Web interface to list and filter steps
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/behat/locallib.php');
+require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
+
+$filter = optional_param('filter', '', PARAM_ALPHANUMEXT);
+$type = optional_param('type', false, PARAM_ALPHAEXT);
+$component = optional_param('component', '', PARAM_ALPHAEXT);
+
+admin_externalpage_setup('toolbehat');
+
+// Getting available steps definitions from behat.
+$steps = tool_behat::stepsdefinitions($type, $component, $filter);
+
+// Form.
+$componentswithsteps = array('' => get_string('allavailablesteps', 'tool_behat'));
+
+// Complete the components list with the moodle steps definitions.
+$components = behat_config_manager::get_components_steps_definitions();
+if ($components) {
+    foreach ($components as $component => $filepath) {
+        // TODO Use a class static attribute instead of the class name.
+        $componentswithsteps[$component] = 'Moodle ' . substr($component, 6);
+    }
+}
+$form = new steps_definitions_form(null, array('components' => $componentswithsteps));
+
+// Output contents.
+$renderer = $PAGE->get_renderer('tool_behat');
+echo $renderer->render_stepsdefinitions($steps, $form);
+
diff --git a/admin/tool/behat/lang/en/tool_behat.php b/admin/tool/behat/lang/en/tool_behat.php
new file mode 100644 (file)
index 0000000..8bb1f1d
--- /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/>.
+
+/**
+ * Strings for tool_behat
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['allavailablesteps'] = 'All the available steps definitions';
+$string['giveninfo'] = 'Given. Processes to set up the environment';
+$string['installinfo'] = 'Read {$a} for installation and tests execution info';
+$string['moreinfoin'] = 'More info in {$a}';
+$string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
+$string['newtestsinfo'] = 'Read {$a} for info about how to write new tests';
+$string['nostepsdefinitions'] = 'There aren\'t steps definitions matching this filters';
+$string['pluginname'] = 'Acceptance testing';
+$string['runclitool'] = 'To list the steps definitions you need to run the Behat CLI tool to create the $CFG->behat_dataroot directory. Go to your moodle dirroot and run "{$a}"';
+$string['stepsdefinitionscomponent'] = 'Area';
+$string['stepsdefinitionscontains'] = 'Contains';
+$string['stepsdefinitionsfilters'] = 'Steps definitions';
+$string['stepsdefinitionstype'] = 'Type';
+$string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected ones';
+$string['viewsteps'] = 'Filter';
+$string['wheninfo'] = 'When. Actions that provokes an event';
+$string['wrongbehatsetup'] = 'Something is wrong with the setup, ensure you ran the composer installer and vendor/bin/behat file has execution permissions';
diff --git a/admin/tool/behat/locallib.php b/admin/tool/behat/locallib.php
new file mode 100644 (file)
index 0000000..33a5fd8
--- /dev/null
@@ -0,0 +1,84 @@
+<?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/>.
+
+/**
+ * Behat commands
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/behat/classes/behat_command.php');
+require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/behat/steps_definitions_form.php');
+
+/**
+ * Behat commands manager
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_behat {
+
+    /**
+     * Lists the available steps definitions
+     *
+     * @param string $type
+     * @param string $component
+     * @param string $filter
+     * @return string
+     */
+    public static function stepsdefinitions($type, $component, $filter) {
+
+        // We don't require the test environment to be enabled to list the steps definitions
+        // so test writers can more easily set up the environment.
+        behat_command::check_behat_setup();
+
+        // The loaded steps depends on the component specified.
+        behat_config_manager::update_config_file($component, false);
+
+        // The Moodle\BehatExtension\HelpPrinter\MoodleDefinitionsPrinter will parse this search format.
+        if ($type) {
+            $filter .= '&&' . $type;
+        }
+
+        if ($filter) {
+            $filteroption = ' -d "' . $filter . '"';
+        } else {
+            $filteroption = ' -di';
+        }
+
+        // Get steps definitions from Behat.
+        $options = ' --config="'.behat_config_manager::get_steps_list_config_filepath(). '" '.$filteroption;
+        list($steps, $code) = behat_command::run($options);
+
+        if ($steps) {
+            $stepshtml = implode('', $steps);
+        }
+
+        if (empty($stepshtml)) {
+            $stepshtml = get_string('nostepsdefinitions', 'tool_behat');
+        }
+
+        return $stepshtml;
+    }
+
+}
diff --git a/admin/tool/behat/renderer.php b/admin/tool/behat/renderer.php
new file mode 100644 (file)
index 0000000..1a70fce
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Behat tool renderer
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/behat/classes/behat_command.php');
+
+/**
+ * Renderer for behat tool web features
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_behat_renderer extends plugin_renderer_base {
+
+    /**
+     * Renders the list of available steps according to the submitted filters
+     *
+     * @param string     $stepsdefinitions HTML from behat with the available steps
+     * @param moodleform $form
+     * @return string HTML code
+     */
+    public function render_stepsdefinitions($stepsdefinitions, $form) {
+
+        $title = get_string('pluginname', 'tool_behat');
+
+        // Header.
+        $html = $this->output->header();
+        $html .= $this->output->heading($title);
+
+        // Info.
+        $installurl = behat_command::DOCS_URL . '#Installation';
+        $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
+        $writetestsurl = behat_command::DOCS_URL . '#Writting_features';
+        $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
+        $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
+        $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
+        $infos = array(
+            get_string('installinfo', 'tool_behat', $installlink),
+            get_string('newtestsinfo', 'tool_behat', $writetestslink),
+            get_string('newstepsinfo', 'tool_behat', $writestepslink)
+        );
+
+        // List of steps
+        $html .= $this->output->box_start();
+        $html .= html_writer::tag('h1', 'Info');
+        $html .= html_writer::empty_tag('div');
+        $html .= html_writer::empty_tag('ul');
+        $html .= html_writer::empty_tag('li');
+        $html .= implode(html_writer::end_tag('li') . html_writer::empty_tag('li'), $infos);
+        $html .= html_writer::end_tag('li');
+        $html .= html_writer::end_tag('ul');
+        $html .= html_writer::end_tag('div');
+        $html .= $this->output->box_end();
+
+        // Form.
+        ob_start();
+        $form->display();
+        $html .= ob_get_contents();
+        ob_end_clean();
+
+        // Steps definitions.
+        $html .= html_writer::tag('div', $stepsdefinitions, array('class' => 'steps-definitions'));
+
+        $html .= $this->output->footer();
+
+        return $html;
+    }
+}
diff --git a/admin/tool/behat/settings.php b/admin/tool/behat/settings.php
new file mode 100644 (file)
index 0000000..7f4b5a0
--- /dev/null
@@ -0,0 +1,31 @@
+<?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/>.
+
+/**
+ * Adds behat tests link in admin tree
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+    $url = $CFG->wwwroot . '/' . $CFG->admin . '/tool/behat/index.php';
+    $ADMIN->add('development', new admin_externalpage('toolbehat', get_string('pluginname', 'tool_behat'), $url));
+}
diff --git a/admin/tool/behat/steps_definitions_form.php b/admin/tool/behat/steps_definitions_form.php
new file mode 100644 (file)
index 0000000..c6b06f3
--- /dev/null
@@ -0,0 +1,67 @@
+<?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 form
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Form to display the available steps definitions
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class steps_definitions_form extends moodleform {
+
+    /**
+     * Form definition
+     * @return void
+     */
+    public function definition() {
+
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'filters', get_string('stepsdefinitionsfilters', 'tool_behat'));
+
+        $types = array(
+            '' => get_string('allavailablesteps', 'tool_behat'),
+            'given' => get_string('giveninfo', 'tool_behat'),
+            'when' => get_string('wheninfo', 'tool_behat'),
+            'then' => get_string('theninfo', 'tool_behat')
+        );
+        $mform->addElement('select', 'type', get_string('stepsdefinitionstype', 'tool_behat'), $types);
+
+        $mform->addElement(
+            'select',
+            'component',
+            get_string('stepsdefinitionscomponent', 'tool_behat'),
+            $this->_customdata['components']
+        );
+
+        $mform->addElement('text', 'filter', get_string('stepsdefinitionscontains', 'tool_behat'));
+
+        $mform->addElement('submit', 'submit', get_string('viewsteps', 'tool_behat'));
+    }
+}
diff --git a/admin/tool/behat/styles.css b/admin/tool/behat/styles.css
new file mode 100644 (file)
index 0000000..d7ca4d1
--- /dev/null
@@ -0,0 +1,5 @@
+.steps-definitions{border-style:solid;border-width:1px;border-color:#BBB;padding:5px;margin:auto;width:50%;}
+.steps-definitions .step{margin: 10px 0px 10px 0px;}
+.steps-definitions .stepdescription{color:#bf8c12;}
+.steps-definitions .steptype{color:#1467a6;margin-right: 5px;}
+.steps-definitions .stepregex{color:#060;}
diff --git a/admin/tool/behat/tests/behat/data_generators.feature b/admin/tool/behat/tests/behat/data_generators.feature
new file mode 100644 (file)
index 0000000..19aeb51
--- /dev/null
@@ -0,0 +1,109 @@
+@tool_behat
+Feature: Set up contextual data for tests
+  In order to write tests quickly
+  As a moodle developer
+  I need to fill the database with fixtures
+
+  Scenario: Add a bunch of users
+    Given the following "users" exists:
+      | username  | password  | firstname | lastname |
+      | testuser  | testuser  |  |  |
+      | testuser2 | testuser2 | TestFirstname | TestLastname |
+    And I log in as "testuser"
+    And I log out
+    When I log in as "testuser2"
+    Then I should see "TestFirstname"
+
+  @javascript
+  Scenario: Add a bunch of courses and categories
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | CAT1 | CAT2 |
+      | Cat 3 | CAT1 | CAT3 |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | COURSE1 | CAT3 |
+      | Course 2 | COURSE2 | CAT3 |
+      | Course 3 | COURSE3 | 0 |
+    When I log in as "admin"
+    Then I should see "Course 1"
+    And I should see "Course 2"
+    And I should see "Course 3"
+
+  @javascript
+  Scenario: Add a bunch of groups and groupings
+    Given the following "courses" exists:
+      | fullname | shortname |
+      | Course 1 | C1 |
+    And the following "groups" exists:
+      | name | course | idnumber |
+      | Group 1 | C1 | G1 |
+      | Group 2 | C1 | G2 |
+    And the following "groupings" exists:
+      | name | course | idnumber |
+      | Grouping 1 | C1 | GG1 |
+      | Grouping 2 | C1 | GG2 |
+    When I log in as "admin"
+    And I follow "Course 1"
+    And I expand "Users" node
+    And I follow "Groups"
+    Then I should see "Group 1"
+    And I should see "Group 2"
+    And I follow "Groupings"
+    And I should see "Grouping 1"
+    And I should see "Grouping 2"
+
+  Scenario: Add course enrolments
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | 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 |
+      | student1 | C1 | student |
+    When I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "Topic 1"
+
+  @javascript
+  Scenario: Add relations between users and groups
+    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 |
+      | Course 1 | C1 |
+    And the following "groups" exists:
+      | name | course | idnumber |
+      | Group 1 | C1 | G1 |
+      | Group 2 | C1 | G2 |
+    And the following "groupings" exists:
+      | name | course | idnumber |
+      | Grouping 1 | C1 | GG1 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And the following "group members" exists:
+      | user | group |
+      | student1 | G1 |
+      | student2 | G2 |
+    And the following "grouping groups" exists:
+      | grouping | group |
+      | GG1 | G1 |
+    When I log in as "admin"
+    And I follow "Course 1"
+    And I expand "Users" node
+    And I follow "Groups"
+    Then the "groups" select box should contain "Group 1 (1)"
+    And the "groups" select box should contain "Group 2 (1)"
+    And I select "Group 1 (1)" from "groups"
+    And I wait "1" seconds
+    And the "members" select box should contain "Student 1"
+    And I select "Group 2 (1)" from "groups"
+    And I wait "1" seconds
+    And the "members" select box should contain "Student 2"
diff --git a/admin/tool/behat/tests/behat/list_steps.feature b/admin/tool/behat/tests/behat/list_steps.feature
new file mode 100644 (file)
index 0000000..264c754
--- /dev/null
@@ -0,0 +1,31 @@
+@tool_behat
+Feature: List the system steps definitions
+  In order to create new tests
+  As a tests writer
+  I need to list and filter the system steps definitions
+
+  Background:
+    Given I am on homepage
+    And I log in as "admin"
+    And I expand "Site administration" node
+    And I expand "Development" node
+    And  I follow "Acceptance testing"
+
+  @javascript
+  Scenario: Accessing the list
+    Then I should see "Steps definitions"
+    And I should not see "There aren't steps definitions matching this filter"
+
+  @javascript
+  Scenario: Filtering by type
+    Given I select "Then. Checkings to ensure the outcomes are the expected ones" from "Type"
+    When I press "Filter"
+    Then I should see "Checks, that page contains specified text."
+    And I should not see "Opens Moodle homepage."
+
+  @javascript
+  Scenario: Filtering by keyword
+    Given I fill in "Contains" with "homepage"
+    When I press "Filter"
+    Then I should see "Opens Moodle homepage."
+
diff --git a/admin/tool/behat/tests/behat/test_environment.feature b/admin/tool/behat/tests/behat/test_environment.feature
new file mode 100644 (file)
index 0000000..53d9ee8
--- /dev/null
@@ -0,0 +1,9 @@
+@tool_behat
+Feature: Set up the testing environment
+  In order to execute automated acceptance tests
+  As a moodle developer
+  I need to use the test environment instead of the regular environment
+
+  Scenario: Accessing the site
+    When I am on homepage
+    Then I should see "Acceptance test site"
diff --git a/admin/tool/behat/tests/tool_behat_test.php b/admin/tool/behat/tests/tool_behat_test.php
new file mode 100644 (file)
index 0000000..4c9ae8f
--- /dev/null
@@ -0,0 +1,181 @@
+<?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/>.
+
+/**
+ * Unit tests for admin/tool/behat.
+ *
+ * @package   tool_behat
+ * @copyright  2012 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/' . $CFG->admin .'/tool/behat/locallib.php');
+require_once($CFG->libdir . '/behat/classes/util.php');
+require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
+
+/**
+ * Allows access to internal methods without exposing them.
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_behat_config_manager extends behat_config_manager {
+
+    /**
+     * Allow access to protected method
+     * @see parent::merge_config()
+     * @param mixed $config
+     * @param mixed $localconfig
+     * @return mixed
+     */
+    public static function merge_config($config, $localconfig) {
+        return parent::merge_config($config, $localconfig);
+    }
+
+    /**
+     * Allow access to protected method
+     * @see parent::get_config_file_contents()
+     * @param string $prefix
+     * @param array $features
+     * @param array $stepsdefinitions
+     * @return string
+     */
+    public static function get_config_file_contents($features, $stepsdefinitions) {
+        return parent::get_config_file_contents($features, $stepsdefinitions);
+    }
+}
+
+/**
+ * Tool behat tests.
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_behat_testcase extends advanced_testcase {
+
+    /**
+     * behat_config_manager tests.
+     */
+    public function test_merge_configs() {
+
+        // Simple default config.
+        $array1 = array(
+            'the' => 'same',
+            'simple' => 'value',
+            'array' => array(
+                'one' => 'arrayvalue1',
+                'two' => 'arrayvalue2'
+            )
+        );
+
+        // Simple override.
+        $array2 = array(
+            'simple' => 'OVERRIDDEN1',
+            'array' => array(
+                'one' => 'OVERRIDDEN2'
+            ),
+            'newprofile' => array(
+                'anotherlevel' => array(
+                    'andanotherone' => array(
+                        'list1',
+                        'list2'
+                    )
+                )
+            )
+        );
+
+        $array = testable_behat_config_manager::merge_config($array1, $array2);
+
+        // Overriddes are applied.
+        $this->assertEquals('OVERRIDDEN1', $array['simple']);
+        $this->assertEquals('OVERRIDDEN2', $array['array']['one']);
+
+        // Other values are respected.
+        $this->assertNotEmpty($array['array']['two']);
+
+        // Completely new nodes are added.
+        $this->assertNotEmpty($array['newprofile']);
+        $this->assertNotEmpty($array['newprofile']['anotherlevel']['andanotherone']);
+        $this->assertEquals('list1', $array['newprofile']['anotherlevel']['andanotherone'][0]);
+        $this->assertEquals('list2', $array['newprofile']['anotherlevel']['andanotherone'][1]);
+
+        // Complex override changing vectors to scalars and scalars to vectors.
+        $array2 = array(
+            'simple' => array(
+                'simple' => 'should',
+                'be' => 'overridden',
+                'by' => 'this-array'
+            ),
+            'array' => 'one'
+        );
+
+        $array = testable_behat_config_manager::merge_config($array1, $array2);
+
+        // Overrides applied.
+        $this->assertNotEmpty($array['simple']);
+        $this->assertNotEmpty($array['array']);
+        $this->assertTrue(is_array($array['simple']));
+        $this->assertFalse(is_array($array['array']));
+
+        // Other values are maintained.
+        $this->assertEquals('same', $array['the']);
+    }
+
+    /**
+     * behat_config_manager tests.
+     */
+    public function test_config_file_contents() {
+        global $CFG;
+
+        // Skip tests if behat is not installed.
+        $vendorpath = $CFG->dirroot . '/vendor';
+        if (!file_exists($vendorpath . '/autoload.php') || !is_dir($vendorpath . '/behat')) {
+            $this->markTestSkipped('Behat not installed.');
+        }
+
+        // To avoid user value at config.php level.
+        unset($CFG->behat_config);
+
+        // List.
+        $features = array(
+            'feature1',
+            'feature2',
+            'feature3'
+        );
+
+        // Associative array.
+        $stepsdefinitions = array(
+            'micarro' => '/me/lo/robaron',
+            'anoche' => '/cuando/yo/dormia'
+        );
+
+        $contents = testable_behat_config_manager::get_config_file_contents($features, $stepsdefinitions);
+
+        $this->assertContains('features: ' . $CFG->dirroot . '/lib/behat/features', $contents);
+        $this->assertContains('micarro: /me/lo/robaron', $contents);
+        $this->assertContains('base_url: \'' . $CFG->behat_wwwroot . '\'', $contents);
+        $this->assertContains('class: behat_init_context', $contents);
+        $this->assertContains('- feature1', $contents);
+        $this->assertContains('- feature3', $contents);
+    }
+
+}
+
diff --git a/admin/tool/behat/version.php b/admin/tool/behat/version.php
new file mode 100644 (file)
index 0000000..a5ddb15
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * tool_behat version info
+ *
+ * @package    tool_behat
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2012120700;
+$plugin->requires  = 2012120300; // Requires Moodle 2.5.
+$plugin->component = 'tool_behat';
diff --git a/auth/ldap/tests/ldap_test.php b/auth/ldap/tests/ldap_test.php
new file mode 100644 (file)
index 0000000..fdf3384
--- /dev/null
@@ -0,0 +1,283 @@
+<?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/>.
+
+/**
+ * LDAP authentication plugin tests.
+ *
+ * NOTE: in order to execute this test you need to set up
+ *       OpenLDAP server with core, cosine, nis and internet schemas
+ *       and add configuration constants to config.php or phpunit.xml configuration file:
+ *
+ * define('TEST_AUTH_LDAP_HOST_URL', 'ldap://127.0.0.1');
+ * define('TEST_AUTH_LDAP_BIND_DN', 'cn=someuser,dc=example,dc=local');
+ * define('TEST_AUTH_LDAP_BIND_PW', 'somepassword');
+ * define('TEST_AUTH_LDAP_DOMAIN', 'dc=example,dc=local');
+ *
+ * @package    auth_ldap
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+class auth_ldap_testcase extends advanced_testcase {
+
+    public function test_auth_ldap() {
+        global $CFG, $DB;
+
+        if (!extension_loaded('ldap')) {
+            $this->markTestSkipped('LDAP extension is not loaded.');
+        }
+
+        $this->resetAfterTest();
+
+        require_once($CFG->dirroot.'/auth/ldap/auth.php');
+        require_once($CFG->libdir.'/ldaplib.php');
+
+        if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) {
+            $this->markTestSkipped('External LDAP test server not configured.');
+        }
+
+        // Make sure we can connect the server.
+        $debuginfo = '';
+        if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
+            $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo);
+        }
+
+        $this->enable_plugin();
+
+        // Create new empty test container.
+        $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN;
+
+        $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
+
+        $o = array();
+        $o['objectClass'] = array('dcObject', 'organizationalUnit');
+        $o['dc']         = 'moodletest';
+        $o['ou']         = 'MOODLETEST';
+        if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) {
+            $this->markTestSkipped('Can not create test LDAP container.');
+        }
+
+        // Create a few users.
+        $o = array();
+        $o['objectClass'] = array('organizationalUnit');
+        $o['ou']          = 'users';
+        ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
+
+        for ($i=1; $i<=5; $i++) {
+            $this->create_ldap_user($connection, $topdn, $i);
+        }
+
+        // Set up creators group.
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'creators';
+        $o['gidNumber']   = 1;
+        $o['memberUid']   = array('username1', 'username2');
+        ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o);
+
+        $creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator'));
+        $this->assertNotEmpty($creatorrole);
+
+
+        // Configure the plugin a bit.
+        set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth/ldap');
+        set_config('start_tls', 0, 'auth/ldap');
+        set_config('ldap_version', 3, 'auth/ldap');
+        set_config('ldapencoding', 'utf-8', 'auth/ldap');
+        set_config('pagesize', '2', 'auth/ldap');
+        set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth/ldap');
+        set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth/ldap');
+        set_config('user_type', 'rfc2307', 'auth/ldap');
+        set_config('contexts', 'ou=users,'.$topdn, 'auth/ldap');
+        set_config('search_sub', 0, 'auth/ldap');
+        set_config('opt_deref', LDAP_DEREF_NEVER, 'auth/ldap');
+        set_config('user_attribute', 'cn', 'auth/ldap');
+        set_config('memberattribute', 'memberuid', 'auth/ldap');
+        set_config('memberattribute_isdn', 0, 'auth/ldap');
+        set_config('creators', 'cn=creators,'.$topdn, 'auth/ldap');
+        set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth/ldap');
+
+        set_config('field_map_email', 'mail', 'auth/ldap');
+        set_config('field_updatelocal_email', 'oncreate', 'auth/ldap');
+        set_config('field_updateremote_email', '0', 'auth/ldap');
+        set_config('field_lock_email', 'unlocked', 'auth/ldap');
+
+        set_config('field_map_firstname', 'givenName', 'auth/ldap');
+        set_config('field_updatelocal_firstname', 'oncreate', 'auth/ldap');
+        set_config('field_updateremote_firstname', '0', 'auth/ldap');
+        set_config('field_lock_firstname', 'unlocked', 'auth/ldap');
+
+        set_config('field_map_lastname', 'sn', 'auth/ldap');
+        set_config('field_updatelocal_lastname', 'oncreate', 'auth/ldap');
+        set_config('field_updateremote_lastname', '0', 'auth/ldap');
+        set_config('field_lock_lastname', 'unlocked', 'auth/ldap');
+
+
+        $this->assertEquals(2, $DB->count_records('user'));
+        $this->assertEquals(0, $DB->count_records('role_assignments'));
+
+        /** @var auth_plugin_ldap $auth */
+        $auth = get_auth_plugin('ldap');
+
+        ob_start();
+        $auth->sync_users(true);
+        ob_end_clean();
+
+        $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
+        $this->assertEquals(2, $DB->count_records('role_assignments'));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
+
+        for ($i=1; $i<=5; $i++) {
+            $this->assertTrue($DB->record_exists('user', array('username'=>'username'.$i, 'email'=>'user'.$i.'@example.com', 'firstname'=>'Firstname'.$i, 'lastname'=>'Lastname'.$i)));
+        }
+
+        $this->delete_ldap_user($connection, $topdn, 1);
+
+        ob_start();
+        $auth->sync_users(true);
+        ob_end_clean();
+
+        $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
+        $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
+        $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
+        $this->assertEquals(2, $DB->count_records('role_assignments'));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
+
+
+        set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth/ldap');
+
+        /** @var auth_plugin_ldap $auth */
+        $auth = get_auth_plugin('ldap');
+
+        ob_start();
+        $auth->sync_users(true);
+        ob_end_clean();
+
+        $this->assertEquals(4, $DB->count_records('user', array('auth'=>'ldap')));
+        $this->assertEquals(1, $DB->count_records('user', array('auth'=>'nologin', 'username'=>'username1')));
+        $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
+        $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
+        $this->assertEquals(2, $DB->count_records('role_assignments'));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
+
+        $this->create_ldap_user($connection, $topdn, 1);
+
+        ob_start();
+        $auth->sync_users(true);
+        ob_end_clean();
+
+        $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
+        $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
+        $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
+        $this->assertEquals(2, $DB->count_records('role_assignments'));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
+
+
+        set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth/ldap');
+
+        /** @var auth_plugin_ldap $auth */
+        $auth = get_auth_plugin('ldap');
+
+        $this->delete_ldap_user($connection, $topdn, 1);
+
+        ob_start();
+        $auth->sync_users(true);
+        ob_end_clean();
+
+        $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
+        $this->assertEquals(0, $DB->count_records('user', array('username'=>'username1')));
+        $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
+        $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
+        $this->assertEquals(1, $DB->count_records('role_assignments'));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
+
+        $this->create_ldap_user($connection, $topdn, 1);
+
+        ob_start();
+        $auth->sync_users(true);
+        ob_end_clean();
+
+        $this->assertEquals(6, $DB->count_records('user', array('auth'=>'ldap')));
+        $this->assertEquals(1, $DB->count_records('user', array('username'=>'username1')));
+        $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
+        $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
+        $this->assertEquals(2, $DB->count_records('role_assignments'));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
+
+
+        $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
+        ldap_close($connection);
+    }
+
+    protected function create_ldap_user($connection, $topdn, $i) {
+        $o = array();
+        $o['objectClass']   = array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount');
+        $o['cn']            = 'username'.$i;
+        $o['sn']            = 'Lastname'.$i;
+        $o['givenName']     = 'Firstname'.$i;
+        $o['uid']           = $o['cn'];
+        $o['uidnumber']     = 2000+$i;
+        $o['gidNumber']     = 1000+$i;
+        $o['homeDirectory'] = '/';
+        $o['mail']          = 'user'.$i.'@example.com';
+        $o['userPassword']  = 'pass'.$i;
+        ldap_add($connection, 'cn='.$o['cn'].',ou=users,'.$topdn, $o);
+    }
+
+    protected function delete_ldap_user($connection, $topdn, $i) {
+        ldap_delete($connection, 'cn=username'.$i.',ou=users,'.$topdn);
+    }
+
+    protected function enable_plugin() {
+        $auths = get_enabled_auth_plugins(true);
+        if (!in_array('ldap', $auths)) {
+            $auths[] = 'ldap';
+
+        }
+        set_config('auth', implode(',', $auths));
+    }
+
+    protected function recursive_delete($connection, $dn, $filter) {
+        if ($res = ldap_list($connection, $dn, $filter, array('dn'))) {
+            $info = ldap_get_entries($connection, $res);
+            ldap_free_result($res);
+            if ($info['count'] > 0) {
+                if ($res = ldap_search($connection, "$filter,$dn", 'cn=*', array('dn'))) {
+                    $info = ldap_get_entries($connection, $res);
+                    ldap_free_result($res);
+                    foreach ($info as $i) {
+                        if (isset($i['dn'])) {
+                            ldap_delete($connection, $i['dn']);
+                        }
+                    }
+                }
+                if ($res = ldap_search($connection, "$filter,$dn", 'ou=*', array('dn'))) {
+                    $info = ldap_get_entries($connection, $res);
+                    ldap_free_result($res);
+                    foreach ($info as $i) {
+                        if (isset($i['dn']) and $info[0]['dn'] != $i['dn']) {
+                            ldap_delete($connection, $i['dn']);
+                        }
+                    }
+                }
+                ldap_delete($connection, "$filter,$dn");
+            }
+        }
+    }
+}
diff --git a/auth/tests/behat/behat_auth.php b/auth/tests/behat/behat_auth.php
new file mode 100644 (file)
index 0000000..eab0062
--- /dev/null
@@ -0,0 +1,68 @@
+<?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/>.
+
+/**
+ * Basic authentication steps definitions.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 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;
+use Behat\Behat\Context\Step\When as When;
+
+/**
+ * Log in log out steps definitions.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_auth extends behat_base {
+
+    /**
+     * Logs in the user. There should exist a user with the same value as username and password.
+     *
+     * @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)"$/
+     */
+    public function i_log_in_as($username) {
+
+        return array(new Given('I am on homepage'),
+            new Given('I follow "Login"'),
+            new Given('I fill in "Username" with "'.$username.'"'),
+            new Given('I fill in "Password" with "'.$username.'"'),
+            new When('I press "Login"'),
+            new Given('I should see "You are logged in as"'));
+    }
+
+    /**
+     * Logs out of the system.
+     *
+     * @Given /^I log out$/
+     */
+    public function i_log_out() {
+        return new When('I follow "Logout"');
+    }
+
+}
diff --git a/auth/tests/behat/login.feature b/auth/tests/behat/login.feature
new file mode 100644 (file)
index 0000000..e34bf11
--- /dev/null
@@ -0,0 +1,35 @@
+@auth
+Feature: Authentication
+  In order to validate my credentials in the system
+  As a moodle user
+  I need to log into the system
+
+  Scenario: Login with the predefined admin user
+    Given I log in as "admin"
+
+  Scenario: Login as an existing admin user filling the form
+    Given the following "users" exists:
+      | username | password | firstname | lastname | email |
+      | testuser | testuser | Test | User | moodle@moodlemoodle.com |
+    And I am on homepage
+    When I follow "Login"
+    And I fill in "Username" with "testuser"
+    And I fill in "Password" with "testuser"
+    And I press "Login"
+    Then I should see "You are logged in as"
+
+  Scenario: Login as an unexisting user filling the form
+    Given the following "users" exists:
+      | username | password | firstname | lastname | email |
+      | testuser | testuser | Test | User | moodle@moodlemoodle.com |
+    And I am on homepage
+    When I follow "Login"
+    And I fill in "Username" with "testuser"
+    And I fill in "Password" with "unexisting"
+    And I press "Login"
+    Then I should see "Invalid login, please try again"
+
+  Scenario: Logout
+    Given I log in as "admin"
+    When I log out
+    Then I should see "You are not logged in"
diff --git a/behat.yml.dist b/behat.yml.dist
new file mode 100644 (file)
index 0000000..60cf807
--- /dev/null
@@ -0,0 +1,14 @@
+default:
+  paths:
+    features: lib/behat/features
+    bootstrap: lib/behat/features/bootstrap
+  context:
+    class: behat_init_context
+  extensions:
+    Behat\MinkExtension\Extension:
+      base_url: 'http://localhost:8000'
+      goutte: null
+      selenium2: null
+    Moodle\BehatExtension\Extension:
+      features: {  }
+      steps_definitions: {  }
index 3878964..2b2525a 100644 (file)
@@ -91,7 +91,7 @@ echo $OUTPUT->header();
 
 
 // Display completion status
-echo '<table class="generalbox boxaligncenter"><tbody>';
+echo '<table class="generaltable boxaligncenter"><tbody>';
 
 // If not display logged in user, show user name
 if ($USER->id != $user->id) {
@@ -148,7 +148,7 @@ if (empty($completions)) {
     echo '</td></tr></tbody></table>';
 
     // Generate markup for criteria statuses
-    echo '<table class="generalbox logtable boxaligncenter" id="criteriastatus" width="100%"><tbody>';
+    echo '<table class="generaltable logtable boxaligncenter" id="criteriastatus" width="100%"><tbody>';
     echo '<tr class="ccheader">';
     echo '<th class="c0 header" scope="col">'.get_string('criteriagroup', 'block_completionstatus').'</th>';
     echo '<th class="c1 header" scope="col">'.get_string('criteria', 'completion').'</th>';
index 55e0764..775391f 100644 (file)
@@ -85,9 +85,6 @@ class block_course_overview extends block_base {
             // For each course, build category cache.
             $this->content->text .= $renderer->course_overview($sortedcourses, $overviews);
             $this->content->text .= $renderer->hidden_courses($totalcourses - count($sortedcourses));
-            if ($this->page->user_is_editing() && ajaxenabled()) {
-                $this->page->requires->js_init_call('M.block_course_overview.add_handles');
-            }
         }
 
         return $this->content;
index 3535110..36350d9 100644 (file)
@@ -37,7 +37,11 @@ $string['hiddencoursecount'] = 'You have {$a} hidden course';
 $string['hiddencoursecountplural'] = 'You have {$a} hidden courses';
 $string['message'] = 'message';
 $string['messages'] = 'messages';
+$string['movecourse'] = 'Move course: {$a}';
 $string['movecoursehere'] = 'Move course here';
+$string['movetofirst'] = 'Move {$a} course to top';
+$string['moveafterhere'] = 'Move {$a->movingcoursename} course after {$a->currentcoursename}';
+$string['movingcourse'] = 'You are moving: {$a->fullname} ({$a->cancellink})';
 $string['numtodisplay'] = 'Number of courses to display: ';
 $string['otherexpanded'] = 'Other Courses Expanded';
 $string['pluginname'] = 'Course overview';
index 6a6536d..e900df8 100644 (file)
@@ -2,12 +2,23 @@ M.block_course_overview = {}
 
 M.block_course_overview.add_handles = function(Y) {
     M.block_course_overview.Y = Y;
+    var MOVEICON = {
+        pix: "i/move_2d",
+        component: 'moodle'
+    };
+
     YUI().use('dd-constrain', 'dd-proxy', 'dd-drop', 'dd-plugin', function(Y) {
         //Static Vars
         var goingUp = false, lastY = 0;
 
-        var list = Y.Node.all('#course_list .coursebox');
+        var list = Y.Node.all('.course_list .coursebox');
         list.each(function(v, k) {
+            // Replace move link and image with move_2d image.
+            var imagenode = v.one('.course_title .move a img');
+            imagenode.setAttribute('src', M.util.image_url(MOVEICON.pix, MOVEICON.component));
+            imagenode.addClass('cursor');
+            v.one('.course_title .move a').replace(imagenode);
+
             var dd = new Y.DD.Drag({
                 node: v,
                 target: {
@@ -16,18 +27,11 @@ M.block_course_overview.add_handles = function(Y) {
             }).plug(Y.Plugin.DDProxy, {
                 moveOnEnd: false
             }).plug(Y.Plugin.DDConstrained, {
-                constrain2node: '#course_list'
+                constrain2node: '.course_list'
             });
             dd.addHandle('.course_title .move');
         });
 
-        var drops = Y.Node.all('#coursebox');
-        drops.each(function(v, k) {
-            var tar = new Y.DD.Drop({
-                node: v
-            });
-        });
-
         Y.DD.DDM.on('drag:start', function(e) {
             //Get our drag object
             var drag = e.target;
@@ -101,7 +105,7 @@ M.block_course_overview.add_handles = function(Y) {
 
 M.block_course_overview.save = function() {
     var Y = M.block_course_overview.Y;
-    var sortorder = Y.one('#course_list').get('children').getAttribute('id');
+    var sortorder = Y.one('.course_list').get('children').getAttribute('id');
     for (var i = 0; i < sortorder.length; i++) {
         sortorder[i] = sortorder[i].substring(7);
     }
index 8cb7b1b..2b5e74d 100644 (file)
@@ -27,47 +27,34 @@ require_once(dirname(__FILE__) . '/locallib.php');
 require_sesskey();
 require_login();
 
-$source = required_param('source', PARAM_INT);
-$move = required_param('move', PARAM_INT);
-
-list($courses_sorted, $sitecourses, $coursecount) = block_course_overview_get_sorted_courses();
-$sortorder = array_keys($courses_sorted);
-// Now resort based on new weight for chosen course.
-$neworder = array();
-
-$sourcekey = array_search($source, $sortorder);
-if ($sourcekey === false) {
-    print_error("invalidcourseid", null, null, $source);
+$coursetomove = required_param('courseid', PARAM_INT);
+$moveto = required_param('moveto', PARAM_INT);
+
+list($courses, $sitecourses, $coursecount) = block_course_overview_get_sorted_courses();
+$sortedcourses = array_keys($courses);
+
+$currentcourseindex = array_search($coursetomove, $sortedcourses);
+// If coursetomove is not found or moveto < 0 or > count($sortedcourses) then throw error.
+if ($currentcourseindex === false) {
+    print_error("invalidcourseid", null, null, $coursetomove);
+} else if (($moveto < 0) || ($moveto >= count($sortedcourses))) {
+    print_error("invalidaction");
 }
 
-$destination = $sourcekey + $move;
-if ($destination < 0) {
-    print_error("listcantmoveup");
-} else if ($destination >= count($courses_sorted)) {
-    print_error("listcantmovedown");
+// If current course index is same as destination index then don't do anything.
+if ($currentcourseindex === $moveto) {
+    redirect(new moodle_url('/my/index.php'));
 }
 
 // Create neworder list for courses.
-unset($sortorder[$sourcekey]);
-if ($move == -1) {
-    if ($destination > 0) {
-        $neworder = array_slice($sortorder, 0, $destination, true);
-    }
-    $neworder[] = $source;
-    $remaningcourses = array_slice($sortorder, $destination);
-    foreach ($remaningcourses as $courseid) {
-        $neworder[] = $courseid;
-    }
-} else if (($move == 1)) {
-    $neworder = array_slice($sortorder, 0, $destination);
-    $neworder[] = $source;
-    if (($destination) < count($courses_sorted)) {
-        $remaningcourses = array_slice($sortorder, $destination);
-        foreach ($remaningcourses as $courseid) {
-            $neworder[] = $courseid;
-        }
-    }
-}
+$neworder = array();
 
-block_course_overview_update_myorder($neworder);
+unset($sortedcourses[$currentcourseindex]);
+$neworder = array_slice($sortedcourses, 0, $moveto, true);
+$neworder[] = $coursetomove;
+$remaningcourses = array_slice($sortedcourses, $moveto);
+foreach ($remaningcourses as $courseid) {
+    $neworder[] = $courseid;
+}
+block_course_overview_update_myorder(array_values($neworder));
 redirect(new moodle_url('/my/index.php'));
index 8c42428..b0b4507 100644 (file)
@@ -41,67 +41,64 @@ class block_course_overview_renderer extends plugin_renderer_base {
     public function course_overview($courses, $overviews) {
         $html = '';
         $config = get_config('block_course_overview');
-
-        $html .= html_writer::start_tag('div', array('id' => 'course_list'));
+        $ismovingcourse = false;
         $courseordernumber = 0;
         $maxcourses = count($courses);
-        // Intialize string/icon etc if user is editing.
-        $url = null;
-        $moveicon = null;
-        $moveup[] = null;
-        $movedown[] = null;
-        if ($this->page->user_is_editing()) {
+        $userediting = false;
+        // Intialise string/icon etc if user is editing and courses > 1
+        if ($this->page->user_is_editing() && (count($courses) > 1)) {
+            $userediting = true;
+            // If ajaxenabled then include DND JS and replace link with move image.
             if (ajaxenabled()) {
-                $moveicon = html_writer::tag('div',
-                    html_writer::empty_tag('img',
-                        array('src' => $this->pix_url('i/move_2d')->out(false),
-                            'alt' => get_string('move'), 'class' => 'cursor',
-                            'title' => get_string('move'))
-                    ), array('class' => 'move')
-                );
-            } else {
-                $url = new moodle_url('/blocks/course_overview/move.php', array('sesskey' => sesskey()));
-                $moveup['str'] = get_string('moveup');
-                $moveup['icon'] = $this->pix_url('t/up');
-                $movedown['str'] =  get_string('movedown');
-                $movedown['icon'] = $this->pix_url('t/down');
+                $this->page->requires->js_init_call('M.block_course_overview.add_handles');
             }
+
+            // Check if course is moving
+            $ismovingcourse = optional_param('movecourse', FALSE, PARAM_BOOL);
+            $movingcourseid = optional_param('courseid', 0, PARAM_INT);
+        }
+
+        // Render first movehere icon.
+        if ($ismovingcourse) {
+            // Remove movecourse param from url.
+            $this->page->ensure_param_not_in_url('movecourse');
+
+            // Show moving course notice, so user knows what is being moved.
+            $html .= $this->output->box_start('notice');
+            $a = new stdClass();
+            $a->fullname = $courses[$movingcourseid]->fullname;
+            $a->cancellink = html_writer::link($this->page->url, get_string('cancel'));
+            $html .= get_string('movingcourse', 'block_course_overview', $a);
+            $html .= $this->output->box_end();
+
+            $moveurl = new moodle_url('/blocks/course_overview/move.php',
+                        array('sesskey' => sesskey(), 'moveto' => 0, 'courseid' => $movingcourseid));
+            // Create move icon, so it can be used.
+            $movetofirsticon = html_writer::empty_tag('img',
+                    array('src' => $this->output->pix_url('movehere'),
+                        'alt' => get_string('movetofirst', 'block_course_overview', $courses[$movingcourseid]->fullname),
+                        'title' => get_string('movehere')));
+            $moveurl = html_writer::link($moveurl, $movetofirsticon);
+            $html .= html_writer::tag('div', $moveurl, array('class' => 'movehere'));
         }
 
         foreach ($courses as $key => $course) {
+            // If moving course, then don't show course which needs to be moved.
+            if ($ismovingcourse && ($course->id == $movingcourseid)) {
+                continue;
+            }
             $html .= $this->output->box_start('coursebox', "course-{$course->id}");
             $html .= html_writer::start_tag('div', array('class' => 'course_title'));
-            // Ajax enabled then add moveicon html
-            if (!is_null($moveicon)) {
-                $html .= $moveicon;
-            } else if (!is_null($url)) {
-                // Add course id to move link
-                $url->param('source', $course->id);
-                $html .= html_writer::start_tag('div', array('class' => 'moveicons'));
-                // Add an arrow to move course up.
-                if ($courseordernumber > 0) {
-                    $url->param('move', -1);
-                    $html .= html_writer::link($url,
-                    html_writer::empty_tag('img', array('src' => $moveup['icon'],
-                        'class' => 'up', 'alt' => $moveup['str'])),
-                        array('title' => $moveup['str'], 'class' => 'moveup'));
-                } else {
-                    // Add a spacer to keep keep down arrow icons at right position.
-                    $html .= html_writer::empty_tag('img', array('src' => $this->pix_url('spacer'),
-                        'class' => 'movedownspacer'));
-                }
-                // Add an arrow to move course down.
-                if ($courseordernumber <= $maxcourses-2) {
-                    $url->param('move', 1);
-                    $html .= html_writer::link($url, html_writer::empty_tag('img',
-                        array('src' => $movedown['icon'], 'class' => 'down', 'alt' => $movedown['str'])),
-                        array('title' => $movedown['str'], 'class' => 'movedown'));
-                } else {
-                    // Add a spacer to keep keep up arrow icons at right position.
-                    $html .= html_writer::empty_tag('img', array('src' => $this->pix_url('spacer'),
-                        'class' => 'moveupspacer'));
-                }
-                $html .= html_writer::end_tag('div');
+            // If user is editing, then add move icons.
+            if ($userediting && !$ismovingcourse) {
+                $moveicon = html_writer::empty_tag('img',
+                        array('src' => $this->pix_url('t/move')->out(false),
+                            'alt' => get_string('movecourse', 'block_course_overview', $course->fullname),
+                            'title' => get_string('move')));
+                $moveurl = new moodle_url($this->page->url, array('sesskey' => sesskey(), 'movecourse' => 1, 'courseid' => $course->id));
+                $moveurl = html_writer::link($moveurl, $moveicon);
+                $html .= html_writer::tag('div', $moveurl, array('class' => 'move'));
+
             }
 
             $attributes = array('title' => s($course->fullname));
@@ -125,17 +122,30 @@ class block_course_overview_renderer extends plugin_renderer_base {
                 }
             }
 
-            if (isset($overviews[$course->id])) {
+            // If user is moving courses, then down't show overview.
+            if (isset($overviews[$course->id]) && !$ismovingcourse) {
                 $html .= $this->activity_display($course->id, $overviews[$course->id]);
             }
 
             $html .= $this->output->box('', 'flush');
             $html .= $this->output->box_end();
             $courseordernumber++;
+            if ($ismovingcourse) {
+                $moveurl = new moodle_url('/blocks/course_overview/move.php',
+                            array('sesskey' => sesskey(), 'moveto' => $courseordernumber, 'courseid' => $movingcourseid));
+                $a = new stdClass();
+                $a->movingcoursename = $courses[$movingcourseid]->fullname;
+                $a->currentcoursename = $course->fullname;
+                $movehereicon = html_writer::empty_tag('img',
+                        array('src' => $this->output->pix_url('movehere'),
+                            'alt' => get_string('moveafterhere', 'block_course_overview', $a),
+                            'title' => get_string('movehere')));
+                $moveurl = html_writer::link($moveurl, $movehereicon);
+                $html .= html_writer::tag('div', $moveurl, array('class' => 'movehere'));
+            }
         }
-        $html .= html_writer::end_tag('div');
-
-        return $html;
+        // Wrap course list in a div and return.
+        return html_writer::tag('div', $html, array('class' => 'course_list'));
     }
 
     /**
index 6718d06..69905d7 100644 (file)
     padding: 2px 10px;
 }
 
-.editing .block_course_overview .moveicons {
-    display: block;
-    float: left;
-    padding-right: 5px;
-}
-.dir-rtl.editing .block_course_overview .moveicons {
-    float:right;
-}
-
-.editing .block_course_overview .moveup {
-    padding-right: 5px;
-}
-
-.editing .block_course_overview .movedownspacer {
-    float: left;
-    width: 14px;
-}
-.dir-rtl.editing .block_course_overview .movedownspacer {
-    float:right;
-}
-
-.editing .block_course_overview .moveupspacer {
-    float: right;
-    width: 14px;
-}
-.dir-rtl.editing .block_course_overview .moveupspacer {
-    float:left;
-}
-
-.block_course_overview #course_list {
+.block_course_overview .course_list {
     width: 100%;
 }
 
     text-align: right;
 }
 
-.block_course_overview .coursemovetarget {
-    display: block;
-    height: 1em;
-    margin-bottom: 1em;
-    border-width: 2px;
-    border-style: dashed;
+.block_course_overview .content .course_list .movehere{
+    margin-bottom: 15px;
 }
index 40abe52..47d0d36 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012121000;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013012300;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_course_overview'; // Full name of the plugin (used for diagnostics)
index 8dfa97f..884c909 100644 (file)
@@ -11,10 +11,6 @@ class block_glossary_random extends block_base {
         $this->title = get_string('pluginname','block_glossary_random');
     }
 
-    function applicable_formats() {
-        return array('all' => true, 'mod' => false, 'tag' => false, 'my' => false);
-    }
-
     function specialization() {
         global $CFG, $DB;
 
index e7bb687..0c1acd6 100644 (file)
@@ -26,6 +26,16 @@ defined('MOODLE_INTERNAL') || die();
 
 $capabilities = array(
 
+    'block/glossary_random:myaddinstance' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'user' => CAP_ALLOW
+        ),
+
+        'clonepermissionsfrom' => 'moodle/my:manageblocks'
+    ),
+
     'block/glossary_random:addinstance' => array(
         'riskbitmask' => RISK_SPAM | RISK_XSS,
 
diff --git a/blocks/glossary_random/db/upgrade.php b/blocks/glossary_random/db/upgrade.php
deleted file mode 100644 (file)
index 81aeabe..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * This file keeps track of upgrades to the glossary random block
- *
- * Sometimes, changes between versions involve alterations to database structures
- * and other major things that may break installations.
- *
- * The upgrade function in this file will attempt to perform all the necessary
- * actions to upgrade your older installation to the current version.
- *
- * If there's something it cannot do itself, it will tell you what you need to do.
- *
- * The commands in here will all be database-neutral, using the methods of
- * database_manager class
- *
- * Please do not forget to use upgrade_set_timeout()
- * before any action that may take longer time to finish.
- *
- * @since 2.0
- * @package blocks
- * @copyright 2012 Mark Nelson <markn@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/**
- * Handles upgrading instances of this block.
- *
- * @param int $oldversion
- * @param object $block
- */
-function xmldb_block_glossary_random_upgrade($oldversion, $block) {
-    global $DB;
-
-    // Moodle v2.4.0 release upgrade line
-    // Put any upgrade step following this.
-
-    if ($oldversion < 2012112901) {
-        // Get the instances of this block.
-        if ($blocks = $DB->get_records('block_instances', array('blockname' => 'glossary_random', 'pagetypepattern' => 'my-index'))) {
-            // Loop through and remove them from the My Moodle page.
-            foreach ($blocks as $block) {
-                blocks_delete_instance($block);
-            }
-
-        }
-
-        // Savepoint reached.
-        upgrade_block_savepoint(true, 2012112901, 'glossary_random');
-    }
-
-
-    return true;
-}
\ No newline at end of file
index 0147f00..6366bd4 100644 (file)
@@ -28,6 +28,7 @@ $string['askaddentry'] = 'When users can add entries to the glossary, show a lin
 $string['askinvisible'] = 'When users cannot edit or view the glossary, show this text (without link)';
 $string['askviewglossary'] = 'When users can view the glossary but not add entries, show a link with this text';
 $string['glossary_random:addinstance'] = 'Add a new random glossary entry block';
+$string['glossary_random:myaddinstance'] = 'Add a new random glossary entry block to the My Moodle page';
 $string['intro'] = 'Make sure you have at least one glossary with at least one entry added to this course. Then you can adjust the following settings';
 $string['invisible'] = '(to be continued)';
 $string['lastmodified'] = 'Last modified entry';
index 061ec44..a5d21fa 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112901;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2012112902;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_glossary_random'; // Full name of the plugin (used for diagnostics)
index 85f48f3..aa340af 100644 (file)
@@ -7,7 +7,7 @@ class block_mentees extends block_base {
     }
 
     function applicable_formats() {
-        return array('all' => true, 'tag' => false, 'my' => false);
+        return array('all' => true, 'tag' => false);
     }
 
     function specialization() {
index 489ceb3..ba14b07 100644 (file)
@@ -26,6 +26,16 @@ defined('MOODLE_INTERNAL') || die();
 
 $capabilities = array(
 
+    'block/mentees:myaddinstance' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'user' => CAP_ALLOW
+        ),
+
+        'clonepermissionsfrom' => 'moodle/my:manageblocks'
+    ),
+
     'block/mentees:addinstance' => array(
         'riskbitmask' => RISK_SPAM | RISK_XSS,
 
diff --git a/blocks/mentees/db/upgrade.php b/blocks/mentees/db/upgrade.php
deleted file mode 100644 (file)
index e8aba71..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * This file keeps track of upgrades to the mentees block
- *
- * Sometimes, changes between versions involve alterations to database structures
- * and other major things that may break installations.
- *
- * The upgrade function in this file will attempt to perform all the necessary
- * actions to upgrade your older installation to the current version.
- *
- * If there's something it cannot do itself, it will tell you what you need to do.
- *
- * The commands in here will all be database-neutral, using the methods of
- * database_manager class
- *
- * Please do not forget to use upgrade_set_timeout()
- * before any action that may take longer time to finish.
- *
- * @since 2.0
- * @package blocks
- * @copyright 2012 Mark Nelson <markn@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/**
- * Handles upgrading instances of this block.
- *
- * @param int $oldversion
- * @param object $block
- */
-function xmldb_block_mentees_upgrade($oldversion, $block) {
-    global $DB;
-
-    // Moodle v2.4.0 release upgrade line
-    // Put any upgrade step following this.
-
-    if ($oldversion < 2012112901) {
-        // Get the instances of this block.
-        if ($blocks = $DB->get_records('block_instances', array('blockname' => 'mentees', 'pagetypepattern' => 'my-index'))) {
-            // Loop through and remove them from the My Moodle page.
-            foreach ($blocks as $block) {
-                blocks_delete_instance($block);
-            }
-
-        }
-
-        // Savepoint reached.
-        upgrade_block_savepoint(true, 2012112901, 'mentees');
-    }
-
-
-    return true;
-}
\ No newline at end of file
index 1c0cdab..3a22057 100644 (file)
@@ -27,5 +27,6 @@ $string['configtitle'] = 'Block title';
 $string['configtitleblankhides'] = 'Block title (no title if blank)';
 $string['leaveblanktohide'] = 'leave blank to hide the title';
 $string['mentees:addinstance'] = 'Add a new mentees block';
+$string['mentees:myaddinstance'] = 'Add a new mentees block to the My Moodle page';
 $string['newmenteesblock'] = '(new Mentees block)';
 $string['pluginname'] = 'Mentees';
index c3a8db3..7f9e179 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112901;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2012112902;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_mentees';   // Full name of the plugin (used for diagnostics)
index e803c6e..5ecbec2 100644 (file)
@@ -5,10 +5,6 @@ class block_news_items extends block_base {
         $this->title = get_string('pluginname', 'block_news_items');
     }
 
-    function applicable_formats() {
-        return array('all' => true, 'mod' => false, 'tag' => false, 'my' => false);
-    }
-
     function get_content() {
         global $CFG, $USER;
 
index 452158e..86bb1b7 100644 (file)
@@ -26,6 +26,16 @@ defined('MOODLE_INTERNAL') || die();
 
 $capabilities = array(
 
+    'block/news_items:myaddinstance' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'user' => CAP_ALLOW
+        ),
+
+        'clonepermissionsfrom' => 'moodle/my:manageblocks'
+    ),
+
     'block/news_items:addinstance' => array(
         'riskbitmask' => RISK_SPAM | RISK_XSS,
 
index 1de6507..7d76b9c 100644 (file)
@@ -24,4 +24,5 @@
  */
 
 $string['news_items:addinstance'] = 'Add a new latest news block';
+$string['news_items:myaddinstance'] = 'Add a new latest news block to the My Moodle page';
 $string['pluginname'] = 'Latest news';
index 11d74ca..ba6e931 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112901;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2012112902;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_news_items'; // Full name of the plugin (used for diagnostics)
index 6d14ebb..50ff41a 100644 (file)
@@ -10,10 +10,6 @@ class block_online_users extends block_base {
         $this->title = get_string('pluginname','block_online_users');
     }
 
-    function applicable_formats() {
-        return array('all' => true, 'mod' => false, 'tag' => false, 'my' => false);
-    }
-
     function has_config() {
         return true;
     }
index 41959c4..f238b73 100644 (file)
@@ -26,6 +26,16 @@ defined('MOODLE_INTERNAL') || die();
 
 $capabilities = array(
 
+    'block/online_users:myaddinstance' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'user' => CAP_ALLOW
+        ),
+
+        'clonepermissionsfrom' => 'moodle/my:manageblocks'
+    ),
+
     'block/online_users:addinstance' => array(
         'riskbitmask' => RISK_SPAM | RISK_XSS,
 
diff --git a/blocks/online_users/db/upgrade.php b/blocks/online_users/db/upgrade.php
deleted file mode 100644 (file)
index f45a788..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * This file keeps track of upgrades to the online users block
- *
- * Sometimes, changes between versions involve alterations to database structures
- * and other major things that may break installations.
- *
- * The upgrade function in this file will attempt to perform all the necessary
- * actions to upgrade your older installation to the current version.
- *
- * If there's something it cannot do itself, it will tell you what you need to do.
- *
- * The commands in here will all be database-neutral, using the methods of
- * database_manager class
- *
- * Please do not forget to use upgrade_set_timeout()
- * before any action that may take longer time to finish.
- *
- * @since 2.0
- * @package blocks
- * @copyright 2012 Mark Nelson <markn@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/**
- * Handles upgrading instances of this block.
- *
- * @param int $oldversion
- * @param object $block
- */
-function xmldb_block_online_users_upgrade($oldversion, $block) {
-    global $DB;
-
-    // Moodle v2.4.0 release upgrade line
-    // Put any upgrade step following this.
-
-    if ($oldversion < 2012112901) {
-        // Get the instances of this block.
-        if ($blocks = $DB->get_records('block_instances', array('blockname' => 'online_users', 'pagetypepattern' => 'my-index'))) {
-            // Loop through and remove them from the My Moodle page.
-            foreach ($blocks as $block) {
-                blocks_delete_instance($block);
-            }
-
-        }
-
-        // Savepoint reached.
-        upgrade_block_savepoint(true, 2012112901, 'online_users');
-    }
-
-
-    return true;
-}
\ No newline at end of file
index b7a008e..034fce5 100644 (file)
@@ -25,6 +25,7 @@
 
 $string['configtimetosee'] = 'Number of minutes determining the period of inactivity after which a user is no longer considered to be online.';
 $string['online_users:addinstance'] = 'Add a new online users block';
+$string['online_users:myaddinstance'] = 'Add a new online users block to the My Moodle page';
 $string['online_users:viewlist'] = 'View list of online users';
 $string['periodnminutes'] = 'last {$a} minutes';
 $string['pluginname'] = 'Online users';
index 4463c45..9a32ef0 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112901;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2012112902;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_online_users'; // Full name of the plugin (used for diagnostics)
index 7067835..110c4d2 100644 (file)
@@ -177,7 +177,7 @@ if ($returnurl) {
 $managefeeds = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams);
 
 $PAGE->set_url('/blocks/rss_client/editfeed.php', $urlparams);
-$PAGE->set_pagelayout('base');
+$PAGE->set_pagelayout('standard');
 
 if ($rssid) {
     $isadding = false;
@@ -218,10 +218,9 @@ if ($mform->is_cancelled()) {
     $PAGE->set_title($strtitle);
     $PAGE->set_heading($strtitle);
 
-    $settingsurl = new moodle_url('/admin/settings.php?section=blocksettingrss_client');
     $PAGE->navbar->add(get_string('blocks'));
-    $PAGE->navbar->add(get_string('pluginname', 'block_rss_client'), $settingsurl);
-    $PAGE->navbar->add(get_string('managefeeds', 'block_rss_client'));
+    $PAGE->navbar->add(get_string('pluginname', 'block_rss_client'));
+    $PAGE->navbar->add(get_string('managefeeds', 'block_rss_client'), $managefeeds );
     $PAGE->navbar->add($strtitle);
 
     echo $OUTPUT->header();
index 0340b0f..2252bc9 100644 (file)
@@ -83,10 +83,9 @@ $PAGE->set_pagelayout('standard');
 $PAGE->set_title($strmanage);
 $PAGE->set_heading($strmanage);
 
-$settingsurl = new moodle_url('/admin/settings.php?section=blocksettingrss_client');
 $managefeeds = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams);
 $PAGE->navbar->add(get_string('blocks'));
-$PAGE->navbar->add(get_string('pluginname', 'block_rss_client'), $settingsurl);
+$PAGE->navbar->add(get_string('pluginname', 'block_rss_client'));
 $PAGE->navbar->add(get_string('managefeeds', 'block_rss_client'), $managefeeds);
 echo $OUTPUT->header();
 
index f969a30..1c287d7 100644 (file)
@@ -71,11 +71,10 @@ $strviewfeed = get_string('viewfeed', 'block_rss_client');
 $PAGE->set_title($strviewfeed);
 $PAGE->set_heading($strviewfeed);
 
-$settingsurl = new moodle_url('/admin/settings.php?section=blocksettingrss_client');
 $managefeeds = new moodle_url('/blocks/rss_client/managefeeds.php', $urlparams);
 $PAGE->navbar->add(get_string('blocks'));
-$PAGE->navbar->add(get_string('pluginname', 'block_rss_client'), $settingsurl);
-$PAGE->navbar->add(get_string('managefeeds', 'block_rss_client'));
+$PAGE->navbar->add(get_string('pluginname', 'block_rss_client'));
+$PAGE->navbar->add(get_string('managefeeds', 'block_rss_client'), $managefeeds);
 $PAGE->navbar->add($strviewfeed);
 echo $OUTPUT->header();
 
index 7721931..6e4bd1e 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+/**
+ * This file contains the main class for the section links block.
+ *
+ * @package    block_section_links
+ * @copyright  Jason Hardin
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 
 /**
- * Section links block
+ * Section links block class.
  *
- * @package    moodlecore
+ * @package    block_section_links
+ * @copyright  Jason Hardin
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class block_section_links extends block_base {
 
-    function init() {
+    /**
+     * Initialises the block instance.
+     */
+    public function init() {
         $this->title = get_string('pluginname', 'block_section_links');
     }
 
-    function instance_config($instance) {
-        global $DB;
-
-        parent::instance_config($instance);
-        $course = $this->page->course;
-        if (isset($course->format)) {
-            if ($course->format == 'topics') {
-                $this->title = get_string('topics', 'block_section_links');
-            } else if ($course->format == 'weeks') {
-                $this->title = get_string('weeks', 'block_section_links');
-            } else {
-                $this->title = get_string('pluginname', 'block_section_links');
-            }
-        }
-    }
-
-    function applicable_formats() {
-        return (array('course-view-weeks' => true, 'course-view-topics' => true));
+    /**
+     * Returns an array of formats for which this block can be used.
+     *
+     * @return array
+     */
+    public function applicable_formats() {
+        return array(
+            'course-view-weeks' => true,
+            'course-view-topics' => true
+        );
     }
 
-    function get_content() {
-        global $CFG, $USER, $DB;
-
-        $highlight = 0;
-        if(isset($this->config)){
+    /**
+     * Generates the content of the block and returns it.
+     *
+     * If the content has already been generated then the previously generated content is returned.
+     *
+     * @return stdClass
+     */
+    public function get_content() {
+
+        // The config should be loaded by now.
+        // If its empty then we will use the global config for the section links block.
+        if (isset($this->config)){
             $config = $this->config;
         } else{
-            // TODO: Move these config settings to proper ones using component name
-            $config = get_config('blocks/section_links');
+            $config = get_config('block_section_links');
         }
 
-        if ($this->content !== NULL) {
+        if ($this->content !== null) {
             return $this->content;
         }
 
@@ -72,30 +80,28 @@ class block_section_links extends block_base {
         }
 
         $course = $this->page->course;
-        $courseformatoptions = course_get_format($course)->get_format_options();
+        $courseformat = course_get_format($course);
+        $courseformatoptions = $courseformat->get_format_options();
         $context = context_course::instance($course->id);
 
-        if ($course->format == 'weeks' or $course->format == 'weekscss') {
-            $highlight = ceil((time()-$course->startdate)/604800);
-            $linktext = get_string('jumptocurrentweek', 'block_section_links');
-            $sectionname = 'week';
-        }
-        else if ($course->format == 'topics') {
+        // Prepare the highlight value.
+        if ($course->format == 'weeks') {
+            $highlight = ceil((time() - $course->startdate) / 604800);
+        } else if ($course->format == 'topics') {
             $highlight = $course->marker;
-            $linktext = get_string('jumptocurrenttopic', 'block_section_links');
-            $sectionname = 'topic';
+        } else {
+            $highlight = 0;
         }
-        $inc = 1;
 
-        if(!empty($config->numsections1) and ($courseformatoptions['numsections'] > $config->numsections1)) {
+        // Prepare the increment value.
+        if (!empty($config->numsections1) and ($courseformatoptions['numsections'] > $config->numsections1)) {
             $inc = $config->incby1;
+        } else if ($courseformatoptions['numsections'] > 22) {
+            $inc = 2;
         } else {
-            if ($courseformatoptions['numsections'] > 22) {
-                $inc = 2;
-            }
+            $inc = 1;
         }
-
-        if(!empty($config->numsections2) and ($courseformatoptions['numsections'] > $config->numsections2)) {
+        if (!empty($config->numsections2) and ($courseformatoptions['numsections'] > $config->numsections2)) {
             $inc = $config->incby2;
         } else {
             if ($courseformatoptions['numsections'] > 40) {
@@ -103,58 +109,52 @@ class block_section_links extends block_base {
             }
         }
 
-        $sql = "SELECT section, visible
-                  FROM {course_sections}
-                 WHERE course = ? AND
-                       section < ".($courseformatoptions['numsections']+1)."
-              ORDER BY section";
-
-        if ($sections = $DB->get_records_sql($sql, array($course->id))) {
-            $text = '<ol class="inline-list">';
-            for ($i = $inc; $i <= $courseformatoptions['numsections']; $i += $inc) {
-                if (!isset($sections[$i])) {
-                    continue;
-                }
-                $isvisible = $sections[$i]->visible;
-                if (!$isvisible and !has_capability('moodle/course:update', $context)) {
-                    continue;
-                }
-                $style = ($isvisible) ? '' : ' class="dimmed"';
-                if ($i == $highlight) {
-                    $text .= '<li><a href="'.course_get_url($course, $i)."\"$style><strong>$i</strong></a></li>\n";
-                } else {
-                    $text .= '<li><a href="'.course_get_url($course, $i)."\"$style>$i</a></li>\n";
-                }
+        // Prepare an array of sections to create links for.
+        $sections = array();
+        $canviewhidden = has_capability('moodle/course:update', $context);
+        $coursesections = $courseformat->get_sections();
+        $coursesectionscount = count($coursesections);
+        for ($i = $inc; $i <= $coursesectionscount; $i += $inc) {
+            if ($i > $courseformatoptions['numsections'] || !isset($coursesections[$i])) {
+                continue;
+            }
+            $section = $coursesections[$i];
+            if ($section->section && ($section->visible || $canviewhidden)) {
+                $sections[$i] = (object)array(
+                    'section' => $section->section,
+                    'visible' => $section->visible,
+                    'highlight' => ($section->section == $highlight)
+                );
             }
-            $text .= '</ol>';
-            if ($highlight and isset($sections[$highlight])) {
-                $isvisible = $sections[$highlight]->visible;
-                if ($isvisible or has_capability('moodle/course:update', $context)) {
-                    $style = ($isvisible) ? '' : ' class="dimmed"';
-                    $text .= "\n<a href=\"".course_get_url($course, $highlight)."\"$style>$linktext</a>";
-                }
+        }
+
+        if (!empty($sections)) {
+            $sectiontojumpto = false;
+            if ($highlight && isset($sections[$highlight]) && ($sections[$highlight]->visible || $canviewhidden)) {
+                $sectiontojumpto = $highlight;
             }
+            // Render the sections.
+            $renderer = $this->page->get_renderer('block_section_links');
+            $this->content->text = $renderer->render_section_links($this->page->course, $sections, $sectiontojumpto);
         }
 
-        $this->content->text = $text;
         return $this->content;
     }
     /**
-     * Has instance config
-     * @return boolean
+     * Returns true if this block has instance config.
+     *
+     * @return bool
      **/
-    function instance_allow_config() {
+    public function instance_allow_config() {
         return true;
     }
-    function before_delete() {
-        global $DB;
-        // TODO: Move these config settings to proper ones using component name
-        $DB->delete_records('config_plugins', array('plugin' => 'blocks/section_links'));
-        // Have to manually purge the cache as well
-        cache_helper::invalidate_by_definition('core', 'config', array(), 'blocks/section_links');
-    }
 
-    function has_config() {
+    /**
+     * Returns true if this block has global config.
+     *
+     * @return bool
+     */
+    public function has_config() {
         return true;
     }
 }
diff --git a/blocks/section_links/config_instance.html b/blocks/section_links/config_instance.html
deleted file mode 100644 (file)
index a2765da..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-<?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/>.
-
-
-/**
- * Section links block
- *
- * @package    moodlecore
- * @Author     Jason Hardin
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-$numberofsections = array();
-for ($i = 1; $i < 53; $i++){
-    $numberofsections[$i] = $i;
-}
-
-$increments = array();
-
-for ($i = 1; $i < 11; $i++){
-    $increments[$i] = $i;
-}
-
-if(isset($this->config)){
-    $config = $this->config;
-} else{
-    $config = get_config('blocks/section_links');
-}
-
-$selected = array();
-if (!empty($config->numsections1)) {
-    if (empty($config->incby1)) {
-        $config->incby1 = 2;
-    }
-    $selected[1] = array($config->numsections1, $config->incby1);
-} else {
-    $selected[1] = array(22, 2);
-}
-
-if (!empty($config->numsections2)) {
-    if (empty($config->incby1)) {
-        $config->incby1 = 5;
-    }
-    $selected[2] = array($config->numsections2, $config->incby2);
-} else {
-    $selected[2] = array(40, 5);
-}
-
-?>
-<table cellpadding="9" cellspacing="0">
-<?php
-for($i = 1; $i < 3; $i++){
-?>
-    <tr valign="top">
-        <td align="right">
-            <label for="menunumsections<?php echo $i; ?>"><?php print_string('numsections'.$i, 'block_section_links'); ?>:</label>
-        </td>
-        <td>
-            <?php choose_from_menu($numberofsections, 'numsections'.$i, $selected[$i][0]); ?>
-        </td>
-        <td>
-            <?php print_string('numsectionsdesc'.$i, 'block_section_links'); ?>
-        </td>
-    </tr>
-    <tr valign="top">
-        <td align="right">
-            <label for="menuincby<?php echo $i;?>"><?php print_string('incby'.$i, 'block_section_links'); ?>:</label>
-        </td>
-        <td>
-            <?php choose_from_menu($increments, 'incby'.$i, $selected[$i][1]); ?>
-        </td>
-        <td>
-            <?php print_string('incbydesc'.$i, 'block_section_links'); ?>
-        </td>
-    </tr>
-<?php }
-?>
-<tr>
-    <td colspan="3" align="center">
-        <input type="hidden" name="sesskey" value="<?php echo sesskey();?>">
-        <input type="submit" value="<?php print_string('savechanges') ?>" />
-    </td>
-</tr>
-</table>
similarity index 51%
rename from blocks/news_items/db/upgrade.php
rename to blocks/section_links/db/upgrade.php
index e634e57..dbe36b1 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -16,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * This file keeps track of upgrades to the latest news block
+ * This file keeps track of upgrades to the section links block
  *
  * Sometimes, changes between versions involve alterations to database structures
  * and other major things that may break installations.
  * Please do not forget to use upgrade_set_timeout()
  * before any action that may take longer time to finish.
  *
- * @since 2.0
- * @package blocks
- * @copyright 2012 Mark Nelson <markn@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since 2.5
+ * @package block_section_links
+ * @copyright 2013 Sam Hemelryk
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 /**
- * Handles upgrading instances of this block.
+ * Upgrade code for the section links block.
  *
+ * @global moodle_database $DB
  * @param int $oldversion
  * @param object $block
  */
-function xmldb_block_news_items_upgrade($oldversion, $block) {
+function xmldb_block_section_links_upgrade($oldversion, $block) {
     global $DB;
 
+    // Moodle v2.3.0 release upgrade line
+    // Put any upgrade step following this
+
     // Moodle v2.4.0 release upgrade line
-    // Put any upgrade step following this.
+    // Put any upgrade step following this
 
-    if ($oldversion < 2012112901) {
-        // Get the instances of this block.
-        if ($blocks = $DB->get_records('block_instances', array('blockname' => 'news_items', 'pagetypepattern' => 'my-index'))) {
-            // Loop through and remove them from the My Moodle page.
-            foreach ($blocks as $block) {
-                blocks_delete_instance($block);
-            }
+    // Moodle v2.5.0 release upgrade line
+    // Put any upgrade step following this
+
+    if ($oldversion < 2013012200.00) {
 
+        // The section links block used to use its own crazy plugin name.
+        // Here we are converting it to the proper component name.
+        $oldplugin = 'blocks/section_links';
+        $newplugin = 'block_section_links';
+
+        // Use the proper API here... thats what we should be doing as it ensures any caches etc are cleared
+        // along the way!
+        // It may be quicker to just write an SQL statement but that would be reckless.
+        $config = get_config($oldplugin);
+        if (!empty($config)) {
+            foreach ($config as $name => $value) {
+                set_config($name, $value, $newplugin);
+                unset_config($name, $oldplugin);
+            }
         }
 
-        // Savepoint reached.
-        upgrade_block_savepoint(true, 2012112901, 'news_items');
+        // Main savepoint reached.
+        upgrade_block_savepoint(true, 2013012200.00, 'section_links');
     }
 
-
     return true;
-}
\ No newline at end of file
+}
diff --git a/blocks/section_links/edit_form.php b/blocks/section_links/edit_form.php
new file mode 100644 (file)
index 0000000..63a3593
--- /dev/null
@@ -0,0 +1,86 @@
+<?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/>.
+
+/**
+ * Instance configuration for the section links block.
+ *
+ * @package   block_section_links
+ * @copyright 2013 Sam Hemelryk
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Instance configuration form.
+ *
+ * @package   block_section_links
+ * @copyright 2013 Sam Hemelryk
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_section_links_edit_form extends block_edit_form {
+
+    /**
+     * The definition of the fields to use.
+     *
+     * @param MoodleQuickForm $mform
+     */
+    protected function specific_definition($mform) {
+        $mform->addElement('header', 'configheader', get_string('blocksettings', 'block'));
+
+        $numberofsections = array();
+        for ($i = 1; $i < 53; $i++){
+            $numberofsections[$i] = $i;
+        }
+
+        $increments = array();
+
+        for ($i = 1; $i < 11; $i++){
+            $increments[$i] = $i;
+        }
+
+        $config = get_config('block_section_links');
+
+        $selected = array(
+            1 => array(22, 2),
+            2 => array(40, 5),
+        );
+        if (!empty($config->numsections1)) {
+            if (empty($config->incby1)) {
+                $config->incby1 = $selected[1][1];
+            }
+            $selected[1] = array($config->numsections1, $config->incby1);
+        }
+
+        if (!empty($config->numsections2)) {
+            if (empty($config->incby1)) {
+                $config->incby1 = $selected[2][1];
+            }
+            $selected[2] = array($config->numsections2, $config->incby2);
+        }
+
+        for ($i = 1; $i < 3; $i++) {
+            $mform->addElement('select', 'config_numsections'.$i, get_string('numsections'.$i, 'block_section_links'), $numberofsections);
+            $mform->setDefault('config_numsections'.$i, $selected[$i][0]);
+            $mform->setType('config_numsections'.$i, PARAM_INT);
+            $mform->addHelpButton('config_numsections'.$i, 'numsections'.$i, 'block_section_links');
+
+            $mform->addElement('select', 'config_incby'.$i, get_string('incby'.$i, 'block_section_links'), $increments);
+            $mform->setDefault('config_incby'.$i, $selected[$i][1]);
+            $mform->setType('config_incby'.$i, PARAM_INT);
+            $mform->addHelpButton('config_incby'.$i, 'incby'.$i, 'block_section_links');
+        }
+
+    }
+}
\ No newline at end of file
index 3750260..77cf69f 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 /**
  * Strings for component 'block_section_links', language 'en', branch 'MOODLE_20_STABLE'
  *
- * @package   block_section_links
- * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package    block_section_links
+ * @copyright  Jason Hardin
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['incbydesc1'] = 'This is the value the section is incremented each time a section link is displayed starting at 1.';
-$string['incbydesc2'] = 'This is the value the section is incremented each time a section link is displayed starting at 1.';
 $string['incby1'] = 'Increase by';
+$string['incby1_help'] = 'This is the value the section is incremented each time a section link is displayed starting at 1.';
 $string['incby2'] = 'Alternative increase by';
+$string['incby2_help'] = 'This is the value the section is incremented each time a section link is displayed starting at 1.';
 $string['jumptocurrenttopic'] = 'Jump to current topic';
 $string['jumptocurrentweek'] = 'Jump to current week';
-$string['numsectionsdesc1'] = 'Once the number of sections in the course reaches this number then the increment by value is used.';
-$string['numsectionsdesc2'] = 'Once the number of sections in the course reaches this number then the Alternative increment by value is used.';
 $string['numsections1'] = 'Number of sections';
+$string['numsections1_help'] = 'Once the number of sections in the course reaches this number then the increment by value is used.';
 $string['numsections2'] = 'Alternative number of sections';
+$string['numsections2_help'] = 'Once the number of sections in the course reaches this number then the Alternative increment by value is used.';
 $string['pluginname'] = 'Section links';
 $string['section_links:addinstance'] = 'Add a new section links block';
 $string['topics'] = 'Topics';
diff --git a/blocks/section_links/renderer.php b/blocks/section_links/renderer.php
new file mode 100644 (file)
index 0000000..9fb281f
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+/**
+ * Renderer for the section links block.
+ *
+ * @since     2.5
+ * @package   block_section_links
+ * @copyright 2013 Sam Hemelryk
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Renderer for the section links block.
+ *
+ * @package   block_section_links
+ * @copyright 2013 Sam Hemelryk
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_section_links_renderer extends plugin_renderer_base {
+
+    /**
+     * Render a series of section links.
+     *
+     * @param stdClass $course The course we are rendering for.
+     * @param array $sections An array of section objects to render.
+     * @param bool|int The section to provide a jump to link for.
+     * @return string The HTML to display.
+     */
+    public function render_section_links(stdClass $course, array $sections, $jumptosection = false) {
+        $html = html_writer::start_tag('ol', array('class' => 'inline-list'));
+        foreach ($sections as $section) {
+            $attributes = array();
+            if (!$section->visible) {
+                $attributes['class'] = 'dimmed';
+            }
+            $html .= html_writer::start_tag('li');
+            $sectiontext = $section->section;
+            if ($section->highlight) {
+                $sectiontext = html_writer::tag('strong', $sectiontext);
+            }
+            $html .= html_writer::link(course_get_url($course, $section->section), $sectiontext, $attributes);
+            $html .= html_writer::end_tag('li').' ';
+        }
+        $html .= html_writer::end_tag('ol');
+        if ($jumptosection && isset($sections[$jumptosection])) {
+
+            if ($course->format == 'weeks') {
+                $linktext = new lang_string('jumptocurrentweek', 'block_section_links');
+            } else if ($course->format == 'topics') {
+                $linktext = new lang_string('jumptocurrenttopic', 'block_section_links');
+            }
+
+            $attributes = array();
+            if (!$sections[$jumptosection]->visible) {
+                $attributes['class'] = 'dimmed';
+            }
+            $html .= html_writer::link(course_get_url($course, $jumptosection), $linktext, $attributes);
+        }
+
+        return $html;
+    }
+}
\ No newline at end of file
index e7d2ce1..2fcb4a4 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-
 /**
  * Section links block
  *
- * @package    moodlecore
+ * @package    block_section_links
+ * @copyright  Jason Hardin
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die;
 
 if ($ADMIN->fulltree) {
-    $configs = array();
-
     $numberofsections = array();
 
     for ($i = 1; $i < 53; $i++){
@@ -42,17 +40,12 @@ if ($ADMIN->fulltree) {
                       2 => array(40,5));
 
     for($i = 1; $i < 3; $i++){
-        $configs[] = new admin_setting_configselect('numsections'.$i, get_string('numsections'.$i, 'block_section_links'),
-                            get_string('numsectionsdesc'.$i, 'block_section_links'),
-                            $selected[$i][0], $numberofsections);
-
-        $configs[] = new admin_setting_configselect('incby'.$i, get_string('incby'.$i, 'block_section_links'),
-                            get_string('incbydesc'.$i, 'block_section_links'),
-                            $selected[$i][1], $increments);
-    }
+        $settings->add(new admin_setting_configselect('block_section_links/numsections'.$i, get_string('numsections'.$i, 'block_section_links'),
+                            get_string('numsections'.$i.'_help', 'block_section_links'),
+                            $selected[$i][0], $numberofsections));
 
-    foreach ($configs as $config) {
-        $config->plugin = 'blocks/section_links';
-        $settings->add($config);
+        $settings->add(new admin_setting_configselect('block_section_links/incby'.$i, get_string('incby'.$i, 'block_section_links'),
+                            get_string('incby'.$i.'_help', 'block_section_links'),
+                            $selected[$i][1], $increments));
     }
 }
\ No newline at end of file
index 9997363..1bccc90 100644 (file)
 /**
  * Version details
  *
- * @package    block
- * @subpackage section_links
- * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @package    block_section_links
+ * @copyright  Jason Hardin
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112900;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013012200;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_section_links'; // Full name of the plugin (used for diagnostics)
diff --git a/blocks/tests/behat/add_blocks.feature b/blocks/tests/behat/add_blocks.feature
new file mode 100644 (file)
index 0000000..7ee5644
--- /dev/null
@@ -0,0 +1,24 @@
+@blocks
+Feature: Add blocks
+  In order to add more functionality to pages
+  As a teacher
+  I need to add blocks to pages
+
+  @javascript
+  Scenario: Add a block to a course
+    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"
+    And I turn editing mode on
+    When I add the "Blog menu" block
+    Then I should see "View all of my entries"
diff --git a/blocks/tests/behat/behat_blocks.php b/blocks/tests/behat/behat_blocks.php
new file mode 100644 (file)
index 0000000..c90f1df
--- /dev/null
@@ -0,0 +1,52 @@
+<?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 blocks.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2012 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;
+
+/**
+ * Blocks management steps definitions.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_blocks extends behat_base {
+
+    /**
+     * Adds the selected block. Editing mode must be previously enabled.
+     *
+     * @Given /^I add the "(?P<block_name_string>(?:[^"]|\\")*)" block$/
+     * @param string $blockname
+     */
+    public function i_add_the_block($blockname) {
+        return new Given('I select "' . $blockname . '" from "bui_addblock"');
+    }
+
+}
index cf3de51..56592ee 100644 (file)
@@ -129,7 +129,7 @@ if (!empty($action) && confirm_sesskey()) {
             }
             break;
         case 'editdefinitionmapping' : // Edit definition mappings.
-            $definition = required_param('definition', PARAM_TEXT);
+            $definition = required_param('definition', PARAM_ALPHANUMEXT);
             $title = get_string('editdefinitionmappings', 'cache', $definition);
             $mform = new cache_definition_mappings_form($PAGE->url, array('definition' => $definition));
             if ($mform->is_cancelled()) {
@@ -167,6 +167,14 @@ if (!empty($action) && confirm_sesskey()) {
             }
             break;
 
+        case 'purgedefinition': // Purge a specific definition.
+            $definition = required_param('definition', PARAM_ALPHANUMEXT);
+            list($component, $area) = explode('/', $definition, 2);
+            cache_helper::purge_by_definition($component, $area);
+            redirect($PAGE->url, get_string('purgedefinitionsuccess', 'cache'), 5);
+            break;
+
+        case 'purgestore':
         case 'purge': // Purge a store cache.
             $store = required_param('store', PARAM_TEXT);
             cache_helper::purge_store($store);
index 447ef30..c23a147 100644 (file)
@@ -48,6 +48,8 @@ class cache_factory {
     const STATE_SAVING = 2;
     /** The cache is ready to use. */
     const STATE_READY = 3;
+    /** The cache is currently updating itself */
+    const STATE_UPDATING = 4;
     /** The cache encountered an error while initialising. */
     const STATE_ERROR_INITIALISING = 9;
     /** The cache has been disabled. */
@@ -220,7 +222,12 @@ class cache_factory {
      */
     public function create_cache(cache_definition $definition) {
         $class = $definition->get_cache_class();
-        $stores = cache_helper::get_cache_stores($definition);
+        if ($this->is_initialising()) {
+            // Do nothing we just want the dummy store.
+            $stores = array();
+        } else {
+            $stores = cache_helper::get_cache_stores($definition);
+        }
         if (count($stores) === 0) {
             // Hmm no stores, better provide a dummy store to mimick functionality. The dev will be none the wiser.
             $stores[] = $this->create_dummy_store($definition);
@@ -333,21 +340,52 @@ class cache_factory {
             $id .= '::'.$aggregate;
         }
         if (!array_key_exists($id, $this->definitions)) {
-            $instance = $this->create_config_instance();
-            $definition = $instance->get_definition_by_id($id);
-            if (!$definition) {
-                $this->reset();
-                $instance = $this->create_config_instance(true);
-                $instance->update_definitions();
+            // This is the first time this definition has been requested.
+            if ($this->is_initialising()) {
+                // We're initialising the cache right now. Don't try to create another config instance.
+                // We'll just use an ad-hoc cache for the time being.
+                $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $component, $area);
+            } else {
+                // Load all the known definitions and find the desired one.
+                $instance = $this->create_config_instance();
                 $definition = $instance->get_definition_by_id($id);
                 if (!$definition) {
-                    throw new coding_exception('The requested cache definition does not exist.'. $id, $id);
+                    // Oh-oh the definition doesn't exist.
+                    // There are several things that could be going on here.
+                    // We may be installing/upgrading a site and have hit a definition that hasn't been used before.
+                    // Of the developer may be trying to use a newly created definition.
+                    if ($this->is_updating()) {
+                        // The cache is presently initialising and the requested cache definition has not been found.
+                        // This means that the cache initialisation has requested something from a cache (I had recursive nightmares about this).
+                        // To serve this purpose and avoid errors we are going to make use of an ad-hoc cache rather than
+                        // search for the definition which would possibly cause an infitite loop trying to initialise the cache.
+                        $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $component, $area);
+                        if ($aggregate !== null) {
+                            // If you get here you deserve a warning. We have to use an ad-hoc cache here, so we can't find the definition and therefor
+                            // can't find any information about the datasource or any of its aggregated.
+                            // Best of luck.
+                            debugging('An unknown cache was requested during development with an aggregate that could not be loaded. Ad-hoc cache used instead.', DEBUG_DEVELOPER);
+                            $aggregate = null;
+                        }
+                    } else {
+                        // Either a typo of the developer has just created the definition and is using it for the first time.
+                        $this->reset();
+                        $instance = $this->create_config_instance(true);
+                        $instance->update_definitions();
+                        $definition = $instance->get_definition_by_id($id);
+                        if (!$definition) {
+                            throw new coding_exception('The requested cache definition does not exist.'. $id, $id);
+                        } else {
+                            debugging('Cache definitions reparsed causing cache reset in order to locate definition.
+                                You should bump the version number to ensure definitions are reprocessed.', DEBUG_DEVELOPER);
+                        }
+                        $definition = cache_definition::load($id, $definition, $aggregate);
+                    }
                 } else {
-                    debugging('Cache definitions reparsed causing cache reset in order to locate definition.
-                        You should bump the version number to ensure definitions are reprocessed.', DEBUG_DEVELOPER);
+                    $definition = cache_definition::load($id, $definition, $aggregate);
                 }
             }
-            $this->definitions[$id] = cache_definition::load($id, $definition, $aggregate);
+            $this->definitions[$id] = $definition;
         }
         return $this->definitions[$id];
     }
@@ -414,6 +452,29 @@ class cache_factory {
         return true;
     }
 
+    /**
+     * Informs the factory that the cache is currently updating itself.
+     *
+     * This forces the state to upgrading and can only be called once the cache is ready to use.
+     * Calling it ensure we don't try to reinstantite things when requesting cache definitions that don't exist yet.
+     */
+    public function updating_started() {
+        if ($this->state !== self::STATE_READY) {
+            return false;
+        }
+        $this->state = self::STATE_UPDATING;
+        return true;
+    }
+
+    /**
+     * Informs the factory that the upgrading has finished.
+     *
+     * This forces the state back to ready.
+     */
+    public function updating_finished() {
+        $this->state = self::STATE_READY;
+    }
+
     /**
      * Returns true if the cache API has been disabled.
      *
@@ -423,6 +484,26 @@ class cache_factory {
         return $this->state === self::STATE_DISABLED;
     }
 
+    /**
+     * Returns true if the cache is currently initialising itself.
+     *
+     * This includes both initialisation and saving the cache config file as part of that initialisation.
+     *
+     * @return bool
+     */
+    public function is_initialising() {
+        return $this->state === self::STATE_INITIALISING || $this->state === self::STATE_SAVING;
+    }
+
+    /**
+     * Returns true if the cache is currently updating itself.
+     *
+     * @return bool
+     */
+    public function is_updating() {
+        return $this->state === self::STATE_UPDATING;
+    }
+
     /**
      * Disables as much of the cache API as possible.
      *
index 02dd03e..ce926a5 100644 (file)
@@ -411,8 +411,11 @@ class cache_config_writer extends cache_config {
      * @param bool $coreonly If set to true only core definitions will be updated.
      */
     public static function update_definitions($coreonly = false) {
-        $config = self::instance();
+        $factory = cache_factory::instance();
+        $factory->updating_started();
+        $config = $factory->create_config_instance(true);
         $config->write_definitions_to_cache(self::locate_definitions($coreonly));
+        $factory->updating_finished();
     }
 
     /**
@@ -705,6 +708,10 @@ abstract class cache_administration_helper extends cache_helper {
                 array(
                     'text' => get_string('editmappings', 'cache'),
                     'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping', 'sesskey' => sesskey()))
+                ),
+                array(
+                    'text' => get_string('purge', 'cache'),
+                    'url' => new moodle_url('/cache/admin.php', array('action' => 'purgedefinition', 'sesskey' => sesskey()))
                 )
             );
         }
@@ -734,7 +741,7 @@ abstract class cache_administration_helper extends cache_helper {
             }
             $actions[] = array(
                 'text' => get_string('purge', 'cache'),
-                'url' => new moodle_url($baseurl, array('action' => 'purge'))
+                'url' => new moodle_url($baseurl, array('action' => 'purgestore'))
             );
         }
         return $actions;
index 5f618de..e5d73be 100644 (file)
@@ -1,6 +1,13 @@
 {
+    "repositories" : [
+        {
+            "type": "vcs",
+            "url": "https://github.com/moodlehq/moodle-behat-extension"
+        }
+    ],
     "require-dev": {
         "phpunit/phpunit": "3.7.*",
-        "phpunit/dbUnit": "1.2.*"
+        "phpunit/dbUnit": "1.2.*",
+        "moodlehq/behat-extension": "1.0.3"
     }
-}
\ No newline at end of file
+}
index cb1aa0d..3cd7b3e 100644 (file)
@@ -536,11 +536,48 @@ $CFG->admin = 'admin';
 //                                        'otherplugin' => array('mysetting' => 'myvalue', 'thesetting' => 'thevalue'));
 //
 //=========================================================================
-// 9. PHPUNIT SUPPORT
+// 10. PHPUNIT SUPPORT
 //=========================================================================
 // $CFG->phpunit_prefix = 'phpu_';
 // $CFG->phpunit_dataroot = '/home/example/phpu_moodledata';
 // $CFG->phpunit_directorypermissions = 02777; // optional
+//
+//=========================================================================
+// 11. BEHAT SUPPORT
+//=========================================================================
+// Behat uses http://localhost:8000 as default URL to run
+// the acceptance tests, you can override this value.
+// Example:
+//   $CFG->behat_wwwroot = 'http://192.168.1.250:8000';
+//
+// You can override default Moodle configuration for Behat and add your own
+// params; here you can add more profiles, use different Mink drivers than Selenium...
+// These params would be merged with the default Moodle behat.yml, giving priority
+// to the ones specified here. The array format is YAML, following the Behat
+// params hierarchy. More info: http://docs.behat.org/guides/7.config.html
+// Example:
+//   $CFG->behat_config = array(
+//       'default' => array(
+//           'formatter' => array(
+//               'name' => 'pretty',
+//               'parameters' => array(
+//                   'decorated' => true,
+//                   'verbose' => false
+//               )
+//           )
+//       )
+//   );
+//
+// You can completely switch to test environment when "php admin/tool/behat/cli/util --enable",
+// this means that all the site accesses will be routed to the test environment instead of
+// the regular one, so NEVER USE THIS SETTING IN PRODUCTION SITES. This setting is useful
+// when working with cloud CI (continous integration) servers which requires public sites to run the
+// tests, or in testing/development installations when you are developing in a pre-PHP 5.4 server.
+// Note that with this setting enabled $CFG->behat_wwwroot is ignored and $CFG->behat_wwwroot
+// value will be the regular $CFG->wwwroot value.
+// Example:
+//   $CFG->behat_switchcompletely = true;
+//
 
 //=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
index 71ea312..68c8701 100644 (file)
@@ -322,7 +322,7 @@ if (!$courses) {
 
     echo '<form id="movecourses" action="category.php" method="post"><div>';
     echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
-    echo '<table border="0" cellspacing="2" cellpadding="4" class="generalbox boxaligncenter"><tr>';
+    echo '<table border="0" cellspacing="2" cellpadding="4" class="generaltable boxaligncenter"><tr>';
     echo '<th class="header" scope="col">'.get_string('courses').'</th>';
     if ($editingon) {
         echo '<th class="header" scope="col">'.get_string('edit').'</th>';
index 0673ade..c5e9d86 100644 (file)
@@ -356,7 +356,7 @@ function print_log($course, $user=0, $date=0, $order="l.time ASC", $page=0, $per
     echo $OUTPUT->paging_bar($totalcount, $page, $perpage, "$url&perpage=$perpage");
 
     $table = new html_table();
-    $table->classes = array('logtable','generalbox');
+    $table->classes = array('logtable','generaltable');
     $table->align = array('right', 'left', 'left');
     $table->head = array(
         get_string('time'),
@@ -1144,7 +1144,8 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
         // NOTE: this is legacy stuff, module subtypes are very strongly discouraged!!
         $gettypesfunc =  $modname.'_get_types';
         if (function_exists($gettypesfunc)) {
-            if ($types = $gettypesfunc()) {
+            $types = $gettypesfunc();
+            if (is_array($types) && count($types) > 0) {
                 $group = new stdClass();
                 $group->name = $modname;
                 $group->icon = $OUTPUT->pix_icon('icon', '', $modname, array('class' => 'icon'));
@@ -1193,7 +1194,11 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
             $module->archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
             $modlist[$course->id][$modname] = $module;
         }
-        $return[$modname] = $modlist[$course->id][$modname];
+        if (isset($modlist[$course->id][$modname])) {
+            $return[$modname] = $modlist[$course->id][$modname];
+        } else {
+            debugging("Invalid module metadata configuration for {$modname}");
+        }
     }
 
     return $return;
index 020edd0..ea619fe 100644 (file)
@@ -107,6 +107,11 @@ class course_reset_form extends moodleform {
 
         $defaults = array ('reset_events'=>1, 'reset_logs'=>1, 'reset_roles_local'=>1, 'reset_gradebook_grades'=>1, 'reset_notes'=>1);
 
+        // Set student as default in unenrol user list, if role with student archetype exist.
+        if ($studentrole = get_archetype_roles('student')) {
+            $defaults['unenrol_users'] = array_keys($studentrole);
+        }
+
         if ($allmods = $DB->get_records('modules') ) {
             foreach ($allmods as $mod) {
                 $modname = $mod->name;
index 721d968..aeccc4f 100644 (file)
@@ -282,7 +282,7 @@ if ($courses) {
         } else if (!empty($blocklist) and confirm_sesskey()) {
             echo "<input type=\"hidden\" name=\"blocklist\" value=\"$blocklist\" /></div>\n";
         }
-        echo "<table border=\"0\" cellspacing=\"2\" cellpadding=\"4\" class=\"generalbox boxaligncenter\">\n<tr>\n";
+        echo "<table border=\"0\" cellspacing=\"2\" cellpadding=\"4\" class=\"generaltable boxaligncenter\">\n<tr>\n";
         echo "<th scope=\"col\">$strcourses</th>\n";
         echo "<th scope=\"col\">$strcategory</th>\n";
         echo "<th scope=\"col\">$strselect</th>\n";
diff --git a/course/tests/behat/add_activities.feature b/course/tests/behat/add_activities.feature
new file mode 100644 (file)
index 0000000..75eba57
--- /dev/null
@@ -0,0 +1,56 @@
+@core_course
+Feature: Add activities to courses
+  In order to provide tools for students learning
+  As a teacher
+  I need to add activites to a course
+
+  @javascript
+  Scenario: Add an activity to the course
+    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:
+      | Name | Test name |
+      | Introduction | Test database description |
+      | Required entries | 9 |
+      | Comments | Yes |
+      | ID number | ASD123 |
+    Then I should not see "Adding a new"
+    And I follow "Test name"
+    And I follow "Edit settings"
+    And the "Name" field should match "Test name" value
+    And the "Required entries" field should match "9" value
+    And the "Comments" field should match "Yes" value
+    And the "ID number" field should match "ASD123" value
+
+  @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:
+      | Name | Test name |
+    Then I should see "Adding a new"
+    And I should see "Required"
diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php
new file mode 100644 (file)
index 0000000..3f5b4ba
--- /dev/null
@@ -0,0 +1,103 @@
+<?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/>.
+
+/**
+ * Behat course-related steps definitions.
+ *
+ * @package    core_course
+ * @category   test
+ * @copyright  2012 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;
+
+/**
+ * Course-related steps definitions.
+ *
+ * @package    core_course
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_course extends behat_base {
+
+    /**
+     * Turns editing mode on.
+     * @Given /^I turn editing mode on$/
+     */
+    public function i_turn_editing_mode_on() {
+        return new Given('I press "Turn editing on"');
+    }
+
+    /**
+     * Turns editing mode off.
+     * @Given /^I turn editing mode off$/
+     */
+    public function i_turn_editing_mode_off() {
+        return new Given('I press "Turn editing off"');
+    }
+
+    /**
+     * Adds the selected activity/resource filling the form data with the specified field/value pairs.
+     *
+     * @When /^I add a "(?P<activity_or_resource_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)" and I fill the form with:$/
+     * @param string $activity The activity name
+     * @param string $section The section number
+     * @param TableNode $data The activity field/value data
+     */
+    public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
+
+        $activity = $this->fixStepArgument($activity);
+        $section = $this->fixStepArgument($section);
+
+        // The 'I wait until the page is ready' is just in case.
+        return array(
+            new Given('I add a "'.$activity.'" to section "'.$section.'"'),
+            new Given('I fill the moodle form with:', $data),
+            new Given('I press "Save and return to course"')
+        );
+    }
+
+    /**
+     * Opens the activity chooser and opens the activity/resource form page.
+     *
+     * @Given /^I add a "(?P<activity_or_resource_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)"$/
+     * @param string $activity
+     * @param string $section
+     */
+    public function i_add_to_section($activity, $section) {
+
+        $activity = $this->fixStepArgument($activity);
+        $section = $this->fixStepArgument($section);
+
+        // Clicks add activity or resource section link.
+        $sectionxpath = "//*[@id='section-" . $section . "']/*/*/*/div[@class='section-modchooser']/*/*";
+        $section = $this->getSession()->getPage()->find('xpath', $sectionxpath);
+        $section->click();
+
+        // Clicks the selected activity if it exists.
+        $activityxpath = ".//label[contains(.,'" . $activity . "')]/input";
+        $activity = $this->getSession()->getPage()->find('xpath', $activityxpath);
+        $activity->doubleClick();
+    }
+
+}
index b763b92..d6f8fa9 100644 (file)
@@ -322,6 +322,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
+        // Enable course completion.
+        set_config('enablecompletion', 1);
+
         // Set the required capabilities by the external function
         $contextid = context_system::instance()->id;
         $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
@@ -367,7 +370,7 @@ class core_course_external_testcase extends externallib_advanced_testcase {
         foreach ($course3options as $key => $value) {
             $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value);
         }
-        $courses = array($course1, $course2);
+        $courses = array($course1, $course2, $course3);
 
         $createdcourses = core_course_external::create_courses($courses);
 
@@ -375,7 +378,7 @@ class core_course_external_testcase extends externallib_advanced_testcase {
         $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
 
         // Check that right number of courses were created.
-        $this->assertEquals(2, count($createdcourses));
+        $this->assertEquals(3, count($createdcourses));
 
         // Check that the courses were correctly created.
         foreach ($createdcourses as $createdcourse) {
@@ -407,13 +410,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
                     $this->assertEquals($courseinfo->theme, $course2['forcetheme']);
                 }
 
-                if (completion_info::is_enabled_for_site()) {
-                    $this->assertEquals($courseinfo->enablecompletion, $course2['enabledcompletion']);
-                    $this->assertEquals($courseinfo->completionstartonenrol, $course2['completionstartonenrol']);
-                } else {
-                    $this->assertEquals($courseinfo->enablecompletion, 0);
-                    $this->assertEquals($courseinfo->completionstartonenrol, 0);
-                }
+                // We enabled completion at the beginning of the test.
+                $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']);
+                $this->assertEquals($courseinfo->completionstartonenrol, $course2['completionstartonenrol']);
 
             } else if ($createdcourse['shortname'] == $course1['shortname']) {
                 $courseconfig = get_config('moodlecourse');
index 8c652e0..4e6ad15 100644 (file)
@@ -558,8 +558,9 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             // Cancel the edit if we lose focus or the escape key is pressed
             thisevent = editor.on('blur', cancel_edittitle);
             listenevents.push(thisevent);
-            thisevent = Y.one('document').on('keyup', function(e) {
-                if (e.keyCode == 27) {
+            thisevent = Y.one('document').on('keydown', function(e) {
+                if (e.keyCode === 27) {
+                    e.preventDefault();
                     cancel_edittitle(e);
                 }
             });
index 29b2d44..cbe065b 100644 (file)
@@ -614,112 +614,6 @@ class enrol_flatfile_plugin extends enrol_plugin {
         }
     }
 
-    /**
-     * Do any enrolment expiration processing.
-     *
-     * @param progress_trace $trace
-     * @return bool true if any data processed, false if not
-     */
-    protected function process_expirations(progress_trace $trace) {
-        global $DB;
-
-        //TODO: this method should be moved to parent class once we refactor all existing enrols, see MDL-36504.
-
-        $processed = false;
-        $name = $this->get_name();
-
-        // Deal with expired accounts.
-        $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
-
-        if ($action == ENROL_EXT_REMOVED_UNENROL) {
-            $instances = array();
-            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
-                      FROM {user_enrolments} ue
-                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
-                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
-                     WHERE ue.timeend > 0 AND ue.timeend < :now";
-            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name);
-
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $ue) {
-                if (!$processed) {
-                    $trace->output("Starting processing of enrol_$name expirations...");
-                    $processed = true;
-                }
-                if (empty($instances[$ue->enrolid])) {
-                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
-                }
-                $instance = $instances[$ue->enrolid];
-                if (!$this->roles_protected()) {
-                    // Let's just guess what extra roles are supposed to be removed.
-                    if ($instance->roleid) {
-                        role_unassign($instance->roleid, $ue->userid, $ue->contextid);
-                    }
-                }
-                // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
-                $this->unenrol_user($instance, $ue->userid);
-                $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
-            }
-            $rs->close();
-            unset($instances);
-
-        } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
-            $instances = array();
-            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
-                      FROM {user_enrolments} ue
-                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
-                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
-                     WHERE ue.timeend > 0 AND ue.timeend < :now
-                           AND ue.status = :useractive";
-            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name);
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $ue) {
-                if (!$processed) {
-                    $trace->output("Starting processing of enrol_$name expirations...");
-                    $processed = true;
-                }
-                if (empty($instances[$ue->enrolid])) {
-                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
-                }
-                $instance = $instances[$ue->enrolid];
-
-                if (!$this->roles_protected()) {
-                    // Let's just guess what roles should be removed.
-                    $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
-                    if ($count == 1) {
-                        role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
-
-                    } else if ($count > 1 and $instance->roleid) {
-                        role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
-                    }
-                }
-                // In any case remove all roles that belong to this instance and user.
-                role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
-                // Final cleanup of subcontexts if there are no more course roles.
-                if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
-                    role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
-                }
-
-                $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
-                $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
-            }
-            $rs->close();
-            unset($instances);
-
-        } else {
-            // ENROL_EXT_REMOVED_KEEP means no changes.
-        }
-
-        if ($processed) {
-            $trace->output("...finished processing of enrol_$name expirations");
-        } else {
-            $trace->output("No expired enrol_$name enrolments detected");
-        }
-        $trace->finished();
-
-        return $processed;
-    }
-
     /**
      * Returns the user who is responsible for flatfile enrolments in given curse.
      *
index ded9531..2ac3c38 100644 (file)
@@ -58,7 +58,7 @@ class enrol_ldap_testcase extends advanced_testcase {
         // Make sure we can connect the server.
         $debuginfo = '';
         if (!$connection = ldap_connect_moodle(TEST_ENROL_LDAP_HOST_URL, 3, 'rfc2307', TEST_ENROL_LDAP_BIND_DN, TEST_ENROL_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
-            $this->markTestSkipped('Can not connect to LDAP test server.');
+            $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo);
         }
 
         $this->enable_plugin();
index b0e1131..ca3cd74 100644 (file)
@@ -302,110 +302,4 @@ class enrol_paypal_plugin extends enrol_plugin {
         $this->process_expirations($trace);
         return 0;
     }
-
-    /**
-     * Do any enrolment expiration processing.
-     *
-     * @param progress_trace $trace
-     * @return bool true if any data processed, false if not
-     */
-    protected function process_expirations(progress_trace $trace) {
-        global $DB;
-
-        //TODO: this method should be moved to parent class once we refactor all existing enrols, see MDL-36504.
-
-        $processed = false;
-        $name = $this->get_name();
-
-        // Deal with expired accounts.
-        $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
-
-        if ($action == ENROL_EXT_REMOVED_UNENROL) {
-            $instances = array();
-            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
-                      FROM {user_enrolments} ue
-                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
-                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
-                     WHERE ue.timeend > 0 AND ue.timeend < :now";
-            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name);
-
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $ue) {
-                if (!$processed) {
-                    $trace->output("Starting processing of enrol_$name expirations...");
-                    $processed = true;
-                }
-                if (empty($instances[$ue->enrolid])) {
-                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
-                }
-                $instance = $instances[$ue->enrolid];
-                if (!$this->roles_protected()) {
-                    // Let's just guess what extra roles are supposed to be removed.
-                    if ($instance->roleid) {
-                        role_unassign($instance->roleid, $ue->userid, $ue->contextid);
-                    }
-                }
-                // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
-                $this->unenrol_user($instance, $ue->userid);
-                $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
-            }
-            $rs->close();
-            unset($instances);
-
-        } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
-            $instances = array();
-            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
-                      FROM {user_enrolments} ue
-                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
-                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
-                     WHERE ue.timeend > 0 AND ue.timeend < :now
-                           AND ue.status = :useractive";
-            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name);
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $ue) {
-                if (!$processed) {
-                    $trace->output("Starting processing of enrol_$name expirations...");
-                    $processed = true;
-                }
-                if (empty($instances[$ue->enrolid])) {
-                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
-                }
-                $instance = $instances[$ue->enrolid];
-
-                if (!$this->roles_protected()) {
-                    // Let's just guess what roles should be removed.
-                    $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
-                    if ($count == 1) {
-                        role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
-
-                    } else if ($count > 1 and $instance->roleid) {
-                        role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
-                    }
-                }
-                // In any case remove all roles that belong to this instance and user.
-                role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
-                // Final cleanup of subcontexts if there are no more course roles.
-                if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
-                    role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
-                }
-
-                $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
-                $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
-            }
-            $rs->close();
-            unset($instances);
-
-        } else {
-            // ENROL_EXT_REMOVED_KEEP means no changes.
-        }
-
-        if ($processed) {
-            $trace->output("...finished processing of enrol_$name expirations");
-        } else {
-            $trace->output("No expired enrol_$name enrolments detected");
-        }
-        $trace->finished();
-
-        return $processed;
-    }
 }
index c7c3c46..b624d92 100644 (file)
@@ -446,65 +446,11 @@ class enrol_self_plugin extends enrol_plugin {
         }
         $rs->close();
 
-        // Deal with expired accounts.
-        $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
-
-        if ($action == ENROL_EXT_REMOVED_UNENROL) {
-            $instances = array();
-            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
-                      FROM {user_enrolments} ue
-                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self')
-                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
-                     WHERE ue.timeend > 0 AND ue.timeend < :now
-                           $coursesql";
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $ue) {
-                if (empty($instances[$ue->enrolid])) {
-                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
-                }
-                $instance = $instances[$ue->enrolid];
-                if ($instance->roleid) {
-                    role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
-                }
-                $this->unenrol_user($instance, $ue->userid);
-                $trace->output("unenrolling expired user $ue->userid from course $instance->courseid", 1);
-            }
-            $rs->close();
-            unset($instances);
-
-        } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
-            $instances = array();
-            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
-                      FROM {user_enrolments} ue
-                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self')
-                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
-                     WHERE ue.timeend > 0 AND ue.timeend < :now
-                           AND ue.status = :useractive
-                           $coursesql";
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $ue) {
-                if (empty($instances[$ue->enrolid])) {
-                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
-                }
-                $instance = $instances[$ue->enrolid];
-                if (1 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
-                    role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
-                } else if ($instance->roleid) {
-                    role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
-                }
-                $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
-                $trace->output("suspending expired user $ue->userid in course $instance->courseid", 1);
-            }
-            $rs->close();
-            unset($instances);
-
-        } else {
-            // ENROL_EXT_REMOVED_KEEP means no changes.
-        }
-
         $trace->output('...user self-enrolment updates finished.');
         $trace->finished();
 
+        $this->process_expirations($trace, $courseid);
+
         return 0;
     }
 
index afa52c2..9080888 100644 (file)
@@ -56,7 +56,6 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
     Y.extend(ROLE, Y.Base, {
         users : [],
         roleAssignmentPanel : null,
-        panelsubmitevent : null,
         rolesLoadedEvent : null,
         escCloseEvent  : null,
         initializer : function(config) {
@@ -75,14 +74,16 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
             this.rolesLoadedEvent = this.on('assignablerolesloaded', function(){
                 this.rolesLoadedEvent.detach();
                 var panel = this._getRoleAssignmentPanel();
-                this.panelsubmitevent = panel.on('submit', this.addRoleCallback, this);
-                panel.hide().display(user);
+                panel.hide();
+                panel.submitevent = panel.on('submit', this.addRoleCallback, this);
+                panel.display(user);
             }, this);
             this._loadAssignableRoles();
         },
         addRoleCallback : function(e, roleid, userid) {
-            this.panelsubmitevent.detach();
             var panel = this._getRoleAssignmentPanel();
+            panel.submitevent.detach();
+            panel.submitevent = null;
             Y.io(M.cfg.wwwroot+'/enrol/ajax.php', {
                 method:'POST',
                 data:'id='+this.get(COURSEID)+'&action=assign&sesskey='+M.cfg.sesskey+'&roleid='+roleid+'&user='+userid,
@@ -350,6 +351,7 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
     Y.extend(ROLEPANEL, Y.Base, {
         user : null,
         roles : [],
+        submitevent : null,
         initializer : function() {
             var i, m = this.get(MANIPULATOR);
             var element = Y.Node.create('<div class="enrolpanel roleassign"><div class="container"><div class="header"><h2>'+M.str.role.assignroles+'</h2><div class="close"></div></div><div class="content"></div></div></div>');
@@ -398,6 +400,10 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
             this.roles = [];
             this.user = null;
             this.get('elementNode').removeClass('visible');
+            if (this.submitevent) {
+                this.submitevent.detach();
+                this.submitevent = null;
+            }
             this.displayed = false;
             return this;
         },
index 10a199f..320d796 100644 (file)
--- a/index.php
+++ b/index.php
                     $forumname = format_string($newsforum->name, true, array('context' => $newsforumcontext));
                     echo html_writer::tag('a', get_string('skipa', 'access', textlib::strtolower(strip_tags($forumname))), array('href'=>'#skipsitenews', 'class'=>'skip-block'));
 
+                    // wraps site news forum in div container.
+                    echo html_writer::start_tag('div', array('id'=>'site-news-forum'));
+
                     if (isloggedin()) {
                         $SESSION->fromdiscussion = $CFG->wwwroot;
                         $subtext = '';
                     }
 
                     forum_print_latest_discussions($SITE, $newsforum, $SITE->newsitems, 'plain', 'p.modified DESC');
+
+                    //end site news forum div container
+                    echo html_writer::end_tag('div');
+
                     echo html_writer::tag('span', '', array('class'=>'skip-block-to', 'id'=>'skipsitenews'));
                 }
             break;
                 $ncourses = $DB->count_records('course');
                 if (isloggedin() and !$hassiteconfig and !isguestuser() and empty($CFG->disablemycourses)) {
                     echo html_writer::tag('a', get_string('skipa', 'access', textlib::strtolower(get_string('mycourses'))), array('href'=>'#skipmycourses', 'class'=>'skip-block'));
+
+                    //wrap frontpage course list in div container
+                    echo html_writer::start_tag('div', array('id'=>'frontpage-course-list'));
+
                     echo $OUTPUT->heading(get_string('mycourses'), 2, 'headingblock header');
                     print_my_moodle();
+
+                    //end frontpage course list div container
+                    echo html_writer::end_tag('div');
+
                     echo html_writer::tag('span', '', array('class'=>'skip-block-to', 'id'=>'skipmycourses'));
                 } else if ((!$hassiteconfig and !isguestuser()) or ($ncourses <= FRONTPAGECOURSELIMIT)) {
                     // admin should not see list of courses when there are too many of them
                     echo html_writer::tag('a', get_string('skipa', 'access', textlib::strtolower(get_string('availablecourses'))), array('href'=>'#skipavailablecourses', 'class'=>'skip-block'));
+
+                    //wrap frontpage course list in div container
+                    echo html_writer::start_tag('div', array('id'=>'frontpage-course-list'));
+
                     echo $OUTPUT->heading(get_string('availablecourses'), 2, 'headingblock header');
                     print_courses(0);
+
+                    //end frontpage course list div container
+                    echo html_writer::end_tag('div');
+
                     echo html_writer::tag('span', '', array('class'=>'skip-block-to', 'id'=>'skipavailablecourses'));
                 } else {
                     echo html_writer::tag('div', get_string('therearecourses', '', $ncourses), array('class' => 'notifyproblem'));
 
             case FRONTPAGECATEGORYNAMES:
                 echo html_writer::tag('a', get_string('skipa', 'access', textlib::strtolower(get_string('categories'))), array('href'=>'#skipcategories', 'class'=>'skip-block'));
+
+                //wrap frontpage category names in div container
+                echo html_writer::start_tag('div', array('id'=>'frontpage-category-names'));
+
                 echo $OUTPUT->heading(get_string('categories'), 2, 'headingblock header');
                 echo $OUTPUT->box_start('generalbox categorybox');
                 print_whole_category_list(NULL, NULL, NULL, -1, false);
                 echo $OUTPUT->box_end();
                 print_course_search('', false, 'short');
+
+                //end frontpage category names div container
+                echo html_writer::end_tag('div');
+
                 echo html_writer::tag('span', '', array('class'=>'skip-block-to', 'id'=>'skipcategories'));
             break;
 
             case FRONTPAGECATEGORYCOMBO:
                 echo html_writer::tag('a', get_string('skipa', 'access', textlib::strtolower(get_string('courses'))), array('href'=>'#skipcourses', 'class'=>'skip-block'));
+
+                //wrap frontpage category combo in div container
+                echo html_writer::start_tag('div', array('id'=>'frontpage-category-combo'));
+
                 echo $OUTPUT->heading(get_string('courses'), 2, 'headingblock header');
                 $renderer = $PAGE->get_renderer('core','course');
-                // if there are too many courses, budiling course category tree could be slow,
+                // if there are too many courses, building course category tree could be slow,
                 // users should go to course index page to see the whole list.
                 $coursecount = $DB->count_records('course');
                 if (empty($CFG->numcoursesincombo)) {
                     echo $renderer->course_category_tree(get_course_category_tree());
                 }
                 print_course_search('', false, 'short');
+
+                //end frontpage category combo div container
+                echo html_writer::end_tag('div');
+
                 echo html_writer::tag('span', '', array('class'=>'skip-block-to', 'id'=>'skipcourses'));
             break;
 
index df948f3..12854e1 100644 (file)
@@ -105,6 +105,7 @@ $string['cliincorrectvalueerror'] = 'Error, incorrect value "{$a->value}" for "{
 $string['cliincorrectvalueretry'] = 'Incorrect value, please retry';
 $string['clistatusdisabled'] = 'Status: disabled';
 $string['clistatusenabled'] = 'Status: enabled';
+$string['clistatusenabledlater'] = 'status: CLI maintenance mode will be enabled on {$a}';
 $string['clitypevalue'] = 'type value';
 $string['clitypevaluedefault'] = 'type value, press Enter to use default value ({$a})';
 $string['cliunknowoption'] = 'Unrecognised options:
@@ -664,6 +665,7 @@ $string['loginpasswordautocomplete_help'] = 'Having this off will let users save
 $string['loglifetime'] = 'Keep logs for';
 $string['longtimewarning'] = '<b>Please note that this process can take a long time.</b>';
 $string['maintenancemode'] = 'In maintenance mode';
+$string['maintenancemodeisscheduled'] = 'Site is switching to maintenance mode in {$a} minutes';
 $string['maintfileopenerror'] = 'Error opening maintenance files!';
 $string['maintinprogress'] = 'Maintenance is in progress...';
 $string['manageformats'] = 'Manage course formats';
@@ -955,6 +957,7 @@ $string['sitemaintenance'] = 'The site is undergoing maintenance and is currentl
 $string['sitemaintenancemode'] = 'Maintenance mode';
 $string['sitemaintenanceoff'] = 'Maintenance mode has been disabled and the site is running normally again';
 $string['sitemaintenanceon'] = 'Your site is currently in maintenance mode (only admins can log in or use the site).';
+$string['sitemaintenanceoncli'] = 'Your site is currently in CLI maintenance mode, no web access is allowed.';
 $string['sitemaintenancewarning'] = 'Your site is currently in maintenance mode (only admins can log in).  To return this site to normal operation, <a href="maintenance.php">disable maintenance mode</a>.';
 $string['sitemaintenancewarning2'] = 'Your site is currently in maintenance mode (only admins can log in).  To return this site to normal operation, <a href="{$a}">disable maintenance mode</a>.';
 $string['sitepolicies'] = 'Site policies';
index 208e9a4..64d772b 100644 (file)
@@ -92,6 +92,7 @@ $string['none'] = 'None';
 $string['plugin'] = 'Plugin';
 $string['pluginsummaries'] = 'Installed cache stores';
 $string['purge'] = 'Purge';
+$string['purgedefinitionsuccess'] = 'Successfully purged the requested definition.';
 $string['purgestoresuccess'] = 'Successfully purged the requested store.';
 $string['requestcount'] = 'Test with {$a} requests';
 $string['rescandefinitions'] = 'Rescan definitions';
index d926cd2..d5f2c6c 100644 (file)
@@ -41,6 +41,7 @@ $string['err_rangelength'] = 'You must enter between {$a->format[0]} and {$a->fo
 $string['err_required'] = 'You must supply a value here.';
 $string['general'] = 'General';
 $string['hideadvanced'] = 'Hide advanced';
+$string['hideeditortoolbar'] = 'Hide editing tools';
 $string['hour'] = 'Hour';
 $string['minute'] = 'Minute';
 $string['miscellaneoussettings'] = 'Miscellaneous settings';
@@ -57,6 +58,7 @@ $string['security'] = 'Security';
 $string['selectallornone'] = 'Select all/none';
 $string['selected'] = 'Selected';
 $string['showadvanced'] = 'Show advanced';
+$string['showeditortoolbar'] = 'Show editing tools';
 $string['somefieldsrequired'] = 'There are required fields in this form marked {$a}.';
 $string['time'] = 'Time';
 $string['timeunit'] = 'Time unit';
index 6c8ae80..edc77fa 100644 (file)
@@ -37,7 +37,7 @@ $string['blockcontact'] = 'Block contact';
 $string['blockedmessages'] = '{$a} message(s) to/from blocked users';
 $string['blockedusers'] = 'Blocked users ({$a})';
 $string['blocknoncontacts'] = 'Prevent non-contacts from messaging me';
-$string['contactlistempty'] = 'Your contact list is empty';
+$string['contactlistempty'] = 'Contact list empty';
 $string['contacts'] = 'Contacts';
 $string['context'] = 'context';
 $string['defaultmessageoutputs'] = 'Default message outputs';
@@ -83,7 +83,6 @@ $string['messages'] = 'Messages';
 $string['messaging'] = 'Messaging';
 $string['messagingblockednoncontact'] = '{$a} will not be able to reply as you have blocked non-contacts';
 $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
-$string['mycontacts'] = 'My contacts';
 $string['newonlymsg'] = 'Show only new';
 $string['newsearch'] = 'New search';
 $string['noframesjs'] = 'Use more accessible interface';
index 3dea4dd..d7d58fe 100644 (file)
@@ -421,6 +421,7 @@ $string['technicalinfoquestionsummary'] = 'Question summary: {$a}';
 $string['technicalinforightsummary'] = 'Right answer summary: {$a}';
 $string['technicalinfostate'] = 'Question state: {$a}';
 $string['unknownbehaviour'] = 'Unknown behaviour: {$a}.';
+$string['unknownorunhandledtype'] = 'Unknown or unhandled question type: {$a}';
 $string['unknownquestion'] = 'Unknown question: {$a}.';
 $string['unknownquestioncatregory'] = 'Unknown question category: {$a}.';
 $string['unknownquestiontype'] = 'Unknown question type: {$a}.';
index 67590fe..a1dbf29 100644 (file)
@@ -313,6 +313,7 @@ $string['role:override'] = 'Override permissions for others';
 $string['role:review'] = 'Review permissions for others';
 $string['roleprohibitheader'] = 'Prohibit role';
 $string['roleprohibitinfo'] = 'Select a role to be added to the list of prohibited roles in context {$a->context}, capability {$a->cap}:';
+$string['rolerisks'] = 'Role risks';
 $string['roles'] = 'Roles';
 $string['roles_help'] = 'A role is a collection of permissions defined for the whole system that you can assign to specific users in specific contexts.';
 $string['roles_link'] = 'roles';
index 17f22fc..fe4dc93 100644 (file)
@@ -679,6 +679,34 @@ function is_dataroot_insecure($fetchtest=false) {
     return INSECURE_DATAROOT_WARNING;
 }
 
+/**
+ * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
+ */
+function enable_cli_maintenance_mode() {
+    global $CFG;
+
+    if (file_exists("$CFG->dataroot/climaintenance.html")) {
+        unlink("$CFG->dataroot/climaintenance.html");
+    }
+
+    if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
+        $data = $CFG->maintenance_message;
+        $data = bootstrap_renderer::early_error_content($data, null, null, null);
+        $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
+
+    } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
+        $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
+
+    } else {
+        $data = get_string('sitemaintenance', 'admin');
+        $data = bootstrap_renderer::early_error_content($data, null, null, null);
+        $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
+    }
+
+    file_put_contents("$CFG->dataroot/climaintenance.html", $data);
+    chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
+}
+
 /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
 
 
@@ -8043,7 +8071,7 @@ class admin_setting_configcolourpicker extends admin_setting {
         $PAGE->requires->js_init_call('M.util.init_colour_picker', array($this->get_id(), $this->previewconfig));
         $content  = html_writer::start_tag('div', array('class'=>'form-colourpicker defaultsnext'));
         $content .= html_writer::tag('div', $OUTPUT->pix_icon('i/loading', get_string('loading', 'admin'), 'moodle', array('class'=>'loadingicon')), array('class'=>'admin_colourpicker clearfix'));
-        $content .= html_writer::empty_tag('input', array('type'=>'text','id'=>$this->get_id(), 'name'=>$this->get_full_name(), 'value'=>$this->get_setting(), 'size'=>'12'));
+        $content .= html_writer::empty_tag('input', array('type'=>'text','id'=>$this->get_id(), 'name'=>$this->get_full_name(), 'value'=>$data, 'size'=>'12'));
         if (!empty($this->previewconfig)) {
             $content .= html_writer::empty_tag('input', array('type'=>'button','id'=>$this->get_id().'_preview', 'value'=>get_string('preview'), 'class'=>'admin_colourpicker_preview'));
         }
diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php
new file mode 100644 (file)
index 0000000..c957837
--- /dev/null
@@ -0,0 +1,131 @@
+<?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/>.
+
+/**
+ * Base class of all steps definitions.
+ *
+ * This script is only called from Behat as part of it's integration
+ * in Moodle.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2012 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.
+
+/**
+ * Steps definitions base class.
+ *
+ * To extend by the steps definitions of the different Moodle components.
+ *
+ * It can not contain steps definitions to avoid duplicates, only utility
+ * methods shared between steps.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2012 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
+
+    /**
+     * The timeout for each Behat step (load page, wait for an element to load...).
+     */
+    const TIMEOUT = 6;
+
+    /**
+     * Returns fixed step argument (with \\" replaced back to ").
+     *
+     * \\ is the chars combination to add when you
+     * want to escape the " character that is used as var
+     * delimiter.
+     *
+     * @see Behat\MinkExtension\Context\MinkContext
+     * @param string $argument
+     * @return string
+     */
+    protected function fixStepArgument($argument) {
+        return str_replace('\\"', '"', $argument);
+    }
+
+    /**
+     * Locates url, based on provided path.
+     * Override to provide custom routing mechanism.
+     *
+     * @see Behat\MinkExtension\Context\MinkContext
+     * @param string $path
+     * @return string
+     */
+    protected function locatePath($path) {
+        $startUrl = rtrim($this->getMinkParameter('base_url'), '/') . '/';
+        return 0 !== strpos($path, 'http') ? $startUrl . ltrim($path, '/') : $path;
+    }
+
+    /**
+     * Executes the passed closure until returns true or time outs.
+     *
+     * In most cases the document.readyState === 'complete' will be enough, but sometimes JS
+     * requires more time to be completely loaded or an element to be visible or whatever is required to
+     * perform some action on an element; this method receives a closure which should contain the
+     * required statements to ensure the step definition actions and assertions have all their needs
+     * satisfied and executes it until they are satisfied or it timeouts. Redirects the return of the
+     * closure to the caller.
+     *
+     * The closures requirements to work well with this spin method are:
+     * - Must return false, null or '' if something goes wrong
+     * - Must return something != false if finishes as expected, this will be the (mixed) value
+     * returned by spin()
+     *
+     * Requires the exception to provide more accurate feedback to tests writers.
+     *
+     * @throws Exception If it timeouts without receiving something != false from the closure
+     * @param Closure $lambda The function to execute.
+     * @param Exception $exception The exception to throw in case it time outs.
+     * @param array $args Arguments to pass to the closure
+     * @return mixed The value returned by the closure
+     */
+    protected function spin($lambda, $exception, $args, $timeout = false) {
+
+        // Using default timeout which is pretty high.
+        if (!$timeout) {
+            $timeout = self::TIMEOUT;
+        }
+
+        for ($i = 0; $i < $timeout; $i++) {
+
+            // We catch the exception thrown by the step definition to execute it again.
+            try {
+
+                // We don't check with !== because most of the time closures will return
+                // direct Behat methods returns and we are not sure it will be always (bool)false.
+                if ($return = $lambda($this, $args)) {
+                    return $return;
+                }
+            } catch(Exception $e) {
+                // We wait until no exception is thrown or timeout expires.
+                continue;
+            }
+
+            sleep(1);
+        }
+
+        // Throwing exception to the user.
+        throw $exception;
+    }
+
+}
diff --git a/lib/behat/classes/behat_command.php b/lib/behat/classes/behat_command.php
new file mode 100644 (file)
index 0000000..8bdb123
--- /dev/null
@@ -0,0 +1,153 @@
+<?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/>.
+
+/**
+ * Behat command utils
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/../lib.php');
+
+/**
+ * Behat command related utils
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_command {
+
+    /**
+     * Docs url
+     */
+    const DOCS_URL = 'http://docs.moodle.org/dev/Acceptance_testing';
+
+    /**
+     * Ensures the behat dir exists in moodledata
+     * @return string Full path
+     */
+    public static function get_behat_dir() {
+        global $CFG;
+
+        $behatdir = $CFG->behat_dataroot . '/behat';
+
+        if (!is_dir($behatdir)) {
+            if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
+                behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Directory ' . $behatdir . ' can not be created');
+            }
+        }
+
+        if (!is_writable($behatdir)) {
+            behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Directory ' . $behatdir . ' is not writable');
+        }
+
+        return $behatdir;
+    }
+
+    /**
+     * Returns the executable path
+     * @return string
+     */
+    public final static function get_behat_command() {
+        return 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'behat';
+    }
+
+    /**
+     * Runs behat command with provided options
+     *
+     * Execution continues when the process finishes
+     *
+     * @param  string $options  Defaults to '' so tests would be executed
+     * @return array            CLI command outputs [0] => string, [1] => integer
+     */
+    public final static function run($options = '') {
+        global $CFG;
+
+        $currentcwd = getcwd();
+        chdir($CFG->dirroot);
+        exec(self::get_behat_command() . ' ' . $options, $output, $code);
+        chdir($currentcwd);
+
+        return array($output, $code);
+    }
+
+    /**
+     * Checks if behat is set up and working
+     *
+     * Uses notice() instead of behat_error() because is
+     * also called from web interface
+     *
+     * It checks behat dependencies have been installed and runs
+     * the behat help command to ensure it works as expected
+     *
+     * @param  bool $checkphp Extra check for the PHP version
+     * @return void
+     */
+    public static function check_behat_setup($checkphp = false) {
+        global $CFG;
+
+        // We don't check the PHP version if $CFG->behat_switchcompletely has been enabled.
+        // Here we are in CLI.
+        if (empty($CFG->behat_switchcompletely) && $checkphp && version_compare(PHP_VERSION, '5.4.0', '<')) {
+            behat_error(BEHAT_EXITCODE_REQUIREMENT, 'PHP 5.4 is required. See config-dist.php for possible alternatives');
+        }
+
+        // Moodle setting.
+        if (!self::are_behat_dependencies_installed()) {
+
+            $msg = get_string('wrongbehatsetup', 'tool_behat');
+
+            // With HTML.
+            $docslink = self::DOCS_URL . '#Installation';
+            if (!CLI_SCRIPT) {
+                $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
+            }
+            $msg .= '. ' . get_string('moreinfoin', 'tool_behat', $docslink);
+            notice($msg);
+        }
+
+        // Behat test command.
+        list($output, $code) = self::run(' --help');
+
+        if ($code != 0) {
+            notice(get_string('wrongbehatsetup', 'tool_behat'));
+        }
+
+        // Checking behat dataroot existence otherwise notice about admin/tool/behat/cli/util.php.
+        if (empty($CFG->behat_dataroot) || !is_dir($CFG->behat_dataroot) || !is_writable($CFG->behat_dataroot)) {
+            notice(get_string('runclitool', 'tool_behat', 'php admin/tool/behat/cli/util.php'));
+        }
+    }
+
+    /**
+     * Has the site installed composer with --dev option
+     * @return bool
+     */
+    public static function are_behat_dependencies_installed() {
+        if (!is_dir(__DIR__ . '/../../../vendor/behat')) {
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/lib/behat/classes/behat_config_manager.php b/lib/behat/classes/behat_config_manager.php
new file mode 100644 (file)
index 0000000..1c45dbd
--- /dev/null
@@ -0,0 +1,272 @@
+<?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/>.
+
+/**
+ * Utils to set Behat config
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/../lib.php');
+require_once(__DIR__ . '/behat_command.php');
+require_once(__DIR__ . '/../../testing/classes/tests_finder.php');
+
+/**
+ * Behat configuration manager
+ *
+ * Creates/updates Behat config files getting tests
+ * and steps from Moodle codebase
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_config_manager {
+
+    /**
+     * Updates a config file
+     *
+     * The tests runner and the steps definitions list uses different
+     * config files to avoid problems with concurrent executions.
+     *
+     * The steps definitions list can be filtered by component so it's
+     * behat.yml is different from the $CFG->dirroot one.
+     *
+     * @param  string $component Restricts the obtained steps definitions to the specified component
+     * @param  string $testsrunner If the config file will be used to run tests
+     * @return void
+     */
+    public static function update_config_file($component = '', $testsrunner = true) {
+        global $CFG;
+
+        // Behat must have a separate behat.yml to have access to the whole set of features and steps definitions.
+        if ($testsrunner === true) {
+            $configfilepath = behat_command::get_behat_dir() . '/behat.yml';
+        } else {
+            // Alternative for steps definitions filtering, one for each user.
+            $configfilepath = self::get_steps_list_config_filepath();
+        }
+
+        // Gets all the components with features.
+        $features = array();
+        $components = tests_finder::get_components_with_tests('features');
+        if ($components) {
+            foreach ($components as $componentname => $path) {
+                $path = self::clean_path($path) . self::get_behat_tests_path();
+                if (empty($featurespaths[$path]) && file_exists($path)) {
+
+                    // Standarizes separator (some dirs. comes with OS-dependant separator).
+                    $uniquekey = str_replace('\\', '/', $path);
+                    $featurespaths[$uniquekey] = $path;
+                }
+            }
+            $features = array_values($featurespaths);
+        }
+
+        // Gets all the components with steps definitions.
+        $stepsdefinitions = array();
+        $steps = self::get_components_steps_definitions();
+        if ($steps) {
+            foreach ($steps as $key => $filepath) {
+                if ($component == '' || $component === $key) {
+                    $stepsdefinitions[$key] = $filepath;
+                }
+            }
+        }
+
+        // Behat config file specifing the main context class,
+        // the required Behat extensions and Moodle test wwwroot.
+        $contents = self::get_config_file_contents($features, $stepsdefinitions);
+
+        // Stores the file.
+        if (!file_put_contents($configfilepath, $contents)) {
+            behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $configfilepath . ' can not be created');
+        }
+
+    }
+
+    /**
+     * Gets the list of Moodle steps definitions
+     *
+     * Class name as a key and the filepath as value
+     *
+     * Externalized from update_config_file() to use
+     * it from the steps definitions web interface
+     *
+     * @return array
+     */
+    public static function get_components_steps_definitions() {
+
+        $components = tests_finder::get_components_with_tests('stepsdefinitions');
+        if (!$components) {
+            return false;
+        }
+
+        $stepsdefinitions = array();
+        foreach ($components as $componentname => $componentpath) {
+            $componentpath = self::clean_path($componentpath);
+            $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path());
+            $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
+
+            // All behat_*.php inside behat_config_manager::get_behat_tests_path() are added as steps definitions files.
+            foreach ($regite as $file) {
+                $key = $file->getBasename('.php');
+                $stepsdefinitions[$key] = $file->getPathname();
+            }
+        }
+
+        return $stepsdefinitions;
+    }
+
+    /**
+     * Returns the behat config file path used by the steps definition list
+     *
+     * Note this can only be called from web-based scripts so it will return the
+     * production dataroot not behat_dataroot. With this the steps definitions
+     * list is accessible without having to install the behat test site.
+     *
+     * @return string
+     */
+    public static function get_steps_list_config_filepath() {
+        global $USER;
+
+        $userdir = behat_command::get_behat_dir() . '/users/' . $USER->id;
+        make_writable_directory($userdir);
+
+        return $userdir . '/behat.yml';
+    }
+
+    /**
+     * Behat config file specifing the main context class,
+     * the required Behat extensions and Moodle test wwwroot.
+     *
+     * @param array $features The system feature files
+     * @param array $stepsdefinitions The system steps definitions
+     * @return string
+     */
+    protected static function get_config_file_contents($features, $stepsdefinitions) {
+        global $CFG;
+
+        // We require here when we are sure behat dependencies are available.
+        require_once($CFG->dirroot . '/vendor/autoload.php');
+
+        $basedir = $CFG->dirroot . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'behat';
+        $config = array(
+            'default' => array(
+                'paths' => array(
+                    'features' => $basedir . DIRECTORY_SEPARATOR . 'features',
+                    'bootstrap' => $basedir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'bootstrap',
+                ),
+                'context' => array(
+                    'class' => 'behat_init_context'
+                ),
+                'extensions' => array(
+                    'Behat\MinkExtension\Extension' => array(
+                        'base_url' => $CFG->behat_wwwroot,
+                        'goutte' => null,
+                        'selenium2' => null
+                    ),
+                    'Moodle\BehatExtension\Extension' => array(
+                        'features' => $features,
+                        'steps_definitions' => $stepsdefinitions
+                    )
+                )
+            )
+        );
+
+        // In case user defined overrides respect them over our default ones.
+        if (!empty($CFG->behat_config)) {
+            $config = self::merge_config($config, $CFG->behat_config);
+        }
+
+        return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
+    }
+
+    /**
+     * Overrides default config with local config values
+     *
+     * array_merge does not merge completely the array's values
+     *
+     * @param mixed $config The node of the default config
+     * @param mixed $localconfig The node of the local config
+     * @return mixed The merge result
+     */
+    protected static function merge_config($config, $localconfig) {
+
+        if (!is_array($config) && !is_array($localconfig)) {
+            return $localconfig;
+        }
+
+        // Local overrides also deeper default values.
+        if (is_array($config) && !is_array($localconfig)) {
+            return $localconfig;
+        }
+
+        foreach ($localconfig as $key => $value) {
+
+            // If defaults are not as deep as local values let locals override.
+            if (!is_array($config)) {
+                unset($config);
+            }
+
+            // Add the param if it doesn't exists or merge branches.
+            if (empty($config[$key])) {
+                $config[$key] = $value;
+            } else {
+                $config[$key] = self::merge_config($config[$key], $localconfig[$key]);
+            }
+        }
+
+        return $config;
+    }
+
+    /**
+     * Cleans the path returned by get_components_with_tests() to standarize it
+     *
+     * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/
+     * @param string $path
+     * @return string The string without the last /tests part
+     */
+    protected final static function clean_path($path) {
+
+        $path = rtrim($path, DIRECTORY_SEPARATOR);
+
+        $parttoremove = DIRECTORY_SEPARATOR . 'tests';
+
+        $substr = substr($path, strlen($path) - strlen($parttoremove));
+        if ($substr == $parttoremove) {
+            $path = substr($path, 0, strlen($path) - strlen($parttoremove));
+        }
+
+        return rtrim($path, DIRECTORY_SEPARATOR);
+    }
+
+    /**
+     * The relative path where components stores their behat tests
+     *
+     * @return string
+     */
+    protected final static function get_behat_tests_path() {
+        return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat';
+    }
+
+}
diff --git a/lib/behat/classes/util.php b/lib/behat/classes/util.php
new file mode 100644 (file)
index 0000000..5fe4aa3
--- /dev/null
@@ -0,0 +1,262 @@
+<?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/>.
+
+/**
+ * Utils for behat-related stuff
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/../lib.php');
+require_once(__DIR__ . '/../../testing/classes/util.php');
+require_once(__DIR__ . '/behat_command.php');
+require_once(__DIR__ . '/behat_config_manager.php');
+
+require_once(__DIR__ . '/../../filelib.php');
+
+/**
+ * Init/reset utilities for Behat database and dataroot
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2013 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_util extends testing_util {
+
+    /**
+     * @var array Files to skip when resetting dataroot folder
+     */
+    protected static $datarootskiponreset = array('.', '..', 'behat', 'behattestdir.txt');
+
+    /**
+     * @var array Files to skip when dropping dataroot folder
+     */
+    protected static $datarootskipondrop = array('.', '..', 'lock');
+
+    /**
+     * Installs a site using $CFG->dataroot and $CFG->prefix
+     * @throws coding_exception
+     * @return void
+     */
+    public static function install_site() {
+        global $DB;
+
+        if (!defined('BEHAT_UTIL')) {
+            throw new coding_exception('This method can be only used by Behat CLI tool');
+        }
+
+        // New dataroot.
+        self::reset_dataroot();
+
+        $options = array();
+        $options['adminuser'] = 'admin';
+        $options['adminpass'] = 'admin';
+        $options['fullname'] = 'Acceptance test site';
+
+        install_cli_database($options, false);
+
+        // Update admin user info.
+        $user = $DB->get_record('user', array('username' => 'admin'));
+        $user->email = 'moodle@moodlemoodle.com';
+        $user->firstname = 'Admin';
+        $user->lastname = 'User';
+        $user->city = 'Perth';
+        $user->country = 'AU';
+        $DB->update_record('user', $user);
+
+        // Sets maximum debug level.
+        set_config('debug', DEBUG_DEVELOPER);
+        set_config('debugdisplay', true);
+
+        // Keeps the current version of database and dataroot.
+        self::store_versions_hash();
+
+        // Stores the database contents for fast reset.
+        self::store_database_state();
+    }
+
+    /**
+     * Drops dataroot and remove test database tables
+     * @throws coding_exception
+     * @return void
+     */
+    public static function drop_site() {
+
+        if (!defined('BEHAT_UTIL')) {
+            throw new coding_exception('This method can be only used by Behat CLI tool');
+        }
+
+        self::reset_dataroot();
+        self::drop_dataroot();
+        self::drop_database(true);
+    }
+
+    /**
+     * Checks if $CFG->behat_wwwroot is available
+     *
+     * @return bool
+     */
+    public static function is_server_running() {
+        global $CFG;
+
+        $request = new curl();
+        $request->get($CFG->behat_wwwroot);
+
+        if ($request->get_errno() === 0) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether the test database and dataroot is ready
+     * Stops execution if something went wrong
+     * @throws coding_exception
+     * @return void
+     */
+    protected static function test_environment_problem() {
+        global $CFG, $DB;
+
+        if (!defined('BEHAT_UTIL')) {
+            throw new coding_exception('This method can be only used by Behat CLI tool');
+        }
+
+        if (!self::is_test_site()) {
+            behat_error(1, 'This is not a behat test site!');
+        }
+
+        $tables = $DB->get_tables(false);
+        if (empty($tables)) {
+            behat_error(BEHAT_EXITCODE_INSTALL, '');
+        }
+
+        if (!self::is_test_data_updated()) {
+            behat_error(BEHAT_EXITCODE_REINSTALL, 'The test environment was initialised for a different version');
+        }
+    }
+
+    /**
+     * Enables test mode
+     *
+     * It uses CFG->behat_dataroot
+     *
+     * Starts the test mode checking the composer installation and
+     * the test environment and updating the available
+     * features and steps definitions.
+     *
+     * Stores a file in dataroot/behat to allow Moodle to switch
+     * to the test environment when using cli-server (or $CFG->behat_switchcompletely)
+     * @throws coding_exception
+     * @return void
+     */
+    public static function start_test_mode() {
+        global $CFG;
+
+        if (!defined('BEHAT_UTIL')) {
+            throw new coding_exception('This method can be only used by Behat CLI tool');
+        }
+
+        // Checks the behat set up and the PHP version.
+        behat_command::check_behat_setup(true);
+
+        // Check that test environment is correctly set up.
+        self::test_environment_problem();
+
+        // Updates all the Moodle features and steps definitions.
+        behat_config_manager::update_config_file();
+
+        if (self::is_test_mode_enabled()) {
+            return;
+        }
+
+        $contents = '$CFG->behat_wwwroot, $CFG->behat_prefix and $CFG->behat_dataroot' .
+            ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
+        $filepath = self::get_test_file_path();
+        if (!file_put_contents($filepath, $contents)) {
+            behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
+        }
+    }
+
+    /**
+     * Disables test mode
+     * @throws coding_exception
+     * @return void
+     */
+    public static function stop_test_mode() {
+
+        if (!defined('BEHAT_UTIL')) {
+            throw new coding_exception('This method can be only used by Behat CLI tool');
+        }
+
+        $testenvfile = self::get_test_file_path();
+
+        if (!self::is_test_mode_enabled()) {
+            echo "Test environment was already disabled\n";
+        } else {
+            if (!unlink($testenvfile)) {
+                behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test environment file');
+            }
+        }
+    }
+
+    /**
+     * Checks whether test environment is enabled or disabled
+     *
+     * To check is the current script is running in the test
+     * environment
+     *
+     * @see tool_behat::is_using_test_environment()
+     * @return bool
+     */
+    public static function is_test_mode_enabled() {
+
+        $testenvfile = self::get_test_file_path();
+        if (file_exists($testenvfile)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns true if Moodle is currently running with the test database and dataroot
+     * @return bool
+     */
+    public static function is_using_test_environment() {
+        global $CFG;
+
+        if (!empty($CFG->originaldataroot)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the path to the file which specifies if test environment is enabled
+     * @return string
+     */
+    protected final static function get_test_file_path() {
+        return behat_command::get_behat_dir() . '/test_environment_enabled.txt';
+    }
+
+}
diff --git a/lib/behat/features/bootstrap/behat_init_context.php b/lib/behat/features/bootstrap/behat_init_context.php
new file mode 100644 (file)
index 0000000..01a0d18
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * Contexts initializer class
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use Behat\Behat\Context\BehatContext,
+    Behat\MinkExtension\Context\MinkContext,
+    Moodle\BehatExtension\Context\MoodleContext;
+
+/**
+ * Loads main subcontexts
+ *
+ * Loading of moodle subcontexts is done by the Moodle extension
+ *
+ * Renamed from behat FeatureContext class according
+ * to Moodle coding styles conventions
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_init_context extends BehatContext {
+
+    /**
+     * Initializes subcontexts
+     *
+     * @param  array $parameters context parameters (set them up through behat.yml)
+     * @return void
+     */
+    public function __construct(array $parameters) {
+        $this->useContext('moodle', new MoodleContext($parameters));
+    }
+
+}
diff --git a/lib/behat/form_field/behat_form_editor.php b/lib/behat/form_field/behat_form_editor.php
new file mode 100644 (file)
index 0000000..c2bf778
--- /dev/null
@@ -0,0 +1,73 @@
+<?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/>.
+
+/**
+ * Moodle editor field.
+ *
+ * @package    core_form
+ * @category   test
+ * @copyright  2012 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.
+
+use Behat\Mink\Element\NodeElement as NodeElement;
+
+require_once(__DIR__ . '/behat_form_field.php');
+
+/**
+ * Moodle editor field.
+ *
+ * @todo Support for multiple editors
+ * @package   core_form
+ * @category  test
+ * @copyright 2012 David Monllaó
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_form_editor extends behat_form_field {
+
+    /**
+     * Sets the value to a field.
+     *
+     * @param string $value
+     * @return void
+     */
+    public function set_value($value) {
+
+        // Set the value to the iframe and save it to the textarea.
+        $editorid = $this->field->getAttribute('id');
+        $this->session->executeScript('tinyMCE.get("'.$editorid.'").setContent("' . $value . '");');
+        $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+    }
+
+    /**
+     * Returns the editor value.
+     *
+     * @return string
+     */
+    public function get_value() {
+
+        // Save the current iframe value in case default value has been edited.
+        $editorid = $this->field->getAttribute('id');
+        $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+
+        return $this->field->getValue();
+    }
+
+}
+
diff --git a/lib/behat/form_field/behat_form_field.php b/lib/behat/form_field/behat_form_field.php
new file mode 100644 (file)
index 0000000..7c08d73
--- /dev/null
@@ -0,0 +1,78 @@
+<?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/>.
+
+/**
+ * Generic moodleforms field.
+ *
+ * @package    core_form
+ * @category   test
+ * @copyright  2012 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.
+
+use Behat\Mink\Session as Session,
+    Behat\Mink\Element\NodeElement as NodeElement;
+
+/**
+ * Representation of a moodle field.
+ *
+ * Basically an interface with Mink session.
+ *
+ * @package    core_form
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_form_field {
+
+    protected $session;
+    protected $fieldnode;
+
+    /**
+     * General constructor with the node and the session to interact with.
+     *
+     * @param Session $session Reference to Mink session to traverse/modify the page DOM.
+     * @param NodeElement $fieldnode The field DOM node
+     * @return void
+     */
+    public function __construct(Session $session, NodeElement $fieldnode) {
+        $this->session = $session;
+        $this->field = $fieldnode;
+    }
+
+    /**
+     * Sets the value to a field.
+     *
+     * @param string $value
+     * @return void
+     */
+    public function set_value($value) {
+        $this->field->setValue($value);
+    }
+
+    /**
+     * Returns the current value of the select element.
+     *
+     * @return string
+     */
+    public function get_value() {
+        return $this->field->getValue();
+    }
+
+}
diff --git a/lib/behat/form_field/behat_form_select.php b/lib/behat/form_field/behat_form_select.php
new file mode 100644 (file)
index 0000000..4aa392a
--- /dev/null
@@ -0,0 +1,59 @@
+<?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/>.
+
+/**
+ * Single select form field class.
+ *
+ * @package    core_form
+ * @category   test
+ * @copyright  2012 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_field.php');
+
+/**
+ * Single select form field.
+ *
+ * @package    core_form
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_form_select extends behat_form_field {
+
+    /**
+     * Sets the value of a single select.
+     *
+     * @param string $value
+     * @return void
+     */
+    public function set_value($value) {
+        $this->field->selectOption($value);
+    }
+
+    /**
+     * Returns the text of the current value.
+     *
+     * @return string
+     */
+    public function get_value() {
+        $selectedoption = $this->field->find('xpath', '//option[@selected="selected"]');
+        return $selectedoption->getText();
+    }
+}
diff --git a/lib/behat/lib.php b/lib/behat/lib.php
new file mode 100644 (file)
index 0000000..dda2deb
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Behat basic functions
+ *
+ * It does not include MOODLE_INTERNAL because is part of the bootstrap
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../testing/lib.php');
+
+define('BEHAT_EXITCODE_CONFIG', 250);
+define('BEHAT_EXITCODE_REQUIREMENT', 251);
+define('BEHAT_EXITCODE_PERMISSIONS', 252);
+define('BEHAT_EXITCODE_REINSTALL', 253);
+define('BEHAT_EXITCODE_INSTALL', 254);
+
+/**
+ * Exits with an error code
+ *
+ * @param  mixed $errorcode
+ * @param  string $text
+ * @return void Stops execution with error code
+ */
+function behat_error($errorcode, $text = '') {
+
+    // Adding error prefixes.
+    switch ($errorcode) {
+        case BEHAT_EXITCODE_CONFIG:
+            $text = 'Behat config error: ' . $text;
+            break;
+        case BEHAT_EXITCODE_REQUIREMENT:
+            $text = 'Behat requirement not satisfied: ' . $text;
+            break;
+        case BEHAT_EXITCODE_PERMISSIONS:
+            $text = 'Behat permissions problem: ' . $text . ', check the permissions';
+            break;
+        case BEHAT_EXITCODE_REINSTALL:
+            $path = testing_cli_argument_path('/admin/tool/behat/cli/util.php');
+            $text = "Reinstall Behat: ".$text.", use:\n php ".$path." --drop \n php ".$path." --install";
+            break;
+        case BEHAT_EXITCODE_INSTALL:
+            $path = testing_cli_argument_path('/admin/tool/behat/cli/util.php');
+            $text = "Install Behat before enabling it, use:\n php ".$path." --install";
+            break;
+        default:
+            $text = 'Unknown error ' . $errorcode . ' ' . $text;
+            break;
+    }
+
+    testing_error($errorcode, $text);
+}
+
index 548f854..b246000 100644 (file)
@@ -113,7 +113,7 @@ function css_send_ie_css($themename, $rev, $etag, $slasharguments) {
 
     $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot);
 
-    $css  = "/** Unfortunately IE6/7 does not support more than 4096 selectors in one CSS file, which means we have to use some ugly hacks :-( **/";
+    $css  = "/** Unfortunately IE6-9 does not support more than 4096 selectors in one CSS file, which means we have to use some ugly hacks :-( **/";
     if ($slasharguments) {
         $css .= "\n@import url($relroot/styles.php/$themename/$rev/plugins);";
         $css .= "\n@import url($relroot/styles.php/$themename/$rev/parents);";
index 6299390..fa71321 100644 (file)
@@ -102,7 +102,7 @@ $definitions = array(
         'mode' => cache_store::MODE_APPLICATION,
         'simplekeys' => true, // The course id the groupings exist for.
         'simpledata' => true, // Array of stdClass objects containing only strings.
-        'persist' => true, // Likely there will be a couple of calls to this.
+        'persistent' => true, // Likely there will be a couple of calls to this.
         'persistmaxsize' => 2, // The original cache used 1, we've increased that to two.
     ),
      // Used to cache calendar subscriptions.
index 2c7b711..1bc800b 100644 (file)
@@ -41,9 +41,16 @@ class mssql_native_moodle_recordset extends moodle_recordset {
     }
 
     private function fetch_next() {
-        if ($row = mssql_fetch_assoc($this->rsrc)) {
-            $row = array_change_key_case($row, CASE_LOWER);
+        if (!$this->rsrc) {
+            return false;
         }
+        if (!$row = mssql_fetch_assoc($this->rsrc)) {
+            mssql_free_result($this->rsrc);
+            $this->rsrc = null;
+            return false;
+        }
+
+        $row = array_change_key_case($row, CASE_LOWER);
         return $row;
     }
 
index 230ef8c..522eac5 100644 (file)
@@ -49,9 +49,16 @@ class mysqli_native_moodle_recordset extends moodle_recordset {
     }
 
     private function fetch_next() {
-        if ($row = $this->result->fetch_assoc()) {
-            $row = array_change_key_case($row, CASE_LOWER);
+        if (!$this->result) {
+            return false;
         }
+        if (!$row = $this->result->fetch_assoc()) {
+            $this->result->close();
+            $this->result = null;
+            return false;
+        }
+
+        $row = array_change_key_case($row, CASE_LOWER);
         return $row;
     }
 
index e21e231..55ba1af 100644 (file)
@@ -41,11 +41,18 @@ class oci_native_moodle_recordset extends moodle_recordset {
     }
 
     private function fetch_next() {
-        if ($row = oci_fetch_array($this->stmt, OCI_ASSOC + OCI_RETURN_NULLS + OCI_RETURN_LOBS)) {
-            $row = array_change_key_case($row, CASE_LOWER);
-            unset($row['oracle_rownum']);
-            array_walk($row, array('oci_native_moodle_database', 'onespace2empty'));
+        if (!$this->stmt) {
+            return false;
         }
+        if (!$row = oci_fetch_array($this->stmt, OCI_ASSOC + OCI_RETURN_NULLS + OCI_RETURN_LOBS)) {
+            oci_free_statement($this->stmt);
+            $this->stmt = null;
+            return false;
+        }
+
+        $row = array_change_key_case($row, CASE_LOWER);
+        unset($row['oracle_rownum']);
+        array_walk($row, array('oci_native_moodle_database', 'onespace2empty'));
         return $row;
     }
 
index 76fe53e..fd1f905 100644 (file)
@@ -62,7 +62,14 @@ class pgsql_native_moodle_recordset extends moodle_recordset {
     }
 
     private function fetch_next() {
-        $row = pg_fetch_assoc($this->result);
+        if (!$this->result) {
+            return false;
+        }
+        if (!$row = pg_fetch_assoc($this->result)) {
+            pg_free_result($this->result);
+            $this->result = null;
+            return false;
+        }
 
         if ($this->blobs) {
             foreach ($this->blobs as $blob) {
index 6ca88aa..e397e2e 100644 (file)
@@ -41,10 +41,17 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset {
     }
 
     private function fetch_next() {
-        if ($row = sqlsrv_fetch_array($this->rsrc, SQLSRV_FETCH_ASSOC)) {
-            unset($row['sqlsrvrownumber']);
-            $row = array_change_key_case($row, CASE_LOWER);
+        if (!$this->rsrc) {
+            return false;
         }
+        if (!$row = sqlsrv_fetch_array($this->rsrc, SQLSRV_FETCH_ASSOC)) {
+            sqlsrv_free_stmt($this->rsrc);
+            $this->rsrc = null;
+            return false;
+        }
+
+        unset($row['sqlsrvrownumber']);
+        $row = array_change_key_case($row, CASE_LOWER);
         return $row;
     }
 
diff --git a/lib/editor/tinymce/editor_styles.css b/lib/editor/tinymce/editor_styles.css
deleted file mode 100644 (file)
index e69de29..0000000
index ede90a5..277322a 100644 (file)
@@ -106,6 +106,7 @@ class tinymce_texteditor extends texteditor {
         if ($fpoptions) {
             $PAGE->requires->js_init_call('M.editor_tinymce.init_filepicker', array($elementid, $fpoptions), true);
         }
+        $this->initialise_collapse_js();
     }
 
     protected function get_init_params($elementid, array $options=null) {
@@ -274,4 +275,23 @@ class tinymce_texteditor extends texteditor {
         global $CFG;
         return new moodle_url("$CFG->httpswwwroot/lib/editor/tinymce/tiny_mce/$this->version/");
     }
+
+    /**
+     * Initialise javascript form elements
+     * @return void
+     */
+    public function initialise_collapse_js() {
+        global $PAGE;
+        // This method is called for every editor instance. Ensure it's only run once.
+        // Static is a clunky solution but the best we could find to keep everything simple and encapsulated.
+        static $isinitialised;
+        if ($isinitialised) {
+            return;
+        }
+
+        // Initialise language strings.
+        $PAGE->requires->strings_for_js(array('hideeditortoolbar', 'showeditortoolbar'), 'form');
+        $PAGE->requires->yui_module('moodle-editor_tinymce-collapse', 'M.editor_collapse.init');
+        $isinitialised = true;
+    }
 }
diff --git a/lib/editor/tinymce/styles.css b/lib/editor/tinymce/styles.css
new file mode 100644 (file)
index 0000000..f621498
--- /dev/null
@@ -0,0 +1,3 @@
+.mform .felement.feditor .toggle_editor_toolbar {background: #EEEEEE;border-color: #BBBBBB;border-radius: 4px 4px 0 0;border-style: solid solid none;border-width: 1px 1px 0;display: inline-block;font-size: 0.7em;padding: 3px 6px;width: 9em;}
+.mform .felement.feditor .mceStatusbar,
+.mform .felement.feditor iframe {min-width: 35em;}
\ No newline at end of file
diff --git a/lib/editor/tinymce/yui/collapse/collapse.js b/lib/editor/tinymce/yui/collapse/collapse.js
new file mode 100644 (file)
index 0000000..ea68c0d
--- /dev/null
@@ -0,0 +1,166 @@
+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
index 6f1d7ae..e881c9b 100644 (file)
@@ -1820,6 +1820,125 @@ abstract class enrol_plugin {
         return array();
     }
 
+    /**
+     * Do any enrolments need expiration processing.
+     *
+     * Plugins that want to call this functionality must implement 'expiredaction' config setting.
+     *
+     * @param progress_trace $trace
+     * @param int $courseid one course, empty mean all
+     * @return bool true if any data processed, false if not
+     */
+    public function process_expirations(progress_trace $trace, $courseid = null) {
+        global $DB;
+
+        $name = $this->get_name();
+        if (!enrol_is_enabled($name)) {
+            $trace->finished();
+            return false;
+        }
+
+        $processed = false;
+        $params = array();
+        $coursesql = "";
+        if ($courseid) {
+            $coursesql = "AND e.courseid = :courseid";
+        }
+
+        // Deal with expired accounts.
+        $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
+
+        if ($action == ENROL_EXT_REMOVED_UNENROL) {
+            $instances = array();
+            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
+                      FROM {user_enrolments} ue
+                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
+                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
+                     WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
+            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
+
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $ue) {
+                if (!$processed) {
+                    $trace->output("Starting processing of enrol_$name expirations...");
+                    $processed = true;
+                }
+                if (empty($instances[$ue->enrolid])) {
+                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
+                }
+                $instance = $instances[$ue->enrolid];
+                if (!$this->roles_protected()) {
+                    // Let's just guess what extra roles are supposed to be removed.
+                    if ($instance->roleid) {
+                        role_unassign($instance->roleid, $ue->userid, $ue->contextid);
+                    }
+                }
+                // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
+                $this->unenrol_user($instance, $ue->userid);
+                $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
+            }
+            $rs->close();
+            unset($instances);
+
+        } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
+            $instances = array();
+            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
+                      FROM {user_enrolments} ue
+                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
+                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
+                     WHERE ue.timeend > 0 AND ue.timeend < :now
+                           AND ue.status = :useractive $coursesql";
+            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $ue) {
+                if (!$processed) {
+                    $trace->output("Starting processing of enrol_$name expirations...");
+                    $processed = true;
+                }
+                if (empty($instances[$ue->enrolid])) {
+                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
+                }
+                $instance = $instances[$ue->enrolid];
+
+                if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+                    if (!$this->roles_protected()) {
+                        // Let's just guess what roles should be removed.
+                        $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
+                        if ($count == 1) {
+                            role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
+
+                        } else if ($count > 1 and $instance->roleid) {
+                            role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
+                        }
+                    }
+                    // In any case remove all roles that belong to this instance and user.
+                    role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
+                    // Final cleanup of subcontexts if there are no more course roles.
+                    if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
+                        role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
+                    }
+                }
+
+                $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
+                $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
+            }
+            $rs->close();
+            unset($instances);
+
+        } else {
+            // ENROL_EXT_REMOVED_KEEP means no changes.
+        }
+
+        if ($processed) {
+            $trace->output("...finished processing of enrol_$name expirations");
+        } else {
+            $trace->output("No expired enrol_$name enrolments detected");
+        }
+        $trace->finished();
+
+        return $processed;
+    }
+
     /**
      * Send expiry notifications.
      *
@@ -1837,13 +1956,18 @@ abstract class enrol_plugin {
     public function send_expiry_notifications($trace) {
         global $DB, $CFG;
 
+        $name = $this->get_name();
+        if (!enrol_is_enabled($name)) {
+            $trace->finished();
+            return;
+        }
+
         // Unfortunately this may take a long time, it should not be interrupted,
         // otherwise users get duplicate notification.
 
         @set_time_limit(0);
         raise_memory_limit(MEMORY_HUGE);
 
-        $name = $this->get_name();
 
         $expirynotifylast = $this->get_config('expirynotifylast', 0);
         $expirynotifyhour = $this->get_config('expirynotifyhour');
index d71b798..ff260d1 100644 (file)
@@ -362,6 +362,7 @@ class zip_packer_testcase extends advanced_testcase {
         $zip_archive->close();
         $zip_archive->open($archive, file_archive::OPEN);
         $this->assertEquals(2, $zip_archive->count());
+        $zip_archive->close();
 
         unlink($archive);
     }
@@ -398,6 +399,7 @@ class zip_packer_testcase extends advanced_testcase {
         $zip_archive->close();
         $zip_archive->open($archive, file_archive::OPEN);
         $this->assertEquals(1, $zip_archive->count());
+        $zip_archive->close();
 
         unlink($archive);
         $zip_archive = new zip_archive();
@@ -407,5 +409,8 @@ class zip_packer_testcase extends advanced_testcase {
         $zip_archive->close();
         $zip_archive->open($archive, file_archive::OPEN);
         $this->assertEquals(1, $zip_archive->count());
+        $zip_archive->close();
+
+        unlink($archive);
     }
 }
index f7fe3c3..ba96393 100644 (file)
@@ -610,6 +610,13 @@ class zip_archive extends file_archive {
                             case 'ISO-8859-6': $encoding = 'CP720'; break;
                             case 'ISO-8859-7': $encoding = 'CP737'; break;
                             case 'ISO-8859-8': $encoding = 'CP862'; break;
+                            case 'UTF-8':
+                                if ($winchar = get_string('localewincharset', 'langconfig')) {
+                                    // Most probably works only for zh_cn,
+                                    // if there are more problems we could add zipcharset to langconfig files.
+                                    $encoding = $winchar;
+                                }
+                                break;
                         }
                     }
                     $newname = @textlib::convert($name, $encoding, 'utf-8');
index bd04b22..e3e23aa 100644 (file)
@@ -53,7 +53,7 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element {
     /** @var array options provided to initalize filepicker */
     protected $_options = array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 0, 'changeformat' => 0,
             'areamaxbytes' => FILE_AREA_MAX_BYTES_UNLIMITED, 'context' => null, 'noclean' => 0, 'trusttext' => 0,
-            'return_types' => 7);
+            'return_types' => 7, 'collapsible' => 0, 'collapsed' => 0);
     // $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE
 
     /** @var array values for editor */
@@ -383,7 +383,17 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element {
         if (!is_null($this->getAttribute('onblur')) && !is_null($this->getAttribute('onchange'))) {
             $editorrules = ' onblur="'.htmlspecialchars($this->getAttribute('onblur')).'" onchange="'.htmlspecialchars($this->getAttribute('onchange')).'"';
         }
-        $str .= '<div><textarea id="'.$id.'" name="'.$elname.'[text]" rows="'.$rows.'" cols="'.$cols.'"'.$editorrules.'>';
+        $str .= '<div><textarea id="'.$id.'" name="'.$elname.'[text]" rows="'.$rows.'" cols="'.$cols.'"';
+        $classes = array();
+        if (isset($this->_options['collapsed']) && $this->_options['collapsed']) {
+            $this->_options['collapsible'] = 1;
+            $classes[] = 'collapsed';
+        }
+        if (isset($this->_options['collapsible']) && $this->_options['collapsible']) {
+            $classes[] = 'collapsible';
+        }
+        $str .= ' class="' . implode(' ', $classes) . '"';
+        $str .= $editorrules.'>';
         $str .= s($text);
         $str .= '</textarea></div>';
 
index 2a5712e..1c38504 100644 (file)
@@ -1519,7 +1519,7 @@ M.util.help_icon = {
                         });
                         this.overlay.render(Y.one(document.body));
 
-                        footerbtn.on('click', this.overlay.hide, this.overlay);
+                        footerbtn.on('click', this.close, this);
 
                         var boundingBox = this.overlay.get("boundingBox");
 
@@ -1580,8 +1580,14 @@ M.util.help_icon = {
                     },
 
                     display_callback : function(content) {
-                        content = '<div role="alert">' + content + '</div>';
-                        this.overlay.set('bodyContent', content);
+                        var contentnode, heading;
+                        contentnode = Y.Node.create('<div role="alert">' + content + '</div>');
+                        this.overlay.set('bodyContent', contentnode);
+                        heading = contentnode.one('h1');
+                        if (heading) {
+                            heading.set('tabIndex', 0);
+                            heading.focus();
+                        }
                     },
 
                     hideContent : function() {
diff --git