Merge branch 'MDL-37997-muc-stats-display' of git://github.com/mudrd8mz/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 18 Feb 2013 08:17:06 +0000 (16:17 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 18 Feb 2013 08:17:06 +0000 (16:17 +0800)
176 files changed:
admin/tool/behat/cli/util.php
admin/tool/behat/tests/behat/basic_actions.feature [new file with mode: 0644]
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/behat/tests/behat/manipulate_forms.feature [new file with mode: 0644]
admin/tool/uploaduser/index.php
admin/tool/xmldb/actions/XMLDBAction.class.php
admin/tool/xmldb/actions/view_structure_php/view_structure_php.class.php
admin/tool/xmldb/actions/view_table_php/view_table_php.class.php
auth/db/auth.php
auth/email/auth.php
auth/ldap/auth.php
auth/manual/auth.php
auth/none/auth.php
auth/webservice/auth.php
backup/converter/moodle1/lib.php
backup/converter/moodle1/tests/lib_test.php
backup/util/dbops/restore_dbops.class.php
backup/util/ui/base_moodleform.class.php
backup/util/xml/parser/processors/grouped_parser_processor.class.php
backup/util/xml/parser/tests/fixtures/test6.xml [new file with mode: 0644]
backup/util/xml/parser/tests/parser_test.php
blocks/community/communitycourse.php
blocks/community/forms.php
blocks/community/lang/en/block_community.php
blocks/community/styles.css
blocks/dock.js
blocks/glossary_random/block_glossary_random.php
blocks/glossary_random/edit_form.php
blocks/glossary_random/version.php
blocks/html/backup/moodle1/lib.php
blocks/rss_client/backup/moodle1/lib.php
blocks/settings/lang/en/block_settings.php
blog/edit_form.php
cache/classes/config.php
cache/classes/definition.php
cache/classes/factory.php
cache/classes/helper.php
cache/locallib.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
comment/comment_ajax.php
config-dist.php
course/dndupload.js
course/dnduploadlib.php
course/format/formatlegacy.php
course/format/topics/format.js
course/format/topics/lib.php
course/format/weeks/format.js
course/format/weeks/lib.php
course/lib.php
course/tests/behat/add_activities.feature
course/tests/behat/behat_course.php
course/tests/courselib_test.php
enrol/imsenterprise/locallib.php
enrol/locallib.php
enrol/manual/ajax.php
enrol/manual/locallib.php
enrol/manual/yui/quickenrolment/quickenrolment.js
grade/edit/outcome/index.php
grade/report/grader/ajax_callbacks.php
grade/report/grader/lib.php
grade/report/grader/module.js
grade/tests/reportgrader_test.php [new file with mode: 0644]
help.php
install/lang/et/error.php
install/lang/kmr/langconfig.php [new file with mode: 0644]
install/lang/pt_br/install.php
lang/en/admin.php
lang/en/block.php
lang/en/form.php
lang/en/grading.php
lang/en/moodle.php
lang/en/rating.php
lib/accesslib.php
lib/behat/behat_base.php
lib/behat/classes/behat_config_manager.php
lib/cronlib.php
lib/datalib.php
lib/db/install.xml
lib/db/upgrade.php
lib/filelib.php
lib/form/form.js
lib/form/listing.php [new file with mode: 0644]
lib/form/yui/listing/listing.js [new file with mode: 0644]
lib/form/yui/shortforms/shortforms.js [new file with mode: 0644]
lib/form/yui/showadvanced/showadvanced.js [new file with mode: 0644]
lib/formslib.php
lib/grade/tests/fixtures/lib.php
lib/installlib.php
lib/javascript-static.js
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/password_compat/lib/password.php [new file with mode: 0644]
lib/password_compat/readme_moodle.txt [new file with mode: 0644]
lib/password_compat/tests/PasswordGetInfoTest.php [new file with mode: 0644]
lib/password_compat/tests/PasswordHashTest.php [new file with mode: 0644]
lib/password_compat/tests/PasswordNeedsRehashTest.php [new file with mode: 0644]
lib/password_compat/tests/PasswordVerifyTest.php [new file with mode: 0644]
lib/phpunit/bootstrap.php
lib/setuplib.php
lib/statslib.php
lib/testing/classes/util.php
lib/testing/generator/data_generator.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/yui/popuphelp/popuphelp.js [new file with mode: 0644]
lib/yui/tooltip/tooltip.js [new file with mode: 0644]
message/index.php
message/lib.php
message/tests/externallib_test.php
mod/assign/externallib.php
mod/assign/feedback/offline/importgradesform.php
mod/assign/feedback/offline/locallib.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/module.js
mod/assign/submission/file/locallib.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/assignment/type/upload/assignment.class.php
mod/assignment/type/uploadsingle/assignment.class.php
mod/choice/mod_form.php
mod/data/renderer.php
mod/folder/backup/moodle1/lib.php
mod/folder/mod_form.php
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/db/access.php
mod/forum/db/install.xml
mod/forum/db/upgrade.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/mod_form.php
mod/forum/styles.css
mod/forum/upgrade.txt [new file with mode: 0644]
mod/forum/version.php
mod/label/lang/en/label.php
mod/label/lib.php
mod/label/settings.php [new file with mode: 0644]
mod/quiz/lib.php
mod/quiz/settings.php
mod/quiz/version.php
mod/resource/mod_form.php
mod/scorm/report/graphs/graph.php
mod/workshop/mod_form.php
mod/workshop/settings.php
phpunit.xml.dist
question/engine/questionattempt.php
question/type/essay/question.php
report/courseoverview/index.php
report/courseoverview/settings.php
report/security/lang/en/report_security.php
report/security/locallib.php
repository/filepicker.js
tag/coursetagslib.php
tag/index.php
theme/anomaly/style/general.css
theme/base/style/core.css
theme/canvas/style/core.css
theme/formal_white/style/formal_white.css
theme/splash/style/core.css
theme/standard/style/modules.css
theme/yui_combo.php
theme/yui_image.php
user/filters/user_filter_forms.php
version.php
webservice/lib.php

index 4fd147f..cfd3581 100644 (file)
@@ -150,6 +150,7 @@ require_once($CFG->libdir.'/upgradelib.php');
 require_once($CFG->libdir.'/clilib.php');
 require_once($CFG->libdir.'/pluginlib.php');
 require_once($CFG->libdir.'/installlib.php');
