Merge branch 'MDL-50723-master' of git://github.com/andrewnicols/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 24 Aug 2015 13:47:25 +0000 (14:47 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 24 Aug 2015 13:47:25 +0000 (14:47 +0100)
172 files changed:
admin/cli/install.php
admin/settings/plugins.php
admin/tool/behat/tests/behat/nasty_strings.feature
admin/tool/uploaduser/index.php
admin/tool/uploaduser/user_form.php
auth/cas/auth.php
blocks/messages/block_messages.php
blocks/online_users/block_online_users.php
blocks/online_users/classes/fetcher.php [new file with mode: 0644]
blocks/online_users/tests/generator/lib.php
blocks/online_users/tests/online_users_test.php [new file with mode: 0644]
blocks/tag_youtube/block_tag_youtube.php
blocks/tag_youtube/lang/en/block_tag_youtube.php
blocks/tag_youtube/settings.php [new file with mode: 0644]
blocks/tag_youtube/upgrade.txt [new file with mode: 0644]
blocks/tag_youtube/version.php
blocks/tags/tests/behat/tagcloud.feature
blog/tests/behat/blog_visibility.feature
cache/stores/file/lib.php
cache/stores/file/tests/file_test.php
calendar/renderer.php
calendar/set.php
completion/tests/behat/behat_completion.php
course/format/singleactivity/lib.php
course/tests/behat/navigate_course_list.feature
enrol/locallib.php
enrol/meta/locallib.php
enrol/otherusers.php
enrol/renderer.php
grade/import/csv/classes/load_data.php
grade/import/csv/tests/load_data_test.php
grade/import/lib.php
grade/report/singleview/lib.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_view.feature
grade/tests/importlib_test.php [new file with mode: 0644]
lang/en/auth.php
lang/en/cache.php
lang/en/install.php
lib/ajax/blocks.php
lib/amd/build/mustache.min.js
lib/amd/build/templates.min.js
lib/amd/src/mustache.js
lib/amd/src/templates.js
lib/badgeslib.php
lib/behat/classes/behat_command.php
lib/db/caches.php
lib/db/install.xml
lib/db/upgrade.php
lib/editor/atto/plugins/subscript/tests/behat/subscript.feature
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-debug.js
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-min.js
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button.js
lib/editor/atto/plugins/subscript/yui/src/button/js/button.js
lib/editor/atto/plugins/superscript/tests/behat/superscript.feature
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-debug.js
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-min.js
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button.js
lib/editor/atto/plugins/superscript/yui/src/button/js/button.js
lib/editor/atto/plugins/table/lang/en/atto_table.php
lib/editor/atto/plugins/table/lib.php
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js
lib/editor/atto/yui/src/editor/build.json
lib/editor/atto/yui/src/editor/js/commands.js [new file with mode: 0644]
lib/editor/atto/yui/src/editor/js/editor-plugin-buttons.js
lib/editor/atto/yui/src/editor/meta/editor.json
lib/filelib.php
lib/moodlelib.php
lib/mustache/LICENSE
lib/mustache/README.md
lib/mustache/composer.json [deleted file]
lib/mustache/readme_moodle.txt
lib/mustache/src/Mustache/Autoloader.php
lib/mustache/src/Mustache/Cache.php
lib/mustache/src/Mustache/Cache/AbstractCache.php
lib/mustache/src/Mustache/Cache/FilesystemCache.php
lib/mustache/src/Mustache/Cache/NoopCache.php
lib/mustache/src/Mustache/Compiler.php
lib/mustache/src/Mustache/Context.php
lib/mustache/src/Mustache/Engine.php
lib/mustache/src/Mustache/Exception.php
lib/mustache/src/Mustache/Exception/InvalidArgumentException.php
lib/mustache/src/Mustache/Exception/LogicException.php
lib/mustache/src/Mustache/Exception/RuntimeException.php
lib/mustache/src/Mustache/Exception/SyntaxException.php
lib/mustache/src/Mustache/Exception/UnknownFilterException.php
lib/mustache/src/Mustache/Exception/UnknownHelperException.php
lib/mustache/src/Mustache/Exception/UnknownTemplateException.php
lib/mustache/src/Mustache/HelperCollection.php
lib/mustache/src/Mustache/LambdaHelper.php
lib/mustache/src/Mustache/Loader.php
lib/mustache/src/Mustache/Loader/ArrayLoader.php
lib/mustache/src/Mustache/Loader/CascadingLoader.php
lib/mustache/src/Mustache/Loader/FilesystemLoader.php
lib/mustache/src/Mustache/Loader/InlineLoader.php
lib/mustache/src/Mustache/Loader/MutableLoader.php
lib/mustache/src/Mustache/Loader/StringLoader.php
lib/mustache/src/Mustache/Logger.php
lib/mustache/src/Mustache/Logger/AbstractLogger.php
lib/mustache/src/Mustache/Logger/StreamLogger.php
lib/mustache/src/Mustache/Parser.php
lib/mustache/src/Mustache/Template.php
lib/mustache/src/Mustache/Tokenizer.php
lib/myprofilelib.php
lib/outputlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/phpunit/bootstraplib.php
lib/phpunit/classes/constraint_object_is_equal_with_exceptions.php [new file with mode: 0644]
lib/phpunit/classes/util.php
lib/phpunit/lib.php
lib/phpunit/readme.md
lib/testing/lib.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_navigation.php
lib/tests/completionlib_test.php
lib/tests/filelib_test.php
lib/thirdpartylibs.xml
login/signup_form.php
message/lib.php
message/tests/behat/display_history.feature
message/tests/behat/manage_contacts.feature
message/tests/behat/message_participants.feature
message/tests/behat/recent_conversations.feature
message/tests/behat/search_history.feature
mod/assign/lib.php
mod/data/field/checkbox/field.class.php
mod/data/field/date/field.class.php
mod/data/field/file/field.class.php
mod/data/field/latlong/field.class.php
mod/data/field/menu/field.class.php
mod/data/field/multimenu/field.class.php
mod/data/field/picture/field.class.php
mod/data/field/radiobutton/field.class.php
mod/data/field/textarea/field.class.php
mod/data/field/url/field.class.php
mod/data/lib.php
mod/data/styles.css
mod/forum/tests/behat/discussion_display.feature
mod/forum/tests/behat/discussion_navigation.feature
mod/forum/tests/behat/edit_post_student.feature
mod/forum/tests/behat/edit_post_teacher.feature
mod/forum/tests/behat/my_forum_posts.feature
mod/forum/tests/behat/post_to_multiple_groups.feature
mod/forum/tests/behat/single_forum_discussion.feature
mod/forum/tests/behat/track_read_posts.feature
mod/lesson/tests/behat/lesson_practice.feature
mod/lti/mod_form.php
mod/quiz/styles.css
report/outline/tests/behat/user.feature
tag/lib.php
tag/tests/taglib_test.php
theme/base/style/blocks.css
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/renderers/core_renderer.php
theme/bootstrapbase/style/moodle.css
theme/upgrade.txt
user/edit_form.php
user/editadvanced_form.php
user/profile/index.php
version.php

index e43430e..131056d 100644 (file)
@@ -80,6 +80,7 @@ Options:
                       required in non-interactive mode.
 --allow-unstable      Install even if the version is not marked as stable yet,
                       required in non-interactive mode.
+--skip-database       Stop the installation before installing the database.
 -h, --help            Print out this help
 
 Example:
@@ -260,6 +261,7 @@ list($options, $unrecognized) = cli_get_params(
         'non-interactive'   => false,
         'agree-license'     => false,
         'allow-unstable'    => false,
+        'skip-database'     => false,
         'help'              => false
     ),
     array(
@@ -772,7 +774,11 @@ if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
 
-install_cli_database($options, $interactive);
+if (!$options['skip-database']) {
+    install_cli_database($options, $interactive);
+} else {
+    echo get_string('cliskipdatabase', 'install')."\n";
+}
 
 echo get_string('cliinstallfinished', 'install')."\n";
 exit(0); // 0 means success
index f311696..820d25b 100644 (file)
@@ -80,6 +80,9 @@ if ($hassiteconfig) {
     $temp->add(new admin_setting_heading('manageauthscommonheading', new lang_string('commonsettings', 'admin'), ''));
     $temp->add(new admin_setting_special_registerauth());
     $temp->add(new admin_setting_configcheckbox('authloginviaemail', new lang_string('authloginviaemail', 'core_auth'), new lang_string('authloginviaemail_desc', 'core_auth'), 0));
+    $temp->add(new admin_setting_configcheckbox('allowaccountssameemail',
+                    new lang_string('allowaccountssameemail', 'core_auth'),
+                    new lang_string('allowaccountssameemail_desc', 'core_auth'), 0));
     $temp->add(new admin_setting_configcheckbox('authpreventaccountcreation', new lang_string('authpreventaccountcreation', 'admin'), new lang_string('authpreventaccountcreation_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('loginpageautofocus', new lang_string('loginpageautofocus', 'admin'), new lang_string('loginpageautofocus_help', 'admin'), 0));
     $temp->add(new admin_setting_configselect('guestloginbutton', new lang_string('guestloginbutton', 'auth'),
index 41fac8d..4b4120d 100644 (file)
@@ -13,7 +13,6 @@ Feature: Transform steps arguments
     And I follow "Preferences" in the user menu
     And I follow "Edit profile"
 
-  @javascript
   Scenario: Use nasty strings on steps arguments
     When I set the field "Surname" to "$NASTYSTRING1"
     And I set the field "Description" to "$NASTYSTRING2"
@@ -24,7 +23,6 @@ Feature: Transform steps arguments
     And the field "Surname" matches value "$NASTYSTRING1"
     And the field "City/town" matches value "$NASTYSTRING3"
 
-  @javascript
   Scenario: Use nasty strings on table nodes
     When I set the following fields to these values:
       | Surname | $NASTYSTRING1 |
@@ -36,7 +34,6 @@ Feature: Transform steps arguments
     And the field "Surname" matches value "$NASTYSTRING1"
     And the field "City/town" matches value "$NASTYSTRING3"
 
-  @javascript
   Scenario: Use double quotes
     When I set the following fields to these values:
       | First name | va"lue1 |
@@ -49,7 +46,6 @@ Feature: Transform steps arguments
     And the field "Description" matches value "va\\"lue2"
     And the field "City/town" matches value "va\"lue3"
 
-  @javascript
   Scenario: Nasty strings with other contents
     When I set the field "First name" to "My Firstname $NASTYSTRING1"
     And I set the following fields to these values:
index 8d898fd..ad36657 100644 (file)
@@ -168,7 +168,7 @@ if ($formdata = $mform2->is_cancelled()) {
     $allowdeletes      = (!empty($formdata->uuallowdeletes) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC);
     $allowsuspends     = (!empty($formdata->uuallowsuspends));
     $bulk              = $formdata->uubulk;
-    $noemailduplicates = $formdata->uunoemailduplicates;
+    $noemailduplicates = empty($CFG->allowaccountssameemail) ? 1 : $formdata->uunoemailduplicates;
     $standardusernames = $formdata->uustandardusernames;
     $resetpasswords    = isset($formdata->uuforcepasswordchange) ? $formdata->uuforcepasswordchange : UU_PWRESET_NONE;
 
index 2e919fd..04bb24b 100644 (file)
@@ -138,8 +138,13 @@ class admin_uploaduser_form2 extends moodleform {
         $mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDNEW);
         $mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDINC);
 
-        $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploaduser'));
-        $mform->setDefault('uunoemailduplicates', 1);
+        if (!empty($CFG->allowaccountssameemail)) {
+            $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploaduser'));
+            $mform->setDefault('uunoemailduplicates', 1);
+        } else {
+            $mform->addElement('hidden', 'uunoemailduplicates', 1);
+        }
+        $mform->setType('uunoemailduplicates', PARAM_BOOL);
 
         $mform->addElement('selectyesno', 'uustandardusernames', get_string('uustandardusernames', 'tool_uploaduser'));
         $mform->setDefault('uustandardusernames', 1);
index eaf3f6c..22733b2 100644 (file)
@@ -187,7 +187,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
         }
 
         // If Moodle is configured to use a proxy, phpCAS needs some curl options set.
-        if (!empty($CFG->proxyhost) && !is_proxybypass($this->config->hostname)) {
+        if (!empty($CFG->proxyhost) && !is_proxybypass(phpCAS::getServerLoginURL())) {
             phpCAS::setExtraCurlOption(CURLOPT_PROXY, $CFG->proxyhost);
             if (!empty($CFG->proxyport)) {
                 phpCAS::setExtraCurlOption(CURLOPT_PROXYPORT, $CFG->proxyport);
index c9f93b2..f0ed9b2 100644 (file)
@@ -31,7 +31,9 @@ class block_messages extends block_base {
         global $USER, $CFG, $DB, $OUTPUT;
 
         if (!$CFG->messaging) {
+            $this->content = new stdClass;
             $this->content->text = '';
+            $this->content->footer = '';
             if ($this->page->user_is_editing()) {
                 $this->content->text = get_string('disabled', 'message');
             }
index c47d460..d524a23 100644 (file)
@@ -22,6 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+use block_online_users\fetcher;
+
 /**
  * This block needs to be reworked.
  * The new roles system does away with the concepts of rigid student and
@@ -56,7 +58,6 @@ class block_online_users extends block_base {
             $timetoshowusers = $CFG->block_online_users_timetosee * 60;
         }
         $now = time();
-        $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache
 
         //Calculate if we are in separate groups
         $isseparategroups = ($this->page->course->groupmode == SEPARATEGROUPS
@@ -66,82 +67,24 @@ class block_online_users extends block_base {
         //Get the user current group
         $currentgroup = $isseparategroups ? groups_get_course_group($this->page->course) : NULL;
 
-        $groupmembers = "";
-        $groupselect  = "";
-        $params = array();
-
-        //Add this to the SQL to show only group users
-        if ($currentgroup !== NULL) {
-            $groupmembers = ", {groups_members} gm";
-            $groupselect = "AND u.id = gm.userid AND gm.groupid = :currentgroup";
-            $params['currentgroup'] = $currentgroup;
-        }
+        $sitelevel = $this->page->course->id == SITEID || $this->page->context->contextlevel < CONTEXT_COURSE;
 
-        $userfields = user_picture::fields('u', array('username'));
-        $params['now'] = $now;
-        $params['timefrom'] = $timefrom;
-        if ($this->page->course->id == SITEID or $this->page->context->contextlevel < CONTEXT_COURSE) {  // Site-level
-            $sql = "SELECT $userfields, MAX(u.lastaccess) AS lastaccess
-                      FROM {user} u $groupmembers
-                     WHERE u.lastaccess > :timefrom
-                           AND u.lastaccess <= :now
-                           AND u.deleted = 0
-                           $groupselect
-                  GROUP BY $userfields
-                  ORDER BY lastaccess DESC ";
-
-           $csql = "SELECT COUNT(u.id)
-                      FROM {user} u $groupmembers
-                     WHERE u.lastaccess > :timefrom
-                           AND u.lastaccess <= :now
-                           AND u.deleted = 0
-                           $groupselect";
-
-        } else {
-            // Course level - show only enrolled users for now
-            // TODO: add a new capability for viewing of all users (guests+enrolled+viewing)
-
-            list($esqljoin, $eparams) = get_enrolled_sql($this->page->context);
-            $params = array_merge($params, $eparams);
-
-            $sql = "SELECT $userfields, MAX(ul.timeaccess) AS lastaccess
-                      FROM {user_lastaccess} ul $groupmembers, {user} u
-                      JOIN ($esqljoin) euj ON euj.id = u.id
-                     WHERE ul.timeaccess > :timefrom
-                           AND u.id = ul.userid
-                           AND ul.courseid = :courseid
-                           AND ul.timeaccess <= :now
-                           AND u.deleted = 0
-                           $groupselect
-                  GROUP BY $userfields
-                  ORDER BY lastaccess DESC";
-
-           $csql = "SELECT COUNT(u.id)
-                      FROM {user_lastaccess} ul $groupmembers, {user} u
-                      JOIN ($esqljoin) euj ON euj.id = u.id
-                     WHERE ul.timeaccess > :timefrom
-                           AND u.id = ul.userid
-                           AND ul.courseid = :courseid
-                           AND ul.timeaccess <= :now
-                           AND u.deleted = 0
-                           $groupselect";
-
-            $params['courseid'] = $this->page->course->id;
-        }
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $sitelevel,
+                $this->page->context, $this->page->course->id);
 
         //Calculate minutes
         $minutes  = floor($timetoshowusers/60);
 
         // Verify if we can see the list of users, if not just print number of users
         if (!has_capability('block/online_users:viewlist', $this->page->context)) {
-            if (!$usercount = $DB->count_records_sql($csql, $params)) {
+            if (!$usercount = $onlineusers->count_users()) {
                 $usercount = get_string("none");
             }
             $this->content->text = "<div class=\"info\">".get_string("periodnminutes","block_online_users",$minutes).": $usercount</div>";
             return $this->content;
         }
-
-        if ($users = $DB->get_records_sql($sql, $params, 0, 50)) {   // We'll just take the most recent 50 maximum
+        $userlimit = 50; // We'll just take the most recent 50 maximum.
+        if ($users = $onlineusers->get_users($userlimit)) {
             foreach ($users as $user) {
                 $users[$user->id]->fullname = fullname($user);
             }
@@ -149,10 +92,10 @@ class block_online_users extends block_base {
             $users = array();
         }
 
-        if (count($users) < 50) {
+        if (count($users) < $userlimit) {
             $usercount = "";
         } else {
-            $usercount = $DB->count_records_sql($csql, $params);
+            $usercount = $onlineusers->count_users();
             $usercount = ": $usercount";
         }
 
diff --git a/blocks/online_users/classes/fetcher.php b/blocks/online_users/classes/fetcher.php
new file mode 100644 (file)
index 0000000..1c6d15a
--- /dev/null
@@ -0,0 +1,165 @@
+<?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/>.
+
+/**
+ * File containing onlineusers class.
+ *
+ * @package    block_online_users
+ * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_online_users;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class used to list and count online users
+ *
+ * @package    block_online_users
+ * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetcher {
+
+    /** @var string The SQL query for retrieving a list of online users */
+    public $sql;
+    /** @var string The SQL query for counting the number of online users */
+    public $csql;
+    /** @var string The params for the SQL queries */
+    public $params;
+
+    /**
+     * Class constructor
+     *
+     * @param int $currentgroup The group (if any) to filter on
+     * @param int $now Time now
+     * @param int $timetoshowusers Number of seconds to show online users
+     * @param context $context Context object used to generate the sql for users enrolled in a specific course
+     * @param bool $sitelevel Whether to check online users at site level.
+     * @param int $courseid The course id to check
+     */
+    public function __construct($currentgroup, $now, $timetoshowusers, $context, $sitelevel = true, $courseid = null) {
+        $this->set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid);
+    }
+
+    /**
+     * Store the SQL queries & params for listing online users
+     *
+     * @param int $currentgroup The group (if any) to filter on
+     * @param int $now Time now
+     * @param int $timetoshowusers Number of seconds to show online users
+     * @param context $context Context object used to generate the sql for users enrolled in a specific course
+     * @param bool $sitelevel Whether to check online users at site level.
+     * @param int $courseid The course id to check
+     */
+    protected function set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid) {
+        $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache.
+
+        $groupmembers = "";
+        $groupselect  = "";
+        $groupby       = "";
+        $lastaccess    = ", lastaccess";
+        $timeaccess    = ", ul.timeaccess AS lastaccess";
+        $params = array();
+
+        $userfields = \user_picture::fields('u', array('username'));
+
+        // Add this to the SQL to show only group users.
+        if ($currentgroup !== null) {
+            $groupmembers = ", {groups_members} gm";
+            $groupselect = "AND u.id = gm.userid AND gm.groupid = :currentgroup";
+            $groupby = "GROUP BY $userfields";
+            $lastaccess = ", MAX(u.lastaccess) AS lastaccess";
+            $timeaccess = ", MAX(ul.timeaccess) AS lastaccess";
+            $params['currentgroup'] = $currentgroup;
+        }
+
+        $params['now'] = $now;
+        $params['timefrom'] = $timefrom;
+        if ($sitelevel) {
+            $sql = "SELECT $userfields $lastaccess
+                      FROM {user} u $groupmembers
+                     WHERE u.lastaccess > :timefrom
+                           AND u.lastaccess <= :now
+                           AND u.deleted = 0
+                           $groupselect $groupby
+                  ORDER BY lastaccess DESC ";
+
+            $csql = "SELECT COUNT(u.id)
+                      FROM {user} u $groupmembers
+                     WHERE u.lastaccess > :timefrom
+                           AND u.lastaccess <= :now
+                           AND u.deleted = 0
+                           $groupselect";
+
+        } else {
+            // Course level - show only enrolled users for now.
+            // TODO: add a new capability for viewing of all users (guests+enrolled+viewing).
+            list($esqljoin, $eparams) = get_enrolled_sql($context);
+            $params = array_merge($params, $eparams);
+
+            $sql = "SELECT $userfields $timeaccess
+                      FROM {user_lastaccess} ul $groupmembers, {user} u
+                      JOIN ($esqljoin) euj ON euj.id = u.id
+                     WHERE ul.timeaccess > :timefrom
+                           AND u.id = ul.userid
+                           AND ul.courseid = :courseid
+                           AND ul.timeaccess <= :now
+                           AND u.deleted = 0
+                           $groupselect $groupby
+                  ORDER BY lastaccess DESC";
+
+            $csql = "SELECT COUNT(u.id)
+                      FROM {user_lastaccess} ul $groupmembers, {user} u
+                      JOIN ($esqljoin) euj ON euj.id = u.id
+                     WHERE ul.timeaccess > :timefrom
+                           AND u.id = ul.userid
+                           AND ul.courseid = :courseid
+                           AND ul.timeaccess <= :now
+                           AND u.deleted = 0
+                           $groupselect";
+
+            $params['courseid'] = $courseid;
+        }
+        $this->sql = $sql;
+        $this->csql = $csql;
+        $this->params = $params;
+    }
+
+    /**
+     * Get a list of the most recent online users
+     *
+     * @param int $userlimit The maximum number of users that will be returned (optional, unlimited if not set)
+     * @return array
+     */
+    public function get_users($userlimit = 0) {
+        global $DB;
+        $users = $DB->get_records_sql($this->sql, $this->params, 0, $userlimit);
+        return $users;
+    }
+
+    /**
+     * Count the number of online users
+     *
+     * @return int
+     */
+    public function count_users() {
+        global $DB;
+        return $DB->count_records_sql($this->csql, $this->params);
+    }
+
+}
index dce490b..a560fd5 100644 (file)
@@ -60,4 +60,56 @@ class block_online_users_generator extends testing_block_generator {
 
         return $instance;
     }
+
+    /**
+     * Create (simulated) logged in users and add some of them to groups in a course
+     */
+    public function create_logged_in_users() {
+        global $DB;
+
+        $generator = advanced_testcase::getDataGenerator();
+        $data = array();
+
+        // Create 2 courses.
+        $course1 = $generator->create_course();
+        $data['course1'] = $course1;
+        $course2 = $generator->create_course();
+        $data['course2'] = $course2;
+
+        // Create 9 (simulated) logged in users enroled into $course1.
+        for ($i = 1; $i <= 9; $i++) {
+            $user = $generator->create_user();
+            $DB->set_field('user', 'lastaccess', time(), array('id' => $user->id));
+            $generator->enrol_user($user->id, $course1->id);
+            $DB->insert_record('user_lastaccess', array('userid' => $user->id, 'courseid' => $course1->id, 'timeaccess' => time()));
+            $data['user' . $i] = $user;
+        }
+        // Create 3 (simulated) logged in users who are not enroled into $course1.
+        for ($i = 10; $i <= 12; $i++) {
+            $user = $generator->create_user();
+            $DB->set_field('user', 'lastaccess', time(), array('id' => $user->id));
+            $data['user' . $i] = $user;
+        }
+
+        // Create 3 groups in course 1.
+        $group1 = $generator->create_group(array('courseid' => $course1->id));
+        $data['group1'] = $group1;
+        $group2 = $generator->create_group(array('courseid' => $course1->id));
+        $data['group2'] = $group2;
+        $group3 = $generator->create_group(array('courseid' => $course1->id));
+        $data['group3'] = $group3;
+
+        // Add 3 users to course group 1.
+        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user1']->id));
+        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user2']->id));
+        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $data['user3']->id));
+
+        // Add 4 users to course group 2.
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user3']->id));
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user4']->id));
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user5']->id));
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $data['user6']->id));
+
+        return $data; // Return the user, course and group objects.
+    }
 }
diff --git a/blocks/online_users/tests/online_users_test.php b/blocks/online_users/tests/online_users_test.php
new file mode 100644 (file)
index 0000000..9ff1e3c
--- /dev/null
@@ -0,0 +1,151 @@
+<?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/>.
+
+/**
+ * Online users tests
+ *
+ * @package    block_online_users
+ * @category   test
+ * @copyright  2015 University of Nottingham <www.nottingham.ac.uk>
+ * @author     Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use block_online_users\fetcher;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Online users testcase
+ *
+ * @package    block_online_users
+ * @category   test
+ * @copyright  2015 University of Nottingham <www.nottingham.ac.uk>
+ * @author     Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_online_users_testcase extends advanced_testcase {
+
+    protected $data;
+
+    /**
+     * Tests initial setup.
+     *
+     * Prepare the site with some courses, groups, users and
+     * simulate various recent accesses.
+     */
+    protected function setUp() {
+
+        // Generate (simulated) recently logged-in users.
+        $generator = $this->getDataGenerator()->get_plugin_generator('block_online_users');
+        $this->data = $generator->create_logged_in_users();
+
+        // Confirm we have modified the site and requires reset.
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Check logged in group 1, 2 & 3 members in course 1 (should be 3, 4 and 0).
+     *
+     * @param array $data Array of user, course and group objects
+     * @param int $now Current Unix timestamp
+     * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+     */
+    public function test_fetcher_course1_group_members() {
+        global $CFG;
+
+        $groupid = $this->data['group1']->id;
+        $now = time();
+        $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+        $context = context_course::instance($this->data['course1']->id);
+        $courseid = $this->data['course1']->id;
+        $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals(3, $usercount, 'There was a problem counting the number of online users in group 1');
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 1');
+
+        $groupid = $this->data['group2']->id;
+        $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 2');
+        $this->assertEquals(4, $usercount, 'There was a problem counting the number of online users in group 2');
+
+        $groupid = $this->data['group3']->id;
+        $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in group 3');
+        $this->assertEquals(0, $usercount, 'There was a problem counting the number of online users in group 3');
+    }
+
+    /**
+     * Check logged in users in courses 1 & 2 (should be 9 and 0).
+     *
+     * @param array $data Array of user, course and group objects
+     * @param int $now Current Unix timestamp
+     * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+     */
+    public function test_fetcher_courses() {
+
+        global $CFG;
+
+        $currentgroup = null;
+        $now = time();
+        $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+        $context = context_course::instance($this->data['course1']->id);
+        $courseid = $this->data['course1']->id;
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in course 1');
+        $this->assertEquals(9, $usercount, 'There was a problem counting the number of online users in course 1');
+
+        $courseid = $this->data['course2']->id;
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users in course 2');
+        $this->assertEquals(0, $usercount, 'There was a problem counting the number of online users in course 2');
+    }
+
+    /**
+     * Check logged in at the site level (should be 12).
+     *
+     * @param int $now Current Unix timestamp
+     * @param int $timetoshowusers The time window (in seconds) to check for the latest logged in users
+     */
+    public function test_fetcher_sitelevel() {
+        global $CFG;
+
+        $currentgroup = null;
+        $now = time();
+        $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+        $context = context_system::instance();
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, true);
+
+        $usercount = $onlineusers->count_users();
+        $users = $onlineusers->get_users();
+        $this->assertEquals($usercount, count($users), 'There was a problem counting the number of online users at site level');
+        $this->assertEquals(12, $usercount, 'There was a problem counting the number of online users at site level');
+    }
+}
index a06588e..7ab13be 100644 (file)
@@ -26,14 +26,29 @@ define('DEFAULT_NUMBER_OF_VIDEOS', 5);
 
 class block_tag_youtube extends block_base {
 
+    /**
+     * @var Google_Service_Youtube
+     */
+    protected $service = null;
+
     function init() {
         $this->title = get_string('pluginname','block_tag_youtube');
+        $this->config = new stdClass();
     }
 
     function applicable_formats() {
         return array('tag' => true);
     }
 
+    /**
+     * It can be configured.
+     *
+     * @return bool
+     */
+    public function has_config() {
+        return true;
+    }
+
     function specialization() {
         $this->title = !empty($this->config->title) ? $this->config->title : get_string('pluginname', 'block_tag_youtube');
         // Convert numeric categories (old YouTube API) to
@@ -56,6 +71,14 @@ class block_tag_youtube extends block_base {
             return $this->content;
         }
 
+        $this->content = new stdClass();
+        $this->content->footer = '';
+
+        if (!$this->get_service()) {
+            $this->content->text = $this->get_error_message();
+            return $this->content;
+        }
+
         $text = '';
         if(!empty($this->config->playlist)){
             //videos from a playlist
@@ -72,31 +95,41 @@ class block_tag_youtube extends block_base {
             }
         }
 
-        $this->content = new stdClass;
         $this->content->text = $text;
-        $this->content->footer = '';
 
         return $this->content;
     }
 
     function get_videos_by_playlist(){
 
+        if (!$service = $this->get_service()) {
+            return $this->get_error_message();
+        }
+
         $numberofvideos = DEFAULT_NUMBER_OF_VIDEOS;
         if( !empty($this->config->numberofvideos)) {
             $numberofvideos = $this->config->numberofvideos;
         }
 
-        $request = 'http://gdata.youtube.com/feeds/api/playlists/' .
-                   $this->config->playlist .
-                   '?start-index=1&max-results=' .
-                   $numberofvideos .
-                   '&format=5';
+        try {
+            $response = $service->playlistItems->listPlaylistItems('id,snippet', array(
+                'playlistId' => $this->config->playlist,
+                'maxResults' => $numberofvideos
+            ));
+        } catch (Google_Service_Exception $e) {
+            debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+            return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+        }
 
-        return $this->fetch_request($request);
+        return $this->render_items($response);
     }
 
     function get_videos_by_tag(){
 
+        if (!$service = $this->get_service()) {
+            return $this->get_error_message();
+        }
+
         $tagid = optional_param('id', 0, PARAM_INT);   // tag id - for backware compatibility
         $tag = optional_param('tag', '', PARAM_TAG); // tag
 
@@ -117,17 +150,26 @@ class block_tag_youtube extends block_base {
             $numberofvideos = $this->config->numberofvideos;
         }
 
-        $request = 'http://gdata.youtube.com/feeds/api/videos?vq=' .
-                   $querytag .
-                   '&start-index=1&max-results=' .
-                   $numberofvideos .
-                   '&format=5';
+        try {
+            $response = $service->search->listSearch('id,snippet', array(
+                'q' => $querytag,
+                'type' => 'video',
+                'maxResults' => $numberofvideos
+            ));
+        } catch (Google_Service_Exception $e) {
+            debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+            return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+        }
 
-        return $this->fetch_request($request);
+        return $this->render_items($response);
     }
 
     function get_videos_by_tag_and_category(){
 
+        if (!$service = $this->get_service()) {
+            return $this->get_error_message();
+        }
+
         $tagid = optional_param('id', 0, PARAM_INT);   // tag id - for backware compatibility
         $tag = optional_param('tag', '', PARAM_TAG); // tag
 
@@ -148,19 +190,32 @@ class block_tag_youtube extends block_base {
             $numberofvideos = $this->config->numberofvideos;
         }
 
-        $request = 'http://gdata.youtube.com/feeds/api/videos?category=' .
-                   $this->config->category .
-                   '&vq=' .
-                   $querytag .
-                   '&start-index=1&max-results=' .
-                   $numberofvideos .
-                   '&format=5';
-
+        try {
+            $response = $service->search->listSearch('id,snippet', array(
+                'q' => $querytag,
+                'type' => 'video',
+                'maxResults' => $numberofvideos,
+                'videoCategoryId' => $this->config->category
+            ));
+        } catch (Google_Service_Exception $e) {
+            debugging('Google service exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
+            return $this->get_error_message(get_string('requesterror', 'block_tag_youtube'));
+        }
 
-        return $this->fetch_request($request);
+        return $this->render_items($response);
     }
 
-    function fetch_request($request){
+    /**
+     * Sends a request to fetch data.
+     *
+     * @see block_tag_youtube::service
+     * @deprecated since Moodle 2.8.8, 2.9.2 and 3.0 MDL-49085 - please do not use this function any more.
+     * @param string $request
+     * @throws coding_exception
+     */
+    public function fetch_request($request) {
+        throw new coding_exception('Sorry, this function has been deprecated in Moodle 2.8.8, 2.9.2 and 3.0. Use block_tag_youtube::get_service instead.');
+
         $c = new curl(array('cache' => true, 'module_cache'=>'tag_youtube'));
         $c->setopt(array('CURLOPT_TIMEOUT' => 3, 'CURLOPT_CONNECTTIMEOUT' => 3));
 
@@ -170,32 +225,118 @@ class block_tag_youtube extends block_base {
         return $this->render_video_list($xml);
     }
 
+    /**
+     * Renders the video list.
+     *
+     * @see block_tag_youtube::render_items
+     * @deprecated since Moodle 2.8.8, 2.9.2 and 3.0 MDL-49085 - please do not use this function any more.
+     * @param SimpleXMLElement $xml
+     * @throws coding_exception
+     */
     function render_video_list(SimpleXMLElement $xml){
+        throw new coding_exception('Sorry, this function has been deprecated in Moodle 2.8.8, 2.9.2 and 3.0. Use block_tag_youtube::render_items instead.');
+    }
 
-        $text = '';
-        $text .= '<ul class="yt-video-entry unlist img-text">';
-
-        foreach($xml->entry as $entry){
-            $media = $entry->children('http://search.yahoo.com/mrss/');
-            $playerattrs = $media->group->player->attributes();
-            $url = s($playerattrs['url']);
-            $thumbattrs = $media->group->thumbnail[0]->attributes();
-            $thumbnail = s($thumbattrs['url']);
-            $title = s($media->group->title);
-            $yt = $media->children('http://gdata.youtube.com/schemas/2007');
-            $secattrs = $yt->duration->attributes();
-            $seconds = $secattrs['seconds'];
-
-            $text .= '<li>';
-            $text .= '<div class="clearfix">';
-            $text .= '<a href="'. $url . '">';
-            $text .= '<img alt="" class="youtube-thumb" src="'. $thumbnail .'" /></a>';
-            $text .= '</div><span><a href="'. $url . '">'. $title .'</a></span>';
-            $text .= '<div>';
-            $text .= format_time($seconds);
-            $text .= "</div></li>\n";
+    /**
+     * Returns an error message.
+     *
+     * Useful when the block is not properly set or something goes wrong.
+     *
+     * @param string $message The message to display.
+     * @return string HTML
+     */
+    protected function get_error_message($message = null) {
+        global $OUTPUT;
+
+        if (empty($message)) {
+            $message = get_string('apierror', 'block_tag_youtube');
+        }
+        return $OUTPUT->notification($message);
+    }
+
+    /**
+     * Gets the youtube service object.
+     *
+     * @return Google_Service_YouTube
+     */
+    protected function get_service() {
+        global $CFG;
+
+        if (!$apikey = get_config('block_tag_youtube', 'apikey')) {
+            return false;
         }
-        $text .= "</ul><div class=\"clearer\"></div>\n";
+
+        // Wrapped in an if in case we call different get_videos_* multiple times.
+        if (!isset($this->service)) {
+            require_once($CFG->libdir . '/google/lib.php');
+            $client = get_google_client();
+            $client->setDeveloperKey($apikey);
+            $client->setScopes(array(Google_Service_YouTube::YOUTUBE_READONLY));
+            $this->service = new Google_Service_YouTube($client);
+        }
+
+        return $this->service;
+    }
+
+    /**
+     * Renders the list of items.
+     *
+     * @param array $videosdata
+     * @return string HTML
+     */
+    protected function render_items($videosdata) {
+
+        if (!$videosdata || empty($videosdata->items)) {
+            if (!empty($videosdata->error)) {
+                debugging('Error fetching data from youtube: ' . $videosdata->error->message, DEBUG_DEVELOPER);
+            }
+            return '';
+        }
+
+        // If we reach that point we already know that the API key is set.
+        $service = $this->get_service();
+
+        $text = html_writer::start_tag('ul', array('class' => 'yt-video-entry unlist img-text'));
+        foreach ($videosdata->items as $video) {
+
+            // Link to the video included in the playlist if listing a playlist.
+            if (!empty($video->snippet->resourceId)) {
+                $id = $video->snippet->resourceId->videoId;
+                $playlist = '&list=' . $video->snippet->playlistId;
+            } else {
+                $id = $video->id->videoId;
+                $playlist = '';
+            }
+
+            $thumbnail = $video->snippet->getThumbnails()->getDefault();
+            $url = 'http://www.youtube.com/watch?v=' . $id . $playlist;
+
+            $videodetails = $service->videos->listVideos('id,contentDetails', array('id' => $id));
+            if ($videodetails && !empty($videodetails->items)) {
+
+                // We fetch by id so we just use the first one.
+                $details = $videodetails->items[0];
+                $start = new DateTime('@0');
+                $start->add(new DateInterval($details->contentDetails->duration));
+                $seconds = $start->format('U');
+            }
+
+            $text .= html_writer::start_tag('li');
+
+            $imgattrs = array('class' => 'youtube-thumb', 'src' => $thumbnail->url, 'alt' => $video->snippet->title);
+            $thumbhtml = html_writer::empty_tag('img', $imgattrs);
+            $link = html_writer::tag('a', $thumbhtml, array('href' => $url));
+            $text .= html_writer::tag('div', $link, array('class' => 'clearfix'));
+
+            $text .= html_writer::tag('span', html_writer::tag('a', $video->snippet->title, array('href' => $url)));
+
+            if (!empty($seconds)) {
+                $text .= html_writer::tag('div', format_time($seconds));
+            }
+            $text .= html_writer::end_tag('li');
+        }
+        $text .= html_writer::end_tag('ul');
+
         return $text;
     }
 
index 2f3cec1..4cdcdf7 100644 (file)
@@ -23,6 +23,9 @@
  */
 
 $string['anycategory'] = 'Any category';
+$string['apierror'] = 'The Youtube API key is not set. Contact your administrator.';
+$string['apikey'] = 'API key';
+$string['apikeyinfo'] = 'Get a <a href="https://developers.google.com/youtube/v3/getting-started">Google API key</a> for your Moodle site.';
 $string['autosvehicles'] = 'Autos &amp; Vehicles';
 $string['category'] = 'Category';
 $string['comedy'] = 'Comedy';
@@ -39,6 +42,7 @@ $string['numberofvideos'] = 'Number of videos';
 $string['peopleblogs'] = 'People &amp; Blogs';
 $string['petsanimals'] = 'Pets &amp; Animals';
 $string['pluginname'] = 'Youtube';
+$string['requesterror'] = 'Data could not be obtained from the server. Contact your administrator if the problem persist.';
 $string['scienceandtech'] = 'Science &amp; Tech';
 $string['sports'] = 'Sports';
 $string['tag_youtube:addinstance'] = 'Add a new youtube block';
diff --git a/blocks/tag_youtube/settings.php b/blocks/tag_youtube/settings.php
new file mode 100644 (file)
index 0000000..ad9f443
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Settings for the RSS client block.
+ *
+ * @package   block_tag_youtube
+ * @copyright 2015 David Monllao
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($ADMIN->fulltree) {
+    $settings->add(new admin_setting_configtext('block_tag_youtube/apikey', get_string('apikey', 'block_tag_youtube'),
+                       get_string('apikeyinfo', 'block_tag_youtube'), '', PARAM_RAW_TRIMMED, 40));
+}
diff --git a/blocks/tag_youtube/upgrade.txt b/blocks/tag_youtube/upgrade.txt
new file mode 100644 (file)
index 0000000..ae3d80d
--- /dev/null
@@ -0,0 +1,8 @@
+This files describes API changes in the block tag_youtube code.
+
+=== 3.0 ===
+
+* Due to the final YouTube API v2.0 deprecation we needed to adapt the current
+  code to YouTube Data API v3. block_tag_youtube::fetch_request and
+  block_tag_youtube::render_video_list have been deprecated as they can not be
+  used any more.
index 1a5c4de..832b9f1 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015051101;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'block_tag_youtube'; // Full name of the plugin (used for diagnostics)
index 0265fe9..2851ae7 100644 (file)
@@ -27,7 +27,6 @@ Feature: Block tags displaying tag cloud
     And I press "Update profile"
     And I log out
 
-  @javascript
   Scenario: Add Tags block on a front page
     When I log in as "admin"
     And I am on site homepage
@@ -41,7 +40,6 @@ Feature: Block tags displaying tag cloud
     And I click on "Dogs" "link" in the "Tags" "block"
     And I should see "Log in to the site" in the ".breadcrumb" "css_element"
 
-  @javascript
   Scenario: Add Tags block in a course
     When I log in as "teacher1"
     And I follow "Course 1"
index bd38cad..dfb5c8f 100644 (file)
@@ -24,7 +24,6 @@ Feature: Blogs can be set to be only visible by the author.
     And I press "Save changes"
     And I log out
 
-  @javascript
   Scenario: A student can not see another student's blog entries.
     Given I log in as "testuser"
     And I follow "Course 1"
index 514a4bd..e044c91 100644 (file)
@@ -341,8 +341,8 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
             $maxtime = cache::now() - $ttl;
         }
         $readfile = false;
-        if ($this->prescan && array_key_exists($key, $this->keys)) {
-            if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
+        if ($this->prescan && array_key_exists($filename, $this->keys)) {
+            if ((!$ttl || $this->keys[$filename] >= $maxtime) && file_exists($file)) {
                 $readfile = true;
             } else {
                 $this->delete($key);
index be321f2..887c545 100644 (file)
@@ -44,4 +44,34 @@ class cachestore_file_test extends cachestore_tests {
     protected function get_class_name() {
         return 'cachestore_file';
     }
+
+    /**
+     * Testing cachestore_file::get with prescan enabled and with
+     * deleting the cache between the prescan and the call to get.
+     *
+     * The deleting of cache simulates some other process purging
+     * the cache.
+     */
+    public function test_cache_get_with_prescan_and_purge() {
+        global $CFG;
+
+        $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cachestore_file', 'phpunit_test');
+        $name = 'File test';
+
+        $path = make_cache_directory('cachestore_file_test');
+        $cache = new cachestore_file($name, array('path' => $path, 'prescan' => true));
+        $cache->initialise($definition);
+
+        $cache->set('testing', 'value');
+
+        $path  = make_cache_directory('cachestore_file_test');
+        $cache = new cachestore_file($name, array('path' => $path, 'prescan' => true));
+        $cache->initialise($definition);
+
+        // Let's pretend that some other process purged caches.
+        remove_dir($CFG->cachedir.'/cachestore_file_test', true);
+        make_cache_directory('cachestore_file_test');
+
+        $cache->get('testing');
+    }
 }
\ No newline at end of file
index 46e6c56..d11b6d9 100644 (file)
@@ -612,7 +612,7 @@ class core_calendar_renderer extends plugin_renderer_base {
 
         if (empty($subscriptions)) {
             $cell = new html_table_cell(get_string('nocalendarsubscriptions', 'calendar'));
-            $cell->colspan = 4;
+            $cell->colspan = 5;
             $table->data[] = new html_table_row(array($cell));
         }
         $strnever = new lang_string('never', 'calendar');
index 345f9e7..fab8aa5 100644 (file)
@@ -41,8 +41,6 @@
 require_once('../config.php');
 require_once($CFG->dirroot.'/calendar/lib.php');
 
-require_sesskey();
-
 $var = required_param('var', PARAM_ALPHA);
 $return = clean_param(base64_decode(required_param('return', PARAM_RAW)), PARAM_LOCALURL);
 $courseid = optional_param('id', -1, PARAM_INT);
@@ -51,6 +49,12 @@ if ($courseid != -1) {
 } else {
     $return = new moodle_url($return);
 }
+
+if (!confirm_sesskey()) {
+    // Do not call require_sesskey() since this page may be accessed without session (for example by bots).
+    redirect($return);
+}
+
 $url = new moodle_url('/calendar/set.php', array('return'=>base64_encode($return->out_as_local_url(false)), 'course' => $courseid, 'var'=>$var, 'sesskey'=>sesskey()));
 $PAGE->set_url($url);
 $PAGE->set_context(context_system::instance());
index 5125a88..6790dbf 100644 (file)
@@ -27,7 +27,8 @@
 
 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 
-use Behat\Behat\Context\Step\Given as Given,
+use Behat\Behat\Context\Step\Given,
+    Behat\Behat\Context\Step\Then,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
@@ -56,7 +57,7 @@ class behat_completion extends behat_base {
 
         return array(
             new Given('I go to the current course activity completion report'),
-            new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
+            new Then('"' . $this->escape($xpath) . '" "xpath_element" should exist')
         );
     }
 
@@ -75,7 +76,7 @@ class behat_completion extends behat_base {
             "/descendant::img[contains(@title, $titleliteral)]";
         return array(
             new Given('I go to the current course activity completion report'),
-            new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
+            new Then('"' . $this->escape($xpath) . '" "xpath_element" should exist')
         );
 
         return $steps;
index 4c9289b..65cd546 100644 (file)
@@ -338,16 +338,10 @@ class format_singleactivity extends format_base {
      * @return bool|null (null if the check is not possible)
      */
     public function activity_has_subtypes() {
-        global $CFG;
         if (!($modname = $this->get_activitytype())) {
             return null;
         }
-        $libfile = "$CFG->dirroot/mod/$modname/lib.php";
-        if (!file_exists($libfile)) {
-            return null;
-        }
-        include_once($libfile);
-        return function_exists($modname. '_get_types');
+        return component_callback('mod_' . $modname, 'get_types', array(), MOD_SUBTYPE_NO_CHILDREN) !== MOD_SUBTYPE_NO_CHILDREN;
     }
 
     /**
index f1c2e88..cabeace 100644 (file)
@@ -17,7 +17,6 @@ Feature: Browse course list and return back from enrolment page
       | Sample course | C1        | 0        |
       | Course 1      | COURSE1   | CAT1     |
 
-  @javascript
   Scenario: A user can return to the category page from enrolment page
     When I log in as "user2"
     And I click on "Courses" "link" in the "Navigation" "block"
index 2115b15..8ea4d24 100644 (file)
@@ -237,14 +237,14 @@ class course_enrolment_manager {
             $extrafields = get_extra_user_fields($this->get_context());
             $extrafields[] = 'lastaccess';
             $ufields = user_picture::fields('u', $extrafields);
-            $sql = "SELECT DISTINCT $ufields, ul.timeaccess AS lastseen
+            $sql = "SELECT DISTINCT $ufields, COALESCE(ul.timeaccess, 0) AS lastcourseaccess
                       FROM {user} u
                       JOIN {user_enrolments} ue ON (ue.userid = u.id  AND ue.enrolid $instancessql)
                       JOIN {enrol} e ON (e.id = ue.enrolid)
                  LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = u.id)
                  LEFT JOIN {groups_members} gm ON u.id = gm.userid
                      WHERE $filtersql
-                  ORDER BY u.$sort $direction";
+                  ORDER BY $sort $direction";
             $this->users[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage);
         }
         return $this->users[$key];
@@ -334,20 +334,22 @@ class course_enrolment_manager {
             list($ctxcondition, $params) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'ctx');
             $params['courseid'] = $this->course->id;
             $params['cid'] = $this->course->id;
-            $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, u.*, ue.lastseen
+            $extrafields = get_extra_user_fields($this->get_context());
+            $ufields = user_picture::fields('u', $extrafields);
+            $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, $ufields,
+                        coalesce(u.lastaccess,0) AS lastaccess
                     FROM {role_assignments} ra
                     JOIN {user} u ON u.id = ra.userid
                     JOIN {context} ctx ON ra.contextid = ctx.id
                LEFT JOIN (
-                       SELECT ue.id, ue.userid, ul.timeaccess AS lastseen
+                       SELECT ue.id, ue.userid
                          FROM {user_enrolments} ue
-                    LEFT JOIN {enrol} e ON e.id=ue.enrolid
-                    LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = ue.userid)
+                         JOIN {enrol} e ON e.id = ue.enrolid
                         WHERE e.courseid = :courseid
                        ) ue ON ue.userid=u.id
                    WHERE ctx.id $ctxcondition AND
                          ue.id IS NULL
-                ORDER BY u.$sort $direction, ctx.depth DESC";
+                ORDER BY $sort $direction, ctx.depth DESC";
             $this->otherusers[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage);
         }
         return $this->otherusers[$key];
@@ -1091,7 +1093,7 @@ class course_enrolment_manager {
      * @param array $extrafields The list of fields as returned from get_extra_user_fields used to determine which
      * additional fields may be displayed
      * @param int $now The time used for lastaccess calculation
-     * @return array The fields to be displayed including userid, courseid, picture, firstname, lastseen and any
+     * @return array The fields to be displayed including userid, courseid, picture, firstname, lastcourseaccess, lastaccess and any
      * additional fields from $extrafields
      */
     private function prepare_user_for_display($user, $extrafields, $now) {
@@ -1100,7 +1102,7 @@ class course_enrolment_manager {
             'courseid'         => $this->get_course()->id,
             'picture'          => new user_picture($user),
             'firstname'        => fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())),
-            'lastseen'         => get_string('never'),
+            'lastaccess'       => get_string('never'),
             'lastcourseaccess' => get_string('never'),
         );
         foreach ($extrafields as $field) {
@@ -1108,13 +1110,13 @@ class course_enrolment_manager {
         }
 
         // Last time user has accessed the site.
-        if ($user->lastaccess) {
-            $details['lastseen'] = format_time($now - $user->lastaccess);
+        if (!empty($user->lastaccess)) {
+            $details['lastaccess'] = format_time($now - $user->lastaccess);
         }
 
         // Last time user has accessed the course.
-        if ($user->lastseen) {
-            $details['lastcourseaccess'] = format_time($now - $user->lastseen);
+        if (!empty($user->lastcourseaccess)) {
+            $details['lastcourseaccess'] = format_time($now - $user->lastcourseaccess);
         }
         return $details;
     }
index c9c8013..08c50c8 100644 (file)
@@ -80,6 +80,7 @@ class enrol_meta_handler {
      */
     protected static function sync_with_parent_course(stdClass $instance, $userid) {
         global $DB, $CFG;
+        require_once($CFG->dirroot . '/group/lib.php');
 
         $plugin = enrol_get_plugin('meta');
 
index 50c2bcb..188c9d5 100644 (file)
@@ -60,7 +60,7 @@ foreach ($extrafields as $field) {
 
 $fields = array(
     'userdetails' => $userdetails,
-    'lastseen' => get_string('lastaccess'),
+    'lastaccess' => get_string('lastaccess'),
     'role' => get_string('roles', 'role')
 );
 
@@ -68,7 +68,7 @@ $fields = array(
 if (!has_capability('moodle/course:viewhiddenuserfields', $context)) {
     $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
     if (isset($hiddenfields['lastaccess'])) {
-        unset($fields['lastseen']);
+        unset($fields['lastaccess']);
     }
 }
 
index 72b0cb2..c0c3ac8 100644 (file)
@@ -421,7 +421,7 @@ class course_enrolment_table extends html_table implements renderable {
      * @var array
      */
     protected static $sortablefields = array('firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
-            'alternatename', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department' );
+            'alternatename', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department', 'lastaccess', 'lastcourseaccess' );
 
     /**
      * Constructs the table
@@ -513,9 +513,10 @@ class course_enrolment_table extends html_table implements renderable {
                     if (!in_array($n, self::$sortablefields)) {
                         $bits[] = $l;
                     } else {
-                        $link = html_writer::link(new moodle_url($url, array(self::SORTVAR=>$n)), $fields[$name][$n]);
+                        $sorturl = new moodle_url($url, array(self::SORTVAR => $n, self::SORTDIRECTIONVAR => $this->get_field_sort_direction($n)));
+                        $link = html_writer::link($sorturl, $fields[$name][$n]);
                         if ($this->sort == $n) {
-                            $link .= html_writer::link(new moodle_url($url, array(self::SORTVAR=>$n, self::SORTDIRECTIONVAR=>$this->get_field_sort_direction($n))), $this->get_direction_icon($output, $n));
+                            $link .= $this->get_direction_icon($output, $n);
                         }
                         $bits[] = html_writer::tag('span', $link, array('class'=>'subheading_'.$n));
 
@@ -526,9 +527,10 @@ class course_enrolment_table extends html_table implements renderable {
                 if (!in_array($name, self::$sortablefields)) {
                     $newlabel = $label;
                 } else {
-                    $newlabel  = html_writer::link(new moodle_url($url, array(self::SORTVAR=>$name)), $fields[$name]);
+                    $sorturl = new moodle_url($url, array(self::SORTVAR => $name, self::SORTDIRECTIONVAR => $this->get_field_sort_direction($name)));
+                    $newlabel  = html_writer::link($sorturl, $fields[$name]);
                     if ($this->sort == $name) {
-                        $newlabel .= html_writer::link(new moodle_url($url, array(self::SORTVAR=>$name, self::SORTDIRECTIONVAR=>$this->get_field_sort_direction($name))), $this->get_direction_icon($output, $name));
+                        $newlabel .= $this->get_direction_icon($output, $name);
                     }
                 }
             }
@@ -704,13 +706,6 @@ class course_enrolment_table extends html_table implements renderable {
  */
 class course_enrolment_users_table extends course_enrolment_table {
 
-    /**
-     * An array of sortable fields
-     * @static
-     * @var array
-     */
-    protected static $sortablefields = array('firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
-            'alternatename', 'email', 'lastaccess');
 }
 
 /**
index a099f59..e9744b1 100644 (file)
@@ -577,6 +577,7 @@ class gradeimport_csv_load_data {
 
                     } else {
                         // The grade item for this is not updated.
+                        $newfeedback->importonlyfeedback = true;
                         $insertid = self::insert_grade_record($newfeedback, $this->studentid);
                         // Check to see if the insert was successful.
                         if (empty($insertid)) {
index 118906c..352365e 100644 (file)
@@ -210,6 +210,7 @@ Bobby,Bunce,,"Moodle HQ","Rock on!",student5@example.com,75.00,,75.00,{exportdat
         $testarray[$key]->feedback = $record->feedback;
         $testarray[$key]->importcode = $testobject->get_importcode();
         $testarray[$key]->importer = $USER->id;
+        $testarray[$key]->importonlyfeedback = 0;
 
         // Check that the record was inserted into the database.
         $this->assertEquals($gradeimportvalues, $testarray);
index f8d8e57..655c54b 100644 (file)
@@ -37,9 +37,10 @@ function get_new_importcode() {
  * (grade_import_value and grade_import_newitem)
  * If this function is called, we assume that all data collected
  * up to this point is fine and we can go ahead and commit
- * @param int courseid - id of the course
- * @param string importcode - import batch identifier
- * @param feedback print feedback and continue button
+ * @param int $courseid - ID of the course.
+ * @param int $importcode - Import batch identifier.
+ * @param bool $importfeedback - Whether to import feedback as well.
+ * @param bool $verbose - Print feedback and continue button.
  * @return bool success
  */
 function grade_import_commit($courseid, $importcode, $importfeedback=true, $verbose=true) {
@@ -114,6 +115,10 @@ function grade_import_commit($courseid, $importcode, $importfeedback=true, $verb
                     if (!$importfeedback) {
                         $grade->feedback = false; // ignore it
                     }
+                    if ($grade->importonlyfeedback) {
+                        // False means do not change. See grade_itme::update_final_grade().
+                        $grade->finalgrade = false;
+                    }
                     if (!$gradeitem->update_final_grade($grade->userid, $grade->finalgrade, 'import', $grade->feedback)) {
                         $errordata = new stdClass();
                         $errordata->itemname = $gradeitem->itemname;
index 5ecb430..cd4fead 100644 (file)
@@ -52,7 +52,7 @@ class gradereport_singleview extends grade_report {
      * @return array List of warnings
      */
     public function process_data($data) {
-        if (has_capability('moodle/grade:manage', $this->context)) {
+        if (has_capability('moodle/grade:edit', $this->context)) {
             return $this->screen->process($data);
         }
     }
index e87d71b..d10d14a 100644 (file)
@@ -62,7 +62,6 @@ Feature: We can use calculated grade totals
     And I set the field "Grade display type" to "Real (percentage)"
     And I press "Save changes"
 
-  @javascript
   Scenario: Mean of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Mean of grades |
@@ -81,7 +80,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "30.42 (30.42 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Weighted mean of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Weighted mean of grades |
@@ -104,7 +102,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "26.94 (26.94 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Simple weighted mean of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Simple weighted mean of grades |
@@ -125,7 +122,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "48.57 (48.57 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Mean of grades (with extra credits) aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Mean of grades (with extra credits) |
@@ -146,7 +142,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "47.22 (47.22 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Median of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation | Median of grades |
@@ -165,7 +160,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "25.83 (25.83 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Lowest grade aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation | Lowest grade |
@@ -188,7 +182,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "0.00 (0.00 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Highest grade aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Highest grade |
@@ -209,7 +202,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Mode of grades aggregation
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Mode of grades |
@@ -230,7 +222,6 @@ Feature: We can use calculated grade totals
     And I follow "Grades" in the user menu
     And I should see "50.00 (50.00 %)" in the "overview-grade" "table"
 
-  @javascript
   Scenario: Natural aggregation on outcome items with natural weights
     And the following config values are set as admin:
       | enableoutcomes | 1 |
@@ -292,7 +283,6 @@ Feature: We can use calculated grade totals
     And I follow "Course 1"
     And "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
 
-  @javascript
   Scenario: Natural aggregation on outcome items with modified weights
     And the following config values are set as admin:
       | enableoutcomes | 1 |
@@ -329,7 +319,6 @@ Feature: We can use calculated grade totals
     And I follow "Course 1"
     And "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)"
 
-  @javascript
   Scenario: Natural aggregation
     And I set the following settings for grade item "Sub category 1":
       | Aggregation          | Natural |
@@ -353,8 +342,8 @@ Feature: We can use calculated grade totals
     And I set the field "Show contribution to course total" to "Show"
     And I set the field "Show weightings" to "Show"
     And I press "Save changes"
-    And I set the field "Grade report" to "User report"
-    And I set the field "Select all or one user" to "Student 1"
+    And I select "User report" from the "Grade report" singleselect
+    And I select "Student 1" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item | Calculated weight | Grade | Range | Contribution to course total |
       | Test assignment five | 28.57 % | 10.00 (50.00 %) | 0–20 | 1.03 % |
@@ -382,7 +371,6 @@ Feature: We can use calculated grade totals
       | Test assignment three | 30.93 %( Extra credit ) | 40.00 (26.67 %) | 0–150 | 8.25 % |
       | Test assignment four | 30.93 % | - | 0–150 | 0.00 % |
 
-  @javascript
   Scenario: Natural aggregation with drop lowest
     When I log out
     And I log in as "admin"
@@ -401,7 +389,6 @@ Feature: We can use calculated grade totals
       | Exclude empty grades | 0       |
     And I navigate to "Categories and items" node in "Grade administration > Setup"
     And I press "Add category"
-    And I click on "Show more" "link"
     And I set the following fields to these values:
       | Category name | Sub category 3 |
       | Aggregation | Natural |
@@ -465,7 +452,7 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Natural aggregation from the setup screen
-    And I set the field "Grade report" to "Categories and items"
+    And I select "Categories and items" from the "Grade report" singleselect
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Natural |
     And I set the following settings for grade item "Sub category 1":
@@ -527,7 +514,7 @@ Feature: We can use calculated grade totals
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
     And I turn editing mode off
-    And I set the field "Grade report" to "Categories and items"
+    And I select "Categories and items" from the "Grade report" singleselect
     And I set the field "Override weight of Test assignment one" to "1"
     And I set the field "Weight of Test assignment one" to "0"
     And I set the field "Override weight of Test assignment six" to "1"
@@ -542,8 +529,8 @@ Feature: We can use calculated grade totals
     And I set the field "Show weightings" to "Show"
     And I press "Save changes"
     Then I should see "75.00 (16.85 %)" in the ".course" "css_element"
-    And I set the field "Grade report" to "User report"
-    And I set the field "Select all or one user" to "Student 1"
+    And I select "User report" from the "Grade report" singleselect
+    And I select "Student 1" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item            | Calculated weight | Grade           | Contribution to course total |
       | Test assignment five  | 57.14 %           | 10.00 (50.00 %) | 2.25 %                        |
index a854732..5cd48b0 100644 (file)
@@ -70,7 +70,6 @@ Feature: View gradebook when scales are used
     And I follow "Grader report"
     And I turn editing mode on
 
-  @javascript
   Scenario: Test displaying scales in gradebook in aggregation method Natural
     When I turn editing mode off
     Then the following should exist in the "user-grades" table:
@@ -85,14 +84,13 @@ Feature: View gradebook when scales are used
       | Range              | F–A      | 0.00–5.00      | 0.00–5.00    |
       | Overall average    | C        | 3.00           | 3.00         |
     And I follow "User report"
-    And I set the field "Select all or one user" to "Student 3"
-    And I click on "Select all or one user" "select"
+    And I select "Student 3" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range | Percentage | Contribution to course total |
       | Test assignment one | C     | F–A   | 50.00 %    | 60.00 %                      |
       | Sub category 1 total      | 3.00  | 0–5   | 60.00 %    | -                            |
       | Course total        | 3.00  | 0–5   | 60.00 %    | -                            |
-    And I set the field "jump" to "Categories and items"
+    And I select "Categories and items" from the "Grade report" singleselect
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | 5.00      |
@@ -108,7 +106,6 @@ Feature: View gradebook when scales are used
       | Sub category 1 total      | 4.00  | 0–5   | 80.00 %    | -                            |
       | Course total        | 4.00  | 0–5   | 80.00 %    | -                            |
 
-  @javascript
   Scenario Outline: Test displaying scales in gradebook in all other aggregation methods
     When I follow "Edit   Course 1"
     And I set the field "Aggregation" to "<aggregation>"
@@ -133,14 +130,13 @@ Feature: View gradebook when scales are used
       | Range              | F–A      | 1.00–5.00      | 0.00–100.00    |
       | Overall average    | C        | 3.00           | <overallavg>   |
     And I follow "User report"
-    And I set the field "Select all or one user" to "Student 3"
-    And I click on "Select all or one user" "select"
+    And I select "Student 3" from the "Select all or one user" singleselect
     And the following should exist in the "user-grade" table:
       | Grade item                   | Grade          | Range | Percentage    | Contribution to course total |
       | Test assignment one          | C              | F–A   | 50.00 %       | <contrib3>                   |
       | Sub category (<aggregation>) total<aggregation>. | 3.00           | 1–5   | 50.00 %       | -                            |
       | Course total<aggregation>.   | <coursetotal3> | 0–100 | <courseperc3> | -                            |
-    And I set the field "jump" to "Categories and items"
+    And I select "Categories and items" from the "Grade report" singleselect
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | A (5)     |
index 5662e39..9aa8684 100644 (file)
@@ -57,9 +57,8 @@ Feature: We can enter in grades and view reports from the gradebook
     And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment name 2"
     And I press "Save changes"
 
-  @javascript
   Scenario: Grade a grade item and ensure the results display correctly in the gradebook
-    When I set the field "Grade report" to "User report"
+    When I select "User report" from the "Grade report" singleselect
     And the "Grade report" select box should contain "Grader report"
     And the "Grade report" select box should contain "Outcomes report"
     And the "Grade report" select box should contain "User report"
@@ -80,14 +79,13 @@ Feature: We can enter in grades and view reports from the gradebook
     And "Course 1" row "Grade" column of "overview-grade" table should contain "170.00"
     And "Course 1" row "Grade" column of "overview-grade" table should not contain "90.00"
 
-  @javascript
   Scenario: We can add a weighting to a grade item and it is displayed properly in the user report
-    When I set the field "Grade report" to "Categories and items"
+    When I select "Categories and items" from the "Grade report" singleselect
     And I set the following settings for grade item "Course 1":
       | Aggregation | Weighted mean of grades |
     And I set the field "Extra credit value for Test assignment name" to "0.72"
     And I press "Save changes"
-    And I set the field "Grade report" to "User report"
+    And I select "User report" from the "Grade report" singleselect
     And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the following fields to these values:
       | Show weightings | Show |
diff --git a/grade/tests/importlib_test.php b/grade/tests/importlib_test.php
new file mode 100644 (file)
index 0000000..0acea9e
--- /dev/null
@@ -0,0 +1,208 @@
+<?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 grade/import/lib.php.
+ *
+ * @package   core_grade
+ * @category  phpunit
+ * @copyright 2015 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/grade/import/lib.php');
+
+/**
+ * Tests grade_import_lib functions.
+ */
+class core_grade_import_lib_test extends advanced_testcase {
+
+    /**
+     * Import grades into 'grade_import_values' table. This is done differently in the various import plugins,
+     * so there is no direct API to call.
+     *
+     * @param array $data Information to be inserted into the table.
+     * @return int The insert ID of the sql statement.
+     */
+    private function import_grades($data) {
+        global $DB, $USER;
+        $graderecord = new stdClass();
+        $graderecord->importcode = $data['importcode'];
+        if (isset($data['itemid'])) {
+            $graderecord->itemid = $data['itemid'];
+        }
+        $graderecord->userid = $data['userid'];
+        if (isset($data['importer'])) {
+            $graderecord->importer = $data['importer'];
+        } else {
+            $graderecord->importer = $USER->id;
+        }
+        if (isset($data['finalgrade'])) {
+            $graderecord->finalgrade = $data['finalgrade'];
+        } else {
+            $graderecord->finalgrade = rand(0, 100);
+        }
+        if (isset($data['feedback'])) {
+            $graderecord->feedback = $data['feedback'];
+        }
+        if (isset($data['importonlyfeedback'])) {
+            $graderecord->importonlyfeedback = $data['importonlyfeedback'];
+        } else {
+            $graderecord->importonlyfeedback = false;
+        }
+        if (isset($data['newgradeitem'])) {
+            $graderecord->newgradeitem = $data['newgradeitem'];
+        }
+        return $DB->insert_record('grade_import_values', $graderecord);
+    }
+
+    /**
+     * Tests for importing grades from an external source.
+     */
+    public function test_grade_import_commit() {
+        global $USER, $DB, $CFG;
+        $this->resetAfterTest();
+
+        $importcode = get_new_importcode();
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course();
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
+        $itemname = $assign->name;
+        $modulecontext = context_module::instance($assign->cmid);
+        // The generator returns a dummy object, lets get the real assign object.
+        $assign = new assign($modulecontext, false, false);
+        $cm = $assign->get_course_module();
+
+        // Enrol users in the course.
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+
+        // Enter a new grade into an existing grade item.
+        $gradeitem = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod'));
+
+        // Keep this value around for a test further down.
+        $originalgrade = 55;
+        $this->import_grades(array(
+            'importcode' => $importcode,
+            'itemid' => $gradeitem->id,
+            'userid' => $user1->id,
+            'finalgrade' => $originalgrade
+        ));
+
+        $status = grade_import_commit($course->id, $importcode, false, false);
+        $this->assertTrue($status);
+
+        // Get imported grade_grade.
+        $gradegrade = grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $user1->id));
+        $this->assertEquals($originalgrade, $gradegrade->finalgrade);
+        // Overriden field will be a timestamp and will evaluate out to true.
+        $this->assertTrue($gradegrade->is_overridden());
+
+        // Create a new grade item and import into that.
+        $importcode = get_new_importcode();
+        $record = new stdClass();
+        $record->itemname = 'New grade item';
+        $record->importcode = $importcode;
+        $record->importer = $USER->id;
+        $insertid = $DB->insert_record('grade_import_newitem', $record);
+
+        $finalgrade = 75;
+        $this->import_grades(array(
+            'importcode' => $importcode,
+            'userid' => $user1->id,
+            'finalgrade' => $finalgrade,
+            'newgradeitem' => $insertid
+        ));
+
+        $status = grade_import_commit($course->id, $importcode, false, false);
+        $this->assertTrue($status);
+        // Check that we have a new grade_item.
+        $gradeitem = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'manual'));
+        $this->assertEquals($record->itemname, $gradeitem->itemname);
+        // Grades were imported.
+        $gradegrade = grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $user1->id));
+        $this->assertEquals($finalgrade, $gradegrade->finalgrade);
+        // As this is a new item the grade has not been overridden.
+        $this->assertFalse($gradegrade->is_overridden());
+
+        // Import feedback only.
+        $importcode = get_new_importcode();
+        $gradeitem = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod'));
+
+        $originalfeedback = 'feedback can be useful';
+        $this->import_grades(array(
+            'importcode' => $importcode,
+            'userid' => $user1->id,
+            'itemid' => $gradeitem->id,
+            'feedback' => $originalfeedback,
+            'importonlyfeedback' => true
+        ));
+
+        $status = grade_import_commit($course->id, $importcode, true, false);
+        $this->assertTrue($status);
+        $gradegrade = grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $user1->id));
+        // The final grade should be the same as the first record further up. We are only altering the feedback.
+        $this->assertEquals($originalgrade, $gradegrade->finalgrade);
+        $this->assertTrue($gradegrade->is_overridden());
+
+        // Import grades only.
+        $importcode = get_new_importcode();
+        $gradeitem = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod'));
+
+        $finalgrade = 60;
+        $this->import_grades(array(
+            'importcode' => $importcode,
+            'userid' => $user1->id,
+            'itemid' => $gradeitem->id,
+            'finalgrade' => $finalgrade,
+            'feedback' => 'feedback can still be useful'
+        ));
+
+        $status = grade_import_commit($course->id, $importcode, false, false);
+        $this->assertTrue($status);
+        $gradegrade = grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $user1->id));
+        $this->assertEquals($finalgrade, $gradegrade->finalgrade);
+        // The final feedback should not have changed.
+        $this->assertEquals($originalfeedback, $gradegrade->feedback);
+        $this->assertTrue($gradegrade->is_overridden());
+
+        // Check that printing of import status is correct.
+        $importcode = get_new_importcode();
+        $gradeitem = grade_item::fetch(array('courseid' => $course->id, 'itemtype' => 'mod'));
+
+        $this->import_grades(array(
+            'importcode' => $importcode,
+            'userid' => $user1->id,
+            'itemid' => $gradeitem->id
+        ));
+
+        $url = $CFG->wwwroot . '/grade/index.php';
+        $expectedresponse = "++ Grade import success ++
+<div class=\"continuebutton\"><form method=\"get\" action=\"$url\"><div><input type=\"submit\" value=\"Continue\" /><input type=\"hidden\" name=\"id\" value=\"$course->id\" /></div></form></div>";
+
+        ob_start();
+        $status = grade_import_commit($course->id, $importcode);
+        $output = ob_get_contents();
+        ob_end_clean();
+        $this->assertTrue($status);
+        $this->assertEquals($expectedresponse, $output);
+    }
+}
index 61e1d68..6822e34 100644 (file)
@@ -114,6 +114,8 @@ $string['limitconcurrentlogins_desc'] = 'If enabled the number of concurrent bro
 $string['locked'] = 'Locked';
 $string['authloginviaemail'] = 'Allow log in via email';
 $string['authloginviaemail_desc'] = 'Allow users to use both username and email address (if unique) for site login.';
+$string['allowaccountssameemail'] = 'Allow accounts with same email';
+$string['allowaccountssameemail_desc'] = 'If enabled, more than one user account can share the same email address. This may result in security or privacy issues, for example with the password change confirmation email.';
 $string['md5'] = 'MD5 hash';
 $string['nopasswordchange'] = 'Password can not be changed';
 $string['nopasswordchangeforced'] = 'You cannot proceed without changing your password, however there is no available page for changing it. Please contact your Moodle Administrator.';
index 731a5b0..257bb4d 100644 (file)
@@ -49,7 +49,6 @@ $string['cachedef_databasemeta'] = 'Database meta information';
 $string['cachedef_eventinvalidation'] = 'Event invalidation';
 $string['cachedef_externalbadges'] = 'External badges for particular user';
 $string['cachedef_suspended_userids'] = 'List of suspended users per course';
-$string['cachedef_gradecondition'] = 'User grades cached for evaluating conditional availability';
 $string['cachedef_groupdata'] = 'Course group information';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_langmenu'] = 'List of available languages';
index 3d84695..5386298 100644 (file)
@@ -47,6 +47,7 @@ $string['clialreadyinstalled'] = 'The configuration file config.php already exis
 $string['cliinstallfinished'] = 'Installation completed successfully.';
 $string['cliinstallheader'] = 'Moodle {$a} command line installation program';
 $string['climustagreelicense'] = 'In non interactive mode you must agree to license by specifying --agree-license option';
+$string['cliskipdatabase'] = 'Skipping database installation.';
 $string['clitablesexist'] = 'Database tables already present, cli installation can not continue.';
 $string['compatibilitysettings'] = 'Checking your PHP settings ...';
 $string['compatibilitysettingshead'] = 'Checking your PHP settings ...';
index 1cfd346..83625f7 100644 (file)
@@ -63,13 +63,12 @@ switch ($pagetype[0]) {
         $PAGE->set_blocks_editing_capability('moodle/my:manageblocks');
         break;
     case 'user':
-        if ($pagelayout == 'mydashboard') {
-            // If it's not the current user's profile, we need a different capability.
-            if ($PAGE->context->contextlevel == CONTEXT_USER && $PAGE->context->instanceid != $USER->id) {
-                $PAGE->set_blocks_editing_capability('moodle/user:manageblocks');
-            } else {
-                $PAGE->set_blocks_editing_capability('moodle/user:manageownblocks');
-            }
+        if ($pagetype[1] === 'profile' && $PAGE->context->contextlevel == CONTEXT_USER
+                && $PAGE->context->instanceid == $USER->id) {
+            // A user can only move blocks on their own site profile.
+            $PAGE->set_blocks_editing_capability('moodle/user:manageownblocks');
+        } else {
+            $PAGE->set_blocks_editing_capability('moodle/user:manageblocks');
         }
         break;
 }
index e321d41..bcfca2c 100644 (file)
Binary files a/lib/amd/build/mustache.min.js and b/lib/amd/build/mustache.min.js differ
index 62d34b1..ed4aa32 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 6c96ca4..afaca9c 100644 (file)
 //
 
 // Description of import into Moodle:
-// Download from https://github.com/janl/mustache.js/releases
+// Checkout from https://github.com/moodle/custom-mustache.js
+// Rebase onto latest release tag from https://github.com/janl/mustache.js
 // Copy mustache.js into lib/amd/src/ in Moodle folder.
 // Add the license as a comment to the file and these instructions.
+// Add jshint tags so this file is not linted.
+// Remove the "global define:" comment (hint for linter)
 
 /*!
  * mustache.js - Logic-less {{mustache}} templates with JavaScript
  * http://github.com/janl/mustache.js
  */
+
 /* jshint ignore:start */
 
-(function (global, factory) {
-  if (typeof exports === "object" && exports) {
+(function defineMustache (global, factory) {
+  if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') {
     factory(exports); // CommonJS
-  } else if (typeof define === "function" && define.amd) {
+  } else if (typeof define === 'function' && define.amd) {
     define(['exports'], factory); // AMD
   } else {
-    factory(global.Mustache = {}); // <script>
+    global.Mustache = {};
+    factory(Mustache); // script, wsh, asp
   }
-}(this, function (mustache) {
+}(this, function mustacheFactory (mustache) {
 
-  var Object_toString = Object.prototype.toString;
-  var isArray = Array.isArray || function (object) {
-    return Object_toString.call(object) === '[object Array]';
+  var objectToString = Object.prototype.toString;
+  var isArray = Array.isArray || function isArrayPolyfill (object) {
+    return objectToString.call(object) === '[object Array]';
   };
 
-  function isFunction(object) {
+  function isFunction (object) {
     return typeof object === 'function';
   }
 
-  function escapeRegExp(string) {
-    return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+  /**
+   * More correct typeof string handling array
+   * which normally returns typeof 'object'
+   */
+  function typeStr (obj) {
+    return isArray(obj) ? 'array' : typeof obj;
+  }
+
+  function escapeRegExp (string) {
+    return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
+  }
+
+  /**
+   * Null safe way of checking whether or not an object,
+   * including its prototype, has a given property
+   */
+  function hasProperty (obj, propName) {
+    return obj != null && typeof obj === 'object' && (propName in obj);
   }
 
   // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
   // See https://github.com/janl/mustache.js/issues/189
-  var RegExp_test = RegExp.prototype.test;
-  function testRegExp(re, string) {
-    return RegExp_test.call(re, string);
+  var regExpTest = RegExp.prototype.test;
+  function testRegExp (re, string) {
+    return regExpTest.call(re, string);
   }
 
   var nonSpaceRe = /\S/;
-  function isWhitespace(string) {
+  function isWhitespace (string) {
     return !testRegExp(nonSpaceRe, string);
   }
 
   var entityMap = {
-    "&": "&amp;",
-    "<": "&lt;",
-    ">": "&gt;",
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
     '"': '&quot;',
     "'": '&#39;',
-    "/": '&#x2F;'
+    '/': '&#x2F;'
   };
 
-  function escapeHtml(string) {
-    return String(string).replace(/[&<>"'\/]/g, function (s) {
+  function escapeHtml (string) {
+    return String(string).replace(/[&<>"'\/]/g, function fromEntityMap (s) {
       return entityMap[s];
     });
   }
   var spaceRe = /\s+/;
   var equalsRe = /\s*=/;
   var curlyRe = /\s*\}/;
-  var tagRe = /#|\^|\/|>|\{|&|=|!/;
+  var tagRe = /#|\^|\/|>|\{|&|=|!|\$|</;
 
   /**
    * Breaks up the given `template` string into a tree of tokens. If the `tags`
    * array of tokens in the subtree and 2) the index in the original template at
    * which the closing tag for that section begins.
    */
-  function parseTemplate(template, tags) {
+  function parseTemplate (template, tags) {
     if (!template)
       return [];
 
 
     // Strips all whitespace tokens array for the current line
     // if there was a {{#tag}} on it and otherwise only space.
-    function stripSpace() {
+    function stripSpace () {
       if (hasTag && !nonSpace) {
         while (spaces.length)
           delete tokens[spaces.pop()];
     }
 
     var openingTagRe, closingTagRe, closingCurlyRe;
-    function compileTags(tags) {
-      if (typeof tags === 'string')
-        tags = tags.split(spaceRe, 2);
+    function compileTags (tagsToCompile) {
+      if (typeof tagsToCompile === 'string')
+        tagsToCompile = tagsToCompile.split(spaceRe, 2);
 
-      if (!isArray(tags) || tags.length !== 2)
-        throw new Error('Invalid tags: ' + tags);
+      if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
+        throw new Error('Invalid tags: ' + tagsToCompile);
 
-      openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
-      closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
-      closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
+      openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
+      closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
+      closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
     }
 
     compileTags(tags || mustache.tags);
       token = [ type, value, start, scanner.pos ];
       tokens.push(token);
 
-      if (type === '#' || type === '^') {
+      if (type === '#' || type === '^' || type === '$' || type === '<') {
         sections.push(token);
       } else if (type === '/') {
         // Check section nesting.
    * Combines the values of consecutive text tokens in the given `tokens` array
    * to a single token.
    */
-  function squashTokens(tokens) {
+  function squashTokens (tokens) {
     var squashedTokens = [];
 
     var token, lastToken;
    * all tokens that appear in that section and 2) the index in the original
    * template that represents the end of that section.
    */
-  function nestTokens(tokens) {
+  function nestTokens (tokens) {
     var nestedTokens = [];
     var collector = nestedTokens;
     var sections = [];
       token = tokens[i];
 
       switch (token[0]) {
+      case '$':
+      case '<':
       case '#':
       case '^':
         collector.push(token);
    * A simple string scanner that is used by the template parser to find
    * tokens in template strings.
    */
-  function Scanner(string) {
+  function Scanner (string) {
     this.string = string;
     this.tail = string;
     this.pos = 0;
   /**
    * Returns `true` if the tail is empty (end of string).
    */
-  Scanner.prototype.eos = function () {
-    return this.tail === "";
+  Scanner.prototype.eos = function eos () {
+    return this.tail === '';
   };
 
   /**
    * Tries to match the given regular expression at the current position.
    * Returns the matched text if it can match, the empty string otherwise.
    */
-  Scanner.prototype.scan = function (re) {
+  Scanner.prototype.scan = function scan (re) {
     var match = this.tail.match(re);
 
     if (!match || match.index !== 0)
    * Skips all text until the given regular expression can be matched. Returns
    * the skipped string, which is the entire tail if no match can be made.
    */
-  Scanner.prototype.scanUntil = function (re) {
+  Scanner.prototype.scanUntil = function scanUntil (re) {
     var index = this.tail.search(re), match;
 
     switch (index) {
     case -1:
       match = this.tail;
-      this.tail = "";
+      this.tail = '';
       break;
     case 0:
-      match = "";
+      match = '';
       break;
     default:
       match = this.tail.substring(0, index);
    * Represents a rendering context by wrapping a view object and
    * maintaining a reference to the parent context.
    */
-  function Context(view, parentContext) {
-    this.view = view == null ? {} : view;
+  function Context (view, parentContext) {
+    this.view = view;
+    this.blocks = {};
     this.cache = { '.': this.view };
     this.parent = parentContext;
   }
    * Creates a new context using the given view with this context
    * as the parent.
    */
-  Context.prototype.push = function (view) {
+  Context.prototype.push = function push (view) {
     return new Context(view, this);
   };
 
+  /**
+   * Set a value in the current block context.
+   */
+  Context.prototype.setBlockVar = function set (name, value) {
+    var blocks = this.blocks;
+
+    blocks[name] = value;
+
+    return value;
+  };
+
+  /**
+   * Clear all current block vars.
+   */
+  Context.prototype.clearBlockVars = function clearBlockVars () {
+    this.blocks = {};
+  };
+
+  /**
+   * Get a value only from the current block context.
+   */
+  Context.prototype.getBlockVar = function getBlockVar (name) {
+    var blocks = this.blocks;
+
+    var value;
+    if (blocks.hasOwnProperty(name)) {
+      value = blocks[name];
+    } else {
+      if (this.parent) {
+        value = this.parent.getBlockVar(name);
+      }
+    }
+    // Can return undefined.
+    return value;
+  };
+
   /**
    * Returns the value of the given name in this context, traversing
    * up the context hierarchy if the value is absent in this context's view.
    */
-  Context.prototype.lookup = function (name) {
+  Context.prototype.lookup = function lookup (name) {
     var cache = this.cache;
 
     var value;
-    if (name in cache) {
+    if (cache.hasOwnProperty(name)) {
       value = cache[name];
     } else {
-      var context = this, names, index;
+      var context = this, names, index, lookupHit = false;
 
       while (context) {
         if (name.indexOf('.') > 0) {
           names = name.split('.');
           index = 0;
 
-          while (value != null && index < names.length)
+          /**
+           * Using the dot notion path in `name`, we descend through the
+           * nested objects.
+           *
+           * To be certain that the lookup has been successful, we have to
+           * check if the last object in the path actually has the property
+           * we are looking for. We store the result in `lookupHit`.
+           *
+           * This is specially necessary for when the value has been set to
+           * `undefined` and we want to avoid looking up parent contexts.
+           **/
+          while (value != null && index < names.length) {
+            if (index === names.length - 1)
+              lookupHit = hasProperty(value, names[index]);
+
             value = value[names[index++]];
-        } else if (typeof context.view == 'object') {
+          }
+        } else {
           value = context.view[name];
+          lookupHit = hasProperty(context.view, name);
         }
 
-        if (value != null)
+        if (lookupHit)
           break;
 
         context = context.parent;
    * string, given a context. It also maintains a cache of templates to
    * avoid the need to parse the same template twice.
    */
-  function Writer() {
+  function Writer () {
     this.cache = {};
   }
 
   /**
    * Clears all cached templates in this writer.
    */
-  Writer.prototype.clearCache = function () {
+  Writer.prototype.clearCache = function clearCache () {
     this.cache = {};
   };
 
    * Parses and caches the given `template` and returns the array of tokens
    * that is generated from the parse.
    */
-  Writer.prototype.parse = function (template, tags) {
+  Writer.prototype.parse = function parse (template, tags) {
     var cache = this.cache;
     var tokens = cache[template];
 
    * also be a function that is used to load partial templates on the fly
    * that takes a single argument: the name of the partial.
    */
-  Writer.prototype.render = function (template, view, partials) {
+  Writer.prototype.render = function render (template, view, partials) {
     var tokens = this.parse(template);
     var context = (view instanceof Context) ? view : new Context(view);
     return this.renderTokens(tokens, context, partials, template);
    * If the template doesn't use higher-order sections, this argument may
    * be omitted.
    */
-  Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
+  Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate) {
     var buffer = '';
 
-    // This function is used to render an arbitrary template
-    // in the current context by higher-order sections.
-    var self = this;
-    function subRender(template) {
-      return self.render(template, context, partials);
-    }
-
-    var token, value;
+    var token, symbol, value;
     for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      value = undefined;
       token = tokens[i];
+      symbol = token[0];
+
+      if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate);
+      else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate);
+      else if (symbol === '>') value = this.renderPartial(token, context, partials, originalTemplate);
+      else if (symbol === '<') value = this.renderBlock(token, context, partials, originalTemplate);
+      else if (symbol === '$') value = this.renderBlockVariable(token, context, partials, originalTemplate);
+      else if (symbol === '&') value = this.unescapedValue(token, context);
+      else if (symbol === 'name') value = this.escapedValue(token, context);
+      else if (symbol === 'text') value = this.rawValue(token);
+
+      if (value !== undefined)
+        buffer += value;
+    }
 
-      switch (token[0]) {
-      case '#':
-        value = context.lookup(token[1]);
-
-        if (!value)
-          continue;
+    return buffer;
+  };
 
-        if (isArray(value)) {
-          for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
-            buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
-          }
-        } else if (typeof value === 'object' || typeof value === 'string') {
-          buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
-        } else if (isFunction(value)) {
-          if (typeof originalTemplate !== 'string')
-            throw new Error('Cannot use higher-order sections without the original template');
+  Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
+    var self = this;
+    var buffer = '';
+    var value = context.lookup(token[1]);
 
-          // Extract the portion of the original template that the section contains.
-          value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
+    // This function is used to render an arbitrary template
+    // in the current context by higher-order sections.
+    function subRender (template) {
+      return self.render(template, context, partials);
+    }
 
-          if (value != null)
-            buffer += value;
-        } else {
-          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
-        }
+    if (!value) return;
 
-        break;
-      case '^':
-        value = context.lookup(token[1]);
+    if (isArray(value)) {
+      for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
+        buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
+      }
+    } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
+      buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
+    } else if (isFunction(value)) {
+      if (typeof originalTemplate !== 'string')
+        throw new Error('Cannot use higher-order sections without the original template');
 
-        // Use JavaScript's definition of falsy. Include empty arrays.
-        // See https://github.com/janl/mustache.js/issues/186
-        if (!value || (isArray(value) && value.length === 0))
-          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+      // Extract the portion of the original template that the section contains.
+      value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
 
-        break;
-      case '>':
-        if (!partials)
-          continue;
+      if (value != null)
+        buffer += value;
+    } else {
+      buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+    }
+    return buffer;
+  };
 
-        value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+  Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) {
+    var value = context.lookup(token[1]);
 
-        if (value != null)
-          buffer += this.renderTokens(this.parse(value), context, partials, value);
+    // Use JavaScript's definition of falsy. Include empty arrays.
+    // See https://github.com/janl/mustache.js/issues/186
+    if (!value || (isArray(value) && value.length === 0))
+      return this.renderTokens(token[4], context, partials, originalTemplate);
+  };
 
-        break;
-      case '&':
-        value = context.lookup(token[1]);
+  Writer.prototype.renderPartial = function renderPartial (token, context, partials) {
+    if (!partials) return;
 
-        if (value != null)
-          buffer += value;
+    var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+    if (value != null)
+      return this.renderTokens(this.parse(value), context, partials, value);
+  };
 
-        break;
-      case 'name':
-        value = context.lookup(token[1]);
+  Writer.prototype.renderBlock = function renderBlock (token, context, partials, originalTemplate) {
+    if (!partials) return;
+
+    var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+    if (value != null)
+      // Ignore any wrongly set block vars before we started.
+      context.clearBlockVars();
+      // We are only rendering to record the default block variables.
+      this.renderTokens(token[4], context, partials, originalTemplate);
+      // Now we render and return the result.
+      var result = this.renderTokens(this.parse(value), context, partials, value);
+      // Don't leak the block variables outside this include.
+      context.clearBlockVars();
+      return result;
+  };
 
-        if (value != null)
-          buffer += mustache.escape(value);
+  Writer.prototype.renderBlockVariable = function renderBlockVariable (token, context, partials, originalTemplate) {
+    var value = token[1];
 
-        break;
-      case 'text':
-        buffer += token[1];
-        break;
-      }
+    var exists = context.getBlockVar(value);
+    if (!exists) {
+      context.setBlockVar(value, originalTemplate.slice(token[3], token[5]));
+      return this.renderTokens(token[4], context, partials, originalTemplate);
+    } else {
+      return this.renderTokens(this.parse(exists), context, partials, exists);
     }
+  };
 
-    return buffer;
+  Writer.prototype.unescapedValue = function unescapedValue (token, context) {
+    var value = context.lookup(token[1]);
+    if (value != null)
+      return value;
+  };
+
+  Writer.prototype.escapedValue = function escapedValue (token, context) {
+    var value = context.lookup(token[1]);
+    if (value != null)
+      return mustache.escape(value);
   };
 
-  mustache.name = "mustache.js";
-  mustache.version = "1.0.0";
-  mustache.tags = [ "{{", "}}" ];
+  Writer.prototype.rawValue = function rawValue (token) {
+    return token[1];
+  };
+
+  mustache.name = 'mustache.js';
+  mustache.version = '2.1.3';
+  mustache.tags = [ '{{', '}}' ];
 
   // All high-level mustache.* functions use this writer.
   var defaultWriter = new Writer();
   /**
    * Clears all cached templates in the default writer.
    */
-  mustache.clearCache = function () {
+  mustache.clearCache = function clearCache () {
     return defaultWriter.clearCache();
   };
 
    * array of tokens it contains. Doing this ahead of time avoids the need to
    * parse templates on the fly as they are rendered.
    */
-  mustache.parse = function (template, tags) {
+  mustache.parse = function parse (template, tags) {
     return defaultWriter.parse(template, tags);
   };
 
    * Renders the `template` with the given `view` and `partials` using the
    * default writer.
    */
-  mustache.render = function (template, view, partials) {
+  mustache.render = function render (template, view, partials) {
+    if (typeof template !== 'string') {
+      throw new TypeError('Invalid template! Template should be a "string" ' +
+                          'but "' + typeStr(template) + '" was given as the first ' +
+                          'argument for mustache#render(template, view, partials)');
+    }
+
     return defaultWriter.render(template, view, partials);
   };
 
-  // This is here for backwards compatibility with 0.4.x.
-  mustache.to_html = function (template, view, partials, send) {
+  // This is here for backwards compatibility with 0.4.x.,
+  /*eslint-disable */ // eslint wants camel cased function name
+  mustache.to_html = function to_html (template, view, partials, send) {
+    /*eslint-enable*/
+
     var result = mustache.render(template, view, partials);
 
     if (isFunction(send)) {
index d03738d..93ed7d1 100644 (file)
@@ -298,6 +298,7 @@ define([ 'core/mustache',
 
         if (cached) {
             deferred.resolve(cached);
+            templateCache[searchKey] = cached;
             return deferred.promise();
         }
 
index c8af3f8..8e3d7e5 100644 (file)
@@ -472,7 +472,7 @@ class badge {
                     $wheresql = ' WHERE u.id ' . $earnedsql;
                 }
                 list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->get_context(), 'moodle/badges:earnbadge', 0, true);
-                $sql = "SELECT u.id
+                $sql = "SELECT DISTINCT u.id
                         FROM {user} u
                         {$extrajoin}
                         JOIN ({$enrolledsql}) je ON je.id = u.id " . $wheresql . $extrawhere;
index ed309f0..e63aa7e 100644 (file)
@@ -205,7 +205,7 @@ class behat_command {
     }
 
     /**
-     * Has the site installed composer with --dev option
+     * Has the site installed composer.
      * @return bool
      */
     public static function are_behat_dependencies_installed() {
index 45599cc..89406de 100644 (file)
@@ -215,13 +215,6 @@ $definitions = array(
         'simplekeys' => true,
         'simpledata' => true
     ),
-    // Used to cache user grades for conditional availability purposes.
-    'gradecondition' => array(
-        'mode' => cache_store::MODE_APPLICATION,
-        'staticacceleration' => true,
-        'staticaccelerationsize' => 2, // Should not be required for more than one user at a time.
-        'ttl' => 3600,
-    ),
 
     // Used to cache activity completion status.
     'completion' => array(
index 76e23ee..b077fb3 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150814" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20150824" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="feedback" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="importcode" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="similar to backup_code, a unique batch code for identifying one batch of imports"/>
         <FIELD NAME="importer" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="importonlyfeedback" TYPE="int" LENGTH="1" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
     </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index a3e1885..9dcce10 100644 (file)
@@ -4441,7 +4441,22 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015062500.01);
     }
 
-    if ($oldversion < 2015081400.00) {
+    if ($oldversion < 2015081300.01) {
+
+        // Define field importtype to be added to grade_import_values.
+        $table = new xmldb_table('grade_import_values');
+        $field = new xmldb_field('importonlyfeedback', XMLDB_TYPE_INTEGER, '1', null, null, null, '0', 'importer');
+
+        // Conditionally launch add field importtype.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015081300.01);
+    }
+
+    if ($oldversion < 2015082400.00) {
 
         // Define table webdav_locks to be dropped.
         $table = new xmldb_table('webdav_locks');
@@ -4452,7 +4467,7 @@ function xmldb_main_upgrade($oldversion) {
         }
 
         // Main savepoint reached.
-        upgrade_main_savepoint(true, 2015081400.00);
+        upgrade_main_savepoint(true, 2015082400.00);
     }
 
     return true;
index 3ecd8e7..9569b07 100644 (file)
@@ -19,3 +19,20 @@ Feature: Atto subscript button
     And I follow "Edit profile"
     Then I should see "<sub>Submarine</sub>"
 
+  @javascript
+  Scenario: Subscript some text in enclosed in superscript
+    Given I log in as "admin"
+    And I follow "Profile" in the user menu
+    And I follow "Edit profile"
+    And I set the field "Description" to "<sup>Submarine</sup>"
+    And I select the text in the "Description" Atto editor
+    And I click on "Show more buttons" "button"
+    When I click on "Subscript" "button"
+    And I press "Update profile"
+    And I follow "Preferences" in the user menu
+    And I follow "Editor preferences"
+    And I set the field "Text editor" to "Plain text area"
+    And I press "Save changes"
+    And I follow "Edit profile"
+    Then I should see "<sub>Submarine</sub>"
+
index a9d5e22..43a8d5e 100644 (file)
Binary files a/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-debug.js and b/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-debug.js differ
index 9510f93..e92ce23 100644 (file)
Binary files a/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-min.js and b/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-min.js differ
index a9d5e22..43a8d5e 100644 (file)
Binary files a/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button.js and b/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button.js differ
index 337d362..07dc361 100644 (file)
  */
 
 Y.namespace('M.atto_subscript').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
+    /**
+     * A rangy object to alter CSS classes.
+     *
+     * @property _subscriptApplier
+     * @type Object
+     * @private
+     */
+    _subscriptApplier: null,
+
+    /**
+     * A rangy object to alter CSS classes.
+     *
+     * @property _superscriptApplier
+     * @type Object
+     * @private
+     */
+    _superscriptApplier: null,
+
     initializer: function() {
-        this.addBasicButton({
-            exec: 'subscript',
+        this.addButton({
+            buttonName: 'subscript',
+            callback: this.toggleSubscript,
+            icon: 'e/subscript',
+            inlineFormat: true,
 
             // Watch the following tags and add/remove highlighting as appropriate:
             tags: 'sub'
         });
+        this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
+        this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
+    },
+
+    /**
+     * Toggle subscripts in selection
+     *
+     * @method toggleSubscript
+     */
+    toggleSubscript: function() {
+        // Replace all the sub and sup tags.
+        this.get('host').changeToCSS('sup', 'editor-superscript');
+        this.get('host').changeToCSS('sub', 'editor-subscript');
+
+        // Remove all superscripts inselection and toggle subscript.
+        this._superscriptApplier.undoToSelection();
+        this._subscriptApplier.toggleSelection();
+
+        // Replace CSS classes with tags.
+        this.get('host').changeToTags('editor-subscript', 'sub');
+        this.get('host').changeToTags('editor-superscript', 'sup');
     }
 });
index 350b11d..72ce449 100644 (file)
@@ -19,3 +19,20 @@ Feature: Atto superscript button
     And I follow "Edit profile"
     Then I should see "<sup>Helicopter</sup>"
 
+  @javascript
+  Scenario: Superscript some text that is enclosed in subscript
+    Given I log in as "admin"
+    And I follow "Profile" in the user menu
+    And I follow "Edit profile"
+    And I set the field "Description" to "<sub>Helicopter</sub>"
+    And I select the text in the "Description" Atto editor
+    And I click on "Show more buttons" "button"
+    When I click on "Superscript" "button"
+    And I press "Update profile"
+    And I follow "Preferences" in the user menu
+    And I follow "Editor preferences"
+    And I set the field "Text editor" to "Plain text area"
+    And I press "Save changes"
+    And I follow "Edit profile"
+    Then I should see "<sup>Helicopter</sup>"
+
index bcad8e5..0ed8e7c 100644 (file)
Binary files a/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-debug.js and b/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-debug.js differ
index 70ac08d..399aeae 100644 (file)
Binary files a/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-min.js and b/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-min.js differ
index bcad8e5..0ed8e7c 100644 (file)
Binary files a/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button.js and b/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button.js differ
index 80bfeec..fce14a1 100644 (file)
  */
 
 Y.namespace('M.atto_superscript').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
+    /**
+     * A rangy object to alter CSS classes.
+     *
+     * @property _subscriptApplier
+     * @type Object
+     * @private
+     */
+    _subscriptApplier: null,
+
+    /**
+     * A rangy object to alter CSS classes.
+     *
+     * @property _superscriptApplier
+     * @type Object
+     * @private
+     */
+    _superscriptApplier: null,
+
     initializer: function() {
-        this.addBasicButton({
-            exec: 'superscript',
+        this.addButton({
+            buttonName: 'superscript',
+            callback: this.toggleSuperscript,
+            icon: 'e/superscript',
+            inlineFormat: true,
 
             // Watch the following tags and add/remove highlighting as appropriate:
             tags: 'sup'
         });
+        this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
+        this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
+    },
+
+    /**
+     * Toggle superscripts in selection
+     *
+     * @method toggleSuperscript
+     */
+    toggleSuperscript: function() {
+        // Replace all the sub and sup tags.
+        this.get('host').changeToCSS('sub', 'editor-subscript');
+        this.get('host').changeToCSS('sup', 'editor-superscript');
+
+        // Remove all subscripts inselection and toggle superscript.
+        this._superscriptApplier.toggleSelection();
+        this._subscriptApplier.undoToSelection();
+
+        // Replace CSS classes with tags.
+        this.get('host').changeToTags('editor-superscript', 'sup');
+        this.get('host').changeToTags('editor-subscript', 'sub');
     }
 });
index 225ba4a..fa0c076 100644 (file)
@@ -26,6 +26,7 @@ $string['addcolumnafter'] = 'Insert column after';
 $string['addrowafter'] = 'Insert row after';
 $string['both'] = 'Both';
 $string['caption'] = 'Caption';
+$string['captionposition'] = 'Caption position';
 $string['columns'] = 'Columns';
 $string['createtable'] = 'Create table';
 $string['deletecolumn'] = 'Delete column';
index f42dc43..f9c0b5e 100644 (file)
@@ -47,7 +47,12 @@ function atto_table_strings_for_js() {
                                           'moverowdown',
                                           'moverowup',
                                           'deleterow',
-                                          'deletecolumn'),
+                                          'deletecolumn',
+                                          'captionposition'),
                                     'atto_table');
+
+    $PAGE->requires->strings_for_js(array('top',
+                                          'bottom'),
+                                    'editor');
 }
 
index a9ca65c..48f3a7a 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js differ
index 4b84bb2..c7b367e 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js differ
index a9ca65c..48f3a7a 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js differ
index 1f5662b..1c4f4df 100644 (file)
@@ -38,6 +38,13 @@ var COMPONENT = 'atto_table',
             '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
             '<br/>' +
             '<br/>' +
+            '<label for="{{elementid}}_atto_table_captionposition" class="sameline">{{get_string "captionposition" component}}</label>' +
+            '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
+                '<option value=""></option>' +
+                '<option value="top">{{get_string "top" "editor"}}</option>' +
+                '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
+            '</select>' +
+            '<br/>' +
             '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
             '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
@@ -56,6 +63,13 @@ var COMPONENT = 'atto_table',
             '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
             '<br/>' +
             '<br/>' +
+            '<label for="{{elementid}}_atto_table_captionposition" class="sameline">{{get_string "captionposition" component}}</label>' +
+            '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
+                '<option value=""></option>' +
+                '<option value="top">{{get_string "top" "editor"}}</option>' +
+                '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
+            '</select>' +
+            '<br/>' +
             '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
             '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
@@ -77,6 +91,7 @@ var COMPONENT = 'atto_table',
         '</form>',
     CSS = {
         CAPTION: 'caption',
+        CAPTIONPOSITION: 'captionposition',
         HEADERS: 'headers',
         ROWS: 'rows',
         COLUMNS: 'columns',
@@ -85,6 +100,7 @@ var COMPONENT = 'atto_table',
     },
     SELECTORS = {
         CAPTION: '.' + CSS.CAPTION,
+        CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,
         HEADERS: '.' + CSS.HEADERS,
         ROWS: '.' + CSS.ROWS,
         COLUMNS: '.' + CSS.COLUMNS,
@@ -310,6 +326,7 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      */
     _updateTable: function(e) {
         var caption,
+            captionposition,
             headers,
             table,
             captionnode;
@@ -322,16 +339,21 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
 
         // Add/update the caption.
         caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
+        captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
         headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
 
         table = this._lastTarget.ancestor('table');
 
         captionnode = table.one('caption');
         if (!captionnode) {
-            captionnode = Y.Node.create('<caption></caption');
+            captionnode = Y.Node.create('<caption></caption>');
             table.insert(captionnode, 0);
         }
         captionnode.setHTML(caption.get('value'));
+        captionnode.setStyle('caption-side', captionposition.get('value'));
+        if (!captionnode.getAttribute('style')) {
+            captionnode.removeAttribute('style');
+        }
 
         // Add the row headers.
         if (headers.get('value') === 'rows' || headers.get('value') === 'both') {
@@ -403,6 +425,7 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      */
     _setTable: function(e) {
         var caption,
+            captionposition,
             rows,
             cols,
             headers,
@@ -417,6 +440,7 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         }).hide();
 
         caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
+        captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
         rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);
         cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);
         headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
@@ -427,8 +451,12 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
         var nl = "\n";
         tablehtml = '<br/>' + nl + '<table>' + nl;
-        tablehtml += '<caption>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
 
+        var captionstyle = '';
+        if (captionposition.get('value')) {
+            captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"';
+        }
+        tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
         i = 0;
         if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
             i = 1;
@@ -888,6 +916,7 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         // Set the dialogue content, and then show the dialogue.
         var node = this._getEditDialogueContent(),
             captioninput = node.one(SELECTORS.CAPTION),
+            captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),
             headersinput = node.one(SELECTORS.HEADERS),
             table = this._lastTarget.ancestor('table'),
             captionnode = table.one('caption');
@@ -898,6 +927,13 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             captioninput.set('value', '');
         }
 
+        if (captionpositioninput && captionnode && captionnode.getAttribute('style')) {
+            captionpositioninput.set('value', captionnode.getStyle('caption-side'));
+        } else {
+            // Default to none.
+            captionpositioninput.set('value', '');
+        }
+
         var headersvalue = 'columns';
         if (table.one('th[scope="row"]')) {
             headersvalue = 'rows';
index fbf8eef..8ef6171 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 377e5eb..bdcd0dc 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 22e967a..239dbd9 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 3bbe542..e567786 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js differ
index 8f5f3ca..e49012e 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js differ
index a7abfe6..5c75d06 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js differ
index f781a68..3984d2f 100644 (file)
@@ -8,6 +8,7 @@
                 "textarea.js",
                 "autosave.js",
                 "clean.js",
+                "commands.js",
                 "toolbar.js",
                 "toolbar-keyboardnav.js",
                 "selection.js",
diff --git a/lib/editor/atto/yui/src/editor/js/commands.js b/lib/editor/atto/yui/src/editor/js/commands.js
new file mode 100644 (file)
index 0000000..98e25d4
--- /dev/null
@@ -0,0 +1,169 @@
+// 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/>.
+
+/**
+ * @module moodle-editor_atto-editor
+ * @submodule commands
+ */
+
+/**
+ * Selection functions for the Atto editor.
+ *
+ * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
+ *
+ * @namespace M.editor_atto
+ * @class EditorCommand
+ */
+
+function EditorCommand() {}
+
+EditorCommand.ATTRS= {
+};
+
+EditorCommand.prototype = {
+    /**
+     * Applies a callback method to editor if selection is uncollapsed or waits for input to select first.
+     * @method applyFormat
+     * @param e EventTarget Event to be passed to callback if selection is uncollapsed
+     * @param method callback A callback method which changes editor when text is selected.
+     * @param object context Context to be used for callback method
+     * @param array args Array of arguments to pass to callback
+     */
+    applyFormat: function(e, callback, context, args) {
+        function handleInsert(e, callback, context, args, anchorNode, anchorOffset) {
+            // After something is inputed, select it and apply the formating function.
+            Y.soon(Y.bind(function(e, callback, context, args, anchorNode, anchorOffset) {
+                var selection = window.rangy.getSelection();
+
+                // Set the start of the selection to where it was when the method was first called.
+                var range = selection.getRangeAt(0);
+                range.setStart(anchorNode, anchorOffset);
+                selection.setSingleRange(range);
+
+                // Now apply callback to the new text that is selected.
+                callback.apply(context, [e, args]);
+
+                // Collapse selection so cursor is at end of inserted material.
+                selection.collapseToEnd();
+            }, this, e, callback, context, args, anchorNode, anchorOffset));
+        }
+
+        // Set default context for the method.
+        context = context || this;
+
+        // Check whether range is collapsed.
+        var selection = window.rangy.getSelection();
+
+        if (selection.isCollapsed) {
+            // Selection is collapsed so listen for input into editor.
+            var handle = this.editor.once('input', handleInsert, this, callback, context, args,
+                    selection.anchorNode, selection.anchorOffset);
+
+            // Cancel if selection changes before input.
+            this.editor.onceAfter(['click', 'selectstart'], handle.detach, handle);
+
+            return;
+        }
+
+        // The range is not collapsed; so apply callback method immediately.
+        callback.apply(context, [e, args]);
+
+    },
+
+    /**
+     * Replaces all the tags in a node list with new type.
+     * @method replaceTags
+     * @param NodeList nodelist
+     * @param String tag
+     */
+    replaceTags: function(nodelist, tag) {
+        // We mark elements in the node list for iterations.
+        nodelist.setAttribute('data-iterate', true);
+        var node = this.editor.one('[data-iterate="true"]');
+        while (node) {
+            var clone = Y.Node.create('<' + tag + ' />')
+                .setAttrs(node.getAttrs())
+                .removeAttribute('data-iterate');
+            // Copy class and style if not blank.
+            if (node.getAttribute('style')) {
+                clone.setAttribute('style', node.getAttribute('style'));
+            }
+            if (node.getAttribute('class')) {
+                clone.setAttribute('class', node.getAttribute('class'));
+            }
+            // We use childNodes here because we are interested in both type 1 and 3 child nodes.
+            var children = node.getDOMNode().childNodes, child;
+            child = children[0];
+            while (typeof child !== "undefined") {
+                clone.append(child);
+                child = children[0];
+            }
+            node.replace(clone);
+            node = this.editor.one('[data-iterate="true"]');
+        }
+    },
+
+    /**
+     * Change all tags with given type to a span with CSS class attribute.
+     * @method changeToCSS
+     * @param String tag Tag type to be changed to span
+     * @param String markerClass CSS class that corresponds to desired tag
+     */
+    changeToCSS: function(tag, markerClass) {
+        // Save the selection.
+        var selection = window.rangy.saveSelection();
+
+        // Remove display:none from rangy markers so browser doesn't delete them.
+        this.editor.all('.rangySelectionBoundary').setStyle('display', null);
+
+        // Replace tags with CSS classes.
+        this.editor.all(tag).addClass(markerClass);
+        this.replaceTags(this.editor.all('.' + markerClass), 'span');
+
+        // Restore selection and toggle class.
+        window.rangy.restoreSelection(selection);
+    },
+
+    /**
+     * Change spans with CSS classes in editor into elements with given tag.
+     * @method changeToCSS
+     * @param String markerClass CSS class that corresponds to desired tag
+     * @param String tag New tag type to be created
+     */
+    changeToTags: function(markerClass, tag) {
+        // Save the selection.
+        var selection = window.rangy.saveSelection();
+
+        // Remove display:none from rangy markers so browser doesn't delete them.
+        this.editor.all('.rangySelectionBoundary').setStyle('display', null);
+
+        // Replace spans with given tag.
+        this.replaceTags(this.editor.all('span[class="' + markerClass + '"]'), tag);
+        this.editor.all(tag + '[class="' + markerClass + '"]').removeAttribute('class');
+        this.editor.all('.' + markerClass).each(function(n) {
+            n.wrap('<' + tag + '/>');
+            n.removeClass(markerClass);
+        });
+
+        // Remove CSS classes.
+        this.editor.all('[class="' + markerClass + '"]').removeAttribute('class');
+        this.editor.all(tag).removeClass(markerClass);
+
+        // Restore selection.
+        window.rangy.restoreSelection(selection);
+    }
+};
+
+Y.Base.mix(Y.M.editor_atto.Editor, [EditorCommand]);
index cdd6a8d..240efbf 100644 (file)
@@ -540,9 +540,18 @@ EditorPluginButtons.prototype = {
             inheritFrom = {};
         }
 
+
+        // First we wrap the callback in function to handle formating of text inserted into collapsed selection.
+        config.inlineFormat = config.inlineFormat || inheritFrom.inlineFormat;
+        config._inlineCallback = config.callback || inheritFrom.callback;
+        config._callback = config.callback || inheritFrom.callback;
+        if (config.inlineFormat && typeof config._inlineCallback === 'function') {
+            config._callback = function(e, args) {
+                this.get('host').applyFormat(e, config._inlineCallback, this, args);
+            };
+        }
         // We wrap the callback in function to prevent the default action, check whether the editor is
         // active and focus it, and then mark the field as updated.
-        config._callback = config.callback || inheritFrom.callback;
         config.callback = Y.rbind(this._callbackWrapper, this, config._callback, config.callbackArgs);
 
         config._callbackNormalized = true;
index 7c25d19..c990715 100644 (file)
@@ -9,6 +9,7 @@
             "event",
             "event-simulate",
             "event-custom",
+            "node-event-html5",
             "yui-throttle",
             "moodle-core-notification-dialogue",
             "moodle-core-notification-confirm",
index 6375faf..4da13f2 100644 (file)
@@ -1390,6 +1390,29 @@ function &get_mimetypes_array() {
     return core_filetypes::get_types();
 }
 
+/**
+ * Determine a file's MIME type based on the given filename using the function mimeinfo.
+ *
+ * This function retrieves a file's MIME type for a file that will be sent to the user.
+ * This should only be used for file-sending purposes just like in send_stored_file, send_file, and send_temp_file.
+ * Should the file's MIME type cannot be determined by mimeinfo, it will return 'application/octet-stream' as a default
+ * MIME type which should tell the browser "I don't know what type of file this is, so just download it.".
+ *
+ * @param string $filename The file's filename.
+ * @return string The file's MIME type or 'application/octet-stream' if it cannot be determined.
+ */
+function get_mimetype_for_sending($filename = '') {
+    // Guess the file's MIME type using mimeinfo.
+    $mimetype = mimeinfo('type', $filename);
+
+    // Use octet-stream as fallback if MIME type cannot be determined by mimeinfo.
+    if (!$mimetype || $mimetype === 'document/unknown') {
+        $mimetype = 'application/octet-stream';
+    }
+
+    return $mimetype;
+}
+
 /**
  * Obtains information about a filetype based on its extension. Will
  * use a default if no information is present about that particular
@@ -1988,12 +2011,8 @@ function readstring_accel($string, $mimetype, $accelerate) {
 function send_temp_file($path, $filename, $pathisstring=false) {
     global $CFG;
 
-    if (core_useragent::is_firefox()) {
-        // only FF is known to correctly save to disk before opening...
-        $mimetype = mimeinfo('type', $filename);
-    } else {
-        $mimetype = 'application/x-forcedownload';
-    }
+    // Guess the file's MIME type.
+    $mimetype = get_mimetype_for_sending($filename);
 
     // close session - not needed anymore
     \core\session\manager::write_close();
@@ -2076,12 +2095,10 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring
 
     \core\session\manager::write_close(); // Unlock session during file serving.
 
-    // Use given MIME type if specified, otherwise guess it using mimeinfo.
-    // IE, Konqueror and Opera open html file directly in browser from web even when directed to save it to disk :-O
-    // only Firefox saves all files locally before opening when content-disposition: attachment stated
-    $isFF         = core_useragent::is_firefox(); // only FF properly tested
-    $mimetype     = ($forcedownload and !$isFF) ? 'application/x-forcedownload' :
-                         ($mimetype ? $mimetype : mimeinfo('type', $filename));
+    // Use given MIME type if specified, otherwise guess it.
+    if (!$mimetype || $mimetype === 'document/unknown') {
+        $mimetype = get_mimetype_for_sending($filename);
+    }
 
     // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
     if (core_useragent::is_ie()) {
@@ -2254,13 +2271,15 @@ function send_stored_file($stored_file, $lifetime=null, $filter=0, $forcedownloa
 
     \core\session\manager::write_close(); // Unlock session during file serving.
 
-    // Use given MIME type if specified, otherwise guess it using mimeinfo.
-    // IE, Konqueror and Opera open html file directly in browser from web even when directed to save it to disk :-O
-    // only Firefox saves all files locally before opening when content-disposition: attachment stated
     $filename     = is_null($filename) ? $stored_file->get_filename() : $filename;
-    $isFF         = core_useragent::is_firefox(); // only FF properly tested
-    $mimetype     = ($forcedownload and !$isFF) ? 'application/x-forcedownload' :
-                         ($stored_file->get_mimetype() ? $stored_file->get_mimetype() : mimeinfo('type', $filename));
+
+    // Use given MIME type if specified.
+    $mimetype = $stored_file->get_mimetype();
+
+    // Otherwise guess it.
+    if (!$mimetype || $mimetype === 'document/unknown') {
+        $mimetype = get_mimetype_for_sending($filename);
+    }
 
     // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
     if (core_useragent::is_ie()) {
index 72b62b0..a91cab4 100644 (file)
@@ -5617,11 +5617,11 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
             // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
             $attachpath = str_replace('\\', '/', $attachmentpath);
             // Make sure both variables are normalised before comparing.
-            $temppath = str_replace('\\', '/', $CFG->tempdir);
+            $temppath = str_replace('\\', '/', realpath($CFG->tempdir));
 
             // If the attachment is a full path to a file in the tempdir, use it as is,
             // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
-            if (strpos($attachpath, realpath($temppath)) !== 0) {
+            if (strpos($attachpath, $temppath) !== 0) {
                 $attachmentpath = $CFG->dataroot . '/' . $attachmentpath;
             }
 
index 0bdbc04..e0aecc9 100644 (file)
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2010-2014 Justin Hileman
+Copyright (c) 2010-2015 Justin Hileman
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
index b33b355..6a309ae 100644 (file)
@@ -3,9 +3,9 @@ Mustache.php
 
 A [Mustache](http://mustache.github.com/) implementation in PHP.
 
-[![Package version](http://img.shields.io/packagist/v/mustache/mustache.svg)](https://packagist.org/packages/mustache/mustache)
-[![Build status](http://img.shields.io/travis/bobthecow/mustache.php/dev.svg)](http://travis-ci.org/bobthecow/mustache.php)
-[![Monthly downloads](http://img.shields.io/packagist/dm/mustache/mustache.svg)](https://packagist.org/packages/mustache/mustache)
+[![Package version](http://img.shields.io/packagist/v/mustache/mustache.svg?style=flat-square)](https://packagist.org/packages/mustache/mustache)
+[![Build status](http://img.shields.io/travis/bobthecow/mustache.php/dev.svg?style=flat-square)](http://travis-ci.org/bobthecow/mustache.php)
+[![Monthly downloads](http://img.shields.io/packagist/dm/mustache/mustache.svg?style=flat-square)](https://packagist.org/packages/mustache/mustache)
 
 
 Usage
@@ -24,9 +24,9 @@ And a more in-depth example -- this is the canonical Mustache template:
 
 ```html+jinja
 Hello {{name}}
-You have just won ${{value}}!
+You have just won {{value}} dollars!
 {{#in_ca}}
-Well, ${{taxed_value}}, after taxes.
+Well, {{taxed_value}} dollars, after taxes.
 {{/in_ca}}
 ```
 
diff --git a/lib/mustache/composer.json b/lib/mustache/composer.json
deleted file mode 100644 (file)
index 2969d03..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-    "name": "mustache/mustache",
-    "description": "A Mustache implementation in PHP.",
-    "keywords": ["templating", "mustache"],
-    "homepage": "https://github.com/bobthecow/mustache.php",
-    "type": "library",
-    "license": "MIT",
-    "authors": [
-        {
-            "name": "Justin Hileman",
-            "email": "justin@justinhileman.info",
-            "homepage": "http://justinhileman.com"
-        }
-    ],
-    "require": {
-        "php": ">=5.2.4"
-    },
-    "require-dev": {
-        "phpunit/phpunit": "*"
-    },
-    "autoload": {
-        "psr-0": { "Mustache": "src/" }
-    }
-}
index 31ce19d..912fc86 100644 (file)
@@ -1,6 +1,8 @@
 Description of Mustache library import into moodle.
 
-Download from https://github.com/bobthecow/mustache.php
+Clone from https://github.com/moodle/custom-mustache.php
+
+Rebase onto latest tag from https://github.com/bobthecow/mustache.php
 
 Delete folder "test"
 
@@ -9,3 +11,9 @@ Delete phpunit.xml.dist
 Delete hidden files ".*"
 
 Delete folder "bin"
+
+Delete folder "vendor"
+
+Delete composer.json
+
+Copy into this folder, and update this readme to cover any changes.
index 8c8e906..b33bc24 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -24,7 +24,7 @@ class Mustache_Autoloader
     public function __construct($baseDir = null)
     {
         if ($baseDir === null) {
-            $baseDir = dirname(__FILE__).'/..';
+            $baseDir = dirname(__FILE__) . '/..';
         }
 
         // realpath doesn't always work, for example, with stream URIs
index c8fc5d5..5df8a23 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -22,7 +22,7 @@ interface Mustache_Cache
      *
      * @param string $key
      *
-     * @return boolean indicates successfully class load
+     * @return bool indicates successfully class load
      */
     public function load($key);
 
@@ -31,8 +31,6 @@ interface Mustache_Cache
      *
      * @param string $key
      * @param string $value
-     *
-     * @return void
      */
     public function cache($key, $value);
 }
index 98b6451..cf1c041 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -47,9 +47,9 @@ abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
     /**
      * Add a log record if logging is enabled.
      *
-     * @param integer $level   The logging level
-     * @param string  $message The log message
-     * @param array   $context The log context
+     * @param int    $level   The logging level
+     * @param string $message The log message
+     * @param array  $context The log context
      */
     protected function log($level, $message, array $context = array())
     {
index 120ad2d..5b56222 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -41,7 +41,7 @@ class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
      *
      * @param string $key
      *
-     * @return boolean
+     * @return bool
      */
     public function load($key)
     {
@@ -56,12 +56,10 @@ class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
     }
 
     /**
-     * Cache and load the compiled class
+     * Cache and load the compiled class.
      *
      * @param string $key
      * @param string $value
-     *
-     * @return void
      */
     public function cache($key, $value)
     {
@@ -91,7 +89,7 @@ class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
     }
 
     /**
-     * Create cache directory
+     * Create cache directory.
      *
      * @throws Mustache_Exception_RuntimeException If unable to create directory
      *
@@ -119,14 +117,12 @@ class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
     }
 
     /**
-     * Write cache file
+     * Write cache file.
      *
      * @throws Mustache_Exception_RuntimeException If unable to write file
      *
      * @param string $fileName
      * @param string $value
-     *
-     * @return void
      */
     private function writeFile($fileName, $value)
     {
index d3a7e1f..7a4b55b 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -22,7 +22,7 @@ class Mustache_Cache_NoopCache extends Mustache_Cache_AbstractCache
      *
      * @param string $key
      *
-     * @return boolean
+     * @return bool
      */
     public function load($key)
     {
@@ -34,8 +34,6 @@ class Mustache_Cache_NoopCache extends Mustache_Cache_AbstractCache
      *
      * @param string $key
      * @param string $value
-     *
-     * @return void
      */
     public function cache($key, $value)
     {
index 72d3414..7d5c96b 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 class Mustache_Compiler
 {
-
     private $pragmas;
     private $defaultPragmas = array();
     private $sections;
+    private $blocks;
     private $source;
     private $indentNextLine;
     private $customEscape;
@@ -44,6 +44,7 @@ class Mustache_Compiler
     {
         $this->pragmas         = $this->defaultPragmas;
         $this->sections        = array();
+        $this->blocks          = array();
         $this->source          = $source;
         $this->indentNextLine  = true;
         $this->customEscape    = $customEscape;
@@ -196,6 +197,7 @@ class Mustache_Compiler
                 return $buffer;
             }
         %s
+        %s
         }';
 
     const KLASS_NO_LAMBDAS = '<?php
@@ -226,19 +228,19 @@ class Mustache_Compiler
     {
         $code     = $this->walk($tree);
         $sections = implode("\n", $this->sections);
-        $klass    = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
+        $blocks   = implode("\n", $this->blocks);
+        $klass    = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS;
 
         $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
 
-        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
+        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks);
     }
 
     const BLOCK_VAR = '
-        $value = $this->resolveValue($context->findInBlock(%s), $context, $indent);
-        if ($value && !is_array($value) && !is_object($value)) {
-            $buffer .= $value;
-        } else {
-            %s
+        $blockFunction = $context->findInBlock(%s);
+        if (is_callable($blockFunction)) {
+            $buffer .= call_user_func($blockFunction, $context);
+        } else {%s
         }
     ';
 
@@ -259,14 +261,10 @@ class Mustache_Compiler
     {
         $id = var_export($id, true);
 
-        return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $this->walk($nodes, 2));
+        return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $this->walk($nodes, $level));
     }
 
-    const BLOCK_ARG = '
-        // %s block_arg
-        $value = $this->section%s($context, $indent, true);
-        $newContext[%s] = %s$value;
-    ';
+    const BLOCK_ARG = '$newContext[%s] = array($this, \'block%s\');';
 
     /**
      * Generate Mustache Template inheritance block argument PHP source.
@@ -283,10 +281,39 @@ class Mustache_Compiler
      */
     private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
     {
-        $key = $this->section($nodes, $id, array(), $start, $end, $otag, $ctag, $level, true);
-        $id  = var_export($id, true);
+        $key = $this->block($nodes);
+        $keystr = var_export($key, true);
+        $id = var_export($id, true);
+
+        return sprintf($this->prepare(self::BLOCK_ARG, 1), $id, $key);
+    }
+
+    const BLOCK_FUNCTION = '
+        public function block%s($context)
+        {
+            $indent = $buffer = \'\';%s
+
+            return $buffer;
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance block function PHP source.
+     *
+     * @param array $nodes Array of child tokens
+     *
+     * @return string key of new block function
+     */
+    private function block($nodes)
+    {
+        $code = $this->walk($nodes, 0);
+        $key = ucfirst(md5($code));
 
-        return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key, $id, $this->flushIndent());
+        if (!isset($this->blocks[$key])) {
+            $this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code);
+        }
+
+        return $key;
     }
 
     const SECTION_CALL = '
@@ -319,7 +346,8 @@ class Mustache_Compiler
             }
 
             return $buffer;
-        }';
+        }
+    ';
 
     /**
      * Generate Mustache Template section PHP source.
@@ -342,12 +370,12 @@ class Mustache_Compiler
         $callable = $this->getCallable();
 
         if ($otag !== '{{' || $ctag !== '}}') {
-            $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
+            $delims = ', ' . var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
         } else {
             $delims = '';
         }
 
-        $key = ucfirst(md5($delims."\n".$source));
+        $key = ucfirst(md5($delims . "\n" . $source));
 
         if (!isset($this->sections[$key])) {
             $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2));
@@ -369,7 +397,8 @@ class Mustache_Compiler
         $value = $context->%s(%s);%s
         if (empty($value)) {
             %s
-        }';
+        }
+    ';
 
     /**
      * Generate Mustache Template inverted section PHP source.
@@ -458,7 +487,7 @@ class Mustache_Compiler
      *
      * @param array $node
      *
-     * @return boolean True if $node is a block arg token.
+     * @return bool True if $node is a block arg token.
      */
     private static function onlyBlockArgs(array $node)
     {
@@ -475,7 +504,7 @@ class Mustache_Compiler
      *
      * @param string   $id      Variable name
      * @param string[] $filters Array of filters
-     * @param boolean  $escape  Escape the variable value for output?
+     * @param bool     $escape  Escape the variable value for output?
      * @param int      $level
      *
      * @return string Generated variable interpolation PHP source
@@ -544,16 +573,16 @@ class Mustache_Compiler
     /**
      * Prepare PHP source code snippet for output.
      *
-     * @param string  $text
-     * @param int     $bonus          Additional indent level (default: 0)
-     * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
-     * @param boolean $appendNewline  Append a newline to the snippet? (default: false)
+     * @param string $text
+     * @param int    $bonus          Additional indent level (default: 0)
+     * @param bool   $prependNewline Prepend a newline to the snippet? (default: true)
+     * @param bool   $appendNewline  Append a newline to the snippet? (default: false)
      *
      * @return string PHP source code snippet
      */
     private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
     {
-        $text = ($prependNewline ? "\n" : '').trim($text);
+        $text = ($prependNewline ? "\n" : '') . trim($text);
         if ($prependNewline) {
             $bonus++;
         }
@@ -561,7 +590,7 @@ class Mustache_Compiler
             $text .= "\n";
         }
 
-        return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
+        return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text);
     }
 
     const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
@@ -602,6 +631,12 @@ class Mustache_Compiler
             return 'last';
         }
 
+        if (isset($this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) {
+            if (substr($id, 0, 1) === '.') {
+                return 'findAnchoredDot';
+            }
+        }
+
         if (strpos($id, '.') === false) {
             return 'find';
         }
index db03acc..e660b54 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -141,6 +141,42 @@ class Mustache_Context
         return $value;
     }
 
+    /**
+     * Find an 'anchored dot notation' variable in the Context stack.
+     *
+     * This is the same as findDot(), except it looks in the top of the context
+     * stack for the first value, rather than searching the whole context stack
+     * and starting from there.
+     *
+     * @see Mustache_Context::findDot
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if given an invalid anchored dot $id.
+     *
+     * @param string $id Dotted variable selector
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function findAnchoredDot($id)
+    {
+        $chunks = explode('.', $id);
+        $first  = array_shift($chunks);
+        if ($first !== '') {
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id));
+        }
+
+        $value  = $this->last();
+
+        foreach ($chunks as $chunk) {
+            if ($value === '') {
+                return $value;
+            }
+
+            $value = $this->findVariableInStack($chunk, array($value));
+        }
+
+        return $value;
+    }
+
     /**
      * Find an argument in the block context stack.
      *
index 4cac394..08bcc81 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 class Mustache_Engine
 {
-    const VERSION        = '2.7.0';
+    const VERSION        = '2.9.0';
     const SPEC_VERSION   = '1.1.2';
 
-    const PRAGMA_FILTERS = 'FILTERS';
-    const PRAGMA_BLOCKS  = 'BLOCKS';
+    const PRAGMA_FILTERS      = 'FILTERS';
+    const PRAGMA_BLOCKS       = 'BLOCKS';
+    const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT';
 
     // Known pragmas
     private static $knownPragmas = array(
-        self::PRAGMA_FILTERS => true,
-        self::PRAGMA_BLOCKS  => true,
+        self::PRAGMA_FILTERS      => true,
+        self::PRAGMA_BLOCKS       => true,
+        self::PRAGMA_ANCHORED_DOT => true,
     );
 
     // Template cache
@@ -174,7 +176,7 @@ class Mustache_Engine
         }
 
         if (isset($options['entity_flags'])) {
-          $this->entityFlags = $options['entity_flags'];
+            $this->entityFlags = $options['entity_flags'];
         }
 
         if (isset($options['charset'])) {
@@ -405,7 +407,7 @@ class Mustache_Engine
      *
      * @param string $name
      *
-     * @return boolean True if the helper is present
+     * @return bool True if the helper is present
      */
     public function hasHelper($name)
     {
@@ -772,9 +774,9 @@ class Mustache_Engine
     /**
      * Add a log record if logging is enabled.
      *
-     * @param integer $level   The logging level
-     * @param string  $message The log message
-     * @param array   $context The log context
+     * @param int    $level   The logging level
+     * @param string $message The log message
+     * @param array  $context The log context
      */
     private function log($level, $message, array $context = array())
     {
index 8a2b01c..7c04792 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index 9bd1107..c6084b8 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index 255ce54..4bfda5f 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index a3c48f7..5eb90dd 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index 7a16f7e..ab4d6aa 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index b9a315a..43d2c06 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index 226d774..7fe4a71 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index 5dafe89..f1f69bf 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index c7c7950..0a92428 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -103,7 +103,7 @@ class Mustache_HelperCollection
      *
      * @param string $name
      *
-     * @return boolean True if helper is present
+     * @return bool True if helper is present
      */
     public function __isset($name)
     {
@@ -115,7 +115,7 @@ class Mustache_HelperCollection
      *
      * @param string $name
      *
-     * @return boolean True if helper is present
+     * @return bool True if helper is present
      */
     public function has($name)
     {
@@ -163,7 +163,7 @@ class Mustache_HelperCollection
     /**
      * Check whether the helper collection is empty.
      *
-     * @return boolean True if the collection is empty
+     * @return bool True if the collection is empty
      */
     public function isEmpty()
     {
index 7cd8092..de48fd6 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index e75ee3f..1b075d0 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index e7ece91..90cecca 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index d02a273..09a376c 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -18,7 +18,7 @@ class Mustache_Loader_CascadingLoader implements Mustache_Loader
     private $loaders;
 
     /**
-     * Construct a CascadingLoader with an array of loaders:
+     * Construct a CascadingLoader with an array of loaders.
      *
      *     $loader = new Mustache_Loader_CascadingLoader(array(
      *         new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
index 7cbf9cd..e2aab78 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index 9e4ab42..2970937 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -46,7 +46,6 @@
  *
  *     @@ hello
  *     Hello, {{ name }}!
- *
  */
 class Mustache_Loader_InlineLoader implements Mustache_Loader
 {
@@ -56,6 +55,7 @@ class Mustache_Loader_InlineLoader implements Mustache_Loader
 
     /**
      * The InlineLoader requires a filename and offset to process templates.
+     *
      * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
      * perfectly suited to the job:
      *
index 78901be..4855c16 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -18,8 +18,6 @@ interface Mustache_Loader_MutableLoader
      * Set an associative array of Template sources for this loader.
      *
      * @param array $templates
-     *
-     * @return void
      */
     public function setTemplates(array $templates);
 
@@ -28,8 +26,6 @@ interface Mustache_Loader_MutableLoader
      *
      * @param string $name
      * @param string $template Mustache Template source
-     *
-     * @return void
      */
     public function setTemplate($name, $template);
 }
index 72d105d..99fb38e 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index 2e5d674..93c27f6 100644 (file)
@@ -3,14 +3,14 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  */
 
 /**
- * Describes a Mustache logger instance
+ * Describes a Mustache logger instance.
  *
  * This is identical to the Psr\Log\LoggerInterface.
  *
@@ -29,7 +29,7 @@
 interface Mustache_Logger
 {
     /**
-     * Psr\Log compatible log levels
+     * Psr\Log compatible log levels.
      */
     const EMERGENCY = 'emergency';
     const ALERT     = 'alert';
@@ -45,8 +45,6 @@ interface Mustache_Logger
      *
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function emergency($message, array $context = array());
 
@@ -58,8 +56,6 @@ interface Mustache_Logger
      *
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function alert($message, array $context = array());
 
@@ -70,8 +66,6 @@ interface Mustache_Logger
      *
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function critical($message, array $context = array());
 
@@ -81,8 +75,6 @@ interface Mustache_Logger
      *
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function error($message, array $context = array());
 
@@ -94,8 +86,6 @@ interface Mustache_Logger
      *
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function warning($message, array $context = array());
 
@@ -104,8 +94,6 @@ interface Mustache_Logger
      *
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function notice($message, array $context = array());
 
@@ -116,8 +104,6 @@ interface Mustache_Logger
      *
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function info($message, array $context = array());
 
@@ -126,8 +112,6 @@ interface Mustache_Logger
      *
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function debug($message, array $context = array());
 
@@ -137,8 +121,6 @@ interface Mustache_Logger
      * @param mixed  $level
      * @param string $message
      * @param array  $context
-     *
-     * @return null
      */
     public function log($level, $message, array $context = array());
 }
index 3dd96e7..1c27db9 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
index d422340..88f3000 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -39,7 +39,7 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
      * @throws InvalidArgumentException if the logging level is unknown.
      *
      * @param resource|string $stream Resource instance or URL
-     * @param integer         $level  The minimum logging level at which this handler will be triggered
+     * @param int             $level  The minimum logging level at which this handler will be triggered
      */
     public function __construct($stream, $level = Mustache_Logger::ERROR)
     {
@@ -67,7 +67,7 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
      *
      * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown.
      *
-     * @param integer $level The minimum logging level which will be written
+     * @param int $level The minimum logging level which will be written
      */
     public function setLevel($level)
     {
@@ -81,7 +81,7 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
     /**
      * Get the current minimum logging level.
      *
-     * @return integer
+     * @return int
      */
     public function getLevel()
     {
@@ -114,9 +114,9 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
      * @throws Mustache_Exception_LogicException   If neither a stream resource nor url is present.
      * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened.
      *
-     * @param integer $level   The logging level
-     * @param string  $message The log message
-     * @param array   $context The log context
+     * @param int    $level   The logging level
+     * @param string $message The log message
+     * @param array  $context The log context
      */
     protected function writeLog($level, $message, array $context = array())
     {
@@ -141,7 +141,7 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
      *
      * @throws InvalidArgumentException if the logging level is unknown.
      *
-     * @param integer $level
+     * @param int $level
      *
      * @return string
      */
@@ -153,9 +153,9 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
     /**
      * Format a log line for output.
      *
-     * @param integer $level   The logging level
-     * @param string  $message The log message
-     * @param array   $context The log context
+     * @param int    $level   The logging level
+     * @param string $message The log message
+     * @param array  $context The log context
      *
      * @return string
      */
index 0c134ec..7d3559b 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -254,7 +254,7 @@ class Mustache_Parser
      *
      * @param array $token
      *
-     * @return boolean True if token is a whitespace token
+     * @return bool True if token is a whitespace token
      */
     private function tokenIsWhitespace(array $token)
     {
index e7156cf..91074c4 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -22,7 +22,7 @@ abstract class Mustache_Template
     protected $mustache;
 
     /**
-     * @var boolean
+     * @var bool
      */
     protected $strictCallables = false;
 
@@ -37,7 +37,7 @@ abstract class Mustache_Template
     }
 
     /**
-     * Mustache Template instances can be treated as a function and rendered by simply calling them:
+     * Mustache Template instances can be treated as a function and rendered by simply calling them.
      *
      *     $m = new Mustache_Engine;
      *     $tpl = $m->loadTemplate('Hello, {{ name }}!');
@@ -109,7 +109,7 @@ abstract class Mustache_Template
      *
      * @param mixed $value
      *
-     * @return boolean True if the value is 'iterable'
+     * @return bool True if the value is 'iterable'
      */
     protected function isIterable($value)
     {
index 3175a03..a71df2c 100644 (file)
@@ -3,7 +3,7 @@
 /*
  * This file is part of Mustache.php.
  *
- * (c) 2010-2014 Justin Hileman
+ * (c) 2010-2015 Justin Hileman
  *
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
@@ -53,13 +53,6 @@ class Mustache_Tokenizer
         self::T_BLOCK_VAR    => true,
     );
 
-    // Interpolated tags
-    private static $interpolatedTags = array(
-        self::T_ESCAPED     => true,
-        self::T_UNESCAPED   => true,
-        self::T_UNESCAPED_2 => true,
-    );
-
     // Token properties
     const TYPE    = 'type';
     const NAME    = 'name';
@@ -75,7 +68,6 @@ class Mustache_Tokenizer
 
     private $state;
     private $tagType;
-    private $tag;
     private $buffer;
     private $tokens;
     private $seenTag;
@@ -163,7 +155,7 @@ class Mustache_Tokenizer
                             self::OTAG  => $this->otag,
                             self::CTAG  => $this->ctag,
                             self::LINE  => $this->line,
-                            self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen
+                            self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen,
                         );
 
                         if ($this->tagType === self::T_UNESCAPED) {
@@ -224,7 +216,6 @@ class Mustache_Tokenizer
     {
         $this->state   = self::IN_TEXT;
         $this->tagType = null;
-        $this->tag     = null;
         $this->buffer  = '';
         $this->tokens  = array();
         $this->seenTag = false;
@@ -244,7 +235,7 @@ class Mustache_Tokenizer
             $this->tokens[] = array(
                 self::TYPE  => self::T_TEXT,
                 self::LINE  => $this->line,
-                self::VALUE => $this->buffer
+                self::VALUE => $this->buffer,
             );
             $this->buffer   = '';
         }
@@ -261,7 +252,7 @@ class Mustache_Tokenizer
     private function changeDelimiters($text, $index)
     {
         $startIndex = strpos($text, '=', $index) + 1;
-        $close      = '='.$this->ctag;
+        $close      = '=' . $this->ctag;
         $closeIndex = strpos($text, $close, $index);
 
         $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
@@ -322,7 +313,7 @@ class Mustache_Tokenizer
      * @param string $text   Mustache template source
      * @param int    $index  Current tokenizer index
      *
-     * @return boolean True if this is a closing section tag
+     * @return bool True if this is a closing section tag
      */
     private function tagChange($tag, $tagLen, $text, $index)
     {
index a00b5ec..e013d6d 100644 (file)
@@ -192,13 +192,13 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
 
     if (isset($identityfields['department']) && $user->department) {
         $node = new core_user\output\myprofile\node('contact', 'department', get_string('department'), null, null,
-            $user->institution);
+            $user->department);
         $tree->add_node($node);
     }
 
     if (isset($identityfields['idnumber']) && $user->idnumber) {
         $node = new core_user\output\myprofile\node('contact', 'idnumber', get_string('idnumber'), null, null,
-            $user->institution);
+            $user->idnumber);
         $tree->add_node($node);
     }
 
index 35e2f34..87e5c73 100644 (file)
@@ -1660,11 +1660,11 @@ class theme_config {
         global $CFG;
         if ($this->usesvg === null) {
 
-            if (!isset($CFG->svgicons) || !is_bool($CFG->svgicons)) {
+            if (!isset($CFG->svgicons)) {
                 $this->usesvg = core_useragent::supports_svg();
             } else {
                 // Force them on/off depending upon the setting.
-                $this->usesvg = $CFG->svgicons;
+                $this->usesvg = (bool)$CFG->svgicons;
             }
         }
         return $this->usesvg;
index 70307a4..d08fb76 100644 (file)
@@ -106,7 +106,8 @@ class renderer_base {
                 'cache' => $cachedir,
                 'escape' => 's',
                 'loader' => $loader,
-                'helpers' => $helpers));
+                'helpers' => $helpers,
+                'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS]));
 
         }
 
index 9a6df4f..e0f0962 100644 (file)
@@ -148,6 +148,11 @@ class page_requirements_manager {
      */
     protected $YUI_config;
 
+    /**
+     * @var array $yuicssmodules
+     */
+    protected $yuicssmodules = array();
+
     /**
      * @var array Some config vars exposed in JS, please no secret stuff there
      */
@@ -374,6 +379,9 @@ class page_requirements_manager {
                                   'moodle');
             $page->requires->yui_module('moodle-core-blocks', 'M.core_blocks.init_dragdrop', array($params), null, true);
         }
+
+        // Include the YUI CSS Modules.
+        $page->requires->set_yuicssmodules($page->theme->yuicssmodules);
     }
 
     /**
@@ -1047,6 +1055,15 @@ class page_requirements_manager {
         $this->jsinitcode[] = $jscode;
     }
 
+    /**
+     * Set the CSS Modules to be included from YUI.
+     *
+     * @param array $modules The list of YUI CSS Modules to include.
+     */
+    public function set_yuicssmodules(array $modules = array()) {
+        $this->yuicssmodules = $modules;
+    }
+
     /**
      * Ensure that the specified JavaScript function is called from an inline script
      * from page footer.
@@ -1320,19 +1337,53 @@ class page_requirements_manager {
     }
 
     /**
-     * Returns basic YUI3 JS loading code.
-     * YUI3 is using autoloading of both CSS and JS code.
-     *
-     * Major benefit of this compared to standard js/csss loader is much improved
-     * caching, better browser cache utilisation, much fewer http requests.
+     * Returns basic YUI3 CSS code.
      *
-     * @param moodle_page $page
      * @return string
      */
-    protected function get_yui3lib_headcode($page) {
+    protected function get_yui3lib_headcss() {
         global $CFG;
 
+        $yuiformat = '-min';
+        if ($this->yui3loader->filter === 'RAW') {
+            $yuiformat = '';
+        }
+
         $code = '';
+        if ($this->yui3loader->combine) {
+            if (!empty($this->yuicssmodules)) {
+                $modules = array();
+                foreach ($this->yuicssmodules as $module) {
+                    $modules[] = "$CFG->yui3version/$module/$module-min.css";
+                }
+                $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->comboBase.implode('&amp;', $modules).'" />';
+            }
+            $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->local_comboBase.'rollup/'.$CFG->yui3version.'/yui-moodlesimple' . $yuiformat . '.css" />';
+
+        } else {
+            if (!empty($this->yuicssmodules)) {
+                foreach ($this->yuicssmodules as $module) {
+                    $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->base.$module.'/'.$module.'-min.css" />';
+                }
+            }
+            $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->local_comboBase.'rollup/'.$CFG->yui3version.'/yui-moodlesimple' . $yuiformat . '.css" />';
+        }
+
+        if ($this->yui3loader->filter === 'RAW') {
+            $code = str_replace('-min.css', '.css', $code);
+        } else if ($this->yui3loader->filter === 'DEBUG') {
+            $code = str_replace('-min.css', '.css', $code);
+        }
+        return $code;
+    }
+
+    /**
+     * Returns basic YUI3 JS loading code.
+     *
+     * @return string
+     */
+    protected function get_yui3lib_headcode() {
+        global $CFG;
 
         $jsrev = $this->get_jsrev();
 
@@ -1357,35 +1408,18 @@ class page_requirements_manager {
         );
 
         if ($this->yui3loader->combine) {
-            if (!empty($page->theme->yuicssmodules)) {
-                $modules = array();
-                foreach ($page->theme->yuicssmodules as $module) {
-                    $modules[] = "$CFG->yui3version/$module/$module-min.css";
-                }
-                $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->comboBase.implode('&amp;', $modules).'" />';
-            }
-            $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->local_comboBase.'rollup/'.$CFG->yui3version.'/yui-moodlesimple' . $yuiformat . '.css" />';
-            $code .= '<script type="text/javascript" src="'.$this->yui3loader->local_comboBase
-                    . implode('&amp;', $baserollups) . '"></script>';
-
+            return '<script type="text/javascript" src="' .
+                    $this->yui3loader->local_comboBase .
+                    implode('&amp;', $baserollups) .
+                    '"></script>';
         } else {
-            if (!empty($page->theme->yuicssmodules)) {
-                foreach ($page->theme->yuicssmodules as $module) {
-                    $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->base.$module.'/'.$module.'-min.css" />';
-                }
-            }
-            $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->local_comboBase.'rollup/'.$CFG->yui3version.'/yui-moodlesimple' . $yuiformat . '.css" />';
+            $code = '';
             foreach ($baserollups as $rollup) {
                 $code .= '<script type="text/javascript" src="'.$this->yui3loader->local_comboBase.$rollup.'"></script>';
             }
+            return $code;
         }
 
-        if ($this->yui3loader->filter === 'RAW') {
-            $code = str_replace('-min.css', '.css', $code);
-        } else if ($this->yui3loader->filter === 'DEBUG') {
-            $code = str_replace('-min.css', '.css', $code);
-        }
-        return $code;
     }
 
     /**
@@ -1400,12 +1434,15 @@ class page_requirements_manager {
         // It is suitable only for things like mod/data which accepts CSS from teachers.
         $attributes = array('rel'=>'stylesheet', 'type'=>'text/css');
 
+        // Add the YUI code first. We want this to be overridden by any Moodle CSS.
+        $code = $this->get_yui3lib_headcss();
+
         // This line of code may look funny but it is currently required in order
         // to avoid MASSIVE display issues in Internet Explorer.
         // As of IE8 + YUI3.1.1 the reference stylesheet (firstthemesheet) gets
         // ignored whenever another resource is added until such time as a redraw
         // is forced, usually by moving the mouse over the affected element.
-        $code = html_writer::tag('script', '/** Required in order to fix style inclusion problems in IE with YUI **/', array('id'=>'firstthemesheet', 'type'=>'text/css'));
+        $code .= html_writer::tag('script', '/** Required in order to fix style inclusion problems in IE with YUI **/', array('id'=>'firstthemesheet', 'type'=>'text/css'));
 
         $urls = $this->cssthemeurls + $this->cssurls;
         foreach ($urls as $url) {
@@ -1449,6 +1486,9 @@ class page_requirements_manager {
 
         $output = '';
 
+        // Add all standard CSS for this page.
+        $output .= $this->get_css_code();
+
         // Set up the M namespace.
         $js = "var M = {}; M.yui = {};\n";
 
@@ -1470,19 +1510,6 @@ class page_requirements_manager {
 
         $output .= html_writer::script($js);
 
-        // YUI3 JS and CSS need to be loaded in the header but after the YUI_config has been created.
-        // They should be cached well by the browser.
-        $output .= $this->get_yui3lib_headcode($page);
-
-        // Add hacked jQuery support, it is not intended for standard Moodle distribution!
-        $output .= $this->get_jquery_headcode();
-
-        // Now theme CSS + custom CSS in this specific order.
-        $output .= $this->get_css_code();
-
-        // Link our main JS file, all core stuff should be there.
-        $output .= html_writer::script('', $this->js_fix_url('/lib/javascript-static.js'));
-
         // Add variables.
         if ($this->jsinitvariables['head']) {
             $js = '';
@@ -1493,13 +1520,6 @@ class page_requirements_manager {
             $output .= html_writer::script($js);
         }
 
-        // All the other linked things from HEAD - there should be as few as possible.
-        if ($this->jsincludes['head']) {
-            foreach ($this->jsincludes['head'] as $url) {
-                $output .= html_writer::script('', $url);
-            }
-        }
-
         // Mark head sending done, it is not possible to anything there.
         $this->headdone = true;
 
@@ -1524,6 +1544,22 @@ class page_requirements_manager {
         }
         $output = html_writer::tag('div', $links, array('class'=>'skiplinks')) . "\n";
 
+        // YUI3 JS needs to be loaded early in the body. It should be cached well by the browser.
+        $output .= $this->get_yui3lib_headcode();
+
+        // Add hacked jQuery support, it is not intended for standard Moodle distribution!
+        $output .= $this->get_jquery_headcode();
+
+        // Link our main JS file, all core stuff should be there.
+        $output .= html_writer::script('', $this->js_fix_url('/lib/javascript-static.js'));
+
+        // All the other linked things from HEAD - there should be as few as possible.
+        if ($this->jsincludes['head']) {
+            foreach ($this->jsincludes['head'] as $url) {
+                $output .= html_writer::script('', $url);
+            }
+        }
+
         // Then the clever trick for hiding of things not needed when JS works.
         $output .= html_writer::script("document.body.className += ' jsenabled';") . "\n";
         $this->topofbodydone = true;
index 9e02f21..a964099 100644 (file)
@@ -50,7 +50,7 @@ function phpunit_bootstrap_error($errorcode, $text = '') {
             $text = 'Error: '.$text;
             break;
         case PHPUNIT_EXITCODE_PHPUNITMISSING:
-            $text = "Can not find PHPUnit library, to install use: php composer.phar install --dev";
+            $text = "Can not find PHPUnit library, to install use: php composer.phar install";
             break;
         case PHPUNIT_EXITCODE_PHPUNITWRONG:
             $text = 'Moodle requires PHPUnit 3.6.x, '.$text.' is not compatible';
diff --git a/lib/phpunit/classes/constraint_object_is_equal_with_exceptions.php b/lib/phpunit/classes/constraint_object_is_equal_with_exceptions.php
new file mode 100644 (file)
index 0000000..fc5ac7a
--- /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/>.
+
+/**
+ * Constraint that checks a simple object with an isEqual constrain, allowing for exceptions to be made for some fields.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2015 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * Constraint that checks a simple object with an isEqual constrain, allowing for exceptions to be made for some fields.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2015 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class phpunit_constraint_object_is_equal_with_exceptions extends PHPUnit_Framework_Constraint_IsEqual {
+
+    /**
+     * @var array $keys The list of exceptions.
+     */
+    protected $keys = array();
+
+    /**
+     * Add an exception for the named key to use a different comparison
+     * method. Any assertion provided by PHPUnit_Framework_Assert is
+     * acceptable.
+     *
+     * @param string $key The key to except.
+     * @param string $comparator The assertion to use.
+     */
+    public function add_exception($key, $comparator) {
+        $this->keys[$key] = $comparator;
+    }
+
+    /**
+     * Evaluates the constraint for parameter $other
+     *
+     * If $shouldreturnesult is set to false (the default), an exception is thrown
+     * in case of a failure. null is returned otherwise.
+     *
+     * If $shouldreturnesult is true, the result of the evaluation is returned as
+     * a boolean value instead: true in case of success, false in case of a
+     * failure.
+     *
+     * @param  mixed    $other              Value or object to evaluate.
+     * @param  string   $description        Additional information about the test
+     * @param  bool     $shouldreturnesult  Whether to return a result or throw an exception
+     * @return mixed
+     * @throws PHPUnit_Framework_ExpectationFailedException
+     */
+    public function evaluate($other, $description = '', $shouldreturnesult = false) {
+        foreach ($this->keys as $key => $comparison) {
+            if (isset($other->$key) || isset($this->value->$key)) {
+                // One of the keys is present, therefore run the comparison.
+                PHPUnit_Framework_Assert::$comparison($this->value->$key, $other->$key);
+
+                // Unset the keys, otherwise the standard evaluation will take place.
+                unset($other->$key);
+                unset($this->value->$key);
+            }
+        }
+
+        // Run the parent evaluation (isEqual).
+        return parent::evaluate($other, $description, $shouldreturnesult);
+    }
+
+}
index 7e89055..114a55b 100644 (file)
@@ -216,6 +216,11 @@ class phpunit_util extends testing_util {
         filter_manager::reset_caches();
         core_filetypes::reset_caches();
 
+        // Reset static unit test options.
+        if (class_exists('\availability_date\condition', false)) {
+            \availability_date\condition::set_current_time_for_test(0);
+        }
+
         // Reset internal users.
         core_user::reset_internal_users();
 
index daccb17..f271690 100644 (file)
@@ -37,5 +37,6 @@ require_once(__DIR__.'/classes/arraydataset.php');
 require_once(__DIR__.'/classes/advanced_testcase.php');
 require_once(__DIR__.'/classes/unittestcase.php');
 require_once(__DIR__.'/classes/hint_resultprinter.php'); // Loaded here because phpunit.xml does not support relative links for printerFile.
+require_once(__DIR__.'/classes/constraint_object_is_equal_with_exceptions.php');
 require_once(__DIR__.'/../testing/classes/test_lock.php');
 require_once(__DIR__.'/../testing/classes/tests_finder.php');
index 69bcdf0..b15b96e 100644 (file)
@@ -15,7 +15,7 @@ Composer is a dependency manager for PHP projects.
 It installs PHP libraries into /vendor/ subdirectory inside your moodle dirroot.
 
 1. install Composer - [http://getcomposer.org/doc/00-intro.md](http://getcomposer.org/doc/00-intro.md)
-2. install PHUnit and dependencies - go to your Moodle dirroot and execute `php composer.phar install --dev`
+2. install PHUnit and dependencies - go to your Moodle dirroot and execute `php composer.phar install`
 
 
 Configure your server
@@ -53,4 +53,4 @@ How to add more tests?
 
 Windows support
 ---------------
-* use `\` instead of `/` in paths in examples above
\ No newline at end of file
+* use `\` instead of `/` in paths in examples above
index 7bfd3c0..999a500 100644 (file)
@@ -172,8 +172,6 @@ function testing_error($errorcode, $text = '') {
 /**
  * Updates the composer installer and the dependencies.
  *
- * Includes --dev dependencies.
- *
  * @return void exit() if something goes wrong
  */
 function testing_update_composer_dependencies() {
index 31fd15c..7781fb6 100644 (file)
@@ -244,12 +244,11 @@ class behat_general extends behat_base {
      * @param int $seconds
      */
     public function i_wait_seconds($seconds) {
-
-        if (!$this->running_javascript()) {
-            throw new DriverException('Waits are disabled in scenarios without Javascript support');
+        if ($this->running_javascript()) {
+            $this->getSession()->wait($seconds * 1000, false);
+        } else {
+            sleep($seconds);
         }
-
-        $this->getSession()->wait($seconds * 1000, false);
     }
 
     /**
index 9ae6964..3b57e54 100644 (file)
@@ -141,27 +141,24 @@ class behat_navigation extends behat_base {
      * Click on an entry in the user menu.
      * @Given /^I follow "(?P<nodetext_string>(?:[^"]|\\")*)" in the user menu$/
      *
-     * @throws ExpectationException
      * @param string $nodetext
      * @return bool|void
      */
     public function i_follow_in_the_user_menu($nodetext) {
+        $steps = array();
 
-        // The user menu is broken without javascript.
-        if (!$this->running_javascript()) {
-            throw new DriverException('I follow in the user menu step is not available with Javascript disabled');
+        if ($this->running_javascript()) {
+            // The user menu must be expanded when JS is enabled.
+            $xpath = "//div[@class='usermenu']//a[contains(concat(' ', @class, ' '), ' toggle-display ')]";
+            $steps[] = new When('I click on "'.$xpath.'" "xpath_element"');
         }
 
-        $steps = array();
-
-        $xpath = "//div[@class='usermenu']//a[contains(concat(' ', @class, ' '), ' toggle-display ')]";
+        // Now select the link.
+        // The CSS path is always present, with or without JS.
         $csspath = ".usermenu [data-rel='menu-content']";
-
-        $steps[] = new When('I click on "'.$xpath.'" "xpath_element"');
         $steps[] = new When('I click on "'.$nodetext.'" "link" in the "'.$csspath.'" "css_element"');
 
         return $steps;
-
     }
 
     /**
index 0484b7c..f126735 100644 (file)
@@ -195,9 +195,11 @@ class core_completionlib_testcase extends advanced_testcase {
         $changed = clone($current);
         $changed->timemodified = time();
         $changed->completionstate = COMPLETION_INCOMPLETE;
+        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
+        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
         $c->expects($this->at(2))
             ->method('internal_set_data')
-            ->with($cm, $changed);
+            ->with($cm, $comparewith);
         $c->update_state($cm, COMPLETION_INCOMPLETE);
 
         // Auto, change state.
@@ -217,9 +219,11 @@ class core_completionlib_testcase extends advanced_testcase {
         $changed = clone($current);
         $changed->timemodified = time();
         $changed->completionstate = COMPLETION_COMPLETE_PASS;
+        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
+        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
         $c->expects($this->at(3))
             ->method('internal_set_data')
-            ->with($cm, $changed);
+            ->with($cm, $comparewith);
         $c->update_state($cm, COMPLETION_COMPLETE_PASS);
     }
 
index b32c328..555a45c 100644 (file)
@@ -879,6 +879,30 @@ EOF;
         $this->assertEquals(true, $mimeinfo['txt']['defaulticon']);
     }
 
+    /**
+     * Tests for get_mimetype_for_sending function.
+     */
+    public function test_get_mimetype_for_sending() {
+        // Without argument.
+        $this->assertEquals('application/octet-stream', get_mimetype_for_sending());
+
+        // Argument is null.
+        $this->assertEquals('application/octet-stream', get_mimetype_for_sending(null));
+
+        // Filename having no extension.
+        $this->assertEquals('application/octet-stream', get_mimetype_for_sending('filenamewithoutextension'));
+
+        // Test using the extensions listed from the get_mimetypes_array function.
+        $mimetypes = get_mimetypes_array();
+        foreach ($mimetypes as $ext => $info) {
+            if ($ext === 'xxx') {
+                $this->assertEquals('application/octet-stream', get_mimetype_for_sending('SampleFile.' . $ext));
+            } else {
+                $this->assertEquals($info['type'], get_mimetype_for_sending('SampleFile.' . $ext));
+            }
+        }
+    }
+
     /**
      * Test curl agent settings.
      */
index 52d9e91..d560b63 100644 (file)
     <location>mustache</location>
     <name>Mustache</name>
     <license>MIT</license>
-    <version>2.7.0</version>
+    <version>2.9.0</version>
   </library>
   <library>
     <location>amd/src/mustache.js</location>
     <name>Mustache.js</name>
     <license>MIT</license>
-    <version>1.0.0</version>
+    <version>2.1.3</version>
   </library>
 </libraries>
index 83ce9fc..281c76d 100644 (file)
@@ -41,24 +41,24 @@ class login_signup_form extends moodleform {
 
         $mform->addElement('text', 'username', get_string('username'), 'maxlength="100" size="12"');
         $mform->setType('username', PARAM_NOTAGS);
-        $mform->addRule('username', get_string('missingusername'), 'required', null, 'server');
+        $mform->addRule('username', get_string('missingusername'), 'required', null, 'client');
 
         if (!empty($CFG->passwordpolicy)){
             $mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
         }
         $mform->addElement('passwordunmask', 'password', get_string('password'), 'maxlength="32" size="12"');
         $mform->setType('password', PARAM_RAW);
-        $mform->addRule('password', get_string('missingpassword'), 'required', null, 'server');
+        $mform->addRule('password', get_string('missingpassword'), 'required', null, 'client');
 
         $mform->addElement('header', 'supplyinfo', get_string('supplyinfo'),'');
 
         $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="25"');
         $mform->setType('email', PARAM_RAW_TRIMMED);
-        $mform->addRule('email', get_string('missingemail'), 'required', null, 'server');
+        $mform->addRule('email', get_string('missingemail'), 'required', null, 'client');
 
         $mform->addElement('text', 'email2', get_string('emailagain'), 'maxlength="100" size="25"');
         $mform->setType('email2', PARAM_RAW_TRIMMED);
-        $mform->addRule('email2', get_string('missingemail'), 'required', null, 'server');
+        $mform->addRule('email2', get_string('missingemail'), 'required', null, 'client');
 
         $namefields = useredit_get_required_name_fields();
         foreach ($namefields as $field) {
@@ -68,7 +68,7 @@ class login_signup_form extends moodleform {
             if (!get_string_manager()->string_exists($stringid, 'moodle')) {
                 $stringid = 'required';
             }
-            $mform->addRule($field, get_string($stringid), 'required', null, 'server');
+            $mform->addRule($field, get_string($stringid), 'required', null, 'client');
         }
 
         $mform->addElement('text', 'city', get_string('city'), 'maxlength="120" size="20"');
@@ -101,7 +101,7 @@ class login_signup_form extends moodleform {
             $mform->setExpanded('policyagreement');
             $mform->addElement('static', 'policylink', '', '<a href="'.$CFG->sitepolicy.'" onclick="this.target=\'_blank\'">'.get_String('policyagreementclick').'</a>');
             $mform->addElement('checkbox', 'policyagreed', get_string('policyaccept'));
-            $mform->addRule('policyagreed', get_string('policyagree'), 'required', null, 'server');
+            $mform->addRule('policyagreed', get_string('policyagree'), 'required', null, 'client');
         }
 
         // buttons
index 004c183..5160f34 100644 (file)
@@ -977,16 +977,16 @@ function message_format_message_text($message, $forcetexttohtml = false) {
 
     $format = $message->fullmessageformat;
 
-    if ($message->smallmessage !== '') {
+    if (strval($message->smallmessage) !== '') {
         if ($message->notification == 1) {
-            if ($message->fullmessagehtml !== '' or $message->fullmessage !== '') {
+            if (strval($message->fullmessagehtml) !== '' or strval($message->fullmessage) !== '') {
                 $format = FORMAT_PLAIN;
             }
         }
         $messagetext = $message->smallmessage;
 
     } else if ($message->fullmessageformat == FORMAT_HTML) {
-        if ($message->fullmessagehtml !== '') {
+