+require_once($CFG->libdir.'/testing/classes/test_lock.php');
 
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
@@ -165,6 +166,8 @@ if ($options['install']) {
     behat_util::install_site();
     mtrace("Acceptance tests site installed");
 } else if ($options['drop']) {
+    // Ensure no tests are running.
+    test_lock::acquire('behat');
     behat_util::drop_site();
     mtrace("Acceptance tests site dropped");
 } else if ($options['enable']) {
diff --git a/admin/tool/behat/tests/behat/basic_actions.feature b/admin/tool/behat/tests/behat/basic_actions.feature
new file mode 100644 (file)
index 0000000..8adc991
--- /dev/null
@@ -0,0 +1,29 @@
+@tool_behat
+Feature: Page contents assertions
+  In order to write good tests
+  As a tests writer
+  I need to check the page contents
+
+  @javascript
+  Scenario: Basic contents assertions
+    Given I log in as "admin"
+    And I am on homepage
+    And I expand "Users" node
+    And I follow "Groups"
+    And I press "Create group"
+    And I fill the moodle form with:
+      | Group name | I'm the name |
+      | Group description | I'm the description |
+    And I press "Save changes"
+    When I follow "Overview"
+    And I wait until the page is ready
+    And I wait "2" seconds
+    And I hover ".region-content .generaltable td span"
+    Then I should see "I'm the description"
+    And I should see "Filter groups by"
+    And I should not see "Filter groupssss by"
+    And I should see "Group members" in the ".region-content table th.c1" element
+    And I should not see "Group membersssss" in the ".region-content table th.c1" element
+    And I follow "Groups"
+    And the element "#groupeditform #showcreateorphangroupform" should be enabled
+    And the element "#groupeditform #showeditgroupsettingsform" should be disabled
index 19aeb51..ef5a993 100644 (file)
@@ -102,8 +102,8 @@ Feature: Set up contextual data for tests
     Then the "groups" select box should contain "Group 1 (1)"
     And the "groups" select box should contain "Group 2 (1)"
     And I select "Group 1 (1)" from "groups"
-    And I wait "1" seconds
+    And I wait "5" seconds
     And the "members" select box should contain "Student 1"
     And I select "Group 2 (1)" from "groups"
-    And I wait "1" seconds
+    And I wait "5" seconds
     And the "members" select box should contain "Student 2"
diff --git a/admin/tool/behat/tests/behat/manipulate_forms.feature b/admin/tool/behat/tests/behat/manipulate_forms.feature
new file mode 100644 (file)
index 0000000..9db0740
--- /dev/null
@@ -0,0 +1,20 @@
+@tool_behat @core_form
+Feature: Forms manipulation
+  In order to interact with Moodle
+  As a user
+  I need to set forms values
+
+  @javascript
+  Scenario: Basic forms manipulation
+    Given I log in as "admin"
+    And I follow "Admin User"
+    And I follow "Edit profile"
+    When I fill in "First name" with "Field value"
+    And I select "Use standard web forms" from "When editing text"
+    And I check "Unmask"
+    Then the "First name" field should match "Field value" value
+    And the "When editing text" select box should contain "Use standard web forms"
+    And the "Unmask" checkbox should be checked
+    And I uncheck "Unmask"
+    And the "Unmask" checkbox should not be checked
+    And I press "Update profile"
index f249f1d..88aaeda 100644 (file)
@@ -535,6 +535,7 @@ if ($formdata = $mform2->is_cancelled()) {
                     }
                     if (!property_exists($user, $column) or !property_exists($existinguser, $column)) {
                         // this should never happen
+                        debugging("Could not find $column on the user objects", DEBUG_DEVELOPER);
                         continue;
                     }
                     if ($updatetype == UU_UPDATE_MISSING) {
@@ -618,7 +619,7 @@ if ($formdata = $mform2->is_cancelled()) {
                 // Do not mess with passwords of remote users.
 
             } else if (!$isinternalauth) {
-                $existinguser->password = 'not cached';
+                $existinguser->password = AUTH_PASSWORD_NOT_CACHED;
                 $upt->track('password', '-', 'normal', false);
                 // clean up prefs
                 unset_user_preference('create_password', $existinguser);
@@ -626,6 +627,8 @@ if ($formdata = $mform2->is_cancelled()) {
 
             } else if (!empty($user->password)) {
                 if ($updatepasswords) {
+                    // Check for passwords that we want to force users to reset next
+                    // time they log in.
                     $errmsg = null;
                     $weak = !check_password_policy($user->password, $errmsg);
                     if ($resetpasswords == UU_PWRESET_ALL or ($resetpasswords == UU_PWRESET_WEAK and $weak)) {
@@ -638,7 +641,12 @@ if ($formdata = $mform2->is_cancelled()) {
                         unset_user_preference('auth_forcepasswordchange', $existinguser);
                     }
                     unset_user_preference('create_password', $existinguser); // no need to create password any more
-                    $existinguser->password = hash_internal_user_password($user->password);
+
+                    // Use a low cost factor when generating bcrypt hash otherwise
+                    // hashing would be slow when uploading lots of users. Hashes
+                    // will be automatically updated to a higher cost factor the first
+                    // time the user logs in.
+                    $existinguser->password = hash_internal_user_password($user->password, true);
                     $upt->track('password', $user->password, 'normal', false);
                 } else {
                     // do not print password when not changed
@@ -771,10 +779,14 @@ if ($formdata = $mform2->is_cancelled()) {
                         }
                         $forcechangepassword = true;
                     }
-                    $user->password = hash_internal_user_password($user->password);
+                    // Use a low cost factor when generating bcrypt hash otherwise
+                    // hashing would be slow when uploading lots of users. Hashes
+                    // will be automatically updated to a higher cost factor the first
+                    // time the user logs in.
+                    $user->password = hash_internal_user_password($user->password, true);
                 }
             } else {
-                $user->password = 'not cached';
+                $user->password = AUTH_PASSWORD_NOT_CACHED;
                 $upt->track('password', '-', 'normal', false);
             }
 
index 3c7ca7a..d93fba0 100644 (file)
@@ -239,22 +239,22 @@ class XMLDBAction {
         switch ($plugintype ) {
             case 'lib': // has own savepoint function
                 $result = XMLDB_LINEFEED .
-                          '        // Main savepoint reached' . XMLDB_LINEFEED .
+                          '        // Main savepoint reached.' . XMLDB_LINEFEED .
                           '        upgrade_main_savepoint(true, XXXXXXXXXX);' . XMLDB_LINEFEED;
                 break;
             case 'mod': // has own savepoint function
                 $result = XMLDB_LINEFEED .
-                          '        // ' . $pluginname . ' savepoint reached' . XMLDB_LINEFEED .
+                          '        // ' . ucfirst($pluginname) . ' savepoint reached.' . XMLDB_LINEFEED .
                           '        upgrade_mod_savepoint(true, XXXXXXXXXX, ' . "'$pluginname'" . ');' . XMLDB_LINEFEED;
                 break;
             case 'block': // has own savepoint function
                 $result = XMLDB_LINEFEED .
-                          '        // ' . $pluginname . ' savepoint reached' . XMLDB_LINEFEED .
+                          '        // ' . ucfirst($pluginname) . ' savepoint reached.' . XMLDB_LINEFEED .
                           '        upgrade_block_savepoint(true, XXXXXXXXXX, ' . "'$pluginname'" . ');' . XMLDB_LINEFEED;
                 break;
             default: // rest of plugins
                 $result = XMLDB_LINEFEED .
-                          '        // ' . $pluginname . ' savepoint reached' . XMLDB_LINEFEED .
+                          '        // ' . ucfirst($pluginname) . ' savepoint reached.' . XMLDB_LINEFEED .
                           '        upgrade_plugin_savepoint(true, XXXXXXXXXX, ' . "'$plugintype'" . ', ' . "'$pluginname'" . ');' . XMLDB_LINEFEED;
         }
         return $result;
index 0ddf559..99d4524 100644 (file)
@@ -170,10 +170,10 @@ class view_structure_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define table ' . $table->getName() . ' to be created' . XMLDB_LINEFEED;
+        $result .= '        // Define table ' . $table->getName() . ' to be created.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Adding fields to table ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Adding fields to table ' . $table->getName() . '.' . XMLDB_LINEFEED;
         // Iterate over each field
         foreach ($table->getFields() as $field) {
             // The field header, with name
@@ -186,7 +186,7 @@ class view_structure_php extends XMLDBAction {
         // Iterate over each key
         if ($keys = $table->getKeys()) {
             $result .= XMLDB_LINEFEED;
-            $result .= '        // Adding keys to table ' . $table->getName() . XMLDB_LINEFEED;
+            $result .= '        // Adding keys to table ' . $table->getName() . '.' . XMLDB_LINEFEED;
             foreach ($keys as $key) {
                 // The key header, with name
                 $result .= '        $table->add_key(' . "'" . $key->getName() . "', ";
@@ -199,7 +199,7 @@ class view_structure_php extends XMLDBAction {
         // Iterate over each index
         if ($indexes = $table->getIndexes()) {
             $result .= XMLDB_LINEFEED;
-            $result .= '        // Adding indexes to table ' . $table->getName() . XMLDB_LINEFEED;
+            $result .= '        // Adding indexes to table ' . $table->getName() . '.' . XMLDB_LINEFEED;
             foreach ($indexes as $index) {
                 // The index header, with name
                 $result .= '        $table->add_index(' . "'" . $index->getName() . "', ";
@@ -212,7 +212,7 @@ class view_structure_php extends XMLDBAction {
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch create table for ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch create table for ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if (!$dbman->table_exists($table)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->create_table($table);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -250,12 +250,12 @@ class view_structure_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define table ' . $table->getName() . ' to be dropped' . XMLDB_LINEFEED;
+        $result .= '        // Define table ' . $table->getName() . ' to be dropped.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch drop table for ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch drop table for ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if ($dbman->table_exists($table)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->drop_table($table);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -293,12 +293,12 @@ class view_structure_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define table ' . $table->getName() . ' to be renamed to NEWNAMEGOESHERE' . XMLDB_LINEFEED;
+        $result .= '        // Define table ' . $table->getName() . ' to be renamed to NEWNAMEGOESHERE.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch rename table for ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch rename table for ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->rename_table($table, ' . "'NEWNAMEGOESHERE'" . ');' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
index 235056f..e975201 100644 (file)
@@ -314,13 +314,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define field ' . $field->getName() . ' to be added to ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define field ' . $field->getName() . ' to be added to ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch add field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch add field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if (!$dbman->field_exists($table, $field)) {'. XMLDB_LINEFEED;
         $result .= '            $dbman->add_field($table, $field);' . XMLDB_LINEFEED;
         $result .= '        }'. XMLDB_LINEFEED;
@@ -362,13 +362,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define field ' . $field->getName() . ' to be dropped from ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define field ' . $field->getName() . ' to be dropped from ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "'" . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch drop field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch drop field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if ($dbman->field_exists($table, $field)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->drop_field($table, $field);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -410,13 +410,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Rename field ' . $field->getName() . ' on table ' . $table->getName() . ' to NEWNAMEGOESHERE'. XMLDB_LINEFEED;
+        $result .= '        // Rename field ' . $field->getName() . ' on table ' . $table->getName() . ' to NEWNAMEGOESHERE.'. XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch rename field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch rename field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->rename_field($table, $field, ' . "'" . 'NEWNAMEGOESHERE' . "'" . ');' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -465,13 +465,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Changing type of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $type . XMLDB_LINEFEED;
+        $result .= '        // Changing type of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $type . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch change of type for field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch change of type for field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->change_field_type($table, $field);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -517,13 +517,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Changing precision of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $precision . XMLDB_LINEFEED;
+        $result .= '        // Changing precision of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $precision . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
-        $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " .$field->getPHP(true) . ');' . XMLDB_LINEFEED;
+        $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch change of precision for field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch change of precision for field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->change_field_precision($table, $field);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -565,13 +565,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Changing nullability of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $notnull . XMLDB_LINEFEED;
+        $result .= '        // Changing nullability of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $notnull . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch change of nullability for field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch change of nullability for field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->change_field_notnull($table, $field);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -613,13 +613,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Changing the default of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $default . XMLDB_LINEFEED;
+        $result .= '        // Changing the default of field ' . $field->getName() . ' on table ' . $table->getName() . ' to ' . $default . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $field = new xmldb_field(' . "'" . $field->getName() . "', " . $field->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch change of default for field ' . $field->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch change of default for field ' . $field->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->change_field_default($table, $field);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -659,13 +659,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be added to ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be added to ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $key = new xmldb_key(' . "'" . $key->getName() . "', " . $key->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch add key ' . $key->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch add key ' . $key->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->add_key($table, $key);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -705,13 +705,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be dropped form ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be dropped form ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $key = new xmldb_key(' . "'" . $key->getName() . "', " . $key->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch drop key ' . $key->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch drop key ' . $key->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->drop_key($table, $key);' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -754,13 +754,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be renamed to NEWNAMEGOESHERE' . XMLDB_LINEFEED;
+        $result .= '        // Define key ' . $key->getName() . ' ('. $key->getXMLDBKeyName($key->getType()) . ') to be renamed to NEWNAMEGOESHERE.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $key = new xmldb_key(' . "'" . $key->getName() . "', " . $key->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch rename key ' . $key->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch rename key ' . $key->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->rename_key($table, $key, ' . "'" . 'NEWNAMEGOESHERE' . "'" . ');' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
@@ -800,13 +800,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be added to ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be added to ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $index = new xmldb_index(' . "'" . $index->getName() . "', " . $index->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch add index ' . $index->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch add index ' . $index->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if (!$dbman->index_exists($table, $index)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->add_index($table, $index);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -848,13 +848,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be dropped form ' . $table->getName() . XMLDB_LINEFEED;
+        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be dropped form ' . $table->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $index = new xmldb_index(' . "'" . $index->getName() . "', " . $index->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Conditionally launch drop index ' . $index->getName() . XMLDB_LINEFEED;
+        $result .= '        // Conditionally launch drop index ' . $index->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        if ($dbman->index_exists($table, $index)) {' . XMLDB_LINEFEED;
         $result .= '            $dbman->drop_index($table, $index);' . XMLDB_LINEFEED;
         $result .= '        }' . XMLDB_LINEFEED;
@@ -899,13 +899,13 @@ class view_table_php extends XMLDBAction {
 
         // Add contents
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be renamed to NEWNAMEGOESHERE' . XMLDB_LINEFEED;
+        $result .= '        // Define index ' . $index->getName() . ' ('. ($index->getUnique() ? 'unique' : 'not unique') . ') to be renamed to NEWNAMEGOESHERE.' . XMLDB_LINEFEED;
         $result .= '        $table = new xmldb_table(' . "'" . $table->getName() . "'" . ');' . XMLDB_LINEFEED;
         $result .= '        $index = new xmldb_index(' . "'" . $index->getName() . "', " . $index->getPHP(true) . ');' . XMLDB_LINEFEED;
 
         // Launch the proper DDL
         $result .= XMLDB_LINEFEED;
-        $result .= '        // Launch rename index ' . $index->getName() . XMLDB_LINEFEED;
+        $result .= '        // Launch rename index ' . $index->getName() . '.' . XMLDB_LINEFEED;
         $result .= '        $dbman->rename_index($table, $index, ' . "'" . 'NEWNAMEGOESHERE' . "'" . ');' . XMLDB_LINEFEED;
 
         // Add the proper upgrade_xxxx_savepoint call
index 57c46e4..f245d60 100644 (file)
@@ -221,6 +221,9 @@ class auth_plugin_db extends auth_plugin_base {
 
         if ($this->is_internal()) {
             $puser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
+            // This will also update the stored hash to the latest algorithm
+            // if the existing hash is using an out-of-date algorithm (or the
+            // legacy md5 algorithm).
             if (update_internal_user_password($puser, $newpassword)) {
                 $user->password = $puser->password;
                 return true;
index e50c09e..777fe9f 100644 (file)
@@ -59,6 +59,9 @@ class auth_plugin_email extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         return update_internal_user_password($user, $newpassword);
     }
 
index 6cd7fd7..9bdd5b3 100644 (file)
@@ -529,6 +529,9 @@ class auth_plugin_ldap extends auth_plugin_base {
         profile_save_data($user);
 
         $this->update_user_record($user->username);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         update_internal_user_password($user, $plainslashedpassword);
 
         $user = $DB->get_record('user', array('id'=>$user->id));
index 29cb59a..0c521e8 100644 (file)
@@ -82,6 +82,9 @@ class auth_plugin_manual extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         return update_internal_user_password($user, $newpassword);
     }
 
index f0771d8..00eaf04 100644 (file)
@@ -59,6 +59,9 @@ class auth_plugin_none extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         return update_internal_user_password($user, $newpassword);
     }
 
index 15a7598..59e1603 100644 (file)
@@ -85,6 +85,9 @@ class auth_plugin_webservice extends auth_plugin_base {
      */
     function user_update_password($user, $newpassword) {
         $user = get_complete_user_data('id', $user->id);
+        // This will also update the stored hash to the latest algorithm
+        // if the existing hash is using an out-of-date algorithm (or the
+        // legacy md5 algorithm).
         return update_internal_user_password($user, $newpassword);
     }
 
index d91e2f2..8f7ec30 100644 (file)
@@ -1279,6 +1279,12 @@ class moodle1_file_manager implements loggable {
      */
     public function migrate_directory($rootpath, $relpath='/') {
 
+        // Check the trailing slash in the $rootpath
+        if (substr($rootpath, -1) === '/') {
+            debugging('moodle1_file_manager::migrate_directory() expects $rootpath without the trailing slash', DEBUG_DEVELOPER);
+            $rootpath = substr($rootpath, 0, strlen($rootpath) - 1);
+        }
+
         if (!file_exists($this->basepath.'/'.$rootpath.$relpath)) {
             return array();
         }
index 154b6c8..8fb50b6 100644 (file)
@@ -266,13 +266,6 @@ class moodle1_converter_testcase extends advanced_testcase {
         $fileids = $fileman->get_fileids();
         $this->assertEquals(gettype($fileids), 'array');
         $this->assertEquals(0, count($fileids));
-        // try to migrate a non-existing directory
-        $returned = $fileman->migrate_directory('not/existing/directory');
-        $this->assertEquals(gettype($returned), 'array');
-        $this->assertEquals(0, count($returned));
-        $fileids = $fileman->get_fileids();
-        $this->assertEquals(gettype($fileids), 'array');
-        $this->assertEquals(0, count($fileids));
         // try to migrate an invalid file
         $fileman->itemid = 1;
         $thrown = false;
@@ -314,6 +307,72 @@ class moodle1_converter_testcase extends advanced_testcase {
         $converter->drop_stash_storage();
     }
 
+    public function test_migrate_directory() {
+        $this->resetAfterTest(true);
+
+        // Set-up the file manager.
+        $converter = convert_factory::get_converter('moodle1', $this->tempdir);
+        $converter->create_stash_storage();
+        $contextid = $converter->get_contextid(CONTEXT_MODULE, 32);
+        $fileman   = $converter->get_file_manager($contextid, 'mod_unittest', 'testarea');
+        // This fileman has not converted anything yet.
+        $fileids = $fileman->get_fileids();
+        $this->assertEquals(gettype($fileids), 'array');
+        $this->assertEquals(0, count($fileids));
+        // Try to migrate a non-existing directory.
+        $returned = $fileman->migrate_directory('not/existing/directory');
+        $this->assertEquals(gettype($returned), 'array');
+        $this->assertEquals(0, count($returned));
+        $fileids = $fileman->get_fileids();
+        $this->assertEquals(gettype($fileids), 'array');
+        $this->assertEquals(0, count($fileids));
+        // Try to migrate whole course_files.
+        $returned = $fileman->migrate_directory('course_files');
+        $this->assertEquals(gettype($returned), 'array');
+        $this->assertEquals(4, count($returned)); // Two files, two directories.
+        $fileids = $fileman->get_fileids();
+        $this->assertEquals(gettype($fileids), 'array');
+        $this->assertEquals(4, count($fileids));
+        $subdir = substr($this->iconhash, 0, 2);
+        $this->assertTrue(is_file($converter->get_workdir_path().'/files/'.$subdir.'/'.$this->iconhash));
+
+        // Check the file records.
+        $files = array();
+        $filerecordids = $converter->get_stash_itemids('files');
+        foreach ($filerecordids as $filerecordid) {
+            $filerecord = $converter->get_stash('files', $filerecordid);
+            $files[$filerecord['filepath'].$filerecord['filename']] = $filerecord;
+        }
+        $this->assertEquals('array', gettype($files['/.']));
+        $this->assertEquals('array', gettype($files['/file1.gif']));
+        $this->assertEquals('array', gettype($files['/sub1/.']));
+        $this->assertEquals('array', gettype($files['/sub1/file2.gif']));
+        $this->assertEquals(sha1(''), $files['/.']['contenthash']);
+        $this->assertEquals(sha1(''), $files['/sub1/.']['contenthash']);
+        $this->assertEquals($this->iconhash, $files['/file1.gif']['contenthash']);
+        $this->assertEquals($this->iconhash, $files['/sub1/file2.gif']['contenthash']);
+
+        $converter->drop_stash_storage();
+    }
+
+    public function test_migrate_directory_with_trailing_slash() {
+        $this->resetAfterTest(true);
+
+        // Set-up the file manager.
+        $converter = convert_factory::get_converter('moodle1', $this->tempdir);
+        $converter->create_stash_storage();
+        $contextid = $converter->get_contextid(CONTEXT_MODULE, 32);
+        $fileman   = $converter->get_file_manager($contextid, 'mod_unittest', 'testarea');
+        // Try to migrate a subdirectory passed with the trailing slash.
+        $returned = $fileman->migrate_directory('course_files/sub1/');
+        // Debugging message must be thrown in this case.
+        $this->assertDebuggingCalled(null, DEBUG_DEVELOPER);
+        $this->assertEquals(gettype($returned), 'array');
+        $this->assertEquals(2, count($returned)); // One file, one directory.
+
+        $converter->drop_stash_storage();
+    }
+
     public function test_convert_path() {
         $path = new convert_path('foo_bar', '/ROOT/THINGS/FOO/BAR');
         $this->assertEquals('foo_bar', $path->get_name());
index 82c4897..148afa3 100644 (file)
@@ -1052,7 +1052,7 @@ abstract class restore_dbops {
 
                 // Most external plugins do not store passwords locally
                 if (!empty($userauth->preventpassindb)) {
-                    $user->password = 'not cached';
+                    $user->password = AUTH_PASSWORD_NOT_CACHED;
 
                 // If Moodle is responsible for storing/validating pwd and reset functionality is available, mark
                 } else if ($userauth->isinternal and $userauth->canresetpwd) {
index fb0e975..7a733ee 100644 (file)
@@ -81,6 +81,7 @@ abstract class base_moodleform extends moodleform {
     function definition() {
         $ui = $this->uistage->get_ui();
         $mform = $this->_form;
+        $mform->setDisableShortforms();
         $stage = $mform->addElement('hidden', 'stage', $this->uistage->get_stage());
         $stage = $mform->addElement('hidden', $ui->get_name(), $ui->get_uniqueid());
         $params = $this->uistage->get_params();
index d8e260a..48782b7 100644 (file)
@@ -76,6 +76,17 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
      * @param string $path xml path which parsing has started
      */
     public function before_path($path) {
+        if ($this->path_is_grouped($path) and !isset($this->currentdata[$path])) {
+            // If the grouped element itself does not contain any final tags,
+            // we would not get any chunk data for it. So we add an artificial
+            // empty data chunk here that will be eventually replaced with
+            // real data later in {@link self::postprocess_chunk()}.
+            $this->currentdata[$path] = array(
+                'path' => $path,
+                'level' => substr_count($path, '/') + 1,
+                'tags' => array(),
+            );
+        }
         if (!$this->grouped_parent_exists($path)) {
             parent::before_path($path);
         }
@@ -93,7 +104,10 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
             // currentdata, properly built
             $data = $this->currentdata[$path];
             unset($this->currentdata[$path]);
+            // Always, before dispatching any chunk, send all pending start notifications.
+            $this->process_pending_startend_notifications($path, 'start');
             // TODO: If running under DEBUG_DEVELOPER notice about >1MB grouped chunks
+            // And, finally, dispatch it.
             $this->dispatch_chunk($data);
         }
         // Normal notification of path end
@@ -167,7 +181,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
      */
     protected function build_currentdata($grouped, $data) {
         // Check the grouped already exists into currentdata
-        if (!array_key_exists($grouped, $this->currentdata)) {
+        if (!is_array($this->currentdata) or !array_key_exists($grouped, $this->currentdata)) {
             $a = new stdclass();
             $a->grouped = $grouped;
             $a->child = $data['path'];
diff --git a/backup/util/xml/parser/tests/fixtures/test6.xml b/backup/util/xml/parser/tests/fixtures/test6.xml
new file mode 100644 (file)
index 0000000..a0399e8
--- /dev/null
@@ -0,0 +1,60 @@
+<test>
+  <MOODLE_BACKUP>
+    <COURSE>
+      <FORMATDATA>
+        <WEEKS>
+          <WEEK>
+            <SECTION>1</SECTION>
+            <HIDENUMBER>1</HIDENUMBER>
+            <HIDEDATE>0</HIDEDATE>
+            <SHOWTO></SHOWTO>
+            <OFFLINEMATERIAL>0</OFFLINEMATERIAL>
+          </WEEK>
+          <WEEK>
+            <SECTION>2</SECTION>
+            <HIDENUMBER>0</HIDENUMBER>
+            <HIDEDATE>0</HIDEDATE>
+            <RESETNUMBER>0</RESETNUMBER>
+            <SHOWTO></SHOWTO>
+            <OFFLINEMATERIAL>0</OFFLINEMATERIAL>
+          </WEEK>
+        </WEEKS>
+        <IMPORTED>
+        </IMPORTED>
+      </FORMATDATA>
+      <GROUPEDNOTOBSERVED>
+        <NOBODYOBSERVERSTHIS>
+          <NOTOBSERVED>Muhehe</NOTOBSERVED>
+        </NOBODYOBSERVERSTHIS>
+      </GROUPEDNOTOBSERVED>
+      <EMPTYGROUPED>
+      </EMPTYGROUPED>
+      <SECONDGROUPED>
+        <SUBS>
+          <SUB>
+            <PROP>Unit tests rock!</PROP>
+          </SUB>
+        </SUBS>
+      </SECONDGROUPED>
+    </COURSE>
+  </MOODLE_BACKUP>
+  <moodle2>
+    <grouped id="this is not parsed at the moment because there are no final elements">
+      <subs>
+        <sub id="34">
+          <prop>Oh yeah</prop>
+        </sub>
+      </subs>
+    </grouped>
+    <groupednonemptywithattr id="78">
+      <prop>Go baby go</prop>
+      <subs>
+        <sub id="89">
+          <prop>http://moodle.org</prop>
+        </sub>
+      </subs>
+    </groupednonemptywithattr>
+    <groupedemptywithattr attr="ay?">
+    </groupedemptywithattr>
+  </moodle2>
+</test>
index 2a1b5cc..39bf2d7 100644 (file)
@@ -632,6 +632,93 @@ class progressive_parser_test extends advanced_testcase {
         $this->assertEquals($errcount, 0); // No errors found, plz
     }
 
+    /**
+     */
+    function test_grouped_at_empty_node() {
+        global $CFG;
+        // Instantiate progressive_parser.
+        $pp =  new progressive_parser();
+        // Instantiate grouped_parser_processor.
+        $pr = new mock_grouped_parser_processor();
+        $this->assertTrue($pr instanceof progressive_parser_processor);
+        // Add interesting paths - moodle1 style.
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/FORMATDATA', true);
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/FORMATDATA/WEEKS/WEEK');
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', true);
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', true);
+        $pr->add_path('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED/SUBS/SUB');
+        // Add interesting paths - moodle2 style.
+        $pr->add_path('/test/moodle2/grouped', true);
+        $pr->add_path('/test/moodle2/grouped/subs/sub');
+        $pr->add_path('/test/moodle2/groupedemptywithattr', true);
+        $pr->add_path('/test/moodle2/groupednonemptywithattr', true);
+        $pr->add_path('/test/moodle2/groupednonemptywithattr/subs/sub');
+        // Assign processor to parser.
+        $pp->set_processor($pr);
+        // Set file from fixtures.
+        $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test6.xml');
+        // Process the file.
+        $pp->process();
+
+        // Get all the simplified chunks and perform various validations.
+        $chunks = $pr->get_chunks();
+        $this->assertEquals(count($chunks), 6); // All grouped elements.
+
+        // Check some random data.
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $chunks[0]['path']);
+        $this->assertEquals(2, $chunks[0]['tags']['WEEKS']['WEEK'][1]['SECTION']);
+
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $chunks[1]['path']);
+        $this->assertEquals(array(), $chunks[1]['tags']);
+
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $chunks[2]['path']);
+        $this->assertEquals('Unit tests rock!', $chunks[2]['tags']['SUBS']['SUB'][0]['PROP']);
+
+        $this->assertEquals('/test/moodle2/grouped', $chunks[3]['path']);
+        $this->assertFalse(isset($chunks[3]['tags']['id'])); // No final elements, this should be fixed one day.
+        $this->assertEquals(34, $chunks[3]['tags']['subs']['sub'][0]['id']); // We have final element so this is parsed.
+        $this->assertEquals('Oh yeah', $chunks[3]['tags']['subs']['sub'][0]['prop']);
+
+        $this->assertEquals('/test/moodle2/groupednonemptywithattr', $chunks[4]['path']);
+        $this->assertEquals(78, $chunks[4]['tags']['id']); // We have final element so this is parsed.
+        $this->assertEquals('Go baby go', $chunks[4]['tags']['prop']);
+        $this->assertEquals(89, $chunks[4]['tags']['subs']['sub'][0]['id']);
+        $this->assertEquals('http://moodle.org', $chunks[4]['tags']['subs']['sub'][0]['prop']);
+
+        $this->assertEquals('/test/moodle2/groupedemptywithattr', $chunks[5]['path']);
+        $this->assertFalse(isset($chunks[5]['tags']['attr'])); // No final elements, this should be fixed one day.
+
+        // Now check start notifications.
+        $snotifs = $pr->get_start_notifications();
+        // Check we have received the correct number of notifications.
+        $this->assertEquals(count($snotifs), 6);
+        // Check the order of notifications (in order they appear in test6.xml).
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $snotifs[0]);
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $snotifs[1]);
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $snotifs[2]);
+        $this->assertEquals('/test/moodle2/grouped', $snotifs[3]);
+        $this->assertEquals('/test/moodle2/groupednonemptywithattr', $snotifs[4]);
+        $this->assertEquals('/test/moodle2/groupedemptywithattr', $snotifs[5]);
+
+        // Now check end notifications.
+        $enotifs = $pr->get_end_notifications();
+        // Check we have received the correct number of notifications.
+        $this->assertEquals(count($enotifs), 6);
+        // Check the order of notifications (in order they appear in test6.xml).
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $enotifs[0]);
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $enotifs[1]);
+        $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $enotifs[2]);
+        $this->assertEquals('/test/moodle2/grouped', $enotifs[3]);
+        $this->assertEquals('/test/moodle2/groupednonemptywithattr', $enotifs[4]);
+        $this->assertEquals('/test/moodle2/groupedemptywithattr', $enotifs[5]);
+
+        // Now verify that the start/process/end order is correct.
+        $allnotifs = $pr->get_all_notifications();
+        $this->assertEquals(count($allnotifs), count($snotifs) + count($enotifs) + count($chunks));
+        // Check integrity of the notifications.
+        $errcount = $this->helper_check_notifications_order_integrity($allnotifs);
+        $this->assertEquals(0, $errcount);
+    }
 
     /**
      * Helper function that given one array of ordered start/process/end notifications will
index 045e72d..fe21d76 100644 (file)
@@ -142,7 +142,7 @@ $fromformdata['subject'] = optional_param('subject', 'all', PARAM_ALPHANUMEXT);
 $fromformdata['audience'] = optional_param('audience', 'all', PARAM_ALPHANUMEXT);
 $fromformdata['language'] = optional_param('language', current_language(), PARAM_ALPHANUMEXT);
 $fromformdata['educationallevel'] = optional_param('educationallevel', 'all', PARAM_ALPHANUMEXT);
-$fromformdata['downloadable'] = optional_param('downloadable', 0, PARAM_ALPHANUM);
+$fromformdata['downloadable'] = optional_param('downloadable', 1, PARAM_ALPHANUM);
 $fromformdata['orderby'] = optional_param('orderby', 'newest', PARAM_ALPHA);
 $fromformdata['huburl'] = optional_param('huburl', HUB_MOODLEORGHUBURL, PARAM_URL);
 $fromformdata['search'] = $search;
index 641ef17..3e00e62 100644 (file)
@@ -76,7 +76,7 @@ class community_hub_search_form extends moodleform {
         if (isset($this->_customdata['downloadable'])) {
             $downloadable = $this->_customdata['downloadable'];
         } else {
-            $downloadable = 0;
+            $downloadable = 1;
         }
         if (isset($this->_customdata['orderby'])) {
             $orderby = $this->_customdata['orderby'];
@@ -135,60 +135,69 @@ class community_hub_search_form extends moodleform {
         }
 
         if (!empty($hubs)) {
-            //TODO: sort hubs by trusted/prioritize
-            //Public hub list
-            $options = array();
-            $firsthub = false;
+            $htmlhubs = array();
             foreach ($hubs as $hub) {
+                // Name can come from hub directory - need some cleaning.
+                $hubname = clean_text($hub['name'], PARAM_TEXT);
+                $smalllogohtml = '';
                 if (array_key_exists('id', $hub)) {
-                    $params = array('hubid' => $hub['id'],
-                        'filetype' => HUB_HUBSCREENSHOT_FILE_TYPE);
-                    $imgurl = new moodle_url(HUB_HUBDIRECTORYURL .
-                                    "/local/hubdirectory/webservice/download.php", $params);
-                    $ascreenshothtml = html_writer::empty_tag('img',
-                                    array('src' => $imgurl, 'alt' => $hub['name']));
-
-                    $hubdescription = html_writer::tag('a', $hub['name'],
-                                    array('class' => 'hublink clearfix', 'href' => $hub['url'],
-                                        'onclick' => 'this.target="_blank"'));
-                    $hubdescription .= html_writer::tag('span', $ascreenshothtml,
-                                    array('class' => 'hubscreenshot'));
-                    $hubdescriptiontext = html_writer::tag('span', format_text($hub['description'], FORMAT_PLAIN),
-                                    array('class' => 'hubdescription'));
+
+                    // Retrieve hub logo + generate small logo.
+                    $params = array('hubid' => $hub['id'], 'filetype' => HUB_HUBSCREENSHOT_FILE_TYPE);
+                    $imgurl = new moodle_url(HUB_HUBDIRECTORYURL . "/local/hubdirectory/webservice/download.php", $params);
+                    $imgsize = getimagesize($imgurl->out(false));
+                    if ($imgsize[0] > 1) {
+                        $ascreenshothtml = html_writer::empty_tag('img', array('src' => $imgurl, 'alt' => $hubname));
+                        $smalllogohtml = html_writer::empty_tag('img', array('src' => $imgurl, 'alt' => $hubname
+                                        , 'height' => 30, 'width' => 40));
+                    } else {
+                        $ascreenshothtml = '';
+                    }
+                    $hubimage = html_writer::tag('div', $ascreenshothtml, array('class' => 'hubimage'));
+
+                    // Statistics + trusted info.
+                    $hubstats = '';
                     if (isset($hub['enrollablecourses'])) { //check needed to avoid warnings for Moodle version < 2011081700
                         $additionaldesc = get_string('enrollablecourses', 'block_community') . ': ' . $hub['enrollablecourses'] . ' - ' .
                                 get_string('downloadablecourses', 'block_community') . ': ' . $hub['downloadablecourses'];
-                        $hubdescriptiontext .= html_writer::tag('span', $additionaldesc,
-                                        array('class' => 'hubadditionaldesc'));
+                        $hubstats .= html_writer::tag('div', $additionaldesc);
                     }
                     if ($hub['trusted']) {
-                    $hubtrusted =  get_string('hubtrusted', 'block_community');
-                    $hubdescriptiontext .= html_writer::tag('span',
-                                    $hubtrusted . ' ' . $OUTPUT->doc_link('trusted_hubs'),
-                                    array('class' => 'trusted'));
-
+                        $hubtrusted =  get_string('hubtrusted', 'block_community');
+                        $hubstats .= $OUTPUT->doc_link('trusted_hubs') . html_writer::tag('div', $hubtrusted);
                     }
-                    $hubdescriptiontext = html_writer::tag('span', $hubdescriptiontext,
-                            array('class' => 'hubdescriptiontext'));
+                    $hubstats = html_writer::tag('div', $hubstats, array('class' => 'hubstats'));
 
-                    $hubdescription = html_writer::tag('span',
-                                    $hubdescription . $hubdescriptiontext,
-                                    array('class' => $hub['trusted'] ? 'hubtrusted' : 'hubnottrusted'));
-                } else {
-                    $hubdescription = html_writer::tag('a', $hub['name'],
-                                    array('class' => 'hublink hubtrusted', 'href' => $hub['url']));
-                }
+                    // hub name link + hub description.
+                    $hubnamelink = html_writer::link($hub['url'], html_writer::tag('h2',$hubname),
+                                    array('class' => 'hubtitlelink'));
+                    // The description can come from the hub directory - need to clean.
+                    $hubdescription = clean_param($hub['description'], PARAM_TEXT);
+                    $hubdescriptiontext = html_writer::tag('div', format_text($hubdescription, FORMAT_PLAIN),
+                                    array('class' => 'hubdescription'));
 
-                if (empty($firsthub)) {
-                    $mform->addElement('radio', 'huburl', get_string('selecthub', 'block_community'),
-                            $hubdescription, $hub['url']);
-                    $mform->setDefault('huburl', $huburl);
-                    $firsthub = true;
+                    $hubtext = html_writer::tag('div', $hubdescriptiontext . $hubstats, array('class' => 'hubtext'));
+
+                    $hubimgandtext = html_writer::tag('div', $hubimage . $hubtext, array('class' => 'hubimgandtext'));
+
+                    $hubfulldesc = html_writer::tag('div', $hubnamelink . $hubimgandtext, array('class' => 'hubmainhmtl'));
                 } else {
-                    $mform->addElement('radio', 'huburl', '', $hubdescription, $hub['url']);
+                    $hubfulldesc = html_writer::link($hub['url'], $hubname);
                 }
+
+                // Add hub to the hub items.
+                $hubinfo = new stdClass();
+                $hubinfo->mainhtml = $hubfulldesc;
+                $hubinfo->rowhtml = html_writer::tag('div', $smalllogohtml , array('class' => 'hubsmalllogo')) . $hubname;
+                $hubitems[$hub['url']] = $hubinfo;
             }
 
+            // Hub listing form element.
+            $mform->addElement('listing','huburl', '', '', array('items' => $hubitems,
+                'showall' => get_string('showall', 'block_community'),
+                'hideall' => get_string('hideall', 'block_community')));
+            $mform->setDefault('huburl', $huburl);
+
             //display enrol/download select box if the USER has the download capability on the course
             if (has_capability('moodle/community:download',
                             context_course::instance($this->_customdata['courseid']))) {
@@ -197,6 +206,8 @@ class community_hub_search_form extends moodleform {
                 $mform->addElement('select', 'downloadable', get_string('enroldownload', 'block_community'),
                         $options);
                 $mform->addHelpButton('downloadable', 'enroldownload', 'block_community');
+
+                $mform->setDefault('downloadable', $downloadable);
             } else {
                 $mform->addElement('hidden', 'downloadable', 0);
             }
@@ -262,23 +273,30 @@ class community_hub_search_form extends moodleform {
             collatorlib::asort($languages);
             $languages = array_merge(array('all' => get_string('any')), $languages);
             $mform->addElement('select', 'language', get_string('language'), $languages);
+
             $mform->setDefault('language', $language);
             $mform->addHelpButton('language', 'language', 'block_community');
 
-            $mform->addElement('radio', 'orderby', get_string('orderby', 'block_community'),
-                    get_string('orderbynewest', 'block_community'), 'newest');
-            $mform->addElement('radio', 'orderby', null,
-                    get_string('orderbyeldest', 'block_community'), 'eldest');
-            $mform->addElement('radio', 'orderby', null,
-                    get_string('orderbyname', 'block_community'), 'fullname');
-            $mform->addElement('radio', 'orderby', null,
-                    get_string('orderbypublisher', 'block_community'), 'publisher');
-            $mform->addElement('radio', 'orderby', null,
-                    get_string('orderbyratingaverage', 'block_community'), 'ratingaverage');
+            $mform->addElement('select', 'orderby', get_string('orderby', 'block_community'),
+                array('newest' => get_string('orderbynewest', 'block_community'),
+                    'eldest' => get_string('orderbyeldest', 'block_community'),
+                    'fullname' => get_string('orderbyname', 'block_community'),
+                    'publisher' => get_string('orderbypublisher', 'block_community'),
+                    'ratingaverage' => get_string('orderbyratingaverage', 'block_community')));
+
             $mform->setDefault('orderby', $orderby);
+            $mform->addHelpButton('orderby', 'orderby', 'block_community');
             $mform->setType('orderby', PARAM_ALPHA);
 
-            $mform->addElement('text', 'search', get_string('keywords', 'block_community'));
+            $mform->setAdvanced('audience');
+            $mform->setAdvanced('educationallevel');
+            $mform->setAdvanced('subject');
+            $mform->setAdvanced('licence');
+            $mform->setAdvanced('language');
+            $mform->setAdvanced('orderby');
+
+            $mform->addElement('text', 'search', get_string('keywords', 'block_community'),
+                array('size' => 30));
             $mform->addHelpButton('search', 'keywords', 'block_community');
 
 
@@ -298,4 +316,4 @@ class community_hub_search_form extends moodleform {
         return $errors;
     }
 
-}
\ No newline at end of file
+}
index ef509e3..df06794 100644 (file)
@@ -68,9 +68,11 @@ $string['enrollablecourses'] = 'Enrollable courses';
 $string['errorcourselisting'] = 'An error occurred when retrieving the course listing from the selected hub, please try again later. ({$a})';
 $string['errorhublisting'] = 'An error occurred when retrieving the hub listing from Moodle.org, please try again later. ({$a})';
 $string['fileinfo'] = 'Language: {$a->lang} - License: {$a->license} -  Time updated: {$a->timeupdated}';
+$string['hideall'] = 'Hide hubs';
 $string['hub'] = 'hub';
 $string['hubnottrusted'] = 'Not trusted';
 $string['hubtrusted'] = 'This hub is trusted by Moodle.org';
+$string['install'] = 'Install';
 $string['keywords'] = 'Keywords';
 $string['keywords_help'] = 'You can search for courses containing specific text in the name, description and other fields of the database.';
 $string['langdesc'] = 'Language: {$a} - ';
@@ -106,6 +108,7 @@ $string['searchcourse'] = 'Search for community course';
 $string['selecthub'] = 'Select hub';
 $string['selecthub_help'] = 'Select hub where to search the courses.';
 $string['sites'] = 'Sites';
+$string['showall'] = 'Show all hubs';
 $string['subject'] = 'Subject';
 $string['subject_help'] = 'To narrow your search to courses about a particular subject, choose one from this list.';
 $string['userinfo'] = 'Creator: {$a->creatorname} - Publisher: {$a->publishername}';
index 28d6162..96598cf 100644 (file)
@@ -2,30 +2,23 @@
 
 /* HUB SELECTOR */
 #page-blocks-community-communitycourse .hubscreenshot {float: left; }
-#page-blocks-community-communitycourse .hubdescription {
-    color: #003333;
-    font-size: 95%;
-    display:block;
-}
-#page-blocks-community-communitycourse .hubdescriptiontext {margin-left:160px;display:block;}
-#page-blocks-community-communitycourse .hubadditionaldesc {
-    color: #666666;
-    font-size: 90%;
-    display:block;
-}
+#page-blocks-community-communitycourse .hubtitlelink {color: #999; }
+#page-blocks-community-communitycourse .hubsmalllogo {padding-left: 3px; padding-right: 7px; float: left; }
+#page-blocks-community-communitycourse .hubtext {display: block; width: 68%; padding-left: 165px;}
+#page-blocks-community-communitycourse .hubimgandtext {display:table;}
+#page-blocks-community-communitycourse .hubimage {float: left; display: block; width: 100px;}
+#page-blocks-community-communitycourse .hubdescriptiontext {}
+#page-blocks-community-communitycourse .hubstats {padding-top: 10px}
+#page-blocks-community-communitycourse .hubstats .iconhelp {float: left; padding-right: 3px;}
+#page-blocks-community-communitycourse .hubadditionaldesc {color: #666666; font-size: 90%; display:block;}
 #page-blocks-community-communitycourse .hubscreenshot {margin-right: 10px;}
-#page-blocks-community-communitycourse .hubnottrusted {margin-left: 6px;}
-#page-blocks-community-communitycourse .hubtrusted {display:inline;margin-left: 6px;}
+#page-blocks-community-communitycourse .hubnottrusted {}
+#page-blocks-community-communitycourse .hubtrusted {display:inline;}
 #page-blocks-community-communitycourse .hubnottrusted {}
 #page-blocks-community-communitycourse .trustedtr {background-color: #ffe1c3;}
 #page-blocks-community-communitycourse .prioritisetr {background-color: #ffd4ff;}
 #page-blocks-community-communitycourse .blockdescription {font-size: 80%; color: #555555;}
-#page-blocks-community-communitycourse .trusted {
-    font-size: 90%;
-    color: #006633;
-    font-weight: normal;
-    font-style: italic;
-}
+#page-blocks-community-communitycourse .trusted {font-size: 90%; color: #006633; font-weight: normal; font-style: italic;}
 
 /* COURSES RESULT */
 #page-blocks-community-communitycourse .additionaldesc {font-size: 80%; color: #8B8989;}
index 46c0479..d555668 100644 (file)
@@ -906,7 +906,7 @@ M.core_dock.genericblock.prototype = {
         }
 
         // Must set the image src seperatly of we get an error with XML strict headers
-        var movetoimg = Y.Node.create('<img alt="'+M.str.block.undockitem+'" title="'+M.str.block.undockitem+'" />');
+        var movetoimg = Y.Node.create('<img alt="'+M.str.block.undockitem+'" title="'+M.util.get_string('undockblock', 'block', blocktitle.innerHTML)+'" />');
         var icon = 't/dock_to_block';
         if (right_to_left()) {
             icon = 't/dock_to_block_rtl';
index 884c909..db094c9 100644 (file)
@@ -43,8 +43,18 @@ class block_glossary_random extends block_base {
                 $this->instance_config_commit();
             }
 
+            // Get glossary instance, if not found then return without error, as this will be handled in get_content.
+            if (!$glossary = $DB->get_record('glossary', array('id' => $this->config->glossary))) {
+                return false;
+            }
+
+            $this->config->globalglossary = $glossary->globalglossary;
+
+            // Save course id in config, so we can get correct course module.
+            $this->config->courseid = $glossary->course;
+
             // Get module and context, to be able to rewrite urls
-            if (! $cm = get_coursemodule_from_instance("glossary", $this->config->glossary, $this->course->id)) {
+            if (! $cm = get_coursemodule_from_instance('glossary', $glossary->id, $this->config->courseid)) {
                 return false;
             }
             $glossaryctx = context_module::instance($cm->id);
@@ -144,13 +154,22 @@ class block_glossary_random extends block_base {
         }
 
         require_once($CFG->dirroot.'/course/lib.php');
-        $course = $this->page->course;
-        $modinfo = get_fast_modinfo($course);
-        $glossaryid = $this->config->glossary;
 
-        if (!isset($modinfo->instances['glossary'][$glossaryid])) {
-            // we can get here if the glossary has been deleted, so
-            // unconfigure the glossary from the block..
+        // If $this->config->globalglossary is not set then get glossary info from db.
+        if (!isset($this->config->globalglossary)) {
+            if (!$glossary = $DB->get_record('glossary', array('id' => $this->config->glossary))) {
+                return '';
+            } else {
+                $this->config->courseid = $glossary->course;
+                $this->config->globalglossary = $glossary->globalglossary;
+                $this->instance_config_commit();
+            }
+        }
+
+        $modinfo = get_fast_modinfo($this->config->courseid);
+        // If deleted glossary or non-global glossary on different course page, then reset.
+        if (!isset($modinfo->instances['glossary'][$this->config->glossary])
+                || ((empty($this->config->globalglossary) && ($this->config->courseid != $this->page->course->id)))) {
             $this->config->glossary = 0;
             $this->config->cache = '';
             $this->instance_config_commit();
@@ -161,8 +180,7 @@ class block_glossary_random extends block_base {
             return $this->content;
         }
 
-        $cm = $modinfo->instances['glossary'][$glossaryid];
-
+        $cm = $modinfo->instances['glossary'][$this->config->glossary];
         if (!has_capability('mod/glossary:view', context_module::instance($cm->id))) {
             return '';
         }
@@ -176,12 +194,10 @@ class block_glossary_random extends block_base {
         }
 
         $this->content = new stdClass();
-        $this->content->text = $this->config->cache;
-
-        // place link to glossary in the footer if the glossary is visible
 
-        //Obtain the visible property from the instance
-        if ($cm->uservisible) {
+        // Show glossary if visible and place links in footer.
+        if ($cm->visible) {
+            $this->content->text = $this->config->cache;
             if (has_capability('mod/glossary:write', context_module::instance($cm->id))) {
                 $this->content->footer = '<a href="'.$CFG->wwwroot.'/mod/glossary/edit.php?cmid='.$cm->id
                 .'" title="'.$this->config->addentry.'">'.$this->config->addentry.'</a><br />';
@@ -192,7 +208,7 @@ class block_glossary_random extends block_base {
             $this->content->footer .= '<a href="'.$CFG->wwwroot.'/mod/glossary/view.php?id='.$cm->id
                 .'" title="'.$this->config->viewglossary.'">'.$this->config->viewglossary.'</a>';
 
-        // otherwise just place some text, no link
+        // Otherwise just place some text, no link.
         } else {
             $this->content->footer = $this->config->invisible;
         }
index d9c613c..24b2df1 100644 (file)
@@ -41,7 +41,7 @@ class block_glossary_random_edit_form extends block_edit_form {
         $mform->setType('config_title', PARAM_TEXT);
 
         // Select glossaries to put in dropdown box ...
-        $glossaries = $DB->get_records_menu('glossary', array('course' => $this->block->course->id), 'name', 'id,name');
+        $glossaries = $DB->get_records_select_menu('glossary', 'course = ? OR globalglossary = ?', array($this->block->course->id, 1), 'name', 'id,name');
         foreach($glossaries as $key => $value) {
             $glossaries[$key] = strip_tags(format_string($value, true));
         }
index a5d21fa..67280cc 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112902;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013020400;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_glossary_random'; // Full name of the plugin (used for diagnostics)
index d4a491f..6373a01 100644 (file)
@@ -1,46 +1,46 @@
-<?php\r
-\r
-/**\r
- * Provides support for the conversion of moodle1 backup to the moodle2 format\r
- *\r
- * @package    block_html\r
- * @copyright  2012 Paul Nicholls\r
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r
- */\r
-\r
-defined('MOODLE_INTERNAL') || die();\r
-\r
-/**\r
- * Block conversion handler for html\r
- */\r
-class moodle1_block_html_handler extends moodle1_block_handler {\r
-    private $fileman = null;\r
-    protected function convert_configdata(array $olddata) {\r
-        $instanceid = $olddata['id'];\r
-        $contextid  = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);\r
-        $configdata = unserialize(base64_decode($olddata['configdata']));\r
-\r
-        // get a fresh new file manager for this instance\r
-        $this->fileman = $this->converter->get_file_manager($contextid, 'block_html');\r
-\r
-        // convert course files embedded in the block content\r
-        $this->fileman->filearea = 'content';\r
-        $this->fileman->itemid   = 0;\r
-        $configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);\r
-        $configdata->format = FORMAT_HTML;\r
-\r
-        return base64_encode(serialize($configdata));\r
-    }\r
-\r
-    protected function write_inforef_xml($newdata, $data) {\r
-        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");\r
-        $this->xmlwriter->begin_tag('inforef');\r
-        $this->xmlwriter->begin_tag('fileref');\r
-        foreach ($this->fileman->get_fileids() as $fileid) {\r
-            $this->write_xml('file', array('id' => $fileid));\r
-        }\r
-        $this->xmlwriter->end_tag('fileref');\r
-        $this->xmlwriter->end_tag('inforef');\r
-        $this->close_xml_writer();\r
-    }\r
-}
\ No newline at end of file
+<?php
+
+/**
+ * Provides support for the conversion of moodle1 backup to the moodle2 format
+ *
+ * @package    block_html
+ * @copyright  2012 Paul Nicholls
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block conversion handler for html
+ */
+class moodle1_block_html_handler extends moodle1_block_handler {
+    private $fileman = null;
+    protected function convert_configdata(array $olddata) {
+        $instanceid = $olddata['id'];
+        $contextid  = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);
+        $configdata = unserialize(base64_decode($olddata['configdata']));
+
+        // get a fresh new file manager for this instance
+        $this->fileman = $this->converter->get_file_manager($contextid, 'block_html');
+
+        // convert course files embedded in the block content
+        $this->fileman->filearea = 'content';
+        $this->fileman->itemid   = 0;
+        $configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);
+        $configdata->format = FORMAT_HTML;
+
+        return base64_encode(serialize($configdata));
+    }
+
+    protected function write_inforef_xml($newdata, $data) {
+        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");
+        $this->xmlwriter->begin_tag('inforef');
+        $this->xmlwriter->begin_tag('fileref');
+        foreach ($this->fileman->get_fileids() as $fileid) {
+            $this->write_xml('file', array('id' => $fileid));
+        }
+        $this->xmlwriter->end_tag('fileref');
+        $this->xmlwriter->end_tag('inforef');
+        $this->close_xml_writer();
+    }
+}
index 95da634..d698365 100644 (file)
@@ -1,34 +1,34 @@
-<?php\r
-\r
-/**\r
- * Provides support for the conversion of moodle1 backup to the moodle2 format\r
- *\r
- * @package    block_rss_client\r
- * @copyright  2012 Paul Nicholls\r
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r
- */\r
-\r
-defined('MOODLE_INTERNAL') || die();\r
-\r
-/**\r
- * Block conversion handler for rss_client\r
- */\r
-class moodle1_block_rss_client_handler extends moodle1_block_handler {\r
-    public function process_block(array $data) {\r
-        parent::process_block($data);\r
-        $instanceid = $data['id'];\r
-        $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);\r
-\r
-        // Moodle 1.9 backups do not include sufficient data to restore feeds, so we need an empty shell rss_client.xml\r
-        // for the restore process to find\r
-        $this->open_xml_writer("course/blocks/{$data['name']}_{$instanceid}/rss_client.xml");\r
-        $this->xmlwriter->begin_tag('block', array('id' => $instanceid, 'contextid' => $contextid, 'blockname' => 'rss_client'));\r
-        $this->xmlwriter->begin_tag('rss_client', array('id' => $instanceid));\r
-        $this->xmlwriter->full_tag('feeds', '');\r
-        $this->xmlwriter->end_tag('rss_client');\r
-        $this->xmlwriter->end_tag('block');\r
-        $this->close_xml_writer();\r
-\r
-        return $data;\r
-    }\r
-}
\ No newline at end of file
+<?php
+
+/**
+ * Provides support for the conversion of moodle1 backup to the moodle2 format
+ *
+ * @package    block_rss_client
+ * @copyright  2012 Paul Nicholls
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block conversion handler for rss_client
+ */
+class moodle1_block_rss_client_handler extends moodle1_block_handler {
+    public function process_block(array $data) {
+        parent::process_block($data);
+        $instanceid = $data['id'];
+        $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);
+
+        // Moodle 1.9 backups do not include sufficient data to restore feeds, so we need an empty shell rss_client.xml
+        // for the restore process to find
+        $this->open_xml_writer("course/blocks/{$data['name']}_{$instanceid}/rss_client.xml");
+        $this->xmlwriter->begin_tag('block', array('id' => $instanceid, 'contextid' => $contextid, 'blockname' => 'rss_client'));
+        $this->xmlwriter->begin_tag('rss_client', array('id' => $instanceid));
+        $this->xmlwriter->full_tag('feeds', '');
+        $this->xmlwriter->end_tag('rss_client');
+        $this->xmlwriter->end_tag('block');
+        $this->close_xml_writer();
+
+        return $data;
+    }
+}
index 3563843..2ac37aa 100644 (file)
@@ -25,6 +25,6 @@
  */
 
 $string['enabledock'] = 'Allow the user to dock this block';
-$string['pluginname'] = 'Settings';
-$string['settings:addinstance'] = 'Add a new settings block';
-$string['settings:myaddinstance'] = 'Add a new settings block to the My Moodle page';
+$string['pluginname'] = 'Administration';
+$string['settings:addinstance'] = 'Add a new administration block';
+$string['settings:myaddinstance'] = 'Add a new administration block to the My Moodle page';
index 919fed1..42ab6e3 100644 (file)
@@ -87,8 +87,8 @@ class blog_edit_form extends moodleform {
                 }
 
                 if (has_capability('moodle/blog:associatecourse', $context)) {
-                    $mform->addElement('header', 'assochdr', get_string('associations', 'blog'));\r
-                    $mform->addElement('advcheckbox', 'courseassoc', get_string('associatewithcourse', 'blog', $a), null, null, array(0, $contextid));\r
+                    $mform->addElement('header', 'assochdr', get_string('associations', 'blog'));
+                    $mform->addElement('advcheckbox', 'courseassoc', get_string('associatewithcourse', 'blog', $a), null, null, array(0, $contextid));
                     $mform->setDefault('courseassoc', $contextid);
                 }
 
index fee8aea..232aa46 100644 (file)
@@ -71,6 +71,12 @@ class cache_config {
      */
     protected $configlocks = array();
 
+    /**
+     * The site identifier used when the cache config was last saved.
+     * @var string
+     */
+    protected $siteidentifier = null;
+
     /**
      * Please use cache_config::instance to get an instance of the cache config that is ready to be used.
      */
@@ -139,6 +145,12 @@ class cache_config {
         $this->configdefinitionmappings = array();
         $this->configlockmappings = array();
 
+        $siteidentifier = 'unknown';
+        if (array_key_exists('siteidentifier', $configuration)) {
+            $siteidentifier = $configuration['siteidentifier'];
+        }
+        $this->siteidentifier = $siteidentifier;
+
         // Filter the lock instances.
         $defaultlock = null;
         foreach ($configuration['locks'] as $conf) {
@@ -271,6 +283,14 @@ class cache_config {
         return true;
     }
 
+    /**
+     * Returns the site identifier used by the cache API.
+     * @return string
+     */
+    public function get_site_identifier() {
+        return $this->siteidentifier;
+    }
+
     /**
      * Includes the configuration file and makes sure it contains the expected bits.
      *
index e278bbb..8c5a16a 100644 (file)
@@ -723,7 +723,8 @@ class cache_definition {
      */
     public function generate_single_key_prefix() {
         if ($this->keyprefixsingle === null) {
-            $this->keyprefixsingle = $this->mode.'/'.$this->mode;
+            $this->keyprefixsingle = $this->mode.'/'.$this->component.'/'.$this->area;
+            $this->keyprefixsingle .= '/'.$this->get_cache_identifier();
             $identifiers = $this->get_identifiers();
             if ($identifiers) {
                 foreach ($identifiers as $key => $value) {
@@ -746,6 +747,7 @@ class cache_definition {
                 'mode' => $this->mode,
                 'component' => $this->component,
                 'area' => $this->area,
+                'siteidentifier' => $this->get_cache_identifier()
             );
             if (!empty($this->identifiers)) {
                 $identifiers = array();
@@ -785,4 +787,13 @@ class cache_definition {
     public function get_invalidation_events() {
         return $this->invalidationevents;
     }
+
+    /**
+     * Returns a cache identification string.
+     *
+     * @return string A string to be used as part of keys.
+     */
+    protected function get_cache_identifier() {
+        return cache_helper::get_site_identifier();
+    }
 }
\ No newline at end of file
index f3d520c..24c7c1e 100644 (file)
@@ -203,6 +203,7 @@ class cache_factory {
         }
         // Get the class. Note this is a late static binding so we need to use get_called_class.
         $definition = cache_definition::load_adhoc($mode, $component, $area, $options);
+        $config = $this->create_config_instance();
         $definition->set_identifiers($identifiers);
         $cache = $this->create_cache($definition, $identifiers);
         if ($definition->should_be_persistent()) {
index 4439a56..a8f3a26 100644 (file)
@@ -54,6 +54,13 @@ class cache_helper {
      */
     protected static $instance;
 
+    /**
+     * The site identifier used by the cache.
+     * Set the first time get_site_identifier is called.
+     * @var string
+     */
+    protected static $siteidentifier = null;
+
     /**
      * Returns true if the cache API can be initialised before Moodle has finished initialising itself.
      *
@@ -489,11 +496,52 @@ class cache_helper {
      */
     public static function update_definitions($coreonly = false) {
         global $CFG;
-        // Include locallib
+        // Include locallib.
         require_once($CFG->dirroot.'/cache/locallib.php');
         // First update definitions
         cache_config_writer::update_definitions($coreonly);
         // Second reset anything we have already initialised to ensure we're all up to date.
         cache_factory::reset();
     }
-}
\ No newline at end of file
+
+    /**
+     * Update the site identifier stored by the cache API.
+     *
+     * @param string $siteidentifier
+     */
+    public static function update_site_identifier($siteidentifier) {
+        global $CFG;
+        // Include locallib.
+        require_once($CFG->dirroot.'/cache/locallib.php');
+        $factory = cache_factory::instance();
+        $factory->updating_started();
+        $config = $factory->create_config_instance(true);
+        $config->update_site_identifier($siteidentifier);
+        $factory->updating_finished();
+        cache_factory::reset();
+    }
+
+    /**
+     * Returns the site identifier.
+     *
+     * @return string
+     */
+    public static function get_site_identifier() {
+        if (is_null(self::$siteidentifier)) {
+            $factory = cache_factory::instance();
+            $config = $factory->create_config_instance();
+            self::$siteidentifier = $config->get_site_identifier();
+        }
+        return self::$siteidentifier;
+    }
+
+    /**
+     * Returns the site version.
+     *
+     * @return string
+     */
+    public static function get_site_version() {
+        global $CFG;
+        return (string)$CFG->version;
+    }
+}
index ce926a5..4d6d745 100644 (file)
@@ -119,6 +119,7 @@ class cache_config_writer extends cache_config {
      */
     protected function generate_configuration_array() {
         $configuration = array();
+        $configuration['siteidentifier'] = $this->siteidentifier;
         $configuration['stores'] = $this->configstores;
         $configuration['modemappings'] = $this->configmodemappings;
         $configuration['definitions'] = $this->configdefinitions;
@@ -524,6 +525,15 @@ class cache_config_writer extends cache_config {
         $this->config_save();
     }
 
+    /**
+     * Update the site identifier stored by the cache API.
+     *
+     * @param string $siteidentifier
+     */
+    public function update_site_identifier($siteidentifier) {
+        $this->siteidentifier = md5((string)$siteidentifier);
+        $this->config_save();
+    }
 }
 
 /**
@@ -1002,4 +1012,4 @@ abstract class cache_administration_helper extends cache_helper {
         }
         return $locks;
     }
-}
\ No newline at end of file
+}
index 98d9c5b..545986a 100644 (file)
@@ -553,6 +553,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
             'invalidationevents' => array(
                 'crazyevent'
             )
@@ -567,13 +569,16 @@ class cache_phpunit_tests extends advanced_testcase {
 
         // OK data added, data invalidated, and invalidation time has been set.
         // Now we need to manually add back the data and adjust the invalidation time.
-        $timefile = $CFG->dataroot.'/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/a65/a65b1dc524cf6e03c1795197c84d5231eb229b86.cache';
+        $hash = md5(cache_store::MODE_APPLICATION.'/phpunit/eventinvalidationtest/'.$CFG->wwwroot.'phpunit');
+        $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las/lastinvalidation-$hash.cache";
+        // Make sure the file is correct.
+        $this->assertTrue(file_exists($timefile));
         $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
         make_writable_directory(dirname($timefile));
         file_put_contents($timefile, $timecont);
         $this->assertTrue(file_exists($timefile));
 
-        $datafile = $CFG->dataroot.'/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/626/626e9c7a45febd98f064c2b383de8d9d4ebbde7b.cache';
+        $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes/testkey1-$hash.cache";
         $datacont = serialize("test data 1");
         make_writable_directory(dirname($datafile));
         file_put_contents($datafile, $datacont);
@@ -586,6 +591,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
         ));
         $cache = cache::make('phpunit', 'eventinvalidationtest');
         $this->assertEquals('test data 1', $cache->get('testkey1'));
@@ -597,6 +604,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
             'invalidationevents' => array(
                 'crazyevent'
             )
index d5e5297..471521b 100644 (file)
@@ -102,6 +102,16 @@ class cache_config_phpunittest extends cache_config_writer {
             'sort' => (int)$sort
         );
     }
+
+    /**
+     * Overrides the default site identifier used by the Cache API so that we can be sure of what it is.
+     *
+     * @return string
+     */
+    public function get_site_identifier() {
+        global $CFG;
+        return $CFG->wwwroot.'phpunit';
+    }
 }
 
 /**
index 91b99ca..131a803 100644 (file)
@@ -35,6 +35,10 @@ if (empty($CFG->usecomments)) {
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 
+if ( $contextid == SYSCONTEXTID ) {
+    $course = $SITE;
+}
+
 $PAGE->set_url('/comment/comment_ajax.php');
 
 // Allow anonymous user to view comments providing forcelogin now enabled
index 88a2c5b..0cdcc5f 100644 (file)
@@ -63,28 +63,7 @@ $CFG->dboptions = array(
 
 
 //=========================================================================
-// 2. SECRET PASSWORD SALT
-//=========================================================================
-// User password salt is very important security feature, it is created
-// automatically in installer, you have to uncomment and modify value
-// on the next line if you are creating config.php manually.
-//
-// $CFG->passwordsaltmain = 'a_very_long_random_string_of_characters#@6&*1';
-//
-// After changing the main salt you have to copy old value into one
-// of the following settings - this allows migration to the new salt
-// during the next login of each user.
-//
-// $CFG->passwordsaltalt1 = '';
-// $CFG->passwordsaltalt2 = '';
-// $CFG->passwordsaltalt3 = '';
-// ....
-// $CFG->passwordsaltalt19 = '';
-// $CFG->passwordsaltalt20 = '';
-
-
-//=========================================================================
-// 3. WEB SITE LOCATION
+// 2. WEB SITE LOCATION
 //=========================================================================
 // Now you need to tell Moodle where it is located. Specify the full
 // web address to where moodle has been installed.  If your web site
@@ -98,7 +77,7 @@ $CFG->wwwroot   = 'http://example.com/moodle';
 
 
 //=========================================================================
-// 4. DATA FILES LOCATION
+// 3. DATA FILES LOCATION
 //=========================================================================
 // Now you need a place where Moodle can save uploaded files.  This
 // directory should be readable AND WRITEABLE by the web server user
@@ -114,7 +93,7 @@ $CFG->dataroot  = '/home/example/moodledata';
 
 
 //=========================================================================
-// 5. DATA FILES PERMISSIONS
+// 4. DATA FILES PERMISSIONS
 //=========================================================================
 // The following parameter sets the permissions of new directories
 // created by Moodle within the data directory.  The format is in
@@ -128,7 +107,7 @@ $CFG->directorypermissions = 02777;
 
 
 //=========================================================================
-// 6. DIRECTORY LOCATION  (most people can just ignore this setting)
+// 5. DIRECTORY LOCATION  (most people can just ignore this setting)
 //=========================================================================
 // A very few webhosts use /admin as a special URL for you to access a
 // control panel or something.  Unfortunately this conflicts with the
@@ -140,7 +119,7 @@ $CFG->admin = 'admin';
 
 
 //=========================================================================
-// 7. OTHER MISCELLANEOUS SETTINGS (ignore these for new installations)
+// 6. OTHER MISCELLANEOUS SETTINGS (ignore these for new installations)
 //=========================================================================
 //
 // These are additional tweaks for which no GUI exists in Moodle yet.
@@ -471,7 +450,7 @@ $CFG->admin = 'admin';
 //      $CFG->svgicons = false;
 //
 //=========================================================================
-// 8. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
+// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
 //
 // Force a debugging mode regardless the settings in the site administration
@@ -512,7 +491,7 @@ $CFG->admin = 'admin';
 // $CFG->showcrondebugging = true;
 //
 //=========================================================================
-// 9. FORCED SETTINGS
+// 8. FORCED SETTINGS
 //=========================================================================
 // It is possible to specify normal admin settings here, the point is that
 // they can not be changed through the standard admin settings pages any more.
@@ -527,12 +506,35 @@ $CFG->admin = 'admin';
 //                                        'otherplugin' => array('mysetting' => 'myvalue', 'thesetting' => 'thevalue'));
 //
 //=========================================================================
-// 10. PHPUNIT SUPPORT
+// 9. PHPUNIT SUPPORT
 //=========================================================================
 // $CFG->phpunit_prefix = 'phpu_';
 // $CFG->phpunit_dataroot = '/home/example/phpu_moodledata';
 // $CFG->phpunit_directorypermissions = 02777; // optional
 //
+//
+//=========================================================================
+// 10. SECRET PASSWORD SALT
+//=========================================================================
+// A single site-wide password salt is no longer required *unless* you are
+// upgrading an older version of Moodle (prior to 2.5), or if you are using
+// a PHP version below 5.3.7. If upgrading, keep any values from your old
+// config.php file. If you are using PHP < 5.3.7 set to a long random string
+// below:
+//
+// $CFG->passwordsaltmain = 'a_very_long_random_string_of_characters#@6&*1';
+//
+// You may also have some alternative salts to allow migration from previously
+// used salts.
+//
+// $CFG->passwordsaltalt1 = '';
+// $CFG->passwordsaltalt2 = '';
+// $CFG->passwordsaltalt3 = '';
+// ....
+// $CFG->passwordsaltalt19 = '';
+// $CFG->passwordsaltalt20 = '';
+//
+//
 //=========================================================================
 // 11. BEHAT SUPPORT
 //=========================================================================
index 1ee8395..ee8eb70 100644 (file)
@@ -435,7 +435,7 @@ M.course_dndupload = {
      * @param section the DOM element reperesenting the course section
      * @return DOM element containing the new item
      */
-    add_resource_element: function(name, section) {
+    add_resource_element: function(name, section, module) {
         var modsel = this.get_mods_element(section);
 
         var resel = {
@@ -451,7 +451,7 @@ M.course_dndupload = {
             progress: document.createElement('span')
         };
 
-        resel.li.className = 'activity resource modtype_resource';
+        resel.li.className = 'activity ' + module + ' modtype_' + module;
 
         resel.indentdiv.className = 'mod-indent';
         resel.li.appendChild(resel.indentdiv);
@@ -707,7 +707,7 @@ M.course_dndupload = {
         }
 
         // Add the file to the display
-        var resel = this.add_resource_element(file.name, section);
+        var resel = this.add_resource_element(file.name, section, module);
 
         // Update the progress bar as the file is uploaded
         xhr.upload.addEventListener('progress', function(e) {
@@ -726,30 +726,37 @@ M.course_dndupload = {
                     if (result) {
                         if (result.error == 0) {
                             // All OK - update the dummy element
-                            resel.icon.src = result.icon;
-                            resel.a.href = result.link;
-                            resel.namespan.innerHTML = result.name;
-                            if (!parseInt(result.visible, 10)) {
-                                resel.a.className = 'dimmed';
-                            }
-
-                            if (result.groupingname) {
-                                resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                            if (result.content) {
+                                // A label
+                                resel.indentdiv.innerHTML = '<div class="activityinstance" ></div>' + result.content + result.commands;
                             } else {
-                                resel.div.removeChild(resel.groupingspan);
+                                // Not a label
+                                resel.icon.src = result.icon;
+                                resel.a.href = result.link;
+                                resel.namespan.innerHTML = result.name;
+
+                                if (!parseInt(result.visible, 10)) {
+                                    resel.a.className = 'dimmed';
+                                }
+
+                                if (result.groupingname) {
+                                    resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                                } else {
+                                    resel.div.removeChild(resel.groupingspan);
+                                }
+
+                                resel.div.removeChild(resel.progressouter);
+                                resel.indentdiv.innerHTML += result.commands;
+                                if (result.onclick) {
+                                    resel.a.onclick = result.onclick;
+                                }
+                                if (self.Y.UA.gecko > 0) {
+                                    // Fix a Firefox bug which makes sites with a '~' in their wwwroot
+                                    // log the user out when clicking on the link (before refreshing the page).
+                                    resel.div.innerHTML = unescape(resel.div.innerHTML);
+                                }
                             }
-
-                            resel.div.removeChild(resel.progressouter);
                             resel.li.id = result.elementid;
-                            resel.indentdiv.innerHTML += result.commands;
-                            if (result.onclick) {
-                                resel.a.onclick = result.onclick;
-                            }
-                            if (self.Y.UA.gecko > 0) {
-                                // Fix a Firefox bug which makes sites with a '~' in their wwwroot
-                                // log the user out when clicking on the link (before refreshing the page).
-                                resel.div.innerHTML = unescape(resel.div.innerHTML);
-                            }
                             self.add_editing(result.elementid);
                         } else {
                             // Error - remove the dummy element
@@ -905,7 +912,7 @@ M.course_dndupload = {
         var self = this;
 
         // Add the item to the display
-        var resel = this.add_resource_element(name, section);
+        var resel = this.add_resource_element(name, section, module);
 
         // Wait for the AJAX call to complete, then update the
         // dummy element with the returned details
@@ -916,30 +923,37 @@ M.course_dndupload = {
                     if (result) {
                         if (result.error == 0) {
                             // All OK - update the dummy element
-                            resel.icon.src = result.icon;
-                            resel.a.href = result.link;
-                            resel.namespan.innerHTML = result.name;
-                            if (!parseInt(result.visible, 10)) {
-                                resel.a.className = 'dimmed';
-                            }
-
-                            if (result.groupingname) {
-                                resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                            if (result.content) {
+                                // A label
+                                resel.indentdiv.innerHTML = '<div class="activityinstance" ></div>' + result.content + result.commands;
                             } else {
-                                resel.div.removeChild(resel.groupingspan);
+                                // Not a label
+                                resel.icon.src = result.icon;
+                                resel.a.href = result.link;
+                                resel.namespan.innerHTML = result.name;
+
+                                if (!parseInt(result.visible, 10)) {
+                                    resel.a.className = 'dimmed';
+                                }
+
+                                if (result.groupingname) {
+                                    resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                                } else {
+                                    resel.div.removeChild(resel.groupingspan);
+                                }
+
+                                resel.div.removeChild(resel.progressouter);
+                                resel.div.innerHTML += result.commands;
+                                if (result.onclick) {
+                                    resel.a.onclick = result.onclick;
+                                }
+                                if (self.Y.UA.gecko > 0) {
+                                    // Fix a Firefox bug which makes sites with a '~' in their wwwroot
+                                    // log the user out when clicking on the link (before refreshing the page).
+                                    resel.div.innerHTML = unescape(resel.div.innerHTML);
+                                }
                             }
-
-                            resel.div.removeChild(resel.progressouter);
                             resel.li.id = result.elementid;
-                            resel.div.innerHTML += result.commands;
-                            if (result.onclick) {
-                                resel.a.onclick = result.onclick;
-                            }
-                            if (self.Y.UA.gecko > 0) {
-                                // Fix a Firefox bug which makes sites with a '~' in their wwwroot
-                                // log the user out when clicking on the link (before refreshing the page).
-                                resel.div.innerHTML = unescape(resel.div.innerHTML);
-                            }
                             self.add_editing(result.elementid, sectionnumber);
                         } else {
                             // Error - remove the dummy element
index 73c8a79..0cdd8bc 100644 (file)
@@ -678,7 +678,12 @@ class dndupload_ajax_processor {
         $resp->error = self::ERROR_OK;
         $resp->icon = $mod->get_icon_url()->out();
         $resp->name = $mod->name;
-        $resp->link = $mod->get_url()->out();
+        if ($mod->has_view()) {
+            $resp->link = $mod->get_url()->out();
+        } else {
+            $resp->link = null;
+        }
+        $resp->content = $mod->get_content();
         $resp->elementid = 'module-'.$mod->id;
         $actions = course_get_cm_edit_actions($mod, 0, $mod->sectionnum);
         $resp->commands = ' '. $courserenderer->course_section_cm_edit_actions($actions);
index 82af237..f353a4b 100644 (file)
@@ -336,6 +336,7 @@ class format_legacy extends format_base {
      * @return bool whether there were any changes to the options values
      */
     public function update_course_format_options($data, $oldcourse = null) {
+        global $DB;
         if ($oldcourse !== null) {
             $data = (array)$data;
             $oldcourse = (array)$oldcourse;
index 71e8797..172d1d9 100644 (file)
@@ -40,7 +40,7 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap menus
+    // Swap menus.
     sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
 }
 
@@ -59,7 +59,13 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
     };
 
     if (response.action == 'move') {
-        // update titles in all affected sections
+        // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
+        if (sectionfrom > sectionto) {
+            var temp = sectionto;
+            sectionto = sectionfrom;
+            sectionfrom = temp;
+        }
+        // Update titles in all affected sections.
         for (var i = sectionfrom; i <= sectionto; i++) {
             sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
         }
index f26cafd..debe045 100644 (file)
@@ -271,6 +271,7 @@ class format_topics extends format_base {
      * @return bool whether there were any changes to the options values
      */
     public function update_course_format_options($data, $oldcourse = null) {
+        global $DB;
         if ($oldcourse !== null) {
             $data = (array)$data;
             $oldcourse = (array)$oldcourse;
index 28ec82a..6433702 100644 (file)
@@ -40,7 +40,7 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap menus
+    // Swap menus.
     sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
 }
 
@@ -59,7 +59,13 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
     };
 
     if (response.action == 'move') {
-        // update titles in all affected sections
+        // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
+        if (sectionfrom > sectionto) {
+            var temp = sectionto;
+            sectionto = sectionfrom;
+            sectionfrom = temp;
+        }
+        // Update titles in all affected sections.
         for (var i = sectionfrom; i <= sectionto; i++) {
             sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
         }
index a5c1153..40686d4 100644 (file)
@@ -276,6 +276,7 @@ class format_weeks extends format_base {
      * @return bool whether there were any changes to the options values
      */
     public function update_course_format_options($data, $oldcourse = null) {
+        global $DB;
         if ($oldcourse !== null) {
             $data = (array)$data;
             $oldcourse = (array)$oldcourse;
index 241580f..5b901b2 100644 (file)
@@ -2193,15 +2193,22 @@ function course_delete_module($cmid) {
     if (file_exists($modlib)) {
         require_once($modlib);
     } else {
-        throw new moodle_exception("This module is missing mod/$modulename/lib.php", '', '',
-            null, 'failedtodeletemodulemissinglibfile');
+        throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null,
+            "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
     }
 
-    $deleteinstancefunction = $modulename . "_delete_instance";
+    $deleteinstancefunction = $modulename . '_delete_instance';
 
+    // Ensure the delete_instance function exists for this module.
+    if (!function_exists($deleteinstancefunction)) {
+        throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
+            "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
+    }
+
+    // Call the delete_instance function, if it returns false throw an exception.
     if (!$deleteinstancefunction($cm->instance)) {
-        throw new moodle_exception("Could not delete the $modulename (instance)", '', '',
-            null, 'failedtodeletemoduleinstance');
+        throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
+            "Cannot delete the module $modulename (instance).");
     }
 
     // Remove all module files in case modules forget to do that.
@@ -2240,8 +2247,8 @@ function course_delete_module($cmid) {
 
     // Delete module from that section.
     if (!delete_mod_from_section($cm->id, $cm->section)) {
-        throw new moodle_exception("Could not delete the $modulename from section", '', '',
-            null, 'failedtodeletemodulefromsection');
+        throw new moodle_exception('cannotdeletemodulefromsection', '', '', null,
+            "Cannot delete the module $modulename (instance) from section.");
     }
 
     // Trigger a mod_deleted event with information about this module.
index 75eba57..33c0f0a 100644 (file)
@@ -25,14 +25,13 @@ Feature: Add activities to courses
       | Introduction | Test database description |
       | Required entries | 9 |
       | Comments | Yes |
-      | ID number | ASD123 |
+    And I turn editing mode off
     Then I should not see "Adding a new"
     And I follow "Test name"
     And I follow "Edit settings"
     And the "Name" field should match "Test name" value
     And the "Required entries" field should match "9" value
     And the "Comments" field should match "Yes" value
-    And the "ID number" field should match "ASD123" value
 
   @javascript
   Scenario: Add an activity without the required fields
index 3f5b4ba..e20cdc5 100644 (file)
@@ -66,10 +66,6 @@ class behat_course extends behat_base {
      */
     public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
 
-        $activity = $this->fixStepArgument($activity);
-        $section = $this->fixStepArgument($section);
-
-        // The 'I wait until the page is ready' is just in case.
         return array(
             new Given('I add a "'.$activity.'" to section "'.$section.'"'),
             new Given('I fill the moodle form with:', $data),
@@ -81,6 +77,7 @@ class behat_course extends behat_base {
      * Opens the activity chooser and opens the activity/resource form page.
      *
      * @Given /^I add a "(?P<activity_or_resource_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)"$/
+     * @throws ElementNotFoundException Thrown by behat_base::find
      * @param string $activity
      * @param string $section
      */
@@ -90,14 +87,14 @@ class behat_course extends behat_base {
         $section = $this->fixStepArgument($section);
 
         // Clicks add activity or resource section link.
-        $sectionxpath = "//*[@id='section-" . $section . "']/*/*/*/div[@class='section-modchooser']/*/*";
-        $section = $this->getSession()->getPage()->find('xpath', $sectionxpath);
-        $section->click();
+        $sectionxpath = "//*[@id='section-" . $section . "']/*/*/*/div[@class='section-modchooser']/span/a";
+        $sectionnode = $this->find('xpath', $sectionxpath);
+        $sectionnode->click();
 
         // Clicks the selected activity if it exists.
         $activityxpath = ".//label[contains(.,'" . $activity . "')]/input";
-        $activity = $this->getSession()->getPage()->find('xpath', $activityxpath);
-        $activity->doubleClick();
+        $activitynode = $this->find('xpath', $activityxpath);
+        $activitynode->doubleClick();
     }
 
 }
index a59351b..ef8ef1b 100644 (file)
@@ -297,22 +297,71 @@ class courselib_testcase extends advanced_testcase {
     }
 
     public function test_move_module_in_course() {
+        global $DB;
+
         $this->resetAfterTest(true);
         // Setup fixture
-        $course = $this->getDataGenerator()->create_course(array('numsections'=>5));
+        $course = $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections' => true));
         $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
 
         $cms = get_fast_modinfo($course)->get_cms();
         $cm = reset($cms);
 
-        course_create_sections_if_missing($course, 3);
-        $section3 = get_fast_modinfo($course)->get_section_info(3);
+        $newsection = get_fast_modinfo($course)->get_section_info(3);
+        $oldsectionid = $cm->section;
+
+        // Perform the move
+        moveto_module($cm, $newsection);
 
-        moveto_module($cm, $section3);
+        // reset of get_fast_modinfo is usually called the code calling moveto_module so call it here
+        get_fast_modinfo(0, 0, true);
+        $cms = get_fast_modinfo($course)->get_cms();
+        $cm = reset($cms);
 
+        // Check that the cached modinfo contains the correct section info
         $modinfo = get_fast_modinfo($course);
         $this->assertTrue(empty($modinfo->sections[0]));
         $this->assertFalse(empty($modinfo->sections[3]));
+
+        // Check that the old section's sequence no longer contains this ID
+        $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
+        $oldsequences = explode(',', $newsection->sequence);
+        $this->assertFalse(in_array($cm->id, $oldsequences));
+
+        // Check that the new section's sequence now contains this ID
+        $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
+        $newsequences = explode(',', $newsection->sequence);
+        $this->assertTrue(in_array($cm->id, $newsequences));
+
+        // Check that the section number has been changed in the cm
+        $this->assertEquals($newsection->id, $cm->section);
+
+
+        // Perform a second move as some issues were only seen on the second move
+        $newsection = get_fast_modinfo($course)->get_section_info(2);
+        $oldsectionid = $cm->section;
+        $result = moveto_module($cm, $newsection);
+        $this->assertTrue($result);
+
+        // reset of get_fast_modinfo is usually called the code calling moveto_module so call it here
+        get_fast_modinfo(0, 0, true);
+        $cms = get_fast_modinfo($course)->get_cms();
+        $cm = reset($cms);
+
+        // Check that the cached modinfo contains the correct section info
+        $modinfo = get_fast_modinfo($course);
+        $this->assertTrue(empty($modinfo->sections[0]));
+        $this->assertFalse(empty($modinfo->sections[2]));
+
+        // Check that the old section's sequence no longer contains this ID
+        $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
+        $oldsequences = explode(',', $newsection->sequence);
+        $this->assertFalse(in_array($cm->id, $oldsequences));
+
+        // Check that the new section's sequence now contains this ID
+        $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
+        $newsequences = explode(',', $newsection->sequence);
+        $this->assertTrue(in_array($cm->id, $newsequences));
     }
 
     public function test_module_visibility() {
index 13c6114..921bb30 100644 (file)
@@ -114,9 +114,9 @@ class imsenterprise_courses {
      * @return array Array of assignable values
      */
     function get_imsnames($courseattr) {
-\r
-        $values = $this->imsnames;\r
-        if ($courseattr == 'summary') {\r
+
+        $values = $this->imsnames;
+        if ($courseattr == 'summary') {
             $values = array_merge(array('ignore' => get_string('emptyattribute', 'enrol_imsenterprise')), $values);
         }
         return $values;
index 5e947b6..3f6a506 100644 (file)
@@ -318,11 +318,12 @@ class course_enrolment_manager {
      * @param array $params query parameters.
      * @param int $page which page number of the results to show.
      * @param int $perpage number of users per page.
+     * @param int $addedenrollment number of users added to enrollment.
      * @return array with two elememts:
      *      int total number of users matching the search.
      *      array of user objects returned by the query.
      */
-    protected function execute_search_queries($search, $fields, $countfields, $sql, array $params, $page, $perpage) {
+    protected function execute_search_queries($search, $fields, $countfields, $sql, array $params, $page, $perpage, $addedenrollment=0) {
         global $DB, $CFG;
 
         list($sort, $sortparams) = users_order_by_sql('u', $search, $this->get_context());
@@ -330,7 +331,7 @@ class course_enrolment_manager {
 
         $totalusers = $DB->count_records_sql($countfields . $sql, $params);
         $availableusers = $DB->get_records_sql($fields . $sql . $order,
-                array_merge($params, $sortparams), $page*$perpage, $perpage);
+                array_merge($params, $sortparams), ($page*$perpage) - $addedenrollment, $perpage);
 
         return array('totalusers' => $totalusers, 'users' => $availableusers);
     }
@@ -344,9 +345,10 @@ class course_enrolment_manager {
      * @param bool $searchanywhere
      * @param int $page Defaults to 0
      * @param int $perpage Defaults to 25
+     * @param int $addedenrollment Defaults to 0
      * @return array Array(totalusers => int, users => array)
      */
-    public function get_potential_users($enrolid, $search='', $searchanywhere=false, $page=0, $perpage=25) {
+    public function get_potential_users($enrolid, $search='', $searchanywhere=false, $page=0, $perpage=25, $addedenrollment=0) {
         global $DB;
 
         list($ufields, $params, $wherecondition) = $this->get_basic_search_conditions($search, $searchanywhere);
@@ -359,7 +361,7 @@ class course_enrolment_manager {
                       AND ue.id IS NULL";
         $params['enrolid'] = $enrolid;
 
-        return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage);
+        return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage, $addedenrollment);
     }
 
     /**
index 093ee05..ea4acd8 100644 (file)
@@ -67,7 +67,9 @@ switch ($action) {
         $enrolid = required_param('enrolid', PARAM_INT);
         $search = optional_param('search', '', PARAM_RAW);
         $page = optional_param('page', 0, PARAM_INT);
-        $outcome->response = $manager->get_potential_users($enrolid, $search, $searchanywhere, $page);
+        $addedenrollment = optional_param('enrolcount', 0, PARAM_INT);
+        $perpage = optional_param('perpage', 25, PARAM_INT);  //  This value is hard-coded to 25 in quickenrolment.js
+        $outcome->response = $manager->get_potential_users($enrolid, $search, $searchanywhere, $page, $perpage, $addedenrollment);
         $extrafields = get_extra_user_fields($context);
         foreach ($outcome->response['users'] as &$user) {
             $user->picture = $OUTPUT->user_picture($user);
index a969354..16ad4a2 100644 (file)
@@ -406,6 +406,7 @@ function enrol_manual_migrate_plugin_enrolments($enrol) {
 
         if (!$minstance) {
             // This should never happen unless adding of default instance fails unexpectedly.
+            debugging('Failed to find manual enrolment instance', DEBUG_DEVELOPER);
             continue;
         }
 
index aad6e62..8362185 100644 (file)
@@ -23,7 +23,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
         DEFAULTDURATION : 'defaultDuration',
         ASSIGNABLEROLES : 'assignableRoles',
         DISABLEGRADEHISTORY : 'disableGradeHistory',
-        RECOVERGRADESDEFAULT : 'recoverGradesDefault'
+        RECOVERGRADESDEFAULT : 'recoverGradesDefault',
+        ENROLCOUNT : 'enrolCount',
+        PERPAGE : 'perPage'
     };
     /** CSS classes for nodes in structure **/
     var CSS = {
@@ -309,6 +311,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             params['action'] = 'searchusers';
             params['search'] = this.get(UEP.SEARCH).get('value');
             params['page'] = this.get(UEP.PAGE);
+            params['enrolcount'] = this.get(UEP.ENROLCOUNT);
+            params['perpage'] = this.get(UEP.PERPAGE);
+
             if (this.get(UEP.MULTIPLE)) {
                 alert('oh no there are multiple');
             } else {
@@ -376,7 +381,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 var content = create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
                     .append(create('<div class="'+CSS.TOTALUSERS+'">'+usersstr+'</div>'))
                     .append(users);
-                if (result.response.totalusers > (this.get(UEP.PAGE)+1)*25) {
+                if (result.response.totalusers > (this.get(UEP.PAGE)+1)*this.get(UEP.PERPAGE)) {
                     var fetchmore = create('<div class="'+CSS.MORERESULTS+'"><a href="#">'+M.str.enrol.ajaxnext25+'</a></div>');
                     fetchmore.on('click', this.search, this, true);
                     content.append(fetchmore)
@@ -384,7 +389,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 this.setContent(content);
                 Y.delegate("click", this.enrolUser, users, '.'+CSS.USER+' .'+CSS.ENROL, this, args);
             } else {
-                if (result.response.totalusers <= (this.get(UEP.PAGE)+1)*25) {
+                if (result.response.totalusers <= (this.get(UEP.PAGE)+1)*this.get(UEP.PERPAGE)) {
                     this.get(UEP.BASE).one('.'+CSS.MORERESULTS).remove();
                 }
             }
@@ -420,6 +425,8 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                                 args.userNode.addClass(CSS.ENROLLED);
                                 args.userNode.one('.'+CSS.ENROL).remove();
                                 this.set(UEP.REQUIREREFRESH, true);
+                                var countenrol = this.get(UEP.ENROLCOUNT)+1;
+                                this.set(UEP.ENROLCOUNT, countenrol);
                             }
                         } catch (e) {
                             new M.core.exception(e);
@@ -532,6 +539,14 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             },
             recoverGradesDefault : {
                 value : ''
+            },
+            enrolCount : {
+                value : 0,
+                validator : Y.Lang.isNumber
+            },
+            perPage : {
+                value: 25,
+                Validator: Y.Lang.isNumber
             }
         }
     });
index c1d9b5b..658d5ce 100644 (file)
@@ -132,6 +132,7 @@ if ($courseid and $outcomes = grade_outcome::fetch_all_local($courseid)) {
         $scale = $outcome->load_scale();
         if (empty($scale->id)) {   // hopefully never happens
             $line[] = $scale->get_name();
+            debugging("Found a scale with no ID ({$scale->get_name()}) while outputting course outcomes", DEBUG_DEVELOPER);
         } else {
             if (empty($scale->courseid)) {
                 $caneditthisscale = $caneditsystemscales;
@@ -181,6 +182,7 @@ if ($outcomes = grade_outcome::fetch_all_global()) {
         $scale = $outcome->load_scale();
         if (empty($scale->id)) {   // hopefully never happens
             $line[] = $scale->get_name();
+            debugging("Found a scale with no ID ({$scale->get_name()}) while outputting global outcomes", DEBUG_DEVELOPER);
         } else {
             if (empty($scale->courseid)) {
                 $caneditthisscale = $caneditsystemscales;
index 82fc9eb..f0c7188 100644 (file)
@@ -81,10 +81,13 @@ switch ($action) {
                 // Warn if the grade is out of bounds.
                 if (is_null($finalgrade)) {
                     // ok
-                } else if ($finalgrade < $grade_item->grademin) {
-                    $errorstr = 'lessthanmin';
-                } else if ($finalgrade > $grade_item->grademax) {
-                    $errorstr = 'morethanmax';
+                } else {
+                    $bounded = $grade_item->bounded_grade($finalgrade);
+                    if ($bounded > $finalgrade) {
+                        $errorstr = 'lessthanmin';
+                    } else if ($bounded < $finalgrade) {
+                        $errorstr = 'morethanmax';
+                    }
                 }
 
                 if ($errorstr) {
index 2890159..067453c 100644 (file)
@@ -256,7 +256,7 @@ class grade_report_grader extends grade_report {
                         } else {
                             $bounded = $gradeitem->bounded_grade($finalgrade);
                             if ($bounded > $finalgrade) {
-                            $errorstr = 'lessthanmin';
+                                $errorstr = 'lessthanmin';
                             } else if ($bounded < $finalgrade) {
                                 $errorstr = 'morethanmax';
                             }
index 0ee92ba..54af548 100644 (file)
@@ -323,9 +323,9 @@ M.gradereport_grader.classes.ajax = function(report, cfg) {
                 this.existingfields[userid][itemid] = new M.gradereport_grader.classes.existingfield(this, userid, itemid);
             }
         }
-        // Hide the Update button
+        // Disable the Update button as we're saving using ajax.
         submitbutton = this.report.Y.one('#gradersubmit');
-        submitbutton.setStyle('visibility', 'hidden');
+        submitbutton.set('disabled', true);
     }
 };
 /**
diff --git a/grade/tests/reportgrader_test.php b/grade/tests/reportgrader_test.php
new file mode 100644 (file)
index 0000000..07be5e3
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/report/user/lib.php.
+ *
+ * @package  core_grade
+ * @category phpunit
+ * @copyright 2012 Andrew Davis
+ * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/grade/lib.php');
+require_once($CFG->dirroot.'/grade/report/grader/lib.php');
+
+/**
+ * Tests grade_report_grader (the grader report)
+ */
+class grade_report_graderlib_testcase extends advanced_testcase {
+
+    /**
+     * Tests grade_report_grader::process_data()
+     *
+     * process_data() processes submitted grade and feedback data
+     */
+    public function test_process_data() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        // Create and enrol a student.
+        $student = $this->getDataGenerator()->create_user(array('username' => 'Student Sam'));
+        $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $this->getDataGenerator()->enrol_user($student->id, $course->id, $role->id);
+
+        // Test with limited grades.
+        $CFG->unlimitedgrades = 0;
+
+        $forummax = 80;
+        $forum1 = $this->getDataGenerator()->create_module('forum', array('assessed' => 1, 'scale' => $forummax, 'course' => $course->id));
+        // Switch the stdClass instance for a grade item instance.
+        $forum1 = grade_item::fetch(array('itemtype' => 'mod', 'itemmodule' => 'forum', 'iteminstance' => $forum1->id, 'courseid' => $course->id));
+
+        $report = $this->create_report($course, $coursecontext);
+        $testgrade = 60.00;
+
+        $data = new stdClass();
+        $data->id = $course->id;
+        $data->report = 'grader';
+
+        $data->grade = array();
+        $data->grade[$student->id] = array();
+        $data->grade[$student->id][$forum1->id] = $testgrade;
+
+        $warnings = $report->process_data($data);
+        $this->assertEquals(count($warnings), 0);
+
+        $studentgrade = grade_grade::fetch(array('itemid' => $forum1->id, '' => $student->id));
+        $this->assertEquals($studentgrade->finalgrade, $testgrade);
+
+        // Grade above max. Should be pulled down to max.
+        $toobig = 200.00;
+        $data->grade[$student->id][$forum1->id] = $toobig;
+        $warnings = $report->process_data($data);
+        $this->assertEquals(count($warnings), 1);
+
+        $studentgrade = grade_grade::fetch(array('itemid' => $forum1->id, '' => $student->id));
+        $this->assertEquals($studentgrade->finalgrade, $forummax);
+
+        // Grade below min. Should be pulled up to min.
+        $toosmall = -10.00;
+        $data->grade[$student->id][$forum1->id] = $toosmall;
+        $warnings = $report->process_data($data);
+        $this->assertEquals(count($warnings), 1);
+
+        $studentgrade = grade_grade::fetch(array('itemid' => $forum1->id, '' => $student->id));
+        $this->assertEquals($studentgrade->finalgrade, 0);
+
+        // Test unlimited grades so we can give a student a grade about max.
+        $CFG->unlimitedgrades = 1;
+
+        $data->grade[$student->id][$forum1->id] = $toobig;
+        $warnings = $report->process_data($data);
+        $this->assertEquals(count($warnings), 0);
+
+        $studentgrade = grade_grade::fetch(array('itemid' => $forum1->id, '' => $student->id));
+        $this->assertEquals($studentgrade->finalgrade, $toobig);
+    }
+
+    private function create_report($course, $coursecontext) {
+
+        $gpr = new grade_plugin_return(array('type' => 'report', 'plugin'=>'grader', 'courseid' => $course->id));
+        $report = new grade_report_grader($course->id, $gpr, $coursecontext);
+
+        return $report;
+    }
+}
index 6afdd97..4323461 100644 (file)
--- a/help.php
+++ b/help.php
@@ -48,15 +48,15 @@ $PAGE->set_context(context_system::instance());
 
 if ($ajax) {
     @header('Content-Type: text/plain; charset=utf-8');
-} else {
-    echo $OUTPUT->header();
 }
 
 if (!$sm->string_exists($identifier.'_help', $component)) {
-    // strings on-diskc cache may be dirty - try to rebuild it and check again
+    // strings on disk-cache may be dirty - try to rebuild it and check again
     $sm->load_component_strings($component, current_language(), true);
 }
 
+$data = new stdClass();
+
 if ($sm->string_exists($identifier.'_help', $component)) {
     $options = new stdClass();
     $options->trusted = false;
@@ -67,26 +67,38 @@ if ($sm->string_exists($identifier.'_help', $component)) {
     $options->newlines = false;
     $options->overflowdiv = !$ajax;
 
-    if ($ajax) {
-        // When using AJAX, the header should be H2 as it is in the same DOM as the calling page.
-        echo $OUTPUT->heading(format_string(get_string($identifier, $component)), 2, 'helpheading');
-    } else {
-        // When not using AJAX, the header should be H1 as it is in it's own window.
-        echo $OUTPUT->heading(format_string(get_string($identifier, $component)), 1, 'helpheading');
-    }
+    $data->heading = format_string(get_string($identifier, $component));
     // Should be simple wiki only MDL-21695
-    echo format_text(get_string($identifier.'_help', $component), FORMAT_MARKDOWN, $options);
+    $data->text =  format_text(get_string($identifier.'_help', $component), FORMAT_MARKDOWN, $options);
 
-    if ($sm->string_exists($identifier.'_link', $component)) {  // Link to further info in Moodle docs
-        $link = get_string($identifier.'_link', $component);
+    $helplink = $identifier . '_link';
+    if ($sm->string_exists($helplink, $component)) {  // Link to further info in Moodle docs
+        $link = get_string($helplink, $component);
         $linktext = get_string('morehelp');
-        echo '<div class="helpdoclink">'.$OUTPUT->doc_link($link, $linktext).'</div>';
-    }
 
+        $data->doclink = new stdClass();
+        $url = new moodle_url(get_docs_url($link));
+        $data->doclink->link = $url->out();
+        $data->doclink->linktext = $linktext;
+        $data->doclink->class = ($CFG->doctonewwindow) ? 'helplinkpopup' : '';
+
+        $completedoclink = html_writer::tag('div', $OUTPUT->doc_link($link, $linktext), array('class' => 'helpdoclink'));
+    }
 } else {
-    echo "<p><strong>TODO</strong>: missing help string [{$identifier}_help, $component]</p>";
+    $data->text = html_writer::tag('p',
+            html_writer::tag('strong', 'TODO') . ": missing help string [{$identifier}_help, {$component}]");
 }
 
-if (!$ajax) {
+if ($ajax) {
+    echo json_encode($data);
+} else {
+    echo $OUTPUT->header();
+    if (isset($data->heading)) {
+        echo $OUTPUT->heading($data->heading, 1, 'helpheading');
+    }
+    echo $data->text;
+    if (isset($completedoclink)) {
+        echo $completedoclink;
+    }
     echo $OUTPUT->footer();
 }
index b6ba495..3a54324 100644 (file)
@@ -43,6 +43,6 @@ $string['downloadedfilecheckfailed'] = 'Alla laetud faili kontroll ebaõnnestus.
 $string['invalidmd5'] = 'Vigane md5';
 $string['missingrequiredfield'] = 'Mõned nõutud väljad on puudu';
 $string['remotedownloaderror'] = 'Komponendi alla tõmbamine serverisse ebaõnnestus, palun kontrolli proksi seadeid, PHP cURL laiendus on tungivalt soovitatav.<br /><br />Sa pead tõmbama <a href="{$a->url}">{$a->url}</a> faili käsitsi, kopeerima selle "{$a->dest}" oma serveris ja pakkima lahti sinna.';
-$string['wrongdestpath'] = 'Vale sihtrada.';
+$string['wrongdestpath'] = 'Vale sihtrada';
 $string['wrongsourcebase'] = 'Vale allika URL\'i baas.';
-$string['wrongzipfilename'] = 'Vale ZIP failinimi.';
+$string['wrongzipfilename'] = 'Vale ZIP failinimi';
diff --git a/install/lang/kmr/langconfig.php b/install/lang/kmr/langconfig.php
new file mode 100644 (file)
index 0000000..d8fd01a
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Kurmanji';
index 75418a9..9c7880f 100644 (file)
@@ -35,7 +35,7 @@ $string['availablelangs'] = 'Lista de idiomas disponíveis';
 $string['chooselanguagehead'] = 'Escolha um idioma';
 $string['chooselanguagesub'] = 'Por favor, escolha o idioma para a instalação.Este idioma também será utilizado como idioma padrão do site, embora você possa mudar mais tarde.';
 $string['clialreadyconfigured'] = 'Arquivo config.php já existente. Por favor use admin/cli/install_database.php se você quer instalar este site';
-$string['clialreadyinstalled'] = 'O arquivo config.php já existe, por favor use admin/cli/upgrade.php, se você quiser atualizar o seu site.';
+$string['clialreadyinstalled'] = 'O arquivo config.php já existe, por favor use admin/cli/upgrade.php, se você quiser atualizar este site.';
 $string['cliinstallheader'] = 'Programa de instalação por linha de comando do Moodle {$a}';
 $string['databasehost'] = 'Host da base de dados';
 $string['databasename'] = 'Nome da base de dados';
index 4996af6..53f68e2 100644 (file)
@@ -1035,7 +1035,7 @@ $string['updateavailable_version'] = 'Version {$a}';
 $string['updateavailableinstall'] = 'Install this update';
 $string['updateavailablenot'] = 'Your Moodle code is up-to-date!';
 $string['updatenotifications'] = 'Update notifications';
-$string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configured to automatically check for available updates. You are receiving this message as the administrator of the site. You can disable automatic checks for available updates in the Site administration section of the Settings block. You can customize the delivery of this message via your personal Messaging setting in the My profile settings section.';
+$string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configured to automatically check for available updates. You are receiving this message as the administrator of the site. You can disable automatic checks for available updates in the Site administration section of the Administration block. You can customize the delivery of this message via your personal Messaging setting in the My profile settings section.';
 $string['updatenotificationsubject'] = 'Moodle updates are available ({$a->siteurl})';
 $string['updateautocheck'] = 'Automatically check for available updates';
 $string['updateautocheck_desc'] = 'If enabled, your site will automatically check for available updates for both Moodle code and all additional plugins. If there is a new update available, a notification will be sent to site admins.';
index 0a1a1b2..8e7d0c0 100644 (file)
@@ -63,6 +63,7 @@ $string['subpages'] = 'Select pages';
 $string['restrictpagetypes'] = 'Display on page types';
 $string['thisspecificpage'] = 'This specific page';
 $string['undockall'] = 'Undock all';
+$string['undockblock'] = 'Undock {$a} block';
 $string['undockitem'] = 'Undock this item';
 $string['visible'] = 'Visible';
 $string['weight'] = 'Weight';
index d5f2c6c..e1db4bc 100644 (file)
@@ -58,6 +58,8 @@ $string['security'] = 'Security';
 $string['selectallornone'] = 'Select all/none';
 $string['selected'] = 'Selected';
 $string['showadvanced'] = 'Show advanced';
+$string['showless'] = 'Show less...';
+$string['showmore'] = 'Show more...';
 $string['showeditortoolbar'] = 'Show editing tools';
 $string['somefieldsrequired'] = 'There are required fields in this form marked {$a}.';
 $string['time'] = 'Time';
index fed4bbc..3be622d 100644 (file)
@@ -31,7 +31,7 @@ $string['activemethodinfonone'] = 'There is no advanced grading method selected
 $string['changeactivemethod'] = 'Change active grading method to';
 $string['clicktoclose'] = 'click to close';
 $string['exc_gradingformelement'] = 'Unable to instantiate grading form element';
-$string['formnotavailable'] = 'Advanced grading method was selected to use but the grading form is not available yet. You may need to define it first via a link in the Settings block.';
+$string['formnotavailable'] = 'Advanced grading method was selected to use but the grading form is not available yet. You may need to define it first via a link in the Administration block.';
 $string['gradingformunavailable'] = 'Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status.';
 $string['gradingmanagement'] = 'Advanced grading';
 $string['gradingmanagementtitle'] = 'Advanced grading: {$a->component} ({$a->area})';
index 1543b76..93361fe 100644 (file)
@@ -361,7 +361,6 @@ $string['coursestart'] = 'Course start';
 $string['coursesummary'] = 'Course summary';
 $string['coursesummary_help'] = 'The course summary is displayed in the list of courses. A course search searches course summary text in addition to course names.';
 $string['courseupdates'] = 'Course updates';
-$string['courseuploadlimit'] = 'Course upload limit';
 $string['create'] = 'Create';
 $string['createaccount'] = 'Create my new account';
 $string['createcategory'] = 'Create category';
@@ -924,6 +923,7 @@ $string['list'] = 'List';
 $string['listfiles'] = 'List of files in {$a}';
 $string['listofallpeople'] = 'List of all people';
 $string['listofcourses'] = 'List of courses';
+$string['loadinghelp'] = 'Loading...';
 $string['local'] = 'Local';
 $string['localplugindeleteconfirm'] = 'You are about to completely delete the local plugin \'{$a}\'. This will completely delete everything in the database associated with this plugin. Are you SURE you want to continue?';
 $string['localplugins'] = 'Local plugins';
@@ -1714,6 +1714,7 @@ $string['uploadfailednotrecovering'] = 'Your file upload has failed because ther
 $string['uploadfilelog'] = 'Upload log for file {$a}';
 $string['uploadformlimit'] = 'Uploaded file {$a} exceeded the maximum size limit set by the form';
 $string['uploadlabel'] = 'Title:';
+$string['uploadlimitwithsize'] = '{$a->contextname} upload limit ({$a->displaysize})';
 $string['uploadnewfile'] = 'Upload new file';
 $string['uploadnofilefound'] = 'No file was found - are you sure you selected one to upload?';
 $string['uploadnotallowed'] = 'Uploads are not allowed';
index 3ab441e..79cc8d2 100644 (file)
@@ -54,4 +54,4 @@ $string['ratinginvalid'] = 'Rating is invalid';
 $string['ratingtime'] = 'Restrict ratings to items with dates in this range:';
 $string['ratings'] = 'Ratings';
 $string['rolewarning'] = 'Roles with permission to rate';
-$string['rolewarning_help'] = 'To submit ratings users require the moodle/rating:rate capability and any module specific capabilities. Users assigned the following roles should be able to rate items. The list of roles may be amended via the permissions link in the settings block.';
+$string['rolewarning_help'] = 'To submit ratings users require the moodle/rating:rate capability and any module specific capabilities. Users assigned the following roles should be able to rate items. The list of roles may be amended via the permissions link in the administration block.';
index c6d7f2c..5a59ea6 100644 (file)
@@ -393,6 +393,7 @@ function has_capability($capability, context $context, $user = null, $doanything
     if (!isset($USER->id)) {
         // should never happen
         $USER->id = 0;
+        debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
     }
 
     // make sure there is a real user specified
@@ -2062,6 +2063,7 @@ function can_access_course(stdClass $course, $user = null, $withcapability = '',
     if (!isset($USER->id)) {
         // should never happen
         $USER->id = 0;
+        debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
     }
 
     // make sure there is a user specified
index c957837..41505e0 100644 (file)
@@ -28,6 +28,9 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
+use Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
 /**
  * Steps definitions base class.
  *
@@ -76,6 +79,89 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         return 0 !== strpos($path, 'http') ? $startUrl . ltrim($path, '/') : $path;
     }
 
+    /**
+     * Adapter to Behat\Mink\Element\Element::find() using the spin() method.
+     *
+     * @link http://mink.behat.org/#traverse-the-page-selectors
+     * @param Exception $exception Otherwise we throw expcetion with generic info
+     * @param string $selector The selector type (css, xpath, named...)
+     * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator...
+     * @return NodeElement
+     */
+    protected function find($selector, $locator, $exception = false) {
+
+        // Generic info.
+        if (!$exception) {
+
+            // With named selectors we can be more specific.
+            if ($selector == 'named') {
+                $exceptiontype = $locator[0];
+                $exceptionlocator = $locator[1];
+            } else {
+                $exceptiontype = $selector;
+                $exceptionlocator = $locator;
+            }
+
+            $exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator);
+        }
+
+        // Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception.
+        return $this->spin(
+            function($context, $args) {
+                return $context->getSession()->getPage()->find($args[0], $args[1]);
+            },
+            array($selector, $locator),
+            self::TIMEOUT,
+            $exception
+       );
+    }
+
+    /**
+     * Finds DOM nodes in the page using named selectors.
+     *
+     * The point of using this method instead of Mink ones is the spin
+     * method of behat_base::find() that looks for the element until it
+     * is available or it timeouts, this avoids the false failures received
+     * when selenium tries to execute commands on elements that are not
+     * ready to be used.
+     *
+     * All steps that requires elements to be available before interact with
+     * them should use one of the find* methods.
+     *
+     * The methods calls requires a {'find_' . $elementtype}($locator)
+     * format, like find_link($locator), find_select($locator),
+     * find_button($locator)...
+     *
+     * @link http://mink.behat.org/#named-selectors
+     * @throws coding_exception
+     * @param string $method The name of the called method
+     * @param mixed $arguments
+     * @return NodeElement
+     */
+    public function __call($name, $arguments) {
+
+        if (substr($name, 0, 5) !== 'find_') {
+            throw new coding_exception('The "' . $name . '" method does not exist');
+        }
+
+        // Only the named selector identifier.
+        $cleanname = substr($name, 5);
+
+        // All named selectors shares the interface.
+        if (count($arguments) !== 1) {
+            throw new coding_exception('The "' . $cleanname . '" named selector needs the locator as it\'s single argument');
+        }
+
+        // Redirecting execution to the find method with the specified selector.
+        // It will detect if it's pointing to an unexisting named selector.
+        return $this->find('named',
+            array(
+                $cleanname,
+                $this->getSession()->getSelectorsHandler()->xpathLiteral($arguments[0])
+            )
+        );
+    }
+
     /**
      * Executes the passed closure until returns true or time outs.
      *
@@ -91,15 +177,20 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      * - Must return something != false if finishes as expected, this will be the (mixed) value
      * returned by spin()
      *
-     * Requires the exception to provide more accurate feedback to tests writers.
+     * The arguments of the closure are mixed, use $args depending on your needs.
      *
-     * @throws Exception If it timeouts without receiving something != false from the closure
-     * @param Closure $lambda The function to execute.
-     * @param Exception $exception The exception to throw in case it time outs.
-     * @param array $args Arguments to pass to the closure
+     * You can provide an exception to give more accurate feedback to tests writers, otherwise the
+     * closure exception will be used, but you must provide an exception if the closure does not throws
+     * an exception.
+     *
+     * @throws Exception            If it timeouts without receiving something != false from the closure
+     * @param  Closure   $lambda    The function to execute.
+     * @param  mixed     $args      Arguments to pass to the closure
+     * @param  int       $timeout   Timeout
+     * @param  Exception $exception The exception to throw in case it time outs.
      * @return mixed The value returned by the closure
      */
-    protected function spin($lambda, $exception, $args, $timeout = false) {
+    protected function spin($lambda, $args = false, $timeout = false, $exception = false) {
 
         // Using default timeout which is pretty high.
         if (!$timeout) {
@@ -112,11 +203,18 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
             try {
 
                 // We don't check with !== because most of the time closures will return
-                // direct Behat methods returns and we are not sure it will be always (bool)false.
+                // direct Behat methods returns and we are not sure it will be always (bool)false
+                // if it just runs the behat method without returning anything $return == null.
                 if ($return = $lambda($this, $args)) {
                     return $return;
                 }
             } catch(Exception $e) {
+
+                // We would use the first closure exception if no exception has been provided.
+                if (!$exception) {
+                    $exception = $e;
+                }
+
                 // We wait until no exception is thrown or timeout expires.
                 continue;
             }
@@ -124,6 +222,11 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
             sleep(1);
         }
 
+        // Using coding_exception as is a development issue if no exception has been provided.
+        if (!$exception) {
+            $exception = new coding_exception('spin method requires an exception if the closure doesn\'t throw an exception itself');
+        }
+
         // Throwing exception to the user.
         throw $exception;
     }
index 1c45dbd..e0ec4d4 100644 (file)
@@ -189,6 +189,9 @@ class behat_config_manager {
                         'features' => $features,
                         'steps_definitions' => $stepsdefinitions
                     )
+                ),
+                'formatter' => array(
+                    'name' => 'progress'
                 )
             )
         );
index 79e808b..0ae0ae3 100644 (file)
@@ -216,7 +216,11 @@ function cron_run() {
 
         // note: we can not send emails to suspended accounts
         foreach ($newusers as $newuser) {
-            if (setnew_password_and_mail($newuser)) {
+            // Use a low cost factor when generating bcrypt hash otherwise
+            // hashing would be slow when emailing lots of users. Hashes
+            // will be automatically updated to a higher cost factor the first
+            // time the user logs in.
+            if (setnew_password_and_mail($newuser, true)) {
                 unset_user_preference('create_password', $newuser);
                 set_user_preference('auth_forcepasswordchange', 1, $newuser);
             } else {
index a1e5ab9..f54eae6 100644 (file)
@@ -57,7 +57,9 @@ function get_admin() {
     static $mainadmin = null;
     static $prevadmins = null;
 
-    if (empty($CFG->siteadmins)) {  // Should not happen on an ordinary site.
+    if (empty($CFG->siteadmins)) {
+        // Should not happen on an ordinary site.
+        // It does however happen during unit tests.
         return false;
     }
 
index d4eb4fc..1602999 100644 (file)
         <FIELD NAME="suspended" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="suspended flag prevents users to log in"/>
         <FIELD NAME="mnethostid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="username" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="password" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="password" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="idnumber" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="firstname" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="lastname" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
index 3dcc1d1..e8a7278 100644 (file)
@@ -1564,6 +1564,92 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012120300.07);
     }
 
+    if ($oldversion < 2013021100.01) {
+
+        // Changing precision of field password on table user to (255).
+        $table = new xmldb_table('user');
+        $field = new xmldb_field('password', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'username');
+
+        // Launch change of precision for field password.
+        $dbman->change_field_precision($table, $field);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013021100.01);
+    }
+
+    if ($oldversion < 2013021800.00) {
+        // Add the site identifier to the cache config's file.
+        $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
+        cache_helper::update_site_identifier($siteidentifier);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013021800.00);
+    }
+
+    if ($oldversion < 2013021801.00) {
+        // Fixing possible wrong MIME types for SMART Notebook files.
+        $extensions = array('%.gallery', '%.galleryitem', '%.gallerycollection', '%.nbk', '%.notebook', '%.xbk');
+        $select = $DB->sql_like('filename', '?', false);
+        foreach ($extensions as $extension) {
+            $DB->set_field_select(
+                'files',
+                'mimetype',
+                'application/x-smarttech-notebook',
+                $select,
+                array($extension)
+            );
+        }
+        upgrade_main_savepoint(true, 2013021801.00);
+    }
+
+    if ($oldversion < 2013021801.01) {
+        // Retrieve the list of course_sections as a recordset to save memory
+        $coursesections = $DB->get_recordset('course_sections', null, 'course, id', 'id, course, sequence');
+        foreach ($coursesections as $coursesection) {
+            // Retrieve all of the actual modules in this course and section combination to reduce DB calls
+            $actualsectionmodules = $DB->get_records('course_modules',
+                    array('course' => $coursesection->course, 'section' => $coursesection->id), '', 'id, section');
+
+            // Break out the current sequence so that we can compare it
+            $currentsequence = explode(',', $coursesection->sequence);
+            $newsequence = array();
+
+            // Check each of the modules in the current sequence
+            foreach ($currentsequence as $module) {
+                if (isset($actualsectionmodules[$module])) {
+                    $newsequence[] = $module;
+                    // We unset the actualsectionmodules so that we don't get duplicates and that we can add orphaned
+                    // modules later
+                    unset($actualsectionmodules[$module]);
+                }
+            }
+
+            // Append any modules which have somehow been orphaned
+            foreach ($actualsectionmodules as $module) {
+                $newsequence[] = $module->id;
+            }
+
+            // Piece it all back together
+            $sequence = implode(',', $newsequence);
+
+            // Only update if there have been changes
+            if ($sequence !== $coursesection->sequence) {
+                $coursesection->sequence = $sequence;
+                $DB->update_record('course_sections', $coursesection);
+
+                // And clear the sectioncache and modinfo cache - they'll be regenerated on next use
+                $course = new stdClass();
+                $course->id = $coursesection->course;
+                $course->sectioncache = null;
+                $course->modinfo = null;
+                $DB->update_record('course', $course);
+            }
+        }
+        $coursesections->close();
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013021801.01);
+    }
 
     return true;
 }
index 013f6c5..6fb311d 100644 (file)
@@ -1469,6 +1469,10 @@ function &get_mimetypes_array() {
         'fdf'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'flv'  => array ('type'=>'video/x-flv', 'icon'=>'flash', 'groups'=>array('video','web_video'), 'string'=>'video'),
         'f4v'  => array ('type'=>'video/mp4', 'icon'=>'flash', 'groups'=>array('video','web_video'), 'string'=>'video'),
+
+        'gallery'           => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'galleryitem,'      => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'gallerycollection' => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
         'gif'  => array ('type'=>'image/gif', 'icon'=>'gif', 'groups'=>array('image', 'web_image'), 'string'=>'image'),
         'gtar' => array ('type'=>'application/x-gtar', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive'),
         'tgz'  => array ('type'=>'application/g-zip', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive'),
@@ -1512,6 +1516,9 @@ function &get_mimetypes_array() {
         'mpe'  => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video','web_video'), 'string'=>'video'),
         'mpg'  => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video','web_video'), 'string'=>'video'),
 
+        'nbk'       => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'notebook'  => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+
         'odt'  => array ('type'=>'application/vnd.oasis.opendocument.text', 'icon'=>'writer', 'groups'=>array('document')),
         'ott'  => array ('type'=>'application/vnd.oasis.opendocument.text-template', 'icon'=>'writer', 'groups'=>array('document')),
         'oth'  => array ('type'=>'application/vnd.oasis.opendocument.text-web', 'icon'=>'oth', 'groups'=>array('document')),
@@ -1591,6 +1598,8 @@ function &get_mimetypes_array() {
         'webm'  => array ('type'=>'video/webm', 'icon'=>'video', 'groups'=>array('video'), 'string'=>'video'),
         'wmv'  => array ('type'=>'video/x-ms-wmv', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'),
         'asf'  => array ('type'=>'video/x-ms-asf', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'),
+
+        'xbk'  => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
         'xdp'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'xfd'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'xfdf' => array ('type'=>'application/pdf', 'icon'=>'pdf'),
@@ -1605,6 +1614,7 @@ function &get_mimetypes_array() {
 
         'xml'  => array ('type'=>'application/xml', 'icon'=>'markup'),
         'xsl'  => array ('type'=>'text/xml', 'icon'=>'markup'),
+
         'zip'  => array ('type'=>'application/zip', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive')
     );
     return $mimearray;
@@ -4290,7 +4300,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            $bprecord = $DB->get_record('block_positions', array('blockinstanceid' => $context->instanceid), 'visible');
+            $bprecord = $DB->get_record('block_positions', array('contextid' => $context->id, 'blockinstanceid' => $context->instanceid));
             // User can't access file, if block is hidden or doesn't have block:view capability
             if (($bprecord && !$bprecord->visible) || !has_capability('moodle/block:view', $context)) {
                  send_file_not_found();
index 1f7856b..e9f9490 100644 (file)
@@ -6,58 +6,6 @@
 // Namespace for the form bits and bobs
 M.form = M.form || {};
 
-/**
- * Initialises the show advanced functionality and events.
- * This should only ever happen ONCE per page.
- *
- * @param {YUI} Y
- * @param {object} config
- */
-M.form.initShowAdvanced = function(Y, config) {
-    if (M.form.showAdvanced) {
-        return M.form.showAdvanced;
-    }
-    var showAdvanced = function(config) {
-        showAdvanced.superclass.constructor.apply(this, arguments);
-    };
-    showAdvanced.prototype = {
-        _advButtons : [],
-        _advAreas : [],
-        _stateInput : null,
-        initializer : function() {
-            this._advAreas = Y.all('form .advanced');
-            this._advButtons = Y.all('.showadvancedbtn');
-            if (this._advButtons.size() > 0) {
-                this._stateInput = new Y.NodeList(document.getElementsByName('mform_showadvanced_last'));
-                this._advButtons.on('click', this.switchState, this);
-                this._advButtons.set('type', 'button');
-            }
-        },
-        /**
-         * Toggles between showing advanced items and hiding them.
-         * Should be fired by an event.
-         */
-        switchState : function(e) {
-            e.preventDefault();
-            if (this._stateInput.get('value')=='1') {
-                this._stateInput.set('value', '0');
-                this._advButtons.setAttribute('value', M.str.form.showadvanced);
-                this._advAreas.addClass('hide');
-            } else {
-                this._stateInput.set('value', '1');
-                this._advButtons.setAttribute('value', M.str.form.hideadvanced);
-                this._advAreas.removeClass('hide');
-            }
-        }
-    };
-    // Extend it with the YUI widget fw.
-    Y.extend(showAdvanced, Y.Base, showAdvanced.prototype, {
-        NAME : 'mform-showAdvanced'
-    });
-    M.form.showAdvanced = new showAdvanced(config);
-    return M.form.showAdvanced;
-};
-
 /**
  * Stores a list of the dependencyManager for each form on the page.
  */
diff --git a/lib/form/listing.php b/lib/form/listing.php
new file mode 100644 (file)
index 0000000..102b492
--- /dev/null
@@ -0,0 +1,149 @@
+<?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/>.
+
+/**
+ * Listing form element.
+ *
+ * Contains HTML class for a listing form element.
+ *
+ * @package   core_form
+ * @copyright 2012 Jerome Mouneyrac
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+if (!defined('MOODLE_INTERNAL')) {
+    die('Direct access to this script is forbidden.');
+}
+
+require_once("HTML/QuickForm/input.php");
+
+/**
+* The listing element is a simple customizable "select" without the input type=select.
+* One main div contains the "large" html of an item.
+* A show/hide div shows a hidden div containing the list of all items.
+* This list is composed by the "small" html of each item.
+*
+* How to use it:
+* The options parameter is an array containing:
+*   - items => array of object: the key is the value of the form input
+*                               $item->rowhtml => small html
+*                               $item->mainhtml => large html
+*   - showall/hideall => string for the Show/Hide button
+*
+* WARNINGS: The form lets you display HTML. So it is subject to CROSS-SCRIPTING if you send it uncleaned HTML.
+*           Don't forget to escape your HTML as soon as one string comes from an input/external source.
+*
+* How to customize it:
+*   You can change the css in core.css. For example if you remove float:left; from .formlistingrow,
+*   then the item list is not display as tabs but as rows.
+*
+* @package   core_form
+* @copyright 2012 Jerome Mouneyrac
+* @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+*/
+class MoodleQuickForm_listing extends HTML_QuickForm_input {
+
+    /** @var array items to display. */
+    protected $items = array();
+
+    /** @var string language string for Show All. */
+    protected $showall;
+
+    /** @var string language string for Hide. */
+    protected $hideall;
+
+    /**
+     * Constructor.
+     *
+     * @param string $elementName (optional) name of the listing.
+     * @param string $elementLabel (optional) listing label.
+     * @param array $attributes (optional) Either a typical HTML attribute string or an associative array.
+     * @param array $options set of options to initalize listing.
+     */
+    function MoodleQuickForm_listing($elementName=null, $elementLabel=null, $attributes=null, $options=array()) {
+
+       $this->_type = 'listing';
+        if (!empty($options['items'])) {
+            $this->items = $options['items'];
+        }
+        if (!empty($options['showall'])) {
+            $this->showall = $options['showall'];
+        } else {
+            $this->showall = get_string('showall');
+        }
+        if (!empty($options['hideall'])) {
+            $this->hideall = $options['hideall'];
+        } else {
+            $this->hideall = get_string('hide');
+        }
+        parent::HTML_QuickForm_input($elementName, $elementLabel, $attributes);
+    }
+
+    /**
+     * Returns HTML for listing form element.
+     *
+     * @return string the HTML.
+     */
+    function toHtml() {
+        global $CFG, $PAGE;
+
+        $mainhtml = html_writer::tag('div', $this->items[$this->getValue()]->mainhtml,
+                array('id' => $this->getName().'_items_main', 'class' => 'formlistingmain'));
+
+        // Add the main div containing the selected item (+ the caption: "More items").
+        $html = html_writer::tag('div', $mainhtml .
+                    html_writer::tag('div', $this->showall,
+                        array('id' => $this->getName().'_items_caption', 'class' => 'formlistingmore')),
+                    array('id'=>$this->getName().'_items', 'class' => 'formlisting hide'));
+
+        // Add collapsible region: all the items.
+        $itemrows = '';
+        $html .= html_writer::tag('div', $itemrows,
+                array('id' => $this->getName().'_items_all', 'class' => 'formlistingall'));
+
+        // Add radio buttons for non javascript support.
+        $radiobuttons = '';
+        foreach ($this->items as $itemid => $item) {
+            $radioparams = array('name' => $this->getName(), 'value' => $itemid,
+                    'id' => 'id_'.$itemid, 'class' => 'formlistinginputradio', 'type' => 'radio');
+            if ($itemid == $this->getValue()) {
+                $radioparams['checked'] = 'checked';
+            }
+            $radiobuttons .= html_writer::tag('div', html_writer::tag('input',
+                html_writer::tag('div', $item->rowhtml, array('class' => 'formlistingradiocontent')), $radioparams),
+                array('class' => 'formlistingradio'));
+        }
+
+        // Container for the hidden hidden input which will contain the selected item.
+        $html .= html_writer::tag('div', $radiobuttons,
+                array('id' => 'formlistinginputcontainer_' . $this->getName(), 'class' => 'formlistinginputcontainer'));
+
+        $module = array('name'=>'form_listing', 'fullpath'=>'/lib/form/yui/listing/listing.js',
+            'requires'=>array('node', 'event', 'transition', 'escape'));
+
+        $PAGE->requires->js_init_call('M.form_listing.init',
+                 array(array(
+                'elementid' => $this->getName().'_items',
+                'hideall' => $this->hideall,
+                'showall' => $this->showall,
+                'hiddeninputid' => $this->getAttribute('id'),
+                'items' => $this->items,
+                'inputname' => $this->getName(),
+                'currentvalue' => $this->getValue())), true, $module);
+
+        return $html;
+    }
+}
diff --git a/lib/form/yui/listing/listing.js b/lib/form/yui/listing/listing.js
new file mode 100644 (file)
index 0000000..0e420e0
--- /dev/null
@@ -0,0 +1,93 @@
+// 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/>.
+
+/**
+ * Form listing Javascript.
+ *
+ * It mainly handles loading the main content div when cliking on a tab/row.
+ * @copyright 2012 Jerome Mouneyrac
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+M.form_listing = {};
+M.form_listing.Y = null;
+M.form_listing.instances = [];
+
+/**
+ * This function is called for each listing form on page.
+ *
+ * @param {Array} params :  {int} hiddeninputid - the id of the hidden input element
+ *                          {int} elementid - the id of the full form element
+ *                          {Array} items - items has for key the value return by the form, and for content an array with two attributs: mainhtml and rowhtml.
+ *                          {string} hideall - button label to hide all tabs(rows).
+ *                          {string} showall - button label to show all tabs(rows).
+ *                          {string} inputname - the name of the input element
+ *                          {string} currentvalue - the currently selected tab(row)
+ */
+M.form_listing.init = function(Y, params) {
+    if (params && params.hiddeninputid && params.elementid) {
+
+        // Replace the radio buttons by a hidden input.
+        Y.one('#formlistinginputcontainer_' + params.inputname).setHTML('<input name='+params.inputname+' type=hidden id='+params.hiddeninputid+' value='+params.currentvalue+' />');
+
+        var caption = Y.one('#'+params.elementid+'_caption');
+        var allitems = Y.one('#'+params.elementid+'_all');
+        var selecteditem = Y.one('#'+params.elementid+'_main');
+        var hiddeninput = Y.one('#'+params.hiddeninputid);
+
+        // Do not display the listing by default.
+        var show = 0;
+        allitems.hide();
+
+        // Refresh the main item + set the hidden input to its value.
+        var selectitem = function(e) {
+            var index = this.get('id').replace(params.elementid+'_all_',"");
+            hiddeninput.set('value', items[index]);
+            selecteditem.setHTML(params.items[items[index]].mainhtml);
+        }
+
+        // Caption Onlick event to display/hide the listing.
+        var onclick = function(e) {
+            if (!show) {
+                allitems.show(true);
+                show = 1;
+                caption.setHTML(params.hideall);
+            } else {
+                allitems.hide(true);
+                show = 0;
+                caption.setHTML(params.showall);
+            }
+        };
+
+        caption.on('click', onclick);
+
+        // Fill the item rows with html + add event.
+        // PS: we need to save the items into a temporary "items[]" array because params.items keys could be url.
+        // This temporary items[] avoid not working calls like Y.one('#myitems_http:www.google.com').
+        var items = [];
+        var itemindex = 0;
+        for (itemid in params.items) {
+            items[itemindex] = itemid;
+
+            // Add the row.
+            allitems.append("<div id="+params.elementid+'_all_'+itemindex+" class='formlistingrow'>" + params.items[itemid].rowhtml + "</div>");
+
+            // Add click event to the row.
+            Y.one('#'+params.elementid+'_all_'+itemindex).on('click', selectitem);
+
+            itemindex = itemindex + 1;
+        }
+    }
+};
diff --git a/lib/form/yui/shortforms/shortforms.js b/lib/form/yui/shortforms/shortforms.js
new file mode 100644 (file)
index 0000000..c874791
--- /dev/null
@@ -0,0 +1,96 @@
+YUI.add('moodle-form-shortforms', function(Y) {
+    /**
+     * Provides the form shortforms class.
+     *
+     * @module moodle-form-shortforms
+     */
+
+    /**
+     * A class for a shortforms.
+     *
+     * @param {Object} config Object literal specifying shortforms configuration properties.
+     * @class M.form.shortforms
+     * @constructor
+     * @extends Y.Base
+     */
+    function SHORTFORMS(config) {
+        SHORTFORMS.superclass.constructor.apply(this, [config]);
+    }
+
+    var SELECTORS = {
+            FIELDSETCOLLAPSIBLE : 'fieldset.collapsible',
+            LEGENDFTOGGLER : 'legend.ftoggler'
+        },
+        CSS = {
+            COLLAPSED : 'collapsed',
+            FHEADER : 'fheader',
+            JSPROCESSED : 'jsprocessed'
+        },
+        ATTRS = {};
+
+    /**
+     * Static property provides a string to identify the JavaScript class.
+     *
+     * @property NAME
+     * @type String
+     * @static
+     */
+    SHORTFORMS.NAME = 'moodle-form-shortforms';
+
+    /**
+     * Static property used to define the default attribute configuration for the Shortform.
+     *
+     * @property ATTRS
+     * @type String
+     * @static
+     */
+    SHORTFORMS.ATTRS = ATTRS;
+
+    /**
+     * The form ID attribute definition.
+     *
+     * @attribute formid
+     * @type String
+     * @default ''
+     * @writeOnce
+     */
+    ATTRS.formid = {
+        value : null
+    };
+
+    Y.extend(SHORTFORMS, Y.Base, {
+        initializer : function() {
+            var fieldlist = Y.Node.all('#'+this.get('formid')+' '+SELECTORS.FIELDSETCOLLAPSIBLE);
+            // Look through collapsible fieldset divs.
+            fieldlist.each(this.process_fieldset, this);
+            // Subscribe collapsible fieldsets to click event.
+            Y.one('#'+this.get('formid')).delegate('click', this.switch_state, SELECTORS.FIELDSETCOLLAPSIBLE+' .'+CSS.FHEADER);
+        },
+        process_fieldset : function(fieldset) {
+            fieldset.addClass(CSS.JSPROCESSED);
+            // Get legend element.
+            var legendelement = fieldset.one(SELECTORS.LEGENDFTOGGLER);
+
+            // Turn headers to links for accessibility.
+            var headerlink = Y.Node.create('<a href="#"></a>');
+            headerlink.addClass(CSS.FHEADER);
+            headerlink.appendChild(legendelement.get('firstChild'));
+            legendelement.prepend(headerlink);
+        },
+        switch_state : function(e) {
+            e.preventDefault();
+            var fieldset = this.ancestor(SELECTORS.FIELDSETCOLLAPSIBLE);
+            // Toggle collapsed class.
+            fieldset.toggleClass(CSS.COLLAPSED);
+            // Get corresponding hidden variable
+            // - and invert it.
+            var statuselement = new Y.one('input[name=mform_isexpanded_'+fieldset.get('id')+']');
+            statuselement.set('value', Math.abs(Number(statuselement.get('value'))-1));
+        }
+    });
+
+    M.form = M.form || {};
+    M.form.shortforms = M.form.shortforms || function(params) {
+        return new SHORTFORMS(params);
+    };
+}, '@VERSION@', {requires:['base', 'node', 'selector-css3']});
diff --git a/lib/form/yui/showadvanced/showadvanced.js b/lib/form/yui/showadvanced/showadvanced.js
new file mode 100644 (file)
index 0000000..562e20e
--- /dev/null
@@ -0,0 +1,104 @@
+YUI.add('moodle-form-showadvanced', function(Y) {
+    /**
+     * Provides the form showadvanced class.
+     *
+     * @module moodle-form-showadvanced
+     */
+
+    /**
+     * A class for a showadvanced.
+     *
+     * @param {Object} config Object literal specifying showadvanced configuration properties.
+     * @class M.form.showadvanced
+     * @constructor
+     * @extends Y.Base
+     */
+    function SHOWADVANCED(config) {
+        SHOWADVANCED.superclass.constructor.apply(this, [config]);
+    }
+
+    var SELECTORS = {
+            FIELDSETCONTAINSADVANCED : 'fieldset.containsadvancedelements',
+            DIVFITEMADVANCED : 'div.fitem.advanced',
+            DIVFCONTAINER : 'div.fcontainer'
+        },
+        CSS = {
+            HIDE : 'hide',
+            MORELESSTOGGLER : 'morelesstoggler'
+        },
+        ATTRS = {};
+
+    /**
+     * Static property provides a string to identify the JavaScript class.
+     *
+     * @property NAME
+     * @type String
+     * @static
+     */
+    SHOWADVANCED.NAME = 'moodle-form-showadvanced';
+
+    /**
+     * Static property used to define the default attribute configuration for the Showadvanced.
+     *
+     * @property ATTRS
+     * @type String
+     * @static
+     */
+    SHOWADVANCED.ATTRS = ATTRS;
+
+    /**
+     * The form ID attribute definition.
+     *
+     * @attribute formid
+     * @type String
+     * @default ''
+     * @writeOnce
+     */
+    ATTRS.formid = {
+        value : null
+    };
+
+    Y.extend(SHOWADVANCED, Y.Base, {
+        initializer : function() {
+            var fieldlist = Y.Node.all('#'+this.get('formid')+' '+SELECTORS.FIELDSETCONTAINSADVANCED);
+            // Look through fieldset divs that contain advanced elements.
+            fieldlist.each(this.process_fieldset, this);
+            // Subscribe more/less links to click event.
+            Y.one('#'+this.get('formid')).delegate('click', this.switch_state, SELECTORS.FIELDSETCONTAINSADVANCED+' .'+CSS.MORELESSTOGGLER);
+        },
+        process_fieldset : function(fieldset) {
+            var statuselement = new Y.one('input[name=mform_showmore_'+fieldset.get('id')+']');
+            var morelesslink = Y.Node.create('<a href="#"></a>');
+            morelesslink.addClass(CSS.MORELESSTOGGLER);
+            if (statuselement.get('value') === '0') {
+                morelesslink.setHTML(M.str.form.showmore);
+                // Hide advanced stuff initially.
+                fieldset.all(SELECTORS.DIVFITEMADVANCED).addClass(CSS.HIDE);
+            } else {
+                morelesslink.setHTML(M.str.form.showless);
+            }
+            fieldset.one(SELECTORS.DIVFCONTAINER).append(morelesslink);
+        },
+        switch_state : function(e) {
+            e.preventDefault();
+            var fieldset = this.ancestor(SELECTORS.FIELDSETCONTAINSADVANCED);
+            // Toggle collapsed class.
+            fieldset.all(SELECTORS.DIVFITEMADVANCED).toggleClass(CSS.HIDE);
+            // Get corresponding hidden variable.
+            var statuselement = new Y.one('input[name=mform_showmore_'+fieldset.get('id')+']');
+            // Invert it and change the link text.
+            if (statuselement.get('value') === '0') {
+                statuselement.set('value', 1);
+                this.setHTML(M.util.get_string('showless', 'form'));
+            } else {
+                statuselement.set('value', 0);
+                this.setHTML(M.util.get_string('showmore', 'form'));
+            }
+        }
+    });
+
+    M.form = M.form || {};
+    M.form.showadvanced = M.form.showadvanced || function(params) {
+        return new SHOWADVANCED(params);
+    };
+}, '@VERSION@', {requires:['base', 'node', 'selector-css3']});
index 03c8c33..92c16a2 100644 (file)
@@ -1069,6 +1069,9 @@ abstract class moodleform {
                                 $mform->setType($elementname, $params);
                             }
                             break;
+                        case 'expanded' :
+                            $mform->setExpanded($realelementname, $params);
+                            break;
                     }
                 }
             }
@@ -1222,11 +1225,7 @@ abstract class moodleform {
         return array(
             'name' => 'mform',
             'fullpath' => '/lib/form/form.js',
-            'requires' => array('base', 'node'),
-            'strings' => array(
-                array('showadvanced', 'form'),
-                array('hideadvanced', 'form')
-            )
+            'requires' => array('base', 'node')
         );
     }
 }
@@ -1260,8 +1259,19 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
     /** @var array Array whose keys are element names. If the key exists this is a advanced element */
     var $_advancedElements = array();
 
-    /** @var bool Whether to display advanced elements (on page load) */
-    var $_showAdvanced = null;
+    /**
+     * Array whose keys are element names and the the boolean values reflect the current state. If the key exists this is a collapsible element.
+     *
+     * @var array
+     */
+    var $_collapsibleElements = array();
+
+    /**
+     * Whether to enable shortforms for this form
+     *
+     * @var boolean
+     */
+    var $_disableShortforms = false;
 
     /** @var bool whether to automatically initialise M.formchangechecker for this form. */
     protected $_use_form_change_checker = true;
@@ -1280,6 +1290,14 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
      */
     var $_pageparams = '';
 
+    /**
+     * The maximum number of headers the form should contain in order not to be
+     * defined as collapsible.
+     *
+     * @var int
+     */
+    var $_non_collapsible_headers = 2;
+
     /**
      * Class constructor - same parameters as HTML_QuickForm_DHTMLRulesTableless
      *
@@ -1340,59 +1358,84 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
         } elseif (isset($this->_advancedElements[$elementName])) {
             unset($this->_advancedElements[$elementName]);
         }
-        if ($advanced && $this->getElementType('mform_showadvanced_last')===false){
-            $this->setShowAdvanced();
-            $this->registerNoSubmitButton('mform_showadvanced');
+    }
 
-            $this->addElement('hidden', 'mform_showadvanced_last');
-            $this->setType('mform_showadvanced_last', PARAM_INT);
+    /**
+     * Use this method to indicate that the fieldset should be shown as expanded.
+     * The method is applicable to header elements only.
+     *
+     * @param string $headerName header element name
+     * @param boolean $expanded default true sets the element to expanded. False makes the element collapsed.
+     */
+    function setExpanded($headerName, $expanded=true){
+        if ($this->getElementType('mform_isexpanded_'.$headerName)===false) {
+            // see if we the form has been submitted already
+            $formexpanded = optional_param('mform_isexpanded_'.$headerName, -1, PARAM_INT);
+            if (!$expanded && $formexpanded != -1) {
+                // override expanded state with the form variable
+                $expanded = $formexpanded;
+            }
+            // create the form element for storing expanded state
+            $this->addElement('hidden', 'mform_isexpanded_'.$headerName);
+            $this->setType('mform_isexpanded_'.$headerName, PARAM_INT);
+            $this->setConstant('mform_isexpanded_' . $headerName, (int)$expanded);
         }
+        $this->_collapsibleElements[$headerName] = !$expanded;
     }
+
     /**
-     * Set whether to show advanced elements in the form on first displaying form. Default is not to
-     * display advanced elements in the form until 'Show Advanced' is pressed.
-     *
-     * You can get the last state of the form and possibly save it for this user by using
-     * value 'mform_showadvanced_last' in submitted data.
+     * Use this method to add show more/less status element required for passing
+     * over the advanced elements visibility status on the form submission.
      *
-     * @param bool $showadvancedNow if true will show adavance elements.
+     * @param string $headerName header element name.
+     * @param boolean $showmore default false sets the advanced elements to be hidden.
      */
-    function setShowAdvanced($showadvancedNow = null){
-        if ($showadvancedNow === null){
-            if ($this->_showAdvanced !== null){
-                return;
-            } else { //if setShowAdvanced is called without any preference
-                     //make the default to not show advanced elements.
-                $showadvancedNow = get_user_preferences(
-                                textlib::strtolower($this->_formName.'_showadvanced', 0));
-            }
-        }
-        //value of hidden element
-        $hiddenLast = optional_param('mform_showadvanced_last', -1, PARAM_INT);
-        //value of button
-        $buttonPressed = optional_param('mform_showadvanced', 0, PARAM_RAW);
-        //toggle if button pressed or else stay the same
-        if ($hiddenLast == -1) {
-            $next = $showadvancedNow;
-        } elseif ($buttonPressed) { //toggle on button press
-            $next = !$hiddenLast;
-        } else {
-            $next = $hiddenLast;
-        }
-        $this->_showAdvanced = $next;
-        if ($showadvancedNow != $next){
-            set_user_preference($this->_formName.'_showadvanced', $next);
+    function addAdvancedStatusElement($headerName, $showmore=false){
+        // Add extra hidden element to store advanced items state for each section.
+        if ($this->getElementType('mform_showmore_' . $headerName) === false) {
+            // See if we the form has been submitted already.
+            $formshowmore = optional_param('mform_showmore_' . $headerName, -1, PARAM_INT);
+            if (!$showmore && $formshowmore != -1) {
+                // Override showmore state with the form variable.
+                $showmore = $formshowmore;
+            }
+            // Create the form element for storing advanced items state.
+            $this->addElement('hidden', 'mform_showmore_' . $headerName);
+            $this->setType('mform_showmore_' . $headerName, PARAM_INT);
+            $this->setConstant('mform_showmore_' . $headerName, (int)$showmore);
         }
-        $this->setConstants(array('mform_showadvanced_last'=>$next));
     }
 
     /**
-     * Gets show advance value, if advance elements are visible it will return true else false
+     * This function has been deprecated. Show advanced has been replaced by
+     * "Show more.../Show less..." in the shortforms javascript module.
      *
-     * @return bool
-     */
+     * @deprecated since Moodle 2.5
+     * @param bool $showadvancedNow if true will show advanced elements.
+      */
+    function setShowAdvanced($showadvancedNow = null){
+        debugging('Call to deprecated function setShowAdvanced. See "Show more.../Show less..." in shortforms yui module.');
+    }
+
+    /**
+     * This function has been deprecated. Show advanced has been replaced by
+     * "Show more.../Show less..." in the shortforms javascript module.
+     *
+     * @deprecated since Moodle 2.5
+     * @return bool (Always false)
+      */
     function getShowAdvanced(){
-        return $this->_showAdvanced;
+        debugging('Call to deprecated function setShowAdvanced. See "Show more.../Show less..." in shortforms yui module.');
+        return false;
+    }
+
+    /**
+     * Use this method to indicate that the form will not be using shortforms.
+     *
+     * @param boolean $disable default true, controls if the shortforms are disabled.
+     */
+    function setDisableShortforms ($disable = true) {
+        $this->_disableShortforms = $disable;
     }
 
     /**
@@ -1427,13 +1470,14 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
     */
     function accept(&$renderer) {
         if (method_exists($renderer, 'setAdvancedElements')){
-            //check for visible fieldsets where all elements are advanced
+            //Check for visible fieldsets where all elements are advanced
             //and mark these headers as advanced as well.
-            //And mark all elements in a advanced header as advanced
+            //Also mark all elements in a advanced header as advanced.
             $stopFields = $renderer->getStopFieldSetElements();
             $lastHeader = null;
             $lastHeaderAdvanced = false;
             $anyAdvanced = false;
+            $anyError = false;
             foreach (array_keys($this->_elements) as $elementIndex){
                 $element =& $this->_elements[$elementIndex];
 
@@ -1441,6 +1485,7 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
                 if ($element->getType()=='header' || in_array($element->getName(), $stopFields)){
                     if ($anyAdvanced && !is_null($lastHeader)){
                         $this->setAdvanced($lastHeader->getName());
+                        $this->addAdvancedStatusElement($lastHeader->getName(), $anyError);
                     }
                     $lastHeaderAdvanced = false;
                     unset($lastHeader);
@@ -1452,17 +1497,73 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
                 if ($element->getType()=='header'){
                     $lastHeader =& $element;
                     $anyAdvanced = false;
+                    $anyError = false;
                     $lastHeaderAdvanced = isset($this->_advancedElements[$element->getName()]);
                 } elseif (isset($this->_advancedElements[$element->getName()])){
                     $anyAdvanced = true;
+                    if (isset($this->_errors[$element->getName()])) {
+                        $anyError = true;
+                    }
                 }
             }
             // the last header may not be closed yet...
             if ($anyAdvanced && !is_null($lastHeader)){
                 $this->setAdvanced($lastHeader->getName());
+                $this->addAdvancedStatusElement($lastHeader->getName(), $anyError);
             }
             $renderer->setAdvancedElements($this->_advancedElements);
-
+        }
+        if (method_exists($renderer, 'setCollapsibleElements') && !$this->_disableShortforms){
+            // Check how many headers we have in total, if less than $_non_collapsible_headers,
+            // the form should not be collapsible at all (unless overidden in the form definition).
+            $headercounter = 0;
+            foreach (array_keys($this->_elements) as $elementIndex){
+                $element =& $this->_elements[$elementIndex];
+                if ($element->getType()=='header') {
+                    $headercounter++;
+                }
+            }
+            if ($headercounter > $this->_non_collapsible_headers) {
+                // So, we have more than $_non_collapsible_headers headers
+                // add all headers to collapsible elements array (if they have not been added yet).
+                unset($lastHeader);
+                $lastHeader = null;
+                $anyRequiredOrError = false;
+                $headercounter = 0;
+                foreach (array_keys($this->_elements) as $elementIndex){
+                    $element =& $this->_elements[$elementIndex];
+                    if ($element->getType()=='header') {
+                        if (!is_null($lastHeader)) {
+                            // Check if we had any required elements or
+                            // we are at the top header that should be expanded by default.
+                            if ($anyRequiredOrError || $headercounter === 1) {
+                                $this->setExpanded($lastHeader->getName());
+                            } elseif (!isset($this->_collapsibleElements[$lastHeader->getName()])) {
+                                // Define element as collapsed by default.
+                                $this->setExpanded($lastHeader->getName(), false);
+                            }
+                        }
+                        $headercounter++;
+                        $lastHeader =& $element;
+                        $anyRequiredOrError = false;
+                    } elseif (in_array($element->getName(), $this->_required) || isset($this->_errors[$element->getName()])) {
+                        $anyRequiredOrError = true;
+                    }
+                }
+                // Process very last header.
+                if (!is_null($lastHeader)){
+                    // Check if we had any required elements or
+                    // we are at the top header that should be expanded by default.
+                    if ($anyRequiredOrError || $headercounter === 1) {
+                        $this->setExpanded($lastHeader->getName());
+                    } elseif (!isset($this->_collapsibleElements[$lastHeader->getName()])) {
+                        // Define element as collapsed by default.
+                        $this->setExpanded($lastHeader->getName(), false);
+                    }
+                }
+            }
+            // Pass the array to renderer object.
+            $renderer->setCollapsibleElements($this->_collapsibleElements, $this->getAttribute('id'));
         }
         parent::accept($renderer);
     }
@@ -2225,10 +2326,10 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
 
     /** @var string Header Template string */
     var $_headerTemplate =
-       "\n\t\t<legend class=\"ftoggler\">{header}</legend>\n\t\t<div class=\"advancedbutton\">{advancedimg}{button}</div><div class=\"fcontainer clearfix\">\n\t\t";
+       "\n\t\t<legend class=\"ftoggler\">{header}</legend>\n\t\t<div class=\"fcontainer clearfix\">\n\t\t";
 
     /** @var string Template used when opening a fieldset */
-    var $_openFieldsetTemplate = "\n\t<fieldset class=\"clearfix\" {id}>";
+    var $_openFieldsetTemplate = "\n\t<fieldset class=\"{classes}\" {id}>";
 
     /** @var string Template used when closing a fieldset */
     var $_closeFieldsetTemplate = "\n\t\t</div></fieldset>";
@@ -2236,12 +2337,19 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
     /** @var string Required Note template string */
     var $_requiredNoteTemplate =
         "\n\t\t<div class=\"fdescription required\">{requiredNote}</div>";
-
-    /** @var array list of elements which are marked as advance and will be grouped together */
+    /**
+     * Array whose keys are element names. If the key exists this is a advanced element
+     *
+     * @var array
+     */
     var $_advancedElements = array();
 
-    /** @var int Whether to display advanced elements (on page load) 1 => show, 0 => hide */
-    var $_showAdvanced;
+    /**
+     * Array whose keys are element names and the the boolean values reflect the current state. If the key exists this is a collapsible element.
+     *
+     * @var array
+     */
+    var $_collapsibleElements = array();
 
     /**
      * Constructor
@@ -2274,6 +2382,15 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         $this->_advancedElements = $elements;
     }
 
+    /**
+     * Setting collapsible elements
+     *
+     * @param array $elements
+     */
+    function setCollapsibleElements($elements) {
+        $this->_collapsibleElements = $elements;
+    }
+
     /**
      * What to do when starting the form
      *
@@ -2284,7 +2401,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         $this->_reqHTML = $form->getReqHTML();
         $this->_elementTemplates = str_replace('{req}', $this->_reqHTML, $this->_elementTemplates);
         $this->_advancedHTML = $form->getAdvancedHTML();
-        $this->_showAdvanced = $form->getShowAdvanced();
+        $formid = $form->getAttribute('id');
         parent::startForm($form);
         if ($form->isFrozen()){
             $this->_formTemplate = "\n<div class=\"mform frozen\">\n{content}\n</div>";
@@ -2297,11 +2414,18 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $PAGE->requires->yui_module('moodle-core-formchangechecker',
                     'M.core_formchangechecker.init',
                     array(array(
-                        'formid' => $form->getAttribute('id')
+                        'formid' => $formid
                     ))
             );
             $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
         }
+        if (count($this->_collapsibleElements)) {
+            $PAGE->requires->yui_module('moodle-form-shortforms', 'M.form.shortforms', array(array('formid' => $formid)));
+        }
+        if (!empty($this->_advancedElements)){
+            $PAGE->requires->strings_for_js(array('showmore', 'showless'), 'form');
+            $PAGE->requires->yui_module('moodle-form-showadvanced', 'M.form.showadvanced', array(array('formid' => $formid)));
+        }
     }
 
     /**
@@ -2321,13 +2445,9 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $html = $this->_elementTemplates['default'];
 
         }
-        if ($this->_showAdvanced){
-            $advclass = ' advanced';
-        } else {
-            $advclass = ' advanced hide';
-        }
+
         if (isset($this->_advancedElements[$group->getName()])){
-            $html =str_replace(' {advanced}', $advclass, $html);
+            $html =str_replace(' {advanced}', ' advanced', $html);
             $html =str_replace('{advancedimg}', $this->_advancedHTML, $html);
         } else {
             $html =str_replace(' {advanced}', '', $html);
@@ -2354,6 +2474,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         }
         parent::startGroup($group, $required, $error);
     }
+
     /**
      * Renders element
      *
@@ -2376,13 +2497,8 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
         }else{
             $html = $this->_elementTemplates['default'];
         }
-        if ($this->_showAdvanced){
-            $advclass = ' advanced';
-        } else {
-            $advclass = ' advanced hide';
-        }
         if (isset($this->_advancedElements[$element->getName()])){
-            $html =str_replace(' {advanced}', $advclass, $html);
+            $html =str_replace(' {advanced}', ' advanced', $html);
         } else {
             $html =str_replace(' {advanced}', '', $html);
         }
@@ -2451,38 +2567,27 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $header_html = str_replace('{header}', $header->toHtml(), $this->_headerTemplate);
         }
 
-        if (isset($this->_advancedElements[$name])){
-            $header_html =str_replace('{advancedimg}', $this->_advancedHTML, $header_html);
-            $elementName='mform_showadvanced';
-            if ($this->_showAdvanced==0){
-                $buttonlabel = get_string('showadvanced', 'form');
-            } else {
-                $buttonlabel = get_string('hideadvanced', 'form');
-            }
-            $button = '<input name="'.$elementName.'" class="showadvancedbtn" value="'.$buttonlabel.'" type="submit" />';
-            $PAGE->requires->js_init_call('M.form.initShowAdvanced', array(), false, moodleform::get_js_module());
-            $header_html = str_replace('{button}', $button, $header_html);
-        } else {
-            $header_html =str_replace('{advancedimg}', '', $header_html);
-            $header_html = str_replace('{button}', '', $header_html);
-        }
-
         if ($this->_fieldsetsOpen > 0) {
             $this->_html .= $this->_closeFieldsetTemplate;
             $this->_fieldsetsOpen--;
         }
 
-        $openFieldsetTemplate = str_replace('{id}', $id, $this->_openFieldsetTemplate);
-        if ($this->_showAdvanced){
-            $advclass = ' class="advanced"';
-        } else {
-            $advclass = ' class="advanced hide"';
+        // Define collapsible classes for fieldsets.
+        $fieldsetclasses = array('clearfix');
+        if (isset($this->_collapsibleElements[$name])) {
+            $fieldsetclasses[] = 'collapsible';
+            if ($this->_collapsibleElements[$name]) {
+                $fieldsetclasses[] = 'collapsed';
+            }
         }
+
         if (isset($this->_advancedElements[$name])){
-            $openFieldsetTemplate = str_replace('{advancedclass}', $advclass, $openFieldsetTemplate);
-        } else {
-            $openFieldsetTemplate = str_replace('{advancedclass}', '', $openFieldsetTemplate);
+            $fieldsetclasses[] = 'containsadvancedelements';
         }
+
+        $openFieldsetTemplate = str_replace('{id}', $id, $this->_openFieldsetTemplate);
+        $openFieldsetTemplate = str_replace('{classes}', join(' ', $fieldsetclasses), $openFieldsetTemplate);
+
         $this->_html .= $openFieldsetTemplate . $header_html;
         $this->_fieldsetsOpen++;
     }
@@ -2582,6 +2687,7 @@ MoodleQuickForm::registerElementType('group', "$CFG->libdir/form/group.php", 'Mo
 MoodleQuickForm::registerElementType('header', "$CFG->libdir/form/header.php", 'MoodleQuickForm_header');
 MoodleQuickForm::registerElementType('hidden', "$CFG->libdir/form/hidden.php", 'MoodleQuickForm_hidden');
 MoodleQuickForm::registerElementType('htmleditor', "$CFG->libdir/form/htmleditor.php", 'MoodleQuickForm_htmleditor');
+MoodleQuickForm::registerElementType('listing', "$CFG->libdir/form/listing.php", 'MoodleQuickForm_listing');
 MoodleQuickForm::registerElementType('modgrade', "$CFG->libdir/form/modgrade.php", 'MoodleQuickForm_modgrade');
 MoodleQuickForm::registerElementType('modvisible', "$CFG->libdir/form/modvisible.php", 'MoodleQuickForm_modvisible');
 MoodleQuickForm::registerElementType('password', "$CFG->libdir/form/password.php", 'MoodleQuickForm_password');
index 9ebc9b5..dcdb3c8 100644 (file)
@@ -35,7 +35,7 @@ require_once($CFG->libdir . '/gradelib.php');
  * category1 => array(category2 => array(grade_item1, grade_item2), category3 => array(grade_item3))
  * 3 users for 3 grade_items
  */
-class grade_base_testcase extends advanced_testcase {
+abstract class grade_base_testcase extends advanced_testcase {
 
     protected $course;
     protected $activities = array();
index 7b8cca2..30e1297 100644 (file)
@@ -233,7 +233,10 @@ function install_generate_configphp($database, $cfg) {
     }
     $configphp .= '$CFG->directorypermissions = ' . $chmod . ';' . PHP_EOL . PHP_EOL;
 
-    $configphp .= '$CFG->passwordsaltmain = '.var_export(complex_random_string(), true) . ';' . PHP_EOL . PHP_EOL;
+    // A site-wide salt is only needed if bcrypt is not properly supported by the current version of PHP.
+    if (password_compat_not_supported()) {
+        $configphp .= '$CFG->passwordsaltmain = '.var_export(complex_random_string(), true) . ';' . PHP_EOL . PHP_EOL;
+    }
 
     $configphp .= 'require_once(dirname(__FILE__) . \'/lib/setup.php\');' . PHP_EOL . PHP_EOL;
     $configphp .= '// There is no php closing tag in this file,' . PHP_EOL;
index d709396..b1e2ed4 100644 (file)
@@ -1480,131 +1480,27 @@ M.util.help_popups = {
     }
 }
 
+/**
+ * This code bas been deprecated and will be removed from Moodle 2.7
+ *
+ * Please see lib/yui/popuphelp/popuphelp.js for its replacement
+ */
 M.util.help_icon = {
-    Y : null,
-    instance : null,
     initialised : false,
-    setup : function(Y) {
-        if (this.initialised) {
-            // Exit early if we have already completed setup
-            return;
-        }
-        this.Y = Y;
-        Y.one('body').delegate('click', this.display, 'span.helplink a.tooltip', this);
-        this.initialised = true;
-    },
-    add : function(Y, properties) {
-        this.setup(Y);
+    setup : function(Y, properties) {
+        this.add(Y, properties);
     },
-    display : function(event) {
-        event.preventDefault();
-        if (M.util.help_icon.instance === null) {
-            var Y = M.util.help_icon.Y;
-            Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', 'escape', function(Y) {
-                var help_content_overlay = {
-                    helplink : null,
-                    overlay : null,
-                    init : function() {
-
-                        var strclose = Y.Escape.html(M.str.form.close);
-                        var footerbtn = Y.Node.create('<button class="closebtn">'+strclose+'</button>');
-                        // Create an overlay from markup
-                        this.overlay = new Y.Overlay({
-                            footerContent: footerbtn,
-                            bodyContent: '',
-                            id: 'helppopupbox',
-                            width:'400px',
-                            visible : false,
-                            constrain : true
-                        });
-                        this.overlay.render(Y.one(document.body));
-
-                        footerbtn.on('click', this.close, this);
-
-                        var boundingBox = this.overlay.get("boundingBox");
-
-                        //  Hide the menu if the user clicks outside of its content
-                        boundingBox.get("ownerDocument").on("mousedown", function (event) {
-                            var oTarget = event.target;
-                            var menuButton = this.helplink;
-
-                            if (!oTarget.compareTo(menuButton) &&
-                                !menuButton.contains(oTarget) &&
-                                !oTarget.compareTo(boundingBox) &&
-                                !boundingBox.contains(oTarget)) {
-                                this.overlay.hide();
-                            }
-                        }, this);
-                    },
-
-                    close : function(e) {
-                        e.preventDefault();
-                        this.helplink.focus();
-                        this.overlay.hide();
-                    },
-
-                    display : function(event) {
-                        var overlayPosition;
-                        this.helplink = event.target.ancestor('span.helplink a', true);
-                        if (Y.one('html').get('dir') === 'rtl') {
-                            overlayPosition = [Y.WidgetPositionAlign.TR, Y.WidgetPositionAlign.LC];
-                        } else {
-                            overlayPosition = [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC];
-                        }
-
-                        this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
-                        this.overlay.set("align", {node:this.helplink, points: overlayPosition});
-
-                        var cfg = {
-                            method: 'get',
-                            context : this,
-                            data : {
-                                ajax : 1
-                            },
-                            on: {
-                                success: function(id, o, node) {
-                                    this.display_callback(o.responseText);
-                                },
-                                failure: function(id, o, node) {
-                                    var debuginfo = o.statusText;
-                                    if (M.cfg.developerdebug) {
-                                        o.statusText += ' (' + ajaxurl + ')';
-                                    }
-                                    this.display_callback('bodyContent',debuginfo);
-                                }
-                            }
-                        };
-
-                        Y.io(this.helplink.get('href'), cfg);
-                        this.overlay.show();
-                    },
-
-                    display_callback : function(content) {
-                        var contentnode, heading;
-                        contentnode = Y.Node.create('<div role="alert">' + content + '</div>');
-                        this.overlay.set('bodyContent', contentnode);
-                        heading = contentnode.one('h2');
-                        if (heading) {
-                            heading.set('tabIndex', 0);
-                            heading.focus();
-                        }
-                    },
-
-                    hideContent : function() {
-                        help = this;
-                        help.overlay.hide();
-                    }
-                };
-                help_content_overlay.init();
-                M.util.help_icon.instance = help_content_overlay;
-                M.util.help_icon.instance.display(event);
+    add : function(Y) {
+        if (M.cfg.developerdebug) {
+            Y.log("You are using a deprecated function call (M.util.help_icon.add). " +
+                    "Please look at rewriting your call to support lib/yui/popuphelp/popuphelp.js");
+        }
+        if (!this.initialised) {
+            YUI().use('moodle-core-popuphelp', function() {
+                M.core.init_popuphelp([]);
             });
-        } else {
-            M.util.help_icon.instance.display(event);
         }
-    },
-    init : function(Y) {
-        this.Y = Y;
+        this.initialised = true;
     }
 };
 
index 7a3b4fd..6890a83 100644 (file)
@@ -116,7 +116,11 @@ define('PARAM_BOOL',     'bool');
 define('PARAM_CAPABILITY',   'capability');
 
 /**
- * PARAM_CLEANHTML - cleans submitted HTML code. use only for text in HTML format. This cleaning may fix xhtml strictness too.
+ * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
+ * to use this. The normal mode of operation is to use PARAM_RAW when recieving
+ * the input (required/optional_param or formslib) and then sanitse the HTML
+ * using format_text on output. This is for the rare cases when you want to
+ * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
  */
 define('PARAM_CLEANHTML', 'cleanhtml');
 
@@ -493,6 +497,11 @@ define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
 define('COURSE_DISPLAY_SINGLEPAGE', 0); // display all sections on one page
 define('COURSE_DISPLAY_MULTIPAGE', 1); // split pages into a page per section
 
+/**
+ * Authentication constants.
+ */
+define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); // String used in password field when password is not stored.
+
 /// PARAMETER HANDLING ////////////////////////////////////////////////////
 
 /**
@@ -1318,6 +1327,9 @@ function set_config($name, $value, $plugin=NULL) {
                 $DB->insert_record('config', $config, false);
             }
         }
+        if ($name === 'siteidentifier') {
+            cache_helper::update_site_identifier($value);
+        }
         cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
     } else { // plugin scope
         if ($id = $DB->get_field('config_plugins', 'id', array('name'=>$name, 'plugin'=>$plugin))) {
@@ -1351,6 +1363,8 @@ function set_config($name, $value, $plugin=NULL) {
  * If called with 2 parameters it will return a string single
  * value or false if the value is not found.
  *
+ * @static $siteidentifier The site identifier is not cached. We use this static cache so
+ *     that we need only fetch it once per request.
  * @param string $plugin full component name
  * @param string $name default NULL
  * @return mixed hash-like object or single value, return false no config found
@@ -1358,6 +1372,8 @@ function set_config($name, $value, $plugin=NULL) {
 function get_config($plugin, $name = NULL) {
     global $CFG, $DB;
 
+    static $siteidentifier = null;
+
     if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
         $forced =& $CFG->config_php_settings;
         $iscore = true;
@@ -1371,8 +1387,28 @@ function get_config($plugin, $name = NULL) {
         $iscore = false;
     }
 
-    if (!empty($name) && array_key_exists($name, $forced)) {
-        return (string)$forced[$name];
+    if ($siteidentifier === null) {
+        try {
+            // This may fail during installation.
+            // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
+            // install the database.
+            $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
+        } catch (dml_exception $ex) {
+            // It's failed. We'll use this opportunity to disable cache stores so that we don't inadvertingly start using
+            // old caches. People should delete their moodledata dirs when reinstalling the database... but they don't.
+            cache_factory::disable_stores();
+            // Set siteidentifier to false. We don't want to trip this continually.
+            $siteidentifier = false;
+            throw $ex;
+        }
+    }
+
+    if (!empty($name)) {
+        if (array_key_exists($name, $forced)) {
+            return (string)$forced[$name];
+        } else if ($name === 'siteidentifier' && $plugin == 'core') {
+            return $siteidentifier;
+        }
     }
 
     $cache = cache::make('core', 'config');
@@ -1396,6 +1432,10 @@ function get_config($plugin, $name = NULL) {
         return false;
     }
 
+    if ($plugin === 'core') {
+        $result['siteidentifier'] = $siteidentifier;
+    }
+
     foreach ($forced as $key => $value) {
         if (is_null($value) or is_array($value) or is_object($value)) {
             // we do not want any extra mess here, just real settings that could be saved in db
@@ -3845,6 +3885,7 @@ function create_user_record($username, $password, $auth = 'manual') {
     if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})){
         set_user_preference('auth_forcepasswordchange', 1, $user);
     }
+    // Set the password.
     update_internal_user_password($user, $password);
 
     // fetch full user record for the event, the complete user data contains too much info
@@ -4197,7 +4238,10 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
                 $user->auth = $auth;
             }
 
-            update_internal_user_password($user, $password); // just in case salt or encoding were changed (magic quotes too one day)
+            // If the existing hash is using an out-of-date algorithm (or the
+            // legacy md5 algorithm), then we should update to the current
+            // hash algorithm while we have access to the user's password.
+            update_internal_user_password($user, $password);
 
             if ($authplugin->is_synchronised_with_external()) { // update user record from external DB
                 $user = update_user_record($username);
@@ -4307,28 +4351,81 @@ function complete_user_login($user) {
 }
 
 /**
- * Compare password against hash stored in internal user table.
- * If necessary it also updates the stored hash to new format.
+ * Check a password hash to see if it was hashed using the
+ * legacy hash algorithm (md5).
+ *
+ * @param string $password String to check.
+ * @return boolean True if the $password matches the format of an md5 sum.
+ */
+function password_is_legacy_hash($password) {
+    return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
+}
+
+/**
+ * Checks whether the password compatibility library will work with the current
+ * version of PHP. This cannot be done using PHP version numbers since the fix
+ * has been backported to earlier versions in some distributions.
+ *
+ * See https://github.com/ircmaxell/password_compat/issues/10 for
+ * more details.
+ *
+ * @return bool True if the library is NOT supported.
+ */
+function password_compat_not_supported() {
+
+    $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
+
+    // Create a one off application cache to store bcrypt support status as
+    // the support status doesn't change and crypt() is slow.
+    $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'password_compat');
+
+    if (!$bcryptsupport = $cache->get('bcryptsupport')) {
+        $test = crypt('password', $hash);
+        // Cache string instead of boolean to avoid MDL-37472.
+        if ($test == $hash) {
+            $bcryptsupport = 'supported';
+        } else {
+            $bcryptsupport = 'not supported';
+        }
+        $cache->set('bcryptsupport', $bcryptsupport);
+    }
+
+    // Return true if bcrypt *not* supported.
+    return ($bcryptsupport !== 'supported');
+}
+
+/**
+ * Compare password against hash stored in user object to determine if it is valid.
  *
- * @param stdClass $user (password property may be updated)
- * @param string $password plain text password
- * @return bool is password valid?
+ * If necessary it also updates the stored hash to the current format.
+ *
+ * @param stdClass $user (Password property may be updated).
+ * @param string $password Plain text password.
+ * @return bool True if password is valid.
  */
 function validate_internal_user_password($user, $password) {
     global $CFG;
+    require_once($CFG->libdir.'/password_compat/lib/password.php');
 
-    if (!isset($CFG->passwordsaltmain)) {
-        $CFG->passwordsaltmain = '';
+    if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
+        // Internal password is not used at all, it can not validate.
+        return false;
     }
 
-    $validated = false;
+    // If hash isn't a legacy (md5) hash, validate using the library function.
+    if (!password_is_legacy_hash($user->password)) {
+        return password_verify($password, $user->password);
+    }
+
+    // Otherwise we need to check for a legacy (md5) hash instead. If the hash
+    // is valid we can then update it to the new algorithm.
 
-    if ($user->password === 'not cached') {
-        // internal password is not used at all, it can not validate
+    $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
+    $validated = false;
 
-    } else if ($user->password === md5($password.$CFG->passwordsaltmain)
+    if ($user->password === md5($password.$sitesalt)
             or $user->password === md5($password)
-            or $user->password === md5(addslashes($password).$CFG->passwordsaltmain)
+            or $user->password === md5(addslashes($password).$sitesalt)
             or $user->password === md5(addslashes($password))) {
         // note: we are intentionally using the addslashes() here because we
         //       need to accept old password hashes of passwords with magic quotes
@@ -4347,7 +4444,8 @@ function validate_internal_user_password($user, $password) {
     }
 
     if ($validated) {
-        // force update of password hash using latest main password salt and encoding if needed
+        // If the password matches the existing md5 hash, update to the
+        // current hash algorithm while we have access to the user's password.
         update_internal_user_password($user, $password);
     }
 
@@ -4355,39 +4453,85 @@ function validate_internal_user_password($user, $password) {
 }
 
 /**
- * Calculate hashed value from password using current hash mechanism.
+ * Calculate hash for a plain text password.
+ *
+ * @param string $password Plain text password to be hashed.
+ * @param bool $fasthash If true, use a low cost factor when generating the hash
+ *                       This is much faster to generate but makes the hash
+ *                       less secure. It is used when lots of hashes need to
+ *                       be generated quickly.
+ * @return string The hashed password.
  *
- * @param string $password
- * @return string password hash
+ * @throws moodle_exception If a problem occurs while generating the hash.
  */
-function hash_internal_user_password($password) {
+function hash_internal_user_password($password, $fasthash = false) {
     global $CFG;
+    require_once($CFG->libdir.'/password_compat/lib/password.php');
 
-    if (isset($CFG->passwordsaltmain)) {
-        return md5($password.$CFG->passwordsaltmain);
-    } else {
-        return md5($password);
+    // Use the legacy hashing algorithm (md5) if PHP is not new enough
+    // to support bcrypt properly
+    if (password_compat_not_supported()) {
+        if (isset($CFG->passwordsaltmain)) {
+            return md5($password.$CFG->passwordsaltmain);
+        } else {
+            return md5($password);
+        }
+    }
+
+    // Set the cost factor to 4 for fast hashing, otherwise use default cost.
+    $options = ($fasthash) ? array('cost' => 4) : array();
+
+    $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
+
+    if ($generatedhash === false) {
+        throw new moodle_exception('Failed to generate password hash.');
     }
+
+    return $generatedhash;
 }
 
 /**
- * Update password hash in user object.
+ * Update password hash in user object (if necessary).
+ *
+ * The password is updated if:
+ * 1. The password has changed (the hash of $user->password is different
+ *    to the hash of $password).
+ * 2. The existing hash is using an out-of-date algorithm (or the legacy
+ *    md5 algorithm).
  *
- * @param stdClass $user (password property may be updated)
- * @param string $password plain text password
- * @return bool always returns true
+ * Updating the password will modify the $user object and the database
+ * record to use the current hashing algorithm.
+ *
+ * @param stdClass $user User object (password property may be updated).
+ * @param string $password Plain text password.
+ * @return bool Always returns true.
  */
 function update_internal_user_password($user, $password) {
-    global $DB;
+    global $CFG, $DB;
+    require_once($CFG->libdir.'/password_compat/lib/password.php');
 
+    // Use the legacy hashing algorithm (md5) if PHP doesn't support
+    // bcrypt properly.
+    $legacyhash = password_compat_not_supported();
+
+    // Figure out what the hashed password should be.
     $authplugin = get_auth_plugin($user->auth);
     if ($authplugin->prevent_local_passwords()) {
-        $hashedpassword = 'not cached';
+        $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
     } else {
         $hashedpassword = hash_internal_user_password($password);
     }
 
-    if ($user->password !== $hashedpassword) {
+    if ($legacyhash) {
+        $passwordchanged = ($user->password !== $hashedpassword);
+        $algorithmchanged = false;
+    } else {
+        // If verification fails then it means the password has changed.
+        $passwordchanged = !password_verify($password, $user->password);
+        $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
+    }
+
+    if ($passwordchanged || $algorithmchanged) {
         $DB->set_field('user', 'password',  $hashedpassword, array('id'=>$user->id));
         $user->password = $hashedpassword;
     }
@@ -5588,9 +5732,10 @@ function generate_email_supportuser() {
  * @global object
  * @global object
  * @param user $user A {@link $USER} object
+ * @param boolean $fasthash If true, use a low cost factor when generating the hash for speed.
  * @return boolean|string Returns "true" if mail was sent OK and "false" if there was an error
  */
-function setnew_password_and_mail($user) {
+function setnew_password_and_mail($user, $fasthash = false) {
     global $CFG, $DB;
 
     // we try to send the mail in language the user understands,
@@ -5604,7 +5749,8 @@ function setnew_password_and_mail($user) {
 
     $newpassword = generate_password();
 
-    $DB->set_field('user', 'password', hash_internal_user_password($newpassword), array('id'=>$user->id));
+    $hashedpassword = hash_internal_user_password($newpassword, $fasthash);
+    $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
 
     $a = new stdClass();
     $a->firstname   = fullname($user, true);
@@ -6022,7 +6168,10 @@ function get_user_max_upload_file_size($context, $sitebytes=0, $coursebytes=0, $
  * array of possible sizes in an array, translated to the
  * local language.
  *
- * @todo Finish documenting this function
+ * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
+ *
+ * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
+ * with the value set to 0. This option will be the first in the list.
  *
  * @global object
  * @uses SORT_NUMERIC
@@ -6041,7 +6190,7 @@ function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0
     }
 
     $filesize = array();
-    $filesize[intval($maxsize)] = display_size($maxsize);
+    $filesize[(string)intval($maxsize)] = display_size($maxsize);
 
     $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
                       5242880, 10485760, 20971520, 52428800, 104857600);
@@ -6063,12 +6212,31 @@ function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0
     }
 
     foreach ($sizelist as $sizebytes) {
-       if ($sizebytes < $maxsize) {
-           $filesize[intval($sizebytes)] = display_size($sizebytes);
+       if ($sizebytes < $maxsize && $sizebytes > 0) {
+           $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
        }
     }
 
     krsort($filesize, SORT_NUMERIC);
+    $limitlevel = '';
+    $displaysize = '';
+    if ($modulebytes &&
+        (($modulebytes < $coursebytes || $coursebytes == 0) &&
+         ($modulebytes < $sitebytes || $sitebytes == 0))) {
+        $limitlevel = get_string('activity', 'core');
+        $displaysize = display_size($modulebytes);
+    } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
+        $limitlevel = get_string('course', 'core');
+        $displaysize = display_size($coursebytes);
+    } else if ($sitebytes) {
+        $limitlevel = get_string('site', 'core');
+        $displaysize = display_size($sitebytes);
+    }
+
+    if ($limitlevel) {
+        $params = (object) array('contextname'=>$limitlevel, 'displaysize'=>$displaysize);
+        $filesize  = array('0'=>get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
+    }
 
     return $filesize;
 }
index 699cc73..f350701 100644 (file)
@@ -2169,38 +2169,6 @@ class global_navigation extends navigation_node {
             $usernode->add(get_string('notes', 'notes'), $url);
         }
 
-        // Add reports node
-        $reporttab = $usernode->add(get_string('activityreports'));
-        $reports = get_plugin_list_with_function('report', 'extend_navigation_user', 'lib.php');
-        foreach ($reports as $reportfunction) {
-            $reportfunction($reporttab, $user, $course);
-        }
-        $anyreport = has_capability('moodle/user:viewuseractivitiesreport', $usercontext);
-        if ($anyreport || ($course->showreports && $iscurrentuser && $forceforcontext)) {
-            // Add grade hardcoded grade report if necessary
-            $gradeaccess = false;
-            if (has_capability('moodle/grade:viewall', $coursecontext)) {
-                //ok - can view all course grades
-                $gradeaccess = true;
-            } else if ($course->showgrades) {
-                if ($iscurrentuser && has_capability('moodle/grade:view', $coursecontext)) {
-                    //ok - can view own grades
-                    $gradeaccess = true;
-                } else if (has_capability('moodle/grade:viewall', $usercontext)) {
-                    // ok - can view grades of this user - parent most probably
-                    $gradeaccess = true;
-                } else if ($anyreport) {
-                    // ok - can view grades of this user - parent most probably
-                    $gradeaccess = true;
-                }
-            }
-            if ($gradeaccess) {
-                $reporttab->add(get_string('grade'), new moodle_url('/course/user.php', array('mode'=>'grade', 'id'=>$course->id, 'user'=>$usercontext->instanceid)));
-            }
-        }
-        // Check the number of nodes in the report node... if there are none remove the node
-        $reporttab->trim_if_empty();
-
         // If the user is the current user add the repositories for the current user
         $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
         if ($iscurrentuser) {
@@ -2439,26 +2407,6 @@ class global_navigation extends navigation_node {
             $participants = $coursenode->add(get_string('participants'), null, self::TYPE_CONTAINER, get_string('participants'), 'participants');
         }
 
-        // View course reports
-        if (has_capability('moodle/site:viewreports', $this->page->context)) { // basic capability for listing of reports
-            $reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null, new pix_icon('i/stats', ''));
-            $coursereports = get_plugin_list('coursereport'); // deprecated
-            foreach ($coursereports as $report=>$dir) {
-                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
-                if (file_exists($libfile)) {
-                    require_once($libfile);
-                    $reportfunction = $report.'_report_extend_navigation';
-                    if (function_exists($report.'_report_extend_navigation')) {
-                        $reportfunction($reportnav, $course, $this->page->context);
-                    }
-                }
-            }
-
-            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
-            foreach ($reports as $reportfunction) {
-                $reportfunction($reportnav, $course, $this->page->context);
-            }
-        }
         return true;
     }
     /**
@@ -2514,26 +2462,6 @@ class global_navigation extends navigation_node {
             $coursenode->add(get_string('calendar', 'calendar'), $calendarurl, self::TYPE_CUSTOM, null, 'calendar');
         }
 
-        // View course reports
-        if (has_capability('moodle/site:viewreports', $this->page->context)) { // basic capability for listing of reports
-            $reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null, new pix_icon('i/stats', ''));
-            $coursereports = get_plugin_list('coursereport'); // deprecated
-            foreach ($coursereports as $report=>$dir) {
-                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
-                if (file_exists($libfile)) {
-                    require_once($libfile);
-                    $reportfunction = $report.'_report_extend_navigation';
-                    if (function_exists($report.'_report_extend_navigation')) {
-                        $reportfunction($reportnav, $course, $this->page->context);
-                    }
-                }
-            }
-
-            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
-            foreach ($reports as $reportfunction) {
-                $reportfunction($reportnav, $course, $this->page->context);
-            }
-        }
         return true;
     }
 
@@ -3470,6 +3398,28 @@ class settings_navigation extends navigation_node {
             $coursenode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/filter', ''));
         }
 
+        // View course reports.
+        if (has_capability('moodle/site:viewreports', $coursecontext)) { // Basic capability for listing of reports.
+            $reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null,
+                    new pix_icon('i/stats', ''));
+            $coursereports = get_plugin_list('coursereport');
+            foreach ($coursereports as $report => $dir) {
+                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
+                if (file_exists($libfile)) {
+                    require_once($libfile);
+                    $reportfunction = $report.'_report_extend_navigation';
+                    if (function_exists($report.'_report_extend_navigation')) {
+                        $reportfunction($reportnav, $course, $coursecontext);
+                    }
+                }
+            }
+
+            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
+            foreach ($reports as $reportfunction) {
+                $reportfunction($reportnav, $course, $coursecontext);
+            }
+        }
+
         // Add view grade report is permitted
         $reportavailable = false;
         if (has_capability('moodle/grade:viewall', $coursecontext)) {
@@ -3976,6 +3926,40 @@ class settings_navigation extends navigation_node {
             }
         }
 
+        // Add reports node.
+        $reporttab = $usersetting->add(get_string('activityreports'));
+        $reports = get_plugin_list_with_function('report', 'extend_navigation_user', 'lib.php');
+        foreach ($reports as $reportfunction) {
+            $reportfunction($reporttab, $user, $course);
+        }
+        $anyreport = has_capability('moodle/user:viewuseractivitiesreport', $usercontext);
+        if ($anyreport || ($course->showreports && $iscurrentuser && $forceforcontext)) {
+            // Add grade hardcoded grade report if necessary.
+            $gradeaccess = false;
+            if (has_capability('moodle/grade:viewall', $coursecontext)) {
+                // Can view all course grades.
+                $gradeaccess = true;
+            } else if ($course->showgrades) {
+                if ($iscurrentuser && has_capability('moodle/grade:view', $coursecontext)) {
+                    // Can view own grades.
+                    $gradeaccess = true;
+                } else if (has_capability('moodle/grade:viewall', $usercontext)) {
+                    // Can view grades of this user - parent most probably.
+                    $gradeaccess = true;
+                } else if ($anyreport) {
+                    // Can view grades of this user - parent most probably.
+                    $gradeaccess = true;
+                }
+            }
+            if ($gradeaccess) {
+                $reporttab->add(get_string('grade'), new moodle_url('/course/user.php', array('mode'=>'grade', 'id'=>$course->id,
+                        'user'=>$usercontext->instanceid)));
+            }
+        }
+        // Check the number of nodes in the report node... if there are none remove the node
+        $reporttab->trim_if_empty();
+
+
         // Login as ...
         if (!$user->deleted and !$currentuser && !session_is_loggedinas() && has_capability('moodle/user:loginas', $coursecontext) && !is_siteadmin($user->id)) {
             $url = new moodle_url('/course/loginas.php', array('id'=>$course->id, 'user'=>$user->id, 'sesskey'=>sesskey()));
@@ -4141,6 +4125,28 @@ class settings_navigation extends navigation_node {
             $frontpage->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/filter', ''));
         }
 
+        // View course reports.
+        if (has_capability('moodle/site:viewreports', $coursecontext)) { // Basic capability for listing of reports.
+            $frontpagenav = $frontpage->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null,
+                    new pix_icon('i/stats', ''));
+            $coursereports = get_plugin_list('coursereport');
+            foreach ($coursereports as $report=>$dir) {
+                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
+                if (file_exists($libfile)) {
+                    require_once($libfile);
+                    $reportfunction = $report.'_report_extend_navigation';
+                    if (function_exists($report.'_report_extend_navigation')) {
+                        $reportfunction($frontpagenav, $course, $coursecontext);
+                    }
+                }
+            }
+
+            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
+            foreach ($reports as $reportfunction) {
+                $reportfunction($frontpagenav, $course, $coursecontext);
+            }
+        }
+
         // Backup this course
         if (has_capability('moodle/backup:backupcourse', $coursecontext)) {
             $url = new moodle_url('/backup/backup.php', array('id'=>$course->id));
index fb4808c..4e0545b 100644 (file)
@@ -364,9 +364,16 @@ class core_renderer extends renderer_base {
         // flow player embedding support
         $this->page->requires->js_function_call('M.util.load_flowplayer');
 
-        // Set up help link popups for all links with the helplinkpopup class
+        // Set up help link popups for all links with the helptooltip class
         $this->page->requires->js_init_call('M.util.help_popups.setup');
 
+        // Setup help icon overlays.
+        $this->page->requires->yui_module('moodle-core-popuphelp', 'M.core.init_popuphelp');
+        $this->page->requires->strings_for_js(array(
+            'morehelp',
+            'loadinghelp',
+        ), 'moodle');
+
         $this->page->requires->js_function_call('setTimeout', array('fix_column_widths()', 20));
 
         $focus = $this->page->focuscontrol;
@@ -1932,7 +1939,7 @@ class core_renderer extends renderer_base {
         $this->page->requires->string_for_js('close', 'form');
 
         // and finally span
-        return html_writer::tag('span', $output, array('class' => 'helplink'));
+        return html_writer::tag('span', $output, array('class' => 'helptooltip'));
     }
 
     /**
@@ -1989,14 +1996,11 @@ class core_renderer extends renderer_base {
         // note: this title is displayed only if JS is disabled, otherwise the link will have the new ajax tooltip
         $title = get_string('helpprefix2', '', trim($title, ". \t"));
 
-        $attributes = array('href'=>$url, 'title'=>$title, 'aria-haspopup' => 'true', 'class' => 'tooltip');
+        $attributes = array('href' => $url, 'title' => $title, 'aria-haspopup' => 'true');
         $output = html_writer::tag('a', $output, $attributes);
 
-        $this->page->requires->js_init_call('M.util.help_icon.setup');
-        $this->page->requires->string_for_js('close', 'form');
-
         // and finally span
-        return html_writer::tag('span', $output, array('class' => 'helplink'));
+        return html_writer::tag('span', $output, array('class' => 'helptooltip'));
     }
 
     /**
index 941cb1e..89d509f 100644 (file)
@@ -423,7 +423,7 @@ class page_requirements_manager {
                     $module = array('name'     => 'core_dock',
                                     'fullpath' => '/blocks/dock.js',
                                     'requires' => array('base', 'node', 'event-custom', 'event-mouseenter', 'event-resize'),
-                                    'strings' => array(array('addtodock', 'block'),array('undockitem', 'block'),array('undockall', 'block'),array('thisdirectionvertical', 'langconfig'),array('hidedockpanel', 'block'),array('hidepanel', 'block')));
+                                    'strings' => array(array('addtodock', 'block'),array('undockitem', 'block'),array('undockblock', 'block'),array('undockall', 'block'),array('thisdirectionvertical', 'langconfig'),array('hidedockpanel', 'block'),array('hidepanel', 'block')));
                     break;
                 case 'core_message':
                     $module = array('name'     => 'core_message',
diff --git a/lib/password_compat/lib/password.php b/lib/password_compat/lib/password.php
new file mode 100644 (file)
index 0000000..6732e9f
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * @author Anthony Ferrara <ircmaxell@php.net>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ *&nb