Merge branch 'w30_MDL-40627_m26_extramemorylimit' of https://github.com/skodak/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 23 Jul 2013 21:04:13 +0000 (23:04 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 23 Jul 2013 21:04:13 +0000 (23:04 +0200)
250 files changed:
admin/cli/install.php
admin/environment.xml
admin/tests/behat/behat_admin.php
admin/tests/behat/display_short_names.feature
admin/tool/behat/cli/util.php
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/tests/behat/basic_actions.feature
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/profiling/index.php
admin/tool/profiling/lang/en/tool_profiling.php
admin/tool/uploaduser/index.php
admin/tool/uploaduser/user_form.php
auth/classes/event/user_loggedin.php [new file with mode: 0644]
auth/tests/auth_test.php [new file with mode: 0644]
auth/tests/behat/behat_auth.php
backup/backupfilesedit_form.php
backup/util/dbops/restore_dbops.class.php
backup/util/ui/tests/behat/backup_courses.feature
backup/util/ui/tests/behat/behat_backup.php
backup/util/xml/parser/processors/grouped_parser_processor.class.php
badges/backpack.js
badges/badge.php
badges/mybadges.php
badges/renderer.php
blocks/comments/tests/behat/behat_block_comments.php
blocks/course_overview/renderer.php
blocks/recent_activity/block_recent_activity.php
blocks/tests/behat/behat_blocks.php
blocks/tests/behat/configure_block_throughout_site.feature
cache/stores/memcache/addinstanceform.php
cache/stores/memcache/lang/en/cachestore_memcache.php
cache/stores/memcache/lib.php
cache/stores/memcache/tests/memcache_test.php
cache/stores/memcached/addinstanceform.php
cache/stores/memcached/lang/en/cachestore_memcached.php
cache/stores/memcached/lib.php
cache/stores/memcached/tests/memcached_test.php
cache/stores/session/lib.php
cache/stores/session/tests/session_test.php
cache/stores/static/lib.php
cache/stores/static/tests/static_test.php
cache/tests/fixtures/stores.php
cohort/externallib.php
cohort/tests/behat/behat_cohort.php
cohort/tests/behat/upload_cohort_users.feature
cohort/tests/externallib_test.php
cohort/upgrade.txt [new file with mode: 0644]
completion/tests/behat/behat_completion.php
config-dist.php
course/edit_form.php
course/format/singleactivity/format.php [new file with mode: 0644]
course/format/singleactivity/lang/en/format_singleactivity.php [new file with mode: 0644]
course/format/singleactivity/lib.php [new file with mode: 0644]
course/format/singleactivity/renderer.php [new file with mode: 0644]
course/format/singleactivity/settings.php [new file with mode: 0644]
course/format/singleactivity/settingslib.php [new file with mode: 0644]
course/format/singleactivity/styles.css [new file with mode: 0644]
course/format/singleactivity/version.php [new file with mode: 0644]
course/tests/behat/activities_group_icons.feature
course/tests/behat/activities_indentation.feature
course/tests/behat/behat_course.php
course/tests/behat/force_group_mode.feature
enrol/category/classes/observer.php [new file with mode: 0644]
enrol/category/db/events.php
enrol/category/locallib.php
enrol/category/tests/plugin_test.php [moved from enrol/category/tests/sync_test.php with 98% similarity]
enrol/externallib.php
enrol/guest/lib.php
enrol/tests/behat/behat_enrol.php
enrol/upgrade.txt
grade/grading/form/guide/lib.php
grade/grading/form/guide/version.php
grade/grading/form/lib.php
grade/grading/form/rubric/lib.php
grade/grading/form/rubric/styles.css
grade/grading/form/rubric/version.php
grade/grading/form/upgrade.txt [new file with mode: 0644]
grade/import/lib.php
grade/lib.php
grade/report/grader/lib.php
grade/report/lib.php
grade/report/user/lib.php
group/lib.php
group/tests/behat/behat_groups.php
install.php
lang/en/auth.php
lang/en/badges.php
lang/en/cache.php
lib/accesslib.php
lib/authlib.php
lib/badgeslib.php
lib/behat/behat_files.php
lib/behat/form_field/behat_form_select.php
lib/behat/lib.php
lib/classes/component.php
lib/classes/event/base.php [new file with mode: 0644]
lib/classes/event/manager.php [new file with mode: 0644]
lib/classes/event/role_assigned.php [new file with mode: 0644]
lib/classes/event/role_unassigned.php [new file with mode: 0644]
lib/datalib.php
lib/db/caches.php
lib/db/events.php
lib/deprecatedlib.php
lib/dml/moodle_database.php
lib/editor/tinymce/classes/plugin.php
lib/editor/tinymce/plugins/dragmath/lib.php
lib/editor/tinymce/plugins/managefiles/lang/en/tinymce_managefiles.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/lib.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/manage.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/manage_form.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/module.js [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/pix/icon.gif [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/styles.css [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/tinymce/editor_plugin.js [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/tinymce/img/managefiles.png [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/version.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodleemoticon/lib.php
lib/editor/tinymce/plugins/moodlemedia/lib.php
lib/editor/tinymce/plugins/moodlenolink/lib.php
lib/editor/tinymce/plugins/pdw/lib.php
lib/editor/tinymce/plugins/spellchecker/config.php
lib/editor/tinymce/plugins/spellchecker/lib.php
lib/editor/tinymce/plugins/spellchecker/settings.php
lib/editor/tinymce/tests/editor_test.php
lib/editor/tinymce/upgrade.txt
lib/form/form.js
lib/javascript.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputlib.php
lib/outputrequirementslib.php
lib/pagelib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/event_sink.php [new file with mode: 0644]
lib/phpunit/classes/util.php
lib/phpunit/lib.php
lib/pluginlib.php
lib/setup.php
lib/setuplib.php
lib/tablelib.php
lib/testing/classes/util.php
lib/testing/generator/data_generator.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_permissions.php
lib/tests/event_test.php [new file with mode: 0644]
lib/tests/eventslib_test.php
lib/tests/fixtures/event_fixtures.php [new file with mode: 0644]
lib/tests/fixtures/events.php
lib/tests/setuplib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/xhprof/xhprof_moodle.php
lib/yui/build/moodle-core-dock-loader/moodle-core-dock-loader-debug.js [moved from lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-debug.js with 95% similarity]
lib/yui/build/moodle-core-dock-loader/moodle-core-dock-loader-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-dock-loader/moodle-core-dock-loader.js [moved from lib/yui/build/moodle-core-dockloader/moodle-core-dockloader.js with 95% similarity]
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js
lib/yui/build/moodle-core-dock/moodle-core-dock.js
lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-min.js [deleted file]
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-debug.js
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js
lib/yui/src/dock/build.json
lib/yui/src/dock/js/loader.js [moved from lib/yui/src/dock/js/dockloader.js with 96% similarity]
lib/yui/src/dock/meta/dock.json
lib/yui/src/tooltip/meta/tooltip.json
login/index.php
message/tests/behat/behat_message.php
mod/assign/locallib.php
mod/chat/gui_ajax/module.js
mod/chat/gui_ajax/theme/bubble/chat.css
mod/chat/styles.css
mod/choice/tests/behat/behat_mod_choice.php
mod/folder/lang/en/folder.php
mod/folder/renderer.php
mod/forum/lib.php
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/completion_condition_number_discussions.feature
mod/glossary/tests/behat/behat_mod_glossary.php
mod/glossary/tests/behat/print_friendly_version.feature
mod/lesson/tests/behat/lesson_navigation.feature
mod/quiz/attemptlib.php
mod/quiz/locallib.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/styles.css
mod/quiz/tests/attempt_walkthrough_from_csv_test.php [new file with mode: 0644]
mod/quiz/tests/attempt_walkthrough_test.php
mod/quiz/tests/fixtures/questions00.csv [new file with mode: 0644]
mod/quiz/tests/fixtures/results00.csv [new file with mode: 0644]
mod/quiz/tests/fixtures/steps00.csv [new file with mode: 0644]
mod/scorm/module.js
mod/scorm/player.php
mod/wiki/pagelib.php
phpunit.xml.dist
pluginfile.php
question/engine/questionusage.php
question/engine/tests/helpers.php
question/tests/behat/behat_question.php
question/type/match/question.php
question/type/match/tests/helper.php
question/type/multianswer/question.php
question/type/multichoice/question.php
question/type/multichoice/tests/helper.php
question/type/multichoice/tests/questiontype_test.php
question/type/questionbase.php
question/type/random/questiontype.php
question/type/random/tests/questiontype_test.php
report/participation/index.php
report/questioninstances/index.php
repository/areafiles/db/access.php [new file with mode: 0644]
repository/areafiles/db/install.php [new file with mode: 0644]
repository/areafiles/lang/en/repository_areafiles.php [new file with mode: 0644]
repository/areafiles/lib.php [new file with mode: 0644]
repository/areafiles/pix/icon.gif [new file with mode: 0644]
repository/areafiles/version.php [new file with mode: 0644]
repository/recent/tests/behat/behat_repository_recent.php
repository/tests/behat/behat_filepicker.php
repository/tests/behat/cancel_add_file.feature
repository/upload/tests/behat/behat_repository_upload.php
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/bootstrapoverride.less [new file with mode: 0644]
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/expendable.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/style/moodle.css
theme/font.php [new file with mode: 0644]
theme/formal_white/lib.php
theme/formal_white/style/block.css
theme/formal_white/style/calendar.css
theme/formal_white/style/course.css
theme/image.php
theme/index.php
theme/javascript.php
theme/styles.php
user/default/README.txt [deleted file]
user/default/f1.jpg [deleted file]
user/default/f2.jpg [deleted file]
user/index.php
user/profile.php
user/selector/lib.php
version.php

index c2d08d1..2ab997e 100644 (file)
@@ -394,8 +394,9 @@ if ($interactive) {
         cli_error(get_string('pathserrcreatedataroot', 'install', $a));
     }
 }
-$CFG->tempdir  = $CFG->dataroot.'/temp';
-$CFG->cachedir = $CFG->dataroot.'/cache';
+$CFG->tempdir       = $CFG->dataroot.'/temp';
+$CFG->cachedir      = $CFG->dataroot.'/cache';
+$CFG->localcachedir = $CFG->dataroot.'/localcache';
 
 // download required lang packs
 if ($CFG->lang !== 'en') {
index 19997a4..66e4337 100644 (file)
           <ON_ERROR message="ziprequired" />
         </FEEDBACK>
       </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="optional">
+      </PHP_EXTENSION>
       <PHP_EXTENSION name="gd" level="required">
         <FEEDBACK>
           <ON_ERROR message="gdrequired" />
index 0cd2306..6b4f88f 100644 (file)
@@ -62,7 +62,7 @@ class behat_admin extends behat_base {
             }
 
             // Search by label.
-            $searchbox = $this->find_field('Search in settings');
+            $searchbox = $this->find_field(get_string('searchinsettings', 'admin'));
             $searchbox->setValue($label);
             $submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
             $submitsearch->press();
@@ -72,8 +72,12 @@ class behat_admin extends behat_base {
             // Admin settings does not use the same DOM structure than other moodle forms
             // but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
             $exception = new ElementNotFoundException($this->getSession(), '"' . $label . '" administration setting ');
+
+            // The argument should be converted to an xpath literal.
+            $label = $this->getSession()->getSelectorsHandler()->xpathLiteral($label);
+
             $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" .
-                "[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]";
+                "[@id=//label[contains(normalize-space(.), $label)]/@for]";
             $fieldnode = $this->find('xpath', $fieldxpath, $exception);
             $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" .
                 "/child::div[contains(concat(' ', @class, ' '),  ' form-')]/child::*/parent::div");
@@ -90,7 +94,7 @@ class behat_admin extends behat_base {
             $field = behat_field_manager::get_field_instance($type, $fieldnode, $this->getSession());
             $field->set_value($value);
 
-            $this->find_button('Save changes')->press();
+            $this->find_button(get_string('savechanges'))->press();
         }
     }
 
index b6c8258..f17f092 100644 (file)
@@ -15,7 +15,7 @@ Feature: Display extended course names
     And I should not see "C_shortname Course fullname"
 
   Scenario: Courses list with extended course names
-    Given I click on "Courses" "link" in the "//div[@id='settingsnav']//descendant::li[contains(concat(' ', @class, ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
+    Given I click on "Courses" "link" in the "//div[@id='settingsnav']/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
     And I check "Display extended course names"
     When I press "Save changes"
     And I am on homepage
index cef4ea7..ab41e3d 100644 (file)
@@ -143,6 +143,9 @@ foreach ($vars as $var) {
     $CFG->{$var} = $CFG->{'behat_' . $var};
 }
 
+// Clean $CFG extra values before performing any action.
+behat_clean_init_config();
+
 $CFG->noemailever = true;
 $CFG->passwordsaltmain = 'moodle';
 
@@ -151,6 +154,7 @@ $CFG->jsrev = 1;
 
 // Unset cache and temp directories to reset them again with the new $CFG->dataroot.
 unset($CFG->cachedir);
+unset($CFG->localcachedir);
 unset($CFG->tempdir);
 
 // Continues setup.
index 086e78c..9f426cc 100644 (file)
@@ -41,6 +41,5 @@ $string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected o
 $string['viewsteps'] = 'Filter';
 $string['wheninfo'] = 'When. Actions that provokes an event';
 $string['wrongbehatsetup'] = 'Something is wrong with behat setup, ensure:<ul>
-<li>You ran "curl http://getcomposer.org/installer | php"</li>
-<li>You ran "php composer.phar install --dev"</li>
+<li>You ran "php admin/tool/behat/cli/init.php" from your moodle root directory</li>
 <li>vendor/bin/behat file has execution permissions</li></ul>';
index c2f829b..637086f 100644 (file)
@@ -37,7 +37,7 @@ Feature: Page contents assertions
     And I follow "Course 1"
     When I click on "Move this to the dock" "button" in the ".block_settings" "css_element"
     Then I should not see "Question bank"
-    And I click on "//div[@id='dock']/descendant::*[contains(., 'Administration')]/h2" "xpath_element"
+    And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
 
   @javascript
   Scenario: Locators inside specific DOM nodes using XPath
@@ -45,5 +45,5 @@ Feature: Page contents assertions
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And I log in as "admin"
-    When I click on "Move this to the dock" "button" in the "//*[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
+    When I click on "Move this to the dock" "button" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
     Then I should not see "Turn editing on"
index a98adcf..e37f9bc 100644 (file)
@@ -30,17 +30,17 @@ Feature: Set up contextual data for tests
     Then I should see "Course 1"
     And I should see "Course 2"
     And I should see "Course 3"
-    When I go to the courses management page
+    And I go to the courses management page
     And I follow "Cat 1"
-    Then I should see "Cat 2"
+    And I should see "Cat 2"
     And I should see "Cat 3"
-    When I follow "Cat 3"
-    Then I should see "Course 1"
+    And I follow "Cat 3"
+    And I should see "Course 1"
     And I should see "Course 2"
-    When I select "Cat 2" from "Course categories:"
-    Then I should see "No courses in this category"
-    When I select "Miscellaneous" from "Course categories:"
-    Then I should see "Course 3"
+    And I select "Cat 1 / Cat 2" from "Course categories:"
+    And I should see "No courses in this category"
+    And I select "Miscellaneous" from "Course categories:"
+    And I should see "Course 3"
 
   @javascript
   Scenario: Add a bunch of groups and groupings
@@ -79,6 +79,49 @@ Feature: Set up contextual data for tests
     And I follow "Course 1"
     Then I should see "Topic 1"
 
+  Scenario: Add role assigns
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | user1 | User | 1 | user1@moodlemoodle.com |
+      | user2 | User | 2 | user2@moodlemoodle.com |
+      | user3 | User | 3 | user3@moodlemoodle.com |
+    And the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | CAT1 |
+    And the following "role assigns" exists:
+      | user  | role           | contextlevel | reference |
+      | user1 | manager        | System       |           |
+      | user2 | editingteacher | Category     | CAT1      |
+      | user3 | editingteacher | Course       | C1        |
+    When I log in as "user1"
+    Then I should see "Front page settings"
+    And I log out
+    And I log in as "user2"
+    And I follow "Course 1"
+    And I should see "Turn editing on"
+    And I log out
+    And I log in as "user3"
+    And I follow "Course 1"
+    And I should see "Turn editing on"
+
+  Scenario: Add modules
+    Given the following "courses" exists:
+      | fullname | shortname |
+      | Course 1 | C1 |
+    And the following "activities" exists:
+      | activity | name | intro | course | idnumber |
+      | assign   | Test assignment name | Test assignment description | C1 | assign1 |
+      | data     | Test database name | Test database description | C1 | data1 |
+    When I log in as "admin"
+    And I follow "Course 1"
+    Then I should see "Test assignment name"
+    And I should see "Test database name"
+    And I follow "Test assignment name"
+    And I should see "Test assignment description"
+
   @javascript
   Scenario: Add relations between users and groups
     Given the following "users" exists:
index a5e21d3..e602e44 100644 (file)
@@ -74,12 +74,11 @@ if (isset($script)) {
     $prevreferences = $DB->get_records_select('profiling',
                                               'url = ? AND runreference = 1 AND timecreated < ?',
                                               array($run->url, $run->timecreated),
-                                              'timecreated DESC', 'runid', 0, 1);
-    $prevrunid = $prevreferences ? reset($prevreferences)->runid : false;
+                                              'timecreated DESC', 'runid, runcomment, timecreated', 0, 10);
     echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
     $header = get_string('lastrunof', 'tool_profiling', $script);
     echo $OUTPUT->heading($header);
-    $table = profiling_print_run($run, $prevrunid);
+    $table = profiling_print_run($run, $prevreferences);
     echo $table;
     echo $OUTPUT->box_end();
 
@@ -126,12 +125,11 @@ if (isset($script)) {
     $prevreferences = $DB->get_records_select('profiling',
                                               'url = ? AND runreference = 1 AND timecreated < ?',
                                               array($run->url, $run->timecreated),
-                                              'timecreated DESC', 'runid', 0, 1);
-    $prevrunid = $prevreferences ? reset($prevreferences)->runid : false;
+                                              'timecreated DESC', 'runid, runcomment, timecreated', 0, 10);
     echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
     $header = get_string('summaryof', 'tool_profiling', $run->url);
     echo $OUTPUT->heading($header);
-    $table = profiling_print_run($run, $prevrunid);
+    $table = profiling_print_run($run, $prevreferences);
     echo $table;
     echo $OUTPUT->box_end();
 
index 4a687f8..7c80c84 100644 (file)
@@ -51,5 +51,5 @@ $string['referencerun'] = 'Reference run/comment';
 $string['runid'] = 'Run ID';
 $string['summaryof'] = 'Summary of {$a}';
 $string['viewdetails'] = 'View profiling details';
-$string['viewdiff'] = 'View profiling differences with last reference run';
+$string['viewdiff'] = 'View profiling differences with:';
 $string['viewdiffdetails'] = 'View profiling diff details';
index 30a0dbf..c052d1c 100644 (file)
@@ -1110,9 +1110,6 @@ while ($linenum <= $previewrows and $fields = $cir->next()) {
 
     if (isset($rowcols['city'])) {
         $rowcols['city'] = trim($rowcols['city']);
-        if (empty($rowcols['city'])) {
-            $rowcols['status'][] = get_string('fieldrequired', 'error', 'city');
-        }
     }
     // Check if rowcols have custom profile field with correct data and update error state.
     $noerror = uu_check_custom_profile_data($rowcols) && $noerror;
index b8341bd..b377a9b 100644 (file)
@@ -257,9 +257,10 @@ class admin_uploaduser_form2 extends moodleform {
         } else {
             $mform->setDefault('city', $CFG->defaultcity);
         }
-        $mform->addRule('city', get_string('required'), 'required');
 
-        $mform->addElement('select', 'country', get_string('selectacountry'), get_string_manager()->get_list_of_countries());
+        $choices = get_string_manager()->get_list_of_countries();
+        $choices = array(''=>get_string('selectacountry').'...') + $choices;
+        $mform->addElement('select', 'country', get_string('selectacountry'), $choices);
         if (empty($CFG->country)) {
             $mform->setDefault('country', $templateuser->country);
         } else {
diff --git a/auth/classes/event/user_loggedin.php b/auth/classes/event/user_loggedin.php
new file mode 100644 (file)
index 0000000..7427975
--- /dev/null
@@ -0,0 +1,110 @@
+<?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/>.
+
+/**
+ * User login event.
+ *
+ * @package    core_auth
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_auth\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User login event class.
+ *
+ * @package    core_auth
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_loggedin extends \core\event\base {
+
+    /**
+     * Returns localised description of what happened.
+     *
+     * @return \lang_string.
+     */
+    public function get_description() {
+        return new \lang_string('event_user_loggedin_desc', '', $this->get_username());
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    public function get_legacy_logdata() {
+        return array(SITEID, 'user', 'login', "view.php?id=" . $this->data['objectid'] . "&course=".SITEID,
+            $this->data['objectid'], 0, $this->data['objectid']);
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return \lang_string
+     */
+    public static function get_name() {
+        return new \lang_string('event_user_loggedin');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/user/profile.php', array('id' => $this->data['objectid']));
+    }
+
+    /**
+     * Return the username of the logged in user.
+     *
+     * @return string
+     */
+    public function get_username() {
+        return $this->data['other']['username'];
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'r';
+        $this->data['level'] = 50;          // TODO MDL-37658.
+        $this->data['objecttable'] = 'user';
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        if (!isset($this->data['objectid'])) {
+            throw new \coding_exception("objectid has to be specified.");
+        } else if (!isset($this->data['other']['username'])) {
+            throw new \coding_exception("other['username'] has to be specified.");
+        }
+    }
+
+}
diff --git a/auth/tests/auth_test.php b/auth/tests/auth_test.php
new file mode 100644 (file)
index 0000000..0b2b8ae
--- /dev/null
@@ -0,0 +1,75 @@
+<?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/>.
+
+/**
+ * Tests for auth.
+ *
+ * @package    core_auth
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/authlib.php');
+
+/**
+ * Auth testcase class.
+ *
+ * @package    core_auth
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class auth_testcase extends advanced_testcase {
+
+    public function test_user_loggedin_event() {
+        global $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $sink = $this->redirectEvents();
+        $user = clone($USER);
+        login_attempt_valid($user);
+        $events = $sink->get_events();
+        $sink->close();
+
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf('\core_auth\event\user_loggedin', $event);
+        $this->assertEquals('user', $event->objecttable);
+        $this->assertEquals('2', $event->objectid);
+        $this->assertEquals(context_system::instance()->id, $event->contextid);
+        $this->assertEquals($user, $event->get_record_snapshot('user', 2));
+    }
+
+    public function test_user_loggedin_event_exceptions() {
+        try {
+            $event = \core_auth\event\user_loggedin::create(array('objectid' => 1));
+            $this->fail('\core_auth\event\user_loggedin requires other[\'username\']');
+        } catch(Exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            $event = \core_auth\event\user_loggedin::create(array('other' => array('username' => 'test')));
+            $this->fail('\core_auth\event\user_loggedin requires objectid');
+        } catch(Exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+    }
+
+}
index 6ce3a34..4ced154 100644 (file)
@@ -49,10 +49,10 @@ class behat_auth extends behat_base {
     public function i_log_in_as($username) {
 
         return array(new Given('I am on homepage'),
-            new Given('I follow "Log in"'),
-            new Given('I fill in "Username" with "'.$username.'"'),
-            new Given('I fill in "Password" with "'.$username.'"'),
-            new Given('I press "Log in"')
+            new Given('I follow "' . get_string('login') . '"'),
+            new Given('I fill in "' . get_string('username') . '" with "' . $this->escape($username) . '"'),
+            new Given('I fill in "' . get_string('password') . '" with "'. $this->escape($username) . '"'),
+            new Given('I press "' . get_string('login') . '"')
         );
     }
 
@@ -62,7 +62,7 @@ class behat_auth extends behat_base {
      * @Given /^I log out$/
      */
     public function i_log_out() {
-        return new When('I follow "Log out"');
+        return new When('I follow "' . get_string('logout') . '"');
     }
 
 }
index 36a4030..dce09fe 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 require_once($CFG->libdir.'/formslib.php');
 
 class backup_files_edit_form extends moodleform {
-    function definition() {
+
+    /**
+     * Form definition.
+     */
+    public function definition() {
         $mform =& $this->_form;
-        $contextid = $this->_customdata['contextid'];
-        $options = array('subdirs'=>0, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL | FILE_REFERENCE);
+
+        $options = array('subdirs' => 0, 'maxfiles' => -1, 'accepted_types' => '*', 'return_types' => FILE_INTERNAL | FILE_REFERENCE);
+
         $mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options);
+
         $mform->addElement('hidden', 'contextid', $this->_customdata['contextid']);
+        $mform->setType('contextid', PARAM_INT);
+
         $mform->addElement('hidden', 'currentcontext', $this->_customdata['currentcontext']);
+        $mform->setType('currentcontext', PARAM_INT);
+
         $mform->addElement('hidden', 'filearea', $this->_customdata['filearea']);
+        $mform->setType('filearea', PARAM_AREA);
+
         $mform->addElement('hidden', 'component', $this->_customdata['component']);
+        $mform->setType('component', PARAM_COMPONENT);
+
         $mform->addElement('hidden', 'returnurl', $this->_customdata['returnurl']);
+        $mform->setType('returnurl', PARAM_URL);
+
         $this->add_action_buttons(true, get_string('savechanges'));
         $this->set_data($this->_customdata['data']);
     }
index e7a2f91..12f2601 100644 (file)
@@ -614,13 +614,20 @@ abstract class restore_dbops {
                 } else {
                     self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id);
                     $questions = self::restore_get_questions($restoreid, $category->id);
+
+                    // Collect all the questions for this category into memory so we only talk to the DB once.
+                    $questioncache = $DB->get_records_sql_menu("SELECT ".$DB->sql_concat('stamp', "' '", 'version').", id
+                                                                  FROM {question}
+                                                                 WHERE category = ?", array($matchcat->id));
+
                     foreach ($questions as $question) {
-                        $matchq = $DB->get_record('question', array(
-                                      'category' => $matchcat->id,
-                                      'stamp' => $question->stamp,
-                                      'version' => $question->version));
+                        if (isset($questioncache[$question->stamp." ".$question->version])) {
+                            $matchqid = $questioncache[$question->stamp." ".$question->version];
+                        } else {
+                            $matchqid = false;
+                        }
                         // 5a) No match, check if user can add q
-                        if (!$matchq) {
+                        if (!$matchqid) {
                             // 6a) User can, mark the q to be created
                             if ($canadd) {
                                 // Nothing to mark, newitemid means create
@@ -645,7 +652,7 @@ abstract class restore_dbops {
 
                         // 5b) Match, mark q to be mapped
                         } else {
-                            self::set_backup_ids_record($restoreid, 'question', $question->id, $matchq->id);
+                            self::set_backup_ids_record($restoreid, 'question', $question->id, $matchqid);
                         }
                     }
                 }
index 488ab60..f8b85d7 100644 (file)
@@ -31,7 +31,7 @@ Feature: Backup Moodle courses
     And I should not see "Section 3"
     And I press "Continue"
     And I click on "Continue" "button" in the ".bcs-current-course" "css_element"
-    And I click on "//div[contains(concat(' ', @class, ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element"
-    And I click on "setting_root_logs" "checkbox" in the "//div[contains(@class, 'fitem')][contains(., 'Include course logs')]" "xpath_element"
+    And "//div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element" should exists
+    And I check "Include course logs"
     And I press "Cancel"
     And I click on "Cancel" "button" in the ".confirmation-dialogue" "css_element"
index b0d9998..d06185e 100644 (file)
@@ -61,25 +61,25 @@ class behat_backup extends behat_base {
         $this->find_link($backupcourse)->click();
 
         // Click the backup link.
-        $this->find_link('Backup')->click();
+        $this->find_link(get_string('backup'))->click();
 
         // Initial settings.
         $this->fill_backup_restore_form($options);
-        $this->find_button('Next')->press();
+        $this->find_button(get_string('backupstage1action', 'backup'))->press();
 
         // Schema settings.
         $this->fill_backup_restore_form($options);
-        $this->find_button('Next')->press();
+        $this->find_button(get_string('backupstage2action', 'backup'))->press();
 
         // Confirmation and review, backup filename can also be specified.
         $this->fill_backup_restore_form($options);
-        $this->find_button('Perform backup')->press();
+        $this->find_button(get_string('backupstage4action', 'backup'))->press();
 
         // Waiting for it to finish.
         $this->wait(10);
 
         // Last backup continue button.
-        $this->find_button('Continue')->press();
+        $this->find_button(get_string('backupstage16action', 'backup'))->press();
     }
 
     /**
@@ -105,36 +105,37 @@ class behat_backup extends behat_base {
         // Click the course link.
         $this->find_link($tocourse)->click();
 
-        // Click the backup link.
-        $this->find_link('Import')->click();
+        // Click the import link.
+        $this->find_link(get_string('import'))->click();
 
         // Select the course.
         $exception = new ExpectationException('"' . $fromcourse . '" course not found in the list of courses to import from', $this->getSession());
 
-        $fromcourse = str_replace("'", "\'", $fromcourse);
-        $xpath = "//div[contains(concat(' ', @class, ' '), ' ics-results ')]" .
-            "/descendant::tr[contains(., '" . $fromcourse . "')]" .
+        // The argument should be converted to an xpath literal.
+        $fromcourse = $this->getSession()->getSelectorsHandler()->xpathLiteral($fromcourse);
+        $xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' ics-results ')]" .
+            "/descendant::tr[contains(., $fromcourse)]" .
             "/descendant::input[@type='radio']";
         $radionode = $this->find('xpath', $xpath, $exception);
         $radionode->check();
         $radionode->click();
 
-        $this->find_button('Continue')->press();
+        $this->find_button(get_string('continue'))->press();
 
         // Initial settings.
         $this->fill_backup_restore_form($options);
-        $this->find_button('Next')->press();
+        $this->find_button(get_string('importbackupstage1action', 'backup'))->press();
 
         // Schema settings.
         $this->fill_backup_restore_form($options);
-        $this->find_button('Next')->press();
+        $this->find_button(get_string('importbackupstage2action', 'backup'))->press();
 
         // Run it.
-        $this->find_button('Perform import')->press();
+        $this->find_button(get_string('importbackupstage4action', 'backup'))->press();
         $this->wait();
 
         // Continue and redirect to 'to' course.
-        $this->find_button('Continue')->press();
+        $this->find_button(get_string('continue'))->press();
     }
 
     /**
@@ -150,17 +151,20 @@ class behat_backup extends behat_base {
         // Confirm restore.
         $this->select_backup($backupfilename);
 
+        // The argument should be converted to an xpath literal.
+        $existingcourse = $this->getSession()->getSelectorsHandler()->xpathLiteral($existingcourse);
+
         // Selecting the specified course (we can not call behat_forms::select_radio here as is in another behat subcontext).
-        $existingcourse = str_replace("'", "\'", $existingcourse);
-        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]" .
+        $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" .
             "/descendant::div[@class='restore-course-search']" .
-            "/descendant::tr[contains(., '" . $existingcourse . "')]" .
+            "/descendant::tr[contains(., $existingcourse)]" .
             "/descendant::input[@type='radio']");
         $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore into an existing course section.
-        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" .
+            "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
         $continuenode->click();
         $this->wait();
 
@@ -181,14 +185,15 @@ class behat_backup extends behat_base {
         $this->select_backup($backupfilename);
 
         // The first category in the list.
-        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]" .
+        $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
             "/descendant::div[@class='restore-course-search']" .
             "/descendant::input[@type='radio']");
         $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore into an existing course section.
-        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
+            "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
         $continuenode->click();
         $this->wait();
 
@@ -209,14 +214,14 @@ class behat_backup extends behat_base {
         $this->select_backup($backupfilename);
 
         // Merge without deleting radio option.
-        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+        $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
             "/descendant::input[@type='radio'][@name='target'][@value='1']");
         $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore merging section.
-        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
-            "/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
+            "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
         $continuenode->click();
         $this->wait();
 
@@ -237,14 +242,14 @@ class behat_backup extends behat_base {
         $this->select_backup($backupfilename);
 
         // Delete contents radio option.
-        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+        $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
             "/descendant::input[@type='radio'][@name='target'][@value='0']");
         $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore merging section.
-        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
-            "/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" .
+            "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']");
         $continuenode->click();
         $this->wait();
 
@@ -263,12 +268,16 @@ class behat_backup extends behat_base {
 
         // Using xpath as there are other restore links before this one.
         $exception = new ExpectationException('The "' . $backupfilename . '" backup file can not be found in this page', $this->getSession());
-        $xpath = "//tr[contains(., '" . $backupfilename . "')]/descendant::a[contains(., 'Restore')]";
+
+        // The argument should be converted to an xpath literal.
+        $backupfilename = $this->getSession()->getSelectorsHandler()->xpathLiteral($backupfilename);
+
+        $xpath = "//tr[contains(., $backupfilename)]/descendant::a[contains(., '" . get_string('restore') . "')]";
         $restorelink = $this->find('xpath', $xpath, $exception);
         $restorelink->click();
 
         // Confirm the backup contents.
-        $restore = $this->find_button('Continue')->press();
+        $restore = $this->find_button(get_string('continue'))->press();
     }
 
     /**
@@ -284,18 +293,18 @@ class behat_backup extends behat_base {
 
         // Settings.
         $this->fill_backup_restore_form($options);
-        $this->find_button('Next')->press();
+        $this->find_button(get_string('restorestage4action', 'backup'))->press();
 
         // Schema.
         $this->fill_backup_restore_form($options);
-        $this->find_button('Next')->press();
+        $this->find_button(get_string('restorestage8action', 'backup'))->press();
 
         // Review, no options here.
-        $this->find_button('Perform restore')->press();
+        $this->find_button(get_string('restorestage16action', 'backup'))->press();
         $this->wait(10);
 
         // Last restore continue button, redirected to restore course after this.
-        $this->find_button('Continue')->press();
+        $this->find_button(get_string('restorestage32action', 'backup'))->press();
     }
 
     /**
@@ -339,6 +348,10 @@ class behat_backup extends behat_base {
      */
     protected function wait($timeout = false) {
 
+        if (!$this->running_javascript()) {
+            return;
+        }
+
         if (!$timeout) {
             $timeout = self::TIMEOUT;
         }
index 48782b7..748daac 100644 (file)
@@ -43,6 +43,18 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
     protected $groupedpaths; // Paths we are requesting grouped
     protected $currentdata;  // Where we'll be acummulating data
 
+    /**
+     * Keep cache of parent directory paths for XML parsing.
+     * @var array
+     */
+    protected $parentcache = array();
+
+    /**
+     * Remaining space for parent directory paths.
+     * @var integer
+     */
+    protected $parentcacheavailablesize = 2048;
+
     public function __construct(array $paths = array()) {
         $this->groupedpaths = array();
         $this->currentdata = null;
@@ -65,7 +77,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
                 $a->child = $found;
                 throw new progressive_parser_exception('xml_grouped_child_found', $a);
             }
-            $this->groupedpaths[] = $path;
+            $this->groupedpaths[$path] = true;
         }
         parent::add_path($path);
     }
@@ -141,7 +153,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
     }
 
     protected function path_is_grouped($path) {
-        return in_array($path, $this->groupedpaths);
+        return isset($this->groupedpaths[$path]);
     }
 
     /**
@@ -150,16 +162,41 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
      * false if not
      */
     protected function grouped_parent_exists($path) {
-        $parentpath = progressive_parser::dirname($path);
+        $parentpath = $this->get_parent_path($path);
+
         while ($parentpath != '/') {
             if ($this->path_is_grouped($parentpath)) {
                 return $parentpath;
             }
-            $parentpath = progressive_parser::dirname($parentpath);
+            $parentpath = $this->get_parent_path($parentpath);
         }
         return false;
     }
 
+    /**
+     * Get the parent path using a local cache for performance.
+     *
+     * @param $path string The pathname you wish to obtain the parent name for.
+     * @return string The parent pathname.
+     */
+    protected function get_parent_path($path) {
+        if (!isset($this->parentcache[$path])) {
+            $this->parentcache[$path] = progressive_parser::dirname($path);
+            $this->parentcacheavailablesize--;
+            if ($this->parentcacheavailablesize < 0) {
+                // Older first is cheaper than LRU.  We use 10% as items are grouped together and the large quiz
+                // restore from MDL-40585 used only 600 parent paths. This is an XML heirarchy, so common paths
+                // are grouped near each other. eg; /question_bank/question_category/question/element. After keeping
+                // question_bank paths in the cache when we move to another area and the question_bank cache is not
+                // useful any longer.
+                $this->parentcache = array_slice($this->parentcache, 200, null, true);
+                $this->parentcacheavailablesize += 200;
+            }
+        }
+        return $this->parentcache[$path];
+    }
+
+
     /**
      * Function that will look for any grouped
      * child for the given path, returning it if found,
@@ -167,7 +204,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor {
      */
     protected function grouped_child_exists($path) {
         $childpath = $path . '/';
-        foreach ($this->groupedpaths as $groupedpath) {
+        foreach ($this->groupedpaths as $groupedpath => $set) {
             if (strpos($groupedpath, $childpath) === 0) {
                 return $groupedpath;
             }
index 68e736b..98678e8 100644 (file)
@@ -2,7 +2,20 @@
  * Push badges to backpack.
  */
 function addtobackpack(event, args) {
-    OpenBadges.issue([args.assertion], function(errors, successes) { });
+    var badgetable = Y.one('#issued-badge-table');
+    var errordiv = Y.one('#addtobackpack-error');
+    var errortext = M.util.get_string('error:backpackproblem', 'badges');
+    var errorhtml = '<div id="addtobackpack-error" class="box boxaligncenter notifyproblem">' + errortext + '</div>';
+
+    if (typeof OpenBadges !== 'undefined') {
+        OpenBadges.issue([args.assertion], function(errors, successes) { });
+    } else {
+        // Add error div if it doesn't exist yet.
+        if (!errordiv) {
+            var badgerror = Y.Node.create(errorhtml);
+            badgetable.insert(badgerror, 'before');
+        }
+    }
 }
 
 /**
index cd90239..339b147 100644 (file)
@@ -56,10 +56,8 @@ if (isloggedin()) {
     navigation_node::override_active_url($url);
 }
 
-// TODO: Better way of pushing badges to Mozilla backpack?
-if (!empty($CFG->badges_allowexternalbackpack)) {
-    $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
-}
+// Include JS files for backpack support.
+badges_setup_backpack_js();
 
 echo $OUTPUT->header();
 
index 46f4c1d..d944e73 100644 (file)
@@ -90,11 +90,8 @@ $PAGE->set_title($title);
 $PAGE->set_heading($title);
 $PAGE->set_pagelayout('mydashboard');
 
-// TODO: Better way of pushing badges to Mozilla backpack?
-if (!empty($CFG->badges_allowexternalbackpack)) {
-    $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
-    $PAGE->requires->js('/badges/backpack.js', true);
-}
+// Include JS files for backpack support.
+badges_setup_backpack_js();
 
 $output = $PAGE->get_renderer('core', 'badges');
 $badges = badges_get_user_badges($USER->id);
index e6e9d2d..f6abcb6 100644 (file)
@@ -282,6 +282,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $today = strtotime($today_date);
 
         $table = new html_table();
+        $table->id = 'issued-badge-table';
 
         $imagetable = new html_table();
         $imagetable->attributes = array('class' => 'clearfix badgeissuedimage');
@@ -294,11 +295,13 @@ class core_badges_renderer extends plugin_renderer_base {
             $expiration = isset($issued['expires']) ? strtotime($issued['expires']) : $today + 1;
             if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $today) && badges_user_has_backpack($USER->id)) {
                 $assertion = new moodle_url('/badges/assertion.php', array('b' => $ibadge->hash));
+                $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
                 $attributes = array(
-                        'type' => 'button',
-                        'value' => get_string('addtobackpack', 'badges'),
-                        'onclick' => 'OpenBadges.issue(["' . $assertion->out(false) . '"], function(errors, successes) { })');
+                        'type'  => 'button',
+                        'id'    => 'addbutton',
+                        'value' => get_string('addtobackpack', 'badges'));
                 $tobackpack = html_writer::tag('input', '', $attributes);
+                $this->output->add_action_handler($action, 'addbutton');
                 $imagetable->data[] = array($tobackpack);
             }
         }
@@ -459,7 +462,7 @@ class core_badges_renderer extends plugin_renderer_base {
                     get_string('downloadall'), 'POST', array('class' => 'activatebadge'));
 
         // Local badges.
-        $localhtml = html_writer::start_tag('fieldset', array('class' => 'generalbox'));
+        $localhtml = html_writer::start_tag('fieldset', array('id' => 'issued-badge-table', 'class' => 'generalbox'));
         $localhtml .= html_writer::tag('legend',
                     $this->output->heading_with_help(get_string('localbadges', 'badges', $SITE->fullname), 'localbadgesh', 'badges'));
         if ($badges->badges) {
index 5482460..3cac07a 100644 (file)
@@ -63,7 +63,7 @@ class behat_block_comments extends behat_base {
             $commentstextarea = $this->find('css', '.comment-area textarea', $exception);
             $commentstextarea->setValue($comment);
 
-            $this->find_link('Save comment')->click();
+            $this->find_link(get_string('savecomment'))->click();
 
             // Wait for the AJAX request.
             $this->getSession()->wait(4 * 1000, false);
@@ -91,8 +91,11 @@ class behat_block_comments extends behat_base {
 
         $exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment ');
 
-        $commentxpath = "//div[contains(concat(' ', @class, ' '), ' block_comments ')]" .
-            "/descendant::div[@class='comment-message'][contains(., '" . $comment . "')]";
+        // Using xpath liternal to avoid possible problems with comments containing quotes.
+        $commentliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($comment);
+
+        $commentxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_comments ')]" .
+            "/descendant::div[@class='comment-message'][contains(., $commentliteral)]";
         $commentnode = $this->find('xpath', $commentxpath, $exception);
 
         // Click on delete icon.
@@ -101,7 +104,7 @@ class behat_block_comments extends behat_base {
         $deleteicon->click();
 
         // Yes confirm.
-        $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., 'Yes')]");
+        $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., '" . get_string('yes') . "')]");
         $confirmnode->click();
 
         // Wait for the AJAX request.
index bb29569..6f33899 100644 (file)
@@ -103,10 +103,10 @@ class block_course_overview_renderer extends plugin_renderer_base {
 
             // No need to pass title through s() here as it will be done automatically by html_writer.
             $attributes = array('title' => $course->fullname);
-            if (empty($course->visible)) {
-                $attributes['class'] = 'dimmed';
-            }
             if ($course->id > 0) {
+                if (empty($course->visible)) {
+                    $attributes['class'] = 'dimmed';
+                }
                 $courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
                 $coursefullname = format_string($course->fullname, true, $course->id);
                 $link = html_writer::link($courseurl, $coursefullname, $attributes);
index b62eefb..c58c359 100644 (file)
@@ -100,13 +100,17 @@ class block_recent_activity extends block_base {
     }
 
     /**
-     * Returns all recent enrollments
+     * Returns all recent enrolments.
+     *
+     * This function previously used get_recent_enrolments located in lib/deprecatedlib.php which would
+     * return an empty array which was identified in MDL-36993. The use of this function outside the
+     * deprecated lib was removed in MDL-40649.
      *
      * @todo MDL-36993 this function always return empty array
      * @return array array of entries from {user} table
      */
     protected function get_recent_enrolments() {
-        return get_recent_enrolments($this->page->course->id, $this->get_timestart());
+        return array();
     }
 
     /**
index 1c8f70d..42fe9a1 100644 (file)
@@ -46,13 +46,13 @@ class behat_blocks extends behat_base {
      * @param string $blockname
      */
     public function i_add_the_block($blockname) {
-        $steps = new Given('I select "' . $blockname . '" from "bui_addblock"');
+        $steps = new Given('I select "' . $this->escape($blockname) . '" from "bui_addblock"');
 
         // If we are running without javascript we need to submit the form.
         if (!$this->running_javascript()) {
             $steps = array(
                 $steps,
-                new Given('I click on "Go" "button" in the "#add_block" "css_element"')
+                new Given('I click on "' . get_string('go') . '" "button" in the "#add_block" "css_element"')
             );
         }
         return $steps;
index fb97f04..9f74446 100644 (file)
@@ -32,4 +32,4 @@ Feature: Add and configure blocks throughout the site
     And I press "Save changes"
     And I follow "Course 1"
     # The first block matching the pattern should be top-left block
-    And I should see "Comments" in the "//*[@id='region-pre']/descendant::div[contains(concat(' ', @class, ' '), ' block ')]" "xpath_element"
+    And I should see "Comments" in the "//*[@id='region-pre']/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element"
index 984966a..eb7c999 100644 (file)
@@ -49,7 +49,8 @@ class cachestore_memcache_addinstance_form extends cachestore_addinstance_form {
         $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_memcache'),
                 array('maxlength' => 5, 'size' => 5));
         $form->addHelpButton('prefix', 'prefix', 'cachestore_memcache');
-        $form->setType('prefix', PARAM_ALPHAEXT);
+        $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
         $form->setDefault('prefix', 'mdl_');
+        $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_memcache'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
     }
 }
\ No newline at end of file
index 5cba887..fb87d28 100644 (file)
@@ -31,6 +31,7 @@ $string['prefix'] = 'Key prefix';
 $string['prefix_help'] = 'This prefix is used for all key names on the memcache server.
 * If you only have one Moodle instance using this server, you can leave this value default.
 * Due to key length restrictions, a maximum of 5 characters is permitted.';
+$string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
 $string['servers'] = 'Servers';
 $string['servers_help'] = 'This sets the servers that should be utilised by this memcache adapter.
 Servers should be defined one per line and consist of a server address and optionally a port and weight.
index 882ab37..2e104bd 100644 (file)
@@ -404,7 +404,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * Generates an instance of the cache store that can be used for testing.
      *
      * @param cache_definition $definition
-     * @return false
+     * @return cachestore_memcache|false
      */
     public static function initialise_test_instance(cache_definition $definition) {
         if (!self::are_requirements_met()) {
index 023e6cc..4e3c2aa 100644 (file)
@@ -59,4 +59,35 @@ class cachestore_memcache_test extends cachestore_tests {
     protected function get_class_name() {
         return 'cachestore_memcache';
     }
-}
\ No newline at end of file
+
+    /**
+     * Tests the valid keys to ensure they work.
+     */
+    public function test_valid_keys() {
+        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
+        $instance = cachestore_memcache::initialise_test_instance($definition);
+
+        if (!$instance) { // Something prevented memcache store to be inited (extension, TEST_CACHESTORE_MEMCACHE_TESTSERVERS...).
+            $this->markTestSkipped();
+        }
+
+        $keys = array(
+            // Alphanumeric.
+            'abc', 'ABC', '123', 'aB1', '1aB',
+            // Hyphens.
+            'a-1', '1-a', '-a1', 'a1-',
+            // Underscores.
+            'a_1', '1_a', '_a1', 'a1_'
+        );
+        foreach ($keys as $key) {
+            $this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
+        }
+        foreach ($keys as $key) {
+            $this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
+        }
+        $values = $instance->get_many($keys);
+        foreach ($values as $key => $value) {
+            $this->assertEquals($key, $value);
+        }
+    }
+}
index a8e98bf..c833a18 100644 (file)
@@ -57,11 +57,12 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form
         $form->addElement('select', 'serialiser', get_string('useserialiser', 'cachestore_memcached'), $serialiseroptions);
         $form->addHelpButton('serialiser', 'useserialiser', 'cachestore_memcached');
         $form->setDefault('serialiser', Memcached::SERIALIZER_PHP);
-        $form->setType('serialiser', PARAM_NUMBER);
+        $form->setType('serialiser', PARAM_INT);
 
         $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_memcached'), array('size' => 16));
-        $form->setType('prefix', PARAM_ALPHANUM);
+        $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
         $form->addHelpButton('prefix', 'prefix', 'cachestore_memcached');
+        $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_memcached'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
 
         $hashoptions = cachestore_memcached::config_get_hash_options();
         $form->addElement('select', 'hash', get_string('hash', 'cachestore_memcached'), $hashoptions);
@@ -74,4 +75,4 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form
         $form->setDefault('bufferwrites', 0);
         $form->setType('bufferwrites', PARAM_BOOL);
     }
-}
\ No newline at end of file
+}
index 67c3cd2..87713f6 100644 (file)
@@ -42,6 +42,7 @@ $string['hash_murmur'] = 'Murmur';
 $string['pluginname'] = 'Memcached';
 $string['prefix'] = 'Prefix key';
 $string['prefix_help'] = 'This can be used to create a "domain" for your item keys allowing you to create multiple memcached stores on a single memcached installation. It cannot be longer than 16 characters in order to ensure key length issues are not encountered.';
+$string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
 $string['serialiser_igbinary'] = 'The igbinary serializer.';
 $string['serialiser_json'] = 'The JSON serializer.';
 $string['serialiser_php'] = 'The default PHP serializer.';
index 6e3834c..a74dc83 100644 (file)
@@ -339,7 +339,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
             $options[Memcached::SERIALIZER_JSON] = get_string('serialiser_json', 'cachestore_memcached');
         }
         if (Memcached::HAVE_IGBINARY) {
-            $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_php', 'cachestore_memcached');
+            $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_igbinary', 'cachestore_memcached');
         }
         return $options;
     }
@@ -444,7 +444,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
      * Generates an instance of the cache store that can be used for testing.
      *
      * @param cache_definition $definition
-     * @return false
+     * @return cachestore_memcached|false
      */
     public static function initialise_test_instance(cache_definition $definition) {
 
index f0896ca..80a918a 100644 (file)
@@ -59,4 +59,35 @@ class cachestore_memcached_test extends cachestore_tests {
     protected function get_class_name() {
         return 'cachestore_memcached';
     }
-}
\ No newline at end of file
+
+    /**
+     * Tests the valid keys to ensure they work.
+     */
+    public function test_valid_keys() {
+        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
+        $instance = cachestore_memcached::initialise_test_instance($definition);
+
+        if (!$instance) { // Something prevented memcached store to be inited (extension, TEST_CACHESTORE_MEMCACHED_TESTSERVERS...).
+            $this->markTestSkipped();
+        }
+
+        $keys = array(
+            // Alphanumeric.
+            'abc', 'ABC', '123', 'aB1', '1aB',
+            // Hyphens.
+            'a-1', '1-a', '-a1', 'a1-',
+            // Underscores.
+            'a_1', '1_a', '_a1', 'a1_'
+        );
+        foreach ($keys as $key) {
+            $this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
+        }
+        foreach ($keys as $key) {
+            $this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
+        }
+        $values = $instance->get_many($keys);
+        foreach ($values as $key => $value) {
+            $this->assertEquals($key, $value);
+        }
+    }
+}
index 18e0ccf..8221502 100644 (file)
@@ -116,6 +116,18 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
      */
     protected $ttl = 0;
 
+    /**
+     * The maximum size for the store, or false if there isn't one.
+     * @var bool
+     */
+    protected $maxsize = false;
+
+    /**
+     * The number of items currently being stored.
+     * @var int
+     */
+    protected $storecount = 0;
+
     /**
      * Constructs the store instance.
      *
@@ -192,6 +204,12 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
         $this->storeid = $definition->generate_definition_hash();
         $this->store = &self::register_store_id($definition->get_id());
         $this->ttl = $definition->get_ttl();
+        $maxsize = $definition->get_maxsize();
+        if ($maxsize !== null) {
+            // Must be a positive int.
+            $this->maxsize = abs((int)$maxsize);
+            $this->storecount = count($this->store);
+        }
     }
 
     /**
@@ -261,14 +279,25 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
      *
      * @param string $key The key to use.
      * @param mixed $data The data to set.
+     * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required.
      * @return bool True if the operation was a success false otherwise.
      */
-    public function set($key, $data) {
+    public function set($key, $data, $testmaxsize = true) {
+        $testmaxsize = ($testmaxsize && $this->maxsize !== false);
+        if ($testmaxsize) {
+            $increment = (!isset($this->store[$key]));
+        }
         if ($this->ttl == 0) {
             $this->store[$key][0] = $data;
         } else {
             $this->store[$key] = array($data, cache::now());
         }
+        if ($testmaxsize && $increment) {
+            $this->storecount++;
+            if ($this->storecount > $this->maxsize) {
+                $this->reduce_for_maxsize();
+            }
+        }
         return true;
     }
 
@@ -283,9 +312,15 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
     public function set_many(array $keyvaluearray) {
         $count = 0;
         foreach ($keyvaluearray as $pair) {
-            $this->set($pair['key'], $pair['value']);
+            $this->set($pair['key'], $pair['value'], false);
             $count++;
         }
+        if ($this->maxsize !== false) {
+            $this->storecount += $count;
+            if ($this->storecount > $this->maxsize) {
+                $this->reduce_for_maxsize();
+            }
+        }
         return $count;
     }
 
@@ -356,6 +391,9 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
     public function delete($key) {
         $result = isset($this->store[$key]);
         unset($this->store[$key]);
+        if ($this->maxsize !== false) {
+            $this->storecount--;
+        }
         return $result;
     }
 
@@ -373,6 +411,9 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
             }
             unset($this->store[$key]);
         }
+        if ($this->maxsize !== false) {
+            $this->storecount -= $count;
+        }
         return $count;
     }
 
@@ -383,9 +424,34 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
      */
     public function purge() {
         $this->store = array();
+        // Don't worry about checking if we're using max size just set it as thats as fast as the check.
+        $this->storecount = 0;
         return true;
     }
 
+    /**
+     * Reduces the size of the array if maxsize has been hit.
+     *
+     * This function reduces the size of the store reducing it by 10% of its maxsize.
+     * It removes the oldest items in the store when doing this.
+     * The reason it does this an doesn't use a least recently used system is purely the overhead such a system
+     * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches
+     * and avoiding more is of benefit.
+     *
+     * @return int
+     */
+    protected function reduce_for_maxsize() {
+        $diff = $this->storecount - $this->maxsize;
+        if ($diff < 1) {
+            return 0;
+        }
+        // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop.
+        $diff += floor($this->maxsize / 10);
+        $this->store = array_slice($this->store, $diff, null, true);
+        $this->storecount -= $diff;
+        return $diff;
+    }
+
     /**
      * Returns true if the user can add an instance of the store plugin.
      *
@@ -406,7 +472,7 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
      * Generates an instance of the cache store that can be used for testing.
      *
      * @param cache_definition $definition
-     * @return false
+     * @return cachestore_session
      */
     public static function initialise_test_instance(cache_definition $definition) {
         // Do something here perhaps.
index bec0289..3ee20f2 100644 (file)
@@ -44,4 +44,76 @@ class cachestore_session_test extends cachestore_tests {
     protected function get_class_name() {
         return 'cachestore_session';
     }
+
+    /**
+     * Test the maxsize option.
+     */
+    public function test_maxsize() {
+        $defid = 'phpunit/testmaxsize';
+        $config = cache_config_phpunittest::instance();
+        $config->phpunit_add_definition($defid, array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'testmaxsize',
+            'maxsize' => 3
+        ));
+        $definition = cache_definition::load($defid, $config->get_definition_by_id($defid));
+        $instance = cachestore_session::initialise_test_instance($definition);
+
+        $this->assertTrue($instance->set('key1', 'value1'));
+        $this->assertTrue($instance->set('key2', 'value2'));
+        $this->assertTrue($instance->set('key3', 'value3'));
+
+        $this->assertTrue($instance->has('key1'));
+        $this->assertTrue($instance->has('key2'));
+        $this->assertTrue($instance->has('key3'));
+
+        $this->assertTrue($instance->set('key4', 'value4'));
+        $this->assertTrue($instance->set('key5', 'value5'));
+
+        $this->assertFalse($instance->has('key1'));
+        $this->assertFalse($instance->has('key2'));
+        $this->assertTrue($instance->has('key3'));
+        $this->assertTrue($instance->has('key4'));
+        $this->assertTrue($instance->has('key5'));
+
+        $this->assertFalse($instance->get('key1'));
+        $this->assertFalse($instance->get('key2'));
+        $this->assertEquals('value3', $instance->get('key3'));
+        $this->assertEquals('value4', $instance->get('key4'));
+        $this->assertEquals('value5', $instance->get('key5'));
+
+        // Test adding one more.
+        $this->assertTrue($instance->set('key6', 'value6'));
+        $this->assertFalse($instance->get('key3'));
+
+        // Test reducing and then adding to make sure we don't lost one.
+        $this->assertTrue($instance->delete('key6'));
+        $this->assertTrue($instance->set('key7', 'value7'));
+        $this->assertEquals('value4', $instance->get('key4'));
+
+        // Set the same key three times to make sure it doesn't count overrides.
+        for ($i = 0; $i < 3; $i++) {
+            $this->assertTrue($instance->set('key8', 'value8'));
+        }
+        $this->assertEquals('value7', $instance->get('key7'), 'Overrides are incorrectly incrementing size');
+
+        // Test adding many.
+        $this->assertEquals(3, $instance->set_many(array(
+            array('key' => 'keyA', 'value' => 'valueA'),
+            array('key' => 'keyB', 'value' => 'valueB'),
+            array('key' => 'keyC', 'value' => 'valueC')
+        )));
+        $this->assertEquals(array(
+            'key4' => false,
+            'key5' => false,
+            'key6' => false,
+            'key7' => false,
+            'keyA' => 'valueA',
+            'keyB' => 'valueB',
+            'keyC' => 'valueC'
+        ), $instance->get_many(array(
+            'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC'
+        )));
+    }
 }
\ No newline at end of file
index 65c76db..e54ee83 100644 (file)
@@ -112,6 +112,18 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
      */
     protected $ttl = 0;
 
+    /**
+     * The maximum size for the store, or false if there isn't one.
+     * @var bool
+     */
+    protected $maxsize = false;
+
+    /**
+     * The number of items currently being stored.
+     * @var int
+     */
+    protected $storecount = 0;
+
     /**
      * Constructs the store instance.
      *
@@ -188,6 +200,12 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
         $this->storeid = $definition->generate_definition_hash();
         $this->store = &self::register_store_id($this->storeid);
         $this->ttl = $definition->get_ttl();
+        $maxsize = $definition->get_maxsize();
+        if ($maxsize !== null) {
+            // Must be a positive int.
+            $this->maxsize = abs((int)$maxsize);
+            $this->storecount = count($this->store);
+        }
     }
 
     /**
@@ -257,14 +275,25 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
      *
      * @param string $key The key to use.
      * @param mixed $data The data to set.
+     * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required.
      * @return bool True if the operation was a success false otherwise.
      */
-    public function set($key, $data) {
+    public function set($key, $data, $testmaxsize = true) {
+        $testmaxsize = ($testmaxsize && $this->maxsize !== false);
+        if ($testmaxsize) {
+            $increment = (!isset($this->store[$key]));
+        }
         if ($this->ttl == 0) {
             $this->store[$key][0] = $data;
         } else {
             $this->store[$key] = array($data, cache::now());
         }
+        if ($testmaxsize && $increment) {
+            $this->storecount++;
+            if ($this->storecount > $this->maxsize) {
+                $this->reduce_for_maxsize();
+            }
+        }
         return true;
     }
 
@@ -279,9 +308,16 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
     public function set_many(array $keyvaluearray) {
         $count = 0;
         foreach ($keyvaluearray as $pair) {
-            $this->set($pair['key'], $pair['value']);
+            // Don't test the maxsize here. We'll do it once when we are done.
+            $this->set($pair['key'], $pair['value'], false);
             $count++;
         }
+        if ($this->maxsize !== false) {
+            $this->storecount += $count;
+            if ($this->storecount > $this->maxsize) {
+                $this->reduce_for_maxsize();
+            }
+        }
         return $count;
     }
 
@@ -352,6 +388,9 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
     public function delete($key) {
         $result = isset($this->store[$key]);
         unset($this->store[$key]);
+        if ($this->maxsize !== false) {
+            $this->storecount--;
+        }
         return $result;
     }
 
@@ -369,6 +408,9 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
             }
             unset($this->store[$key]);
         }
+        if ($this->maxsize !== false) {
+            $this->storecount -= $count;
+        }
         return $count;
     }
 
@@ -380,9 +422,34 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
     public function purge() {
         $this->flush_store_by_id($this->storeid);
         $this->store = &self::register_store_id($this->storeid);
+        // Don't worry about checking if we're using max size just set it as thats as fast as the check.
+        $this->storecount = 0;
         return true;
     }
 
+    /**
+     * Reduces the size of the array if maxsize has been hit.
+     *
+     * This function reduces the size of the store reducing it by 10% of its maxsize.
+     * It removes the oldest items in the store when doing this.
+     * The reason it does this an doesn't use a least recently used system is purely the overhead such a system
+     * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches
+     * and avoiding more is of benefit.
+     *
+     * @return int
+     */
+    protected function reduce_for_maxsize() {
+        $diff = $this->storecount - $this->maxsize;
+        if ($diff < 1) {
+            return 0;
+        }
+        // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop.
+        $diff += floor($this->maxsize / 10);
+        $this->store = array_slice($this->store, $diff, null, true);
+        $this->storecount -= $diff;
+        return $diff;
+    }
+
     /**
      * Returns true if the user can add an instance of the store plugin.
      *
@@ -403,7 +470,7 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
      * Generates an instance of the cache store that can be used for testing.
      *
      * @param cache_definition $definition
-     * @return false
+     * @return cachestore_static
      */
     public static function initialise_test_instance(cache_definition $definition) {
         // Do something here perhaps.
index b1ebc19..c9795d1 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-// This static is part of Moodle - http://moodle.org/
+// 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
@@ -44,4 +44,77 @@ class cachestore_static_test extends cachestore_tests {
     protected function get_class_name() {
         return 'cachestore_static';
     }
+
+    /**
+     * Test the maxsize option.
+     */
+    public function test_maxsize() {
+        $defid = 'phpunit/testmaxsize';
+        $config = cache_config_phpunittest::instance();
+        $config->phpunit_add_definition($defid, array(
+            'mode' => cache_store::MODE_REQUEST,
+            'component' => 'phpunit',
+            'area' => 'testmaxsize',
+            'maxsize' => 3
+        ));
+        $definition = cache_definition::load($defid, $config->get_definition_by_id($defid));
+        $instance = cachestore_static::initialise_test_instance($definition);
+
+        $this->assertTrue($instance->set('key1', 'value1'));
+        $this->assertTrue($instance->set('key2', 'value2'));
+        $this->assertTrue($instance->set('key3', 'value3'));
+
+        $this->assertTrue($instance->has('key1'));
+        $this->assertTrue($instance->has('key2'));
+        $this->assertTrue($instance->has('key3'));
+
+        $this->assertTrue($instance->set('key4', 'value4'));
+        $this->assertTrue($instance->set('key5', 'value5'));
+
+        $this->assertFalse($instance->has('key1'));
+        $this->assertFalse($instance->has('key2'));
+        $this->assertTrue($instance->has('key3'));
+        $this->assertTrue($instance->has('key4'));
+        $this->assertTrue($instance->has('key5'));
+
+        $this->assertFalse($instance->get('key1'));
+        $this->assertFalse($instance->get('key2'));
+        $this->assertEquals('value3', $instance->get('key3'));
+        $this->assertEquals('value4', $instance->get('key4'));
+        $this->assertEquals('value5', $instance->get('key5'));
+
+        // Test adding one more.
+        $this->assertTrue($instance->set('key6', 'value6'));
+        $this->assertFalse($instance->get('key3'));
+
+        // Test reducing and then adding to make sure we don't lost one.
+        $this->assertTrue($instance->delete('key6'));
+        $this->assertTrue($instance->set('key7', 'value7'));
+        $this->assertEquals('value4', $instance->get('key4'));
+
+        // Set the same key three times to make sure it doesn't count overrides.
+        for ($i = 0; $i < 3; $i++) {
+            $this->assertTrue($instance->set('key8', 'value8'));
+        }
+
+        $this->assertEquals('value7', $instance->get('key7'), 'Overrides are incorrectly incrementing size');
+
+        // Test adding many.
+        $this->assertEquals(3, $instance->set_many(array(
+            array('key' => 'keyA', 'value' => 'valueA'),
+            array('key' => 'keyB', 'value' => 'valueB'),
+            array('key' => 'keyC', 'value' => 'valueC')
+        )));
+        $this->assertEquals(array(
+            'key4' => false,
+            'key5' => false,
+            'key6' => false,
+            'key7' => false,
+            'keyA' => 'valueA',
+            'keyB' => 'valueB',
+            'keyC' => 'valueC'
+        ), $instance->get_many(array(
+            'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC'
+        )));
+    }
 }
\ No newline at end of file
index 2927b4b..a350f6d 100644 (file)
@@ -62,7 +62,7 @@ abstract class cachestore_tests extends advanced_testcase {
             }
         }
         if ($modes & cache_store::MODE_SESSION) {
-            $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
+            $definition = cache_definition::load_adhoc(cache_store::MODE_SESSION, $class, 'phpunit_test');
             $instance = $class::initialise_test_instance($definition);
             if (!$instance) {
                 $this->markTestSkipped('Could not test '.$class.'. No test instance configured for session caches.');
@@ -71,7 +71,7 @@ abstract class cachestore_tests extends advanced_testcase {
             }
         }
         if ($modes & cache_store::MODE_REQUEST) {
-            $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
+            $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $class, 'phpunit_test');
             $instance = $class::initialise_test_instance($definition);
             if (!$instance) {
                 $this->markTestSkipped('Could not test '.$class.'. No test instance configured for request caches.');
index 6821459..2def361 100644 (file)
@@ -258,7 +258,7 @@ class core_cohort_external extends external_api {
         return new external_multiple_structure(
             new external_single_structure(
                 array(
-                    'id' => new external_value(PARAM_NUMBER, 'ID of the cohort'),
+                    'id' => new external_value(PARAM_INT, 'ID of the cohort'),
                     'name' => new external_value(PARAM_RAW, 'cohort name'),
                     'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
@@ -280,7 +280,7 @@ class core_cohort_external extends external_api {
                 'cohorts' => new external_multiple_structure(
                     new external_single_structure(
                         array(
-                            'id' => new external_value(PARAM_NUMBER, 'ID of the cohort'),
+                            'id' => new external_value(PARAM_INT, 'ID of the cohort'),
                             'categorytype' => new external_single_structure(
                                 array(
                                     'type' => new external_value(PARAM_TEXT, 'the name of the field: id (numeric value
index 68ff607..5349c53 100644 (file)
@@ -54,10 +54,10 @@ class behat_cohort extends behat_base {
         $userid = $DB->get_field('user', 'id', array('username' => $username));
 
         $steps = array(
-            new Given('I click on "Assign" "link" in the "//table[@id=\'cohorts\']//tr[contains(., \'' . $cohortidnumber . '\')]" "xpath_element"'),
-            new Given('I select "' . $userid . '" from "Potential users"'),
-            new Given('I press "Add"'),
-            new Given('I press "Back to cohorts"')
+            new Given('I click on "' . get_string('assign', 'cohort') . '" "link" in the "' . $this->escape($cohortidnumber) . '" table row'),
+            new Given('I select "' . $userid . '" from "' . get_string('potusers', 'cohort') . '"'),
+            new Given('I press "' . get_string('add') . '"'),
+            new Given('I press "' . get_string('backtocohorts', 'cohort') . '"')
         );
 
         // If we are not in the cohorts management we should move there before anything else.
@@ -65,11 +65,11 @@ class behat_cohort extends behat_base {
             $steps = array_merge(
                 array(
                     new Given('I am on homepage'),
-                    new Given('I collapse "Front page settings" node'),
-                    new Given('I expand "Site administration" node'),
-                    new Given('I expand "Users" node'),
-                    new Given('I expand "Accounts" node'),
-                    new Given('I follow "Cohorts"')
+                    new Given('I collapse "' . get_string('frontpagesettings', 'admin') . '" node'),
+                    new Given('I expand "' . get_string('administrationsite') . '" node'),
+                    new Given('I expand "' . get_string('users', 'admin') . '" node'),
+                    new Given('I expand "' . get_string('accounts', 'admin') . '" node'),
+                    new Given('I follow "' . get_string('cohorts', 'cohort') . '"')
                 ),
                 $steps
             );
index c40095f..47b169a 100644 (file)
@@ -32,11 +32,11 @@ Feature: Upload users to a cohort
     And I press "Upload users"
     And I press "Continue"
     And I follow "Cohorts"
-    And I click on "Assign" "link" in the "//table[@id='cohorts']//tr[contains(., 'Cohort 1')]" "xpath_element"
+    And I click on "Assign" "link" in the "Cohort 1" table row
     Then the "Current users" select box should contain "Tom Jones (tomjones@example.com)"
     And the "Current users" select box should contain "Bob Jones (bobjones@example.com)"
     And I press "Back to cohorts"
-    And I click on "Assign" "link" in the "//table[@id='cohorts']//tr[contains(., 'Cohort 2')]" "xpath_element"
+    And I click on "Assign" "link" in the "Cohort 2" table row
     And the "Current users" select box should contain "Mary Smith (marysmith@example.com)"
     And the "Current users" select box should contain "Alice Smith (alicesmith@example.com)"
     And I am on homepage
index 39c2ef7..c8137fb 100644 (file)
@@ -147,10 +147,6 @@ class core_cohort_external_testcase extends externallib_advanced_testcase {
         // Check we retrieve the good total number of enrolled cohorts + no error on capability.
         $this->assertEquals(2, count($returnedcohorts));
 
-        // Call the external function.
-        $returnedcohorts = core_cohort_external::get_cohorts(array(
-                    $cohort1->id, $cohort2->id));
-
         foreach ($returnedcohorts as $enrolledcohort) {
             if ($enrolledcohort['idnumber'] == $cohort1->idnumber) {
                 $this->assertEquals($cohort1->name, $enrolledcohort['name']);
@@ -207,6 +203,36 @@ class core_cohort_external_testcase extends externallib_advanced_testcase {
         core_cohort_external::update_cohorts(array($cohort1));
     }
 
+    /**
+     * Verify handling of 'id' param.
+     */
+    public function test_update_cohorts_invalid_id_param() {
+        $this->resetAfterTest(true);
+        $cohort = self::getDataGenerator()->create_cohort();
+
+        $cohort1 = array(
+            'id' => 'THIS IS NOT AN ID',
+            'name' => 'Changed cohort name',
+            'categorytype' => array('type' => 'id', 'value' => '1'),
+            'idnumber' => $cohort->idnumber,
+        );
+
+        try {
+            core_cohort_external::update_cohorts(array($cohort1));
+            $this->fail('Expecting invalid_parameter_exception exception, none occured');
+        } catch (invalid_parameter_exception $e1) {
+            $this->assertContains('Invalid external api parameter: the value is "THIS IS NOT AN ID"', $e1->debuginfo);
+        }
+
+        $cohort1['id'] = 9.999; // Also not a valid id of a cohort.
+        try {
+            core_cohort_external::update_cohorts(array($cohort1));
+            $this->fail('Expecting invalid_parameter_exception exception, none occured');
+        } catch (invalid_parameter_exception $e2) {
+            $this->assertContains('Invalid external api parameter: the value is "9.999"', $e2->debuginfo);
+        }
+    }
+
     /**
      * Test update_cohorts without permission on the dest category.
      */
diff --git a/cohort/upgrade.txt b/cohort/upgrade.txt
new file mode 100644 (file)
index 0000000..d776e6f
--- /dev/null
@@ -0,0 +1,9 @@
+This files describes API changes in /cohort/ information provided here is intended
+especially for developers.
+
+=== 2.6 ===
+* Webservice core_cohort_update_cohorts was incorrectly specifiying float as the parameter type
+  for cohort id. This field is actually int and input is now reported and processed as such.
+* Webservice core_cohort_get_cohorts was incorrectly specifiying float as the return
+  type for cohort id. The actual return type is int and is now reported as such.
+
index ef05aeb..89b6344 100644 (file)
@@ -50,12 +50,13 @@ class behat_completion extends behat_base {
     public function user_has_completed_activity($userfullname, $activityname) {
 
         // Will throw an exception if the element can not be hovered.
+        $titleliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($userfullname . ", " . $activityname . ": Completed");
         $xpath = "//table[@id='completion-progress']" .
-            "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Completed')]";
+            "/descendant::img[contains(@title, $titleliteral)]";
 
         return array(
             new Given('I go to the current course activity completion report'),
-            new Given('I hover "' . $xpath . '" "xpath_element"')
+            new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
         );
     }
 
@@ -68,11 +69,13 @@ class behat_completion extends behat_base {
      */
     public function user_has_not_completed_activity($userfullname, $activityname) {
 
+        // Will throw an exception if the element can not be hovered.
+        $titleliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($userfullname . ", " . $activityname . ": Not completed");
         $xpath = "//table[@id='completion-progress']" .
-            "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Not completed')]";
+            "/descendant::img[contains(@title, $titleliteral)]";
         return array(
             new Given('I go to the current course activity completion report'),
-            new Given('I hover "' . $xpath . '" "xpath_element"')
+            new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"')
         );
 
         return $steps;
@@ -89,15 +92,15 @@ class behat_completion extends behat_base {
 
         // Expand reports node if we can't see the link.
         try {
-            $this->find('xpath', "//*[@id='settingsnav']" .
+            $this->find('xpath', "//div[@id='settingsnav']" .
                 "/descendant::li" .
-                "/descendant::li[not(contains(@class,'collapsed'))]" .
-                "/descendant::p[contains(., 'Activity completion')]");
+                "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]" .
+                "/descendant::p[contains(., '" . get_string('pluginname', 'report_progress') . "')]");
         } catch (ElementNotFoundException $e) {
-            $steps[] = new Given('I expand "Reports" node');
+            $steps[] = new Given('I expand "' . get_string('reports') . '" node');
         }
 
-        $steps[] = new Given('I follow "Activity completion"');
+        $steps[] = new Given('I follow "' . get_string('pluginname', 'report_progress') . '"');
 
         return $steps;
     }
index cd780cf..63e7826 100644 (file)
@@ -213,6 +213,7 @@ $CFG->admin = 'admin';
 //     $CFG->xsendfilealiases = array(
 //         '/dataroot/' => $CFG->dataroot,
 //         '/cachedir/' => '/var/www/moodle/cache',    // for custom $CFG->cachedir locations
+//         '/localcachedir/' => '/var/local/cache',    // for custom $CFG->localcachedir locations
 //         '/tempdir/'  => '/var/www/moodle/temp',     // for custom $CFG->tempdir locations
 //         '/filedir'   => '/var/www/moodle/filedir',  // for custom $CFG->filedir locations
 //     );
@@ -353,10 +354,12 @@ $CFG->admin = 'admin';
 //
 // It is possible to specify different cache and temp directories, use local fast filesystem
 // for normal web servers. Server clusters MUST use shared filesystem for cachedir!
+// Localcachedir is intended for server clusters, it does not have to be shared by cluster nodes.
 // The directories must not be accessible via web.
 //
-//     $CFG->tempdir = '/var/www/moodle/temp';
-//     $CFG->cachedir = '/var/www/moodle/cache';
+//     $CFG->tempdir = '/var/www/moodle/temp';        // Files used during one HTTP request only.
+//     $CFG->cachedir = '/var/www/moodle/cache';      // Directory MUST BE SHARED by all cluster nodes, locking required.
+//     $CFG->localcachedir = '/var/local/cache';      // Intended for local node caching.
 //
 // Some filesystems such as NFS may not support file locking operations.
 // Locking resolves race conditions and is strongly recommended for production servers.
@@ -631,6 +634,11 @@ $CFG->admin = 'admin';
 // Example:
 //   $CFG->behat_restart_browser_after = 7200;     // Restarts the browser session after 2 hours
 //
+// All this page's extra Moodle settings are compared against a white list of allowed settings
+// (the basic and behat_* ones) to avoid problems with production environments. This setting can be
+// used to expand the default white list with an array of extra settings.
+// Example:
+//   $CFG->behat_extraallowedsettings = array('logsql', 'dblogerror');
 
 //=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
index c845331..06f7435 100644 (file)
@@ -99,11 +99,14 @@ class course_edit_form extends moodleform {
         $mform->addElement('select', 'visible', get_string('visible'), $choices);
         $mform->addHelpButton('visible', 'visible');
         $mform->setDefault('visible', $courseconfig->visible);
-        if (!has_capability('moodle/course:visibility', $context)) {
-            $mform->hardFreeze('visible');
-            if (!empty($course->id)) {
+        if (!empty($course->id)) {
+            if (!has_capability('moodle/course:visibility', $coursecontext)) {
+                $mform->hardFreeze('visible');
                 $mform->setConstant('visible', $course->visible);
-            } else {
+            }
+        } else {
+            if (!guess_if_creator_will_have_course_capability('moodle/course:visibility', $categorycontext)) {
+                $mform->hardFreeze('visible');
                 $mform->setConstant('visible', $courseconfig->visible);
             }
         }
diff --git a/course/format/singleactivity/format.php b/course/format/singleactivity/format.php
new file mode 100644 (file)
index 0000000..5996ef4
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * format.php - course format featuring single activity included from view.php
+ *
+ * if we are not redirected before this point this means we want to
+ * either manage orphaned activities - i.e. display section 1,
+ * or the activity is not setup, does not have url or is not accessible at the
+ * moment
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$courserenderer = $PAGE->get_renderer('format_singleactivity');
+echo $courserenderer->display($course, $section != 0);
diff --git a/course/format/singleactivity/lang/en/format_singleactivity.php b/course/format/singleactivity/lang/en/format_singleactivity.php
new file mode 100644 (file)
index 0000000..9b2887b
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'format_singleactivity'
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['activitytype'] = 'Type of activity';
+$string['activitytype_help'] = 'Choose the type of activity or resource to use in this course';
+$string['defactivitytype'] = 'Default type of activity';
+$string['defactivitytypedesc'] = 'Specify the activity type that will be selected by default when creating a new course';
+$string['erroractivitytype'] = 'Type of activity is not set up in Course settings';
+$string['orphaned'] = 'Orphaned';
+$string['orphanedwarning'] = 'These activities are unreachable by users!';
+$string['pluginname'] = 'Single activity format';
+$string['sectionname'] = '';
+$string['warningchangeformat'] = 'When changing the existing course format to "Single activity" make sure that you removed all extra activities from the course including the "News forum". Note that sections structure may be modified.';
diff --git a/course/format/singleactivity/lib.php b/course/format/singleactivity/lib.php
new file mode 100644 (file)
index 0000000..02795d1
--- /dev/null
@@ -0,0 +1,443 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains main class for the course format singleactivity
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot. '/course/format/lib.php');
+
+/**
+ * Main class for the singleactivity course format
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity extends format_base {
+    /** @var cm_info the current activity. Use get_activity() to retrieve it. */
+    private $activity = false;
+
+    /**
+     * The URL to use for the specified course
+     *
+     * @param int|stdClass $section Section object from database or just field course_sections.section
+     *     if null the course view page is returned
+     * @param array $options options for view URL. At the moment core uses:
+     *     'navigation' (bool) if true and section has no separate page, the function returns null
+     *     'sr' (int) used by multipage formats to specify to which section to return
+     * @return null|moodle_url
+     */
+    public function get_view_url($section, $options = array()) {
+        $sectionnum = $section;
+        if (is_object($sectionnum)) {
+            $sectionnum = $section->section;
+        }
+        if ($sectionnum == 1) {
+            return new moodle_url('/course/view.php', array('id' => $this->courseid, 'section' => 1));
+        }
+        if (!empty($options['navigation']) && $section !== null) {
+            return null;
+        }
+        return new moodle_url('/course/view.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Loads all of the course sections into the navigation
+     *
+     * @param global_navigation $navigation
+     * @param navigation_node $node The course node within the navigation
+     */
+    public function extend_course_navigation($navigation, navigation_node $node) {
+        // Display orphaned activities for the users who can see them.
+        $context = context_course::instance($this->courseid);
+        if (has_all_capabilities(array('moodle/course:viewhiddensections',
+                'moodle/course:viewhiddenactivities'), $context)) {
+            $modinfo = get_fast_modinfo($this->courseid);
+            if (!empty($modinfo->sections[1])) {
+                $section1 = $modinfo->get_section_info(1);
+                // Show orphaned activities.
+                $orphanednode = $node->add(get_string('orphaned', 'format_singleactivity'),
+                        $this->get_view_url(1), navigation_node::TYPE_SECTION, null, $section1->id);
+                $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH;
+                $orphanednode->add_class('orphaned');
+                foreach ($modinfo->sections[1] as $cmid) {
+                    $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a course module to the navigation node
+     *
+     * This is basically copied from function global_navigation::load_section_activities()
+     * because it is not accessible from outside.
+     *
+     * @param navigation_node $node
+     * @param cm_info $cm
+     * @return null|navigation_node
+     */
+    protected function navigation_add_activity(navigation_node $node, $cm) {
+        if (!$cm->uservisible) {
+            return null;
+        }
+        $action = $cm->get_url();
+        if (!$action) {
+            // Do not add to navigation activity without url (i.e. labels).
+            return null;
+        }
+        $activityname = format_string($cm->name, true, array('context' => context_module::instance($cm->id)));
+        if ($cm->icon) {
+            $icon = new pix_icon($cm->icon, $cm->modfullname, $cm->iconcomponent);
+        } else {
+            $icon = new pix_icon('icon', $cm->modfullname, $cm->modname);
+        }
+        $activitynode = $node->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
+        if (global_navigation::module_extends_navigation($cm->modname)) {
+            $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
+        } else {
+            $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
+        }
+        return $activitynode;
+    }
+
+    /**
+     * Returns the list of blocks to be automatically added for the newly created course
+     *
+     * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
+     *     each of values is an array of block names (for left and right side columns)
+     */
+    public function get_default_blocks() {
+        // No blocks for this format because course view page is not displayed anyway.
+        return array(
+            BLOCK_POS_LEFT => array(),
+            BLOCK_POS_RIGHT => array()
+        );
+    }
+
+    /**
+     * Definitions of the additional options that this course format uses for course
+     *
+     * Singleactivity course format uses one option 'activitytype'
+     *
+     * @param bool $foreditform
+     * @return array of options
+     */
+    public function course_format_options($foreditform = false) {
+        static $courseformatoptions = false;
+        if ($courseformatoptions === false) {
+            $config = get_config('format_singleactivity');
+            $courseformatoptions = array(
+                'activitytype' => array(
+                    'default' => $config->activitytype,
+                    'type' => PARAM_TEXT,
+                ),
+            );
+        }
+        if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
+            $availabletypes = get_module_types_names();
+            $courseformatoptionsedit = array(
+                'activitytype' => array(
+                    'label' => new lang_string('activitytype', 'format_singleactivity'),
+                    'help' => 'activitytype',
+                    'help_component' => 'format_singleactivity',
+                    'element_type' => 'select',
+                    'element_attributes' => array($availabletypes),
+                ),
+            );
+            $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
+        }
+        return $courseformatoptions;
+    }
+
+    /**
+     * Adds format options elements to the course/section edit form
+     *
+     * This function is called from {@link course_edit_form::definition_after_data()}
+     *
+     * Format singleactivity adds a warning when format of the course is about to be changed.
+     *
+     * @param MoodleQuickForm $mform form the elements are added to
+     * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
+     * @return array array of references to the added form elements
+     */
+    public function create_edit_form_elements(&$mform, $forsection = false) {
+        global $PAGE;
+        $elements = parent::create_edit_form_elements($mform, $forsection);
+        if (!$forsection && ($course = $PAGE->course) && !empty($course->format) &&
+                $course->format !== 'site' && $course->format !== 'singleactivity') {
+            // This is the existing course in other format, display a warning.
+            $element = $mform->addElement('static', '', '',
+                    html_writer::tag('span', get_string('warningchangeformat', 'format_singleactivity'),
+                            array('class' => 'error')));
+            array_unshift($elements, $element);
+        }
+        return $elements;
+    }
+
+    /**
+     * Make sure that current active activity is in section 0
+     *
+     * All other activities are moved to section 1 that will be displayed as 'Orphaned'.
+     * It may be needed after the course format was changed or activitytype in
+     * course settings has been changed.
+     *
+     * @return null|cm_info current activity
+     */
+    public function reorder_activities() {
+        course_create_sections_if_missing($this->courseid, array(0, 1));
+        foreach ($this->get_sections() as $sectionnum => $section) {
+            if (($sectionnum && $section->visible) ||
+                    (!$sectionnum && !$section->visible)) {
+                // Make sure that 0 section is visible and all others are hidden.
+                set_section_visible($this->courseid, $sectionnum, $sectionnum == 0);
+            }
+        }
+        $modinfo = get_fast_modinfo($this->courseid);
+
+        // Find the current activity (first activity with the specified type in all course activities).
+        $activitytype = $this->get_activitytype();
+        $activity = null;
+        if (!empty($activitytype)) {
+            foreach ($modinfo->sections as $sectionnum => $cmlist) {
+                foreach ($cmlist as $cmid) {
+                    if ($modinfo->cms[$cmid]->modname === $activitytype) {
+                        $activity = $modinfo->cms[$cmid];
+                        break 2;
+                    }
+                }
+            }
+        }
+
+        // Make sure the current activity is in the 0-section.
+        if ($activity && $activity->sectionnum != 0) {
+            moveto_module($activity, $modinfo->get_section_info(0));
+            // Cache was reset so get modinfo again.
+            $modinfo = get_fast_modinfo($this->courseid);
+        }
+
+        // Move all other activities into section 1 (the order must be kept).
+        $hasvisibleactivities = false;
+        $firstorphanedcm = null;
+        foreach ($modinfo->sections as $sectionnum => $cmlist) {
+            if ($sectionnum && !empty($cmlist) && $firstorphanedcm === null) {
+                $firstorphanedcm = reset($cmlist);
+            }
+            foreach ($cmlist as $cmid) {
+                if ($sectionnum > 1) {
+                    moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1));
+                } else if (!$hasvisibleactivities && $sectionnum == 1 && $modinfo->get_cm($cmid)->visible) {
+                    $hasvisibleactivities = true;
+                }
+            }
+        }
+        if (!empty($modinfo->sections[0])) {
+            foreach ($modinfo->sections[0] as $cmid) {
+                if (!$activity || $cmid != $activity->id) {
+                    moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1), $firstorphanedcm);
+                }
+            }
+        }
+        if ($hasvisibleactivities) {
+            set_section_visible($this->courseid, 1, false);
+        }
+        return $activity;
+    }
+
+    /**
+     * Returns the name of activity type used for this course
+     *
+     * @return string|null
+     */
+    protected function get_activitytype() {
+        $options = $this->get_format_options();
+        $availabletypes = get_module_types_names();
+        if (!empty($options['activitytype']) &&
+                array_key_exists($options['activitytype'], $availabletypes)) {
+            return $options['activitytype'];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the current activity if exists
+     *
+     * @return null|cm_info
+     */
+    protected function get_activity() {
+        if ($this->activity === false) {
+            $this->activity = $this->reorder_activities();
+        }
+        return $this->activity;
+    }
+
+    /**
+     * Checks if the current user can add the activity of the specified type to this course.
+     *
+     * @return bool
+     */
+    protected function can_add_activity() {
+        global $CFG;
+        if (!($modname = $this->get_activitytype())) {
+            return false;
+        }
+        if (!has_capability('moodle/course:manageactivities', context_course::instance($this->courseid))) {
+            return false;
+        }
+        if (!course_allowed_module($this->get_course(), $modname)) {
+            return false;
+        }
+        $libfile = "$CFG->dirroot/mod/$modname/lib.php";
+        if (!file_exists($libfile)) {
+            return null;
+        }
+        return true;
+    }
+
+    /**
+     * Checks if the activity type requires subtypes.
+     *
+     * @return bool|null (null if the check is not possible)
+     */
+    public function activity_has_subtypes() {
+        global $CFG;
+        if (!($modname = $this->get_activitytype())) {
+            return null;
+        }
+        $libfile = "$CFG->dirroot/mod/$modname/lib.php";
+        if (!file_exists($libfile)) {
+            return null;
+        }
+        include_once($libfile);
+        return function_exists($modname. '_get_types');
+    }
+
+    /**
+     * Allows course format to execute code on moodle_page::set_course()
+     *
+     * This function is executed before the output starts.
+     *
+     * If everything is configured correctly, user is redirected from the
+     * default course view page to the activity view page.
+     *
+     * "Section 1" is the administrative page to manage orphaned activities
+     *
+     * If user is on course view page and there is no module added to the course
+     * and the user has 'moodle/course:manageactivities' capability, redirect to create module
+     * form.
+     *
+     * @param moodle_page $page instance of page calling set_course
+     */
+    public function page_set_course(moodle_page $page) {
+        global $PAGE;
+        $page->add_body_class('format-'. $this->get_format());
+        if ($PAGE == $page && $page->has_set_url() &&
+                $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
+            $edit = optional_param('edit', -1, PARAM_BOOL);
+            if (($edit == 0 || $edit == 1) && confirm_sesskey()) {
+                // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection.
+                return;
+            }
+            $cm = $this->get_activity();
+            $cursection = optional_param('section', null, PARAM_INT);
+            if (!empty($cursection) && has_capability('moodle/course:viewhiddensections',
+                    context_course::instance($this->courseid))) {
+                // Display orphaned activities (course view page, section 1).
+                return;
+            }
+            if (!$this->get_activitytype()) {
+                if (has_capability('moodle/course:update', context_course::instance($this->courseid))) {
+                    // Teacher is redirected to edit course page.
+                    $url = new moodle_url('/course/edit.php', array('id' => $this->courseid));
+                    redirect($url, get_string('erroractivitytype', 'format_singleactivity'));
+                } else {
+                    // Student sees an empty course page.
+                    return;
+                }
+            }
+            if ($cm === null) {
+                if ($this->can_add_activity()) {
+                    // This is a user who has capability to create an activity.
+                    if ($this->activity_has_subtypes()) {
+                        // Activity that requires subtype can not be added automatically.
+                        if (optional_param('addactivity', 0, PARAM_INT)) {
+                            return;
+                        } else {
+                            $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1));
+                            redirect($url);
+                        }
+                    }
+                    // Redirect to the add activity form.
+                    $url = new moodle_url('/course/mod.php', array('id' => $this->courseid,
+                        'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype()));
+                    redirect($url);
+                } else {
+                    // Student views an empty course page.
+                    return;
+                }
+            } else if (!$cm->uservisible || !$cm->get_url()) {
+                // Activity is set but not visible to current user or does not have url.
+                // Display course page (either empty or with availability restriction info).
+                return;
+            } else {
+                // Everything is set up and accessible, redirect to the activity page!
+                redirect($cm->get_url());
+            }
+        }
+    }
+
+    /**
+     * Allows course format to execute code on moodle_page::set_cm()
+     *
+     * If we are inside the main module for this course, remove extra node level
+     * from navigation: substitute course node with activity node, move all children
+     *
+     * @param moodle_page $page instance of page calling set_cm
+     */
+    public function page_set_cm(moodle_page $page) {
+        global $PAGE;
+        parent::page_set_cm($page);
+        if ($PAGE == $page && ($cm = $this->get_activity()) &&
+                $cm->uservisible &&
+                ($cm->id === $page->cm->id) &&
+                ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) &&
+                ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) {
+            // Substitute course node with activity node, move all children.
+            $node->action = $activitynode->action;
+            $node->type = $activitynode->type;
+            $node->id = $activitynode->id;
+            $node->key = $activitynode->key;
+            $node->isactive = $node->isactive || $activitynode->isactive;
+            $node->icon = null;
+            if ($activitynode->children->count()) {
+                foreach ($activitynode->children as &$child) {
+                    $child->remove();
+                    $node->add_node($child);
+                }
+            } else {
+                $node->search_for_active_node();
+            }
+            $activitynode->remove();
+        }
+    }
+}
diff --git a/course/format/singleactivity/renderer.php b/course/format/singleactivity/renderer.php
new file mode 100644 (file)
index 0000000..e9961a0
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer for outputting the singleactivity course format.
+ *
+ * @package    format_singleactivity
+ * @copyright  2013 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Basic renderer for singleactivity format.
+ *
+ * @package    format_singleactivity
+ * @copyright  2013 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity_renderer extends plugin_renderer_base {
+
+    /**
+     * Displays the activities list in cases when course view page is not
+     * redirected to the activity page.
+     *
+     * @param stdClass $course record from table course
+     * @param bool $orphaned if false displays the main activity (if present)
+     *     if true displays all other activities
+     */
+    public function display($course, $orphaned) {
+        $courserenderer = $this->page->get_renderer('core', 'course');
+        $output = '';
+        $modinfo = get_fast_modinfo($course);
+        if ($orphaned) {
+            if (!empty($modinfo->sections[1])) {
+                $output .= $this->output->heading(get_string('orphaned', 'format_singleactivity'), 3, 'sectionname');
+                $output .= $this->output->box(get_string('orphanedwarning', 'format_singleactivity'));
+                $output .= $courserenderer->course_section_cm_list($course, 1, 1);
+            }
+        } else {
+            $output .= $courserenderer->course_section_cm_list($course, 0, 0);
+            if (empty($modinfo->sections[0]) && course_get_format($course)->activity_has_subtypes()) {
+                // Course format was unable to automatically redirect to add module page.
+                $output .= $courserenderer->course_section_add_cm_control($course, 0, 0);
+            }
+        }
+        return $output;
+    }
+}
diff --git a/course/format/singleactivity/settings.php b/course/format/singleactivity/settings.php
new file mode 100644 (file)
index 0000000..8573600
--- /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/>.
+
+/**
+ * Settings for format_singleactivity
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+require_once($CFG->dirroot. '/course/format/singleactivity/settingslib.php');
+
+if ($ADMIN->fulltree) {
+    $settings->add(new format_singleactivity_admin_setting_activitytype('format_singleactivity/activitytype',
+            new lang_string('defactivitytype', 'format_singleactivity'),
+            new lang_string('defactivitytypedesc', 'format_singleactivity'),
+            'forum', null));
+}
diff --git a/course/format/singleactivity/settingslib.php b/course/format/singleactivity/settingslib.php
new file mode 100644 (file)
index 0000000..75494fc
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Settings class for format_singleactivity
+ *
+ * @package    format_singleactivity
+ * @copyright  2013 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Admin settings class for the format singleactivity activitytype choice
+ *
+ * @package    format_singleactivity
+ * @copyright  2013 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity_admin_setting_activitytype extends admin_setting_configselect {
+    /**
+     * This function may be used in ancestors for lazy loading of choices
+     *
+     * Override this method if loading of choices is expensive, such
+     * as when it requires multiple db requests.
+     *
+     * @return bool true if loaded, false if error
+     */
+    public function load_choices() {
+        global $CFG;
+        require_once($CFG->dirroot. '/course/lib.php');
+        if (is_array($this->choices)) {
+            return true;
+        }
+        $this->choices = get_module_types_names();
+        return true;
+    }
+}
diff --git a/course/format/singleactivity/styles.css b/course/format/singleactivity/styles.css
new file mode 100644 (file)
index 0000000..65f7875
--- /dev/null
@@ -0,0 +1,10 @@
+/* Hide confusing form elements "Display description on course page" and
+"Save and return to course" from module edit form because they
+are not applicable in single activity course format */
+body.format-singleactivity.path-mod.pagelayout-admin form.mform #fitem_id_showdescription,
+body.format-singleactivity.path-mod.pagelayout-admin form.mform .fitem_actionbuttons#fgroup_id_buttonar #id_submitbutton {display:none;}
+
+/* In mod_quiz hide "Back to course" button */
+body.format-singleactivity.path-mod-quiz .quizattempt .continuebutton {display:none;}
+
+body.format-singleactivity .tree_item.orphaned a {color:red;}
diff --git a/course/format/singleactivity/version.php b/course/format/singleactivity/version.php
new file mode 100644 (file)
index 0000000..dea4103
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Version details
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2013071200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2013070800;        // Requires this Moodle version (2.6)
+$plugin->component = 'format_singleactivity';    // Full name of the plugin (used for diagnostics).
index 78c342a..65abea5 100644 (file)
@@ -27,21 +27,21 @@ Feature: Toggle activities groups mode from the course page
       | Force group mode | No |
     When I press "Save changes"
     Then "No groups (Click to change)" "link" should exists
-    And ".//a//img[contains(@src, 'groupn')]" "xpath_element" should exists
+    And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists
     And I click on "No groups (Click to change)" "link" in the "Test forum name" activity
     And I wait "3" seconds
     And "Separate groups (Click to change)" "link" should exists
-    And ".//a//img[contains(@src, 'groups')]" "xpath_element" should exists
+    And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists
     And I reload the page
     And "Separate groups (Click to change)" "link" should exists
-    And ".//a//img[contains(@src, 'groups')]" "xpath_element" should exists
+    And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists
     And I click on "Separate groups (Click to change)" "link" in the "Test forum name" activity
     And I wait "3" seconds
     And "Visible groups (Click to change)" "link" should exists
-    And ".//a//img[contains(@src, 'groupv')]" "xpath_element" should exists
+    And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists
     And I reload the page
     And "Visible groups (Click to change)" "link" should exists
-    And ".//a//img[contains(@src, 'groupv')]" "xpath_element" should exists
+    And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists
     And I click on "Visible groups (Click to change)" "link" in the "Test forum name" activity
     And "No groups (Click to change)" "link" should exists
-    And ".//a//img[contains(@src, 'groupn')]" "xpath_element" should exists
+    And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists
index 5621e49..7cd0079 100644 (file)
@@ -26,7 +26,7 @@ Feature: Indent items on the course page
     When I indent right "Test glossary name" activity
     Then "#section-1 li.glossary div.mod-indent-1" "css_element" should exists
     And I indent right "Test glossary name" activity
-    And "//*[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists
+    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
     And I reload the page
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
@@ -34,4 +34,4 @@ Feature: Indent items on the course page
     And I indent left "Test glossary name" activity
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should not exists
     And "#section-1 li.glossary div.mod-indent-1" "css_element" should not exists
-    And "//*[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists
+    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists
index c12a938..49145f2 100644 (file)
@@ -48,7 +48,7 @@ class behat_course extends behat_base {
      * @Given /^I turn editing mode on$/
      */
     public function i_turn_editing_mode_on() {
-        return new Given('I press "Turn editing on"');
+        return new Given('I press "' . get_string('turneditingon') . '"');
     }
 
     /**
@@ -56,7 +56,7 @@ class behat_course extends behat_base {
      * @Given /^I turn editing mode off$/
      */
     public function i_turn_editing_mode_off() {
-        return new Given('I press "Turn editing off"');
+        return new Given('I press "' . get_string('turneditingoff') . '"');
     }
 
     /**
@@ -68,9 +68,9 @@ class behat_course extends behat_base {
     public function i_create_a_course_with(TableNode $table) {
         return array(
             new Given('I go to the courses management page'),
-            new Given('I press "Add a new course"'),
+            new Given('I press "' . get_string('addnewcourse') . '"'),
             new Given('I fill the moodle form with:', $table),
-            new Given('I press "Save changes"')
+            new Given('I press "' . get_string('savechanges') . '"')
         );
     }
 
@@ -83,9 +83,9 @@ class behat_course extends behat_base {
 
         return array(
             new Given('I am on homepage'),
-            new Given('I expand "Site administration" node'),
-            new Given('I expand "Courses" node'),
-            new Given('I follow "Add/edit courses"'),
+            new Given('I expand "' . get_string('administrationsite') . '" node'),
+            new Given('I expand "' . get_string('courses', 'admin') . '" node'),
+            new Given('I follow "' . get_string('coursemgmt', 'admin') . '"'),
         );
     }
 
@@ -100,9 +100,9 @@ class behat_course extends behat_base {
     public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
 
         return array(
-            new Given('I add a "'.$activity.'" to section "'.$section.'"'),
+            new Given('I add a "' . $this->escape($activity) . '" to section "' . $this->escape($section) . '"'),
             new Given('I fill the moodle form with:', $data),
-            new Given('I press "Save and return to course"')
+            new Given('I press "' . get_string('savechangesandreturntocourse') . '"')
         );
     }
 
@@ -116,7 +116,9 @@ class behat_course extends behat_base {
      */
     public function i_add_to_section($activity, $section) {
 
-        $sectionxpath = "//*[@id='section-" . $section . "']";
+        $sectionxpath = "//li[@id='section-" . $section . "']";
+
+        $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral(ucfirst($activity));
 
         if ($this->running_javascript()) {
 
@@ -126,9 +128,9 @@ class behat_course extends behat_base {
             $sectionnode->click();
 
             // Clicks the selected activity if it exists.
-            $activity = ucfirst($activity);
             $activityxpath = "//div[@id='chooseform']/descendant::label" .
-                "/descendant::span[contains(concat(' ', @class, ' '), ' typename ')][contains(.,'" . $activity . "')]" .
+                "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" .
+                "[contains(., $activityliteral)]" .
                 "/parent::label/child::input";
             $activitynode = $this->find('xpath', $activityxpath);
             $activitynode->doubleClick();
@@ -137,8 +139,8 @@ class behat_course extends behat_base {
             // Without Javascript.
 
             // Selecting the option from the select box which contains the option.
-            $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' section_add_menus ')]" .
-                "/descendant::select[contains(., '" . $activity . "')]";
+            $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" .
+                "/descendant::select[contains(., $activityliteral)]";
             $selectnode = $this->find('xpath', $selectxpath);
             $selectnode->selectOption($activity);
 
@@ -162,7 +164,7 @@ class behat_course extends behat_base {
         $xpath = $this->section_exists($sectionnumber);
 
         return array(
-            new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'),
+            new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
             new Given('I wait "2" seconds')
         );
     }
@@ -179,7 +181,7 @@ class behat_course extends behat_base {
         $xpath = $this->section_exists($sectionnumber);
 
         return array(
-            new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'),
+            new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
             new Given('I wait "2" seconds')
         );
     }
@@ -268,7 +270,7 @@ class behat_course extends behat_base {
 
         // Section should be hidden.
         $exception = new ExpectationException('The section is not hidden', $this->getSession());
-        $this->find('xpath', $sectionxpath . "[contains(concat(' ', @class, ' '), ' hidden ')]", $exception);
+        $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
 
         // The checking are different depending on user permissions.
         if ($this->is_course_editor()) {
@@ -284,8 +286,8 @@ class behat_course extends behat_base {
                 foreach ($activities as $activity) {
 
                     // Dimmed.
-                    $this->find('xpath', "//div[contains(concat(' ', @class, ' '), ' activityinstance ')]" .
-                        "/a[contains(concat(' ', @class, ' '), ' dimmed ')]", $dimmedexception, $activity);
+                    $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' activityinstance ')]" .
+                        "/a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')]", $dimmedexception, $activity);
 
                     // Non-JS browsers can not click on img elements.
                     if ($this->running_javascript()) {
@@ -319,7 +321,8 @@ class behat_course extends behat_base {
         $sectionxpath = $this->section_exists($sectionnumber);
 
         // Section should not be hidden.
-        if (!$this->getSession()->getPage()->find('xpath', $sectionxpath . "[not(contains(concat(' ', @class, ' '), ' hidden '))]")) {
+        $xpath = $sectionxpath . "[not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]";
+        if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
             throw new ExpectationException('The section is hidden', $this->getSession());
         }
 
@@ -449,10 +452,11 @@ class behat_course extends behat_base {
         // JS enabled.
         if ($this->running_javascript()) {
 
-            $destinationxpath = $sectionxpath . "/descendant::ul[contains(@class, 'yui3-dd-drop')]";
+            $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
 
             return array(
-                new Given('I drag "' . $activitynode->getXpath() . '" "xpath_element" and I drop it in "' . $destinationxpath . '" "xpath_element"'),
+                new Given('I drag "' . $this->escape($activitynode->getXpath()) . '" "xpath_element" ' .
+                    'and I drop it in "' . $this->escape($destinationxpath) . '" "xpath_element"'),
             );
 
         } else {
@@ -460,8 +464,8 @@ class behat_course extends behat_base {
 
             // Moving to the fist spot of the section (before all other section's activities).
             return array(
-                new Given('I click on "a.editing_move" "css_element" in the "' . $activityname . '" activity'),
-                new Given('I click on "li.movehere a" "css_element" in the "' . $sectionxpath . '" "xpath_element"'),
+                new Given('I click on "a.editing_move" "css_element" in the "' . $this->escape($activityname) . '" activity'),
+                new Given('I click on "li.movehere a" "css_element" in the "' . $this->escape($sectionxpath) . '" "xpath_element"'),
             );
         }
     }
@@ -482,8 +486,8 @@ class behat_course extends behat_base {
 
         // Adding chr(10) to save changes.
         return array(
-            new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activityname .'" activity'),
-            new Given('I fill in "title" with "' . $newactivityname . chr(10) . '"'),
+            new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $this->escape($activityname) .'" activity'),
+            new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'),
             new Given('I wait "2" seconds')
         );
     }
@@ -497,7 +501,7 @@ class behat_course extends behat_base {
     public function i_indent_right_activity($activityname) {
 
         $steps = array(
-            new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activityname . '" activity')
+            new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $this->escape($activityname) . '" activity')
         );
 
         if ($this->running_javascript()) {
@@ -516,7 +520,7 @@ class behat_course extends behat_base {
     public function i_indent_left_activity($activityname) {
 
         $steps = array(
-            new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activityname . '" activity')
+            new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $this->escape($activityname) . '" activity')
         );
 
         if ($this->running_javascript()) {
@@ -553,7 +557,7 @@ class behat_course extends behat_base {
 
             // With JS disabled.
             $steps = array(
-                new Given('I click on "' . $deletestring . '" "link" in the "' . $activityname . '" activity'),
+                new Given('I click on "' . $this->escape($deletestring) . '" "link" in the "' . $this->escape($activityname) . '" activity'),
                 new Given('I press "' . get_string('yes') . '"')
             );
 
@@ -569,7 +573,7 @@ class behat_course extends behat_base {
      */
     public function i_duplicate_activity($activityname) {
         return array(
-            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'),
+            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
             new Given('I press "' . get_string('continue') .'"'),
             new Given('I press "' . get_string('duplicatecontcourse') .'"')
         );
@@ -584,7 +588,7 @@ class behat_course extends behat_base {
      */
     public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
         return array(
-            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'),
+            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
             new Given('I press "' . get_string('continue') .'"'),
             new Given('I press "' . get_string('duplicatecontedit') . '"'),
             new Given('I fill the moodle form with:', $data),
@@ -657,9 +661,9 @@ class behat_course extends behat_base {
         $courseformat = $this->get_course_format();
 
         // Checking the show button alt text and show icon.
-        $showtext = get_string('showfromothers', $courseformat);
-        $linkxpath = $xpath . "/descendant::a[@title='". $showtext ."']";
-        $imgxpath = $linkxpath . "/descendant::img[@alt='". $showtext ."'][contains(@src, 'show')]";
+        $showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat));
+        $linkxpath = $xpath . "/descendant::a[@title=$showtext]";
+        $imgxpath = $linkxpath . "/descendant::img[@alt=$showtext][contains(@src, 'show')]";
 
         $exception = new ElementNotFoundException($this->getSession(), 'Show section icon ');
         $this->find('xpath', $imgxpath, $exception);
@@ -684,9 +688,9 @@ class behat_course extends behat_base {
         $courseformat = $this->get_course_format();
 
         // Checking the hide button alt text and hide icon.
-        $hidetext = get_string('hidefromothers', $courseformat);
-        $linkxpath = $xpath . "/descendant::a[@title='" . $hidetext . "']";
-        $imgxpath = $linkxpath . "/descendant::img[@alt='" . $hidetext ."'][contains(@src, 'hide')]";
+        $hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat));
+        $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
+        $imgxpath = $linkxpath . "/descendant::img[@alt=$hidetext][contains(@src, 'hide')]";
 
         $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
         $this->find('xpath', $imgxpath, $exception);
@@ -730,7 +734,7 @@ class behat_course extends behat_base {
      */
     protected function get_section_activities($sectionxpath) {
 
-        $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', @class, ' '), ' activity ')]";
+        $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]";
 
         // We spin here, as activities usually require a lot of time to load.
         try {
@@ -751,8 +755,8 @@ class behat_course extends behat_base {
      */
     protected function get_activity_node($activityname) {
 
-        $activityname = str_replace("'", "\'", $activityname);
-        $xpath = "//li[contains(concat(' ', @class, ' '), ' activity ')][contains(., '" .$activityname. "')]";
+        $activityname = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
+        $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]";
 
         return $this->find('xpath', $xpath);
     }
@@ -765,8 +769,8 @@ class behat_course extends behat_base {
     protected function is_course_editor() {
 
         // We don't need to behat_base::spin() here as all is already loaded.
-        if (!$this->getSession()->getPage()->findButton('Turn editing off') &&
-                !$this->getSession()->getPage()->findButton('Turn editing on')) {
+        if (!$this->getSession()->getPage()->findButton(get_string('turneditingoff')) &&
+                !$this->getSession()->getPage()->findButton(get_string('turneditingon'))) {
             return false;
         }
 
index 1d88c8d..59cc321 100644 (file)
@@ -28,11 +28,11 @@ Feature: Force group mode in a course
       | Group mode | Separate groups |
       | Force group mode | Yes |
     When I press "Save changes"
-    Then ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
-    And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
-    And I click on "//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
-    And ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
-    And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
+    Then "//a/child::img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
+    And "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
+    And I click on "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+    And "//a/child::img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists
+    And "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists
 
   @javascript
   Scenario: Forced group mode using visible groups
@@ -40,11 +40,11 @@ Feature: Force group mode in a course
       | Group mode | Visible groups |
       | Force group mode | Yes |
     And I press "Save changes"
-    Then ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
-    And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
-    And I click on "//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
-    And ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
-    And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
+    Then "//a/child::img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
+    And "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
+    And I click on "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+    And "//a/child::img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists
+    And "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists
 
   @javascript
   Scenario: Forced group mode without groups
@@ -52,9 +52,9 @@ Feature: Force group mode in a course
       | Group mode | No groups |
       | Force group mode | Yes |
     And I press "Save changes"
-    Then ".//a//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should not exists
-    And ".//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should exists
-    And I click on "//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
-    And ".//a//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should not exists
-    And ".//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should exists
+    Then "//a/child::img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should not exists
+    And "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should exists
+    And I click on "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element"
+    And "//a/child::img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should not exists
+    And "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should exists
 
diff --git a/enrol/category/classes/observer.php b/enrol/category/classes/observer.php
new file mode 100644 (file)
index 0000000..2b8a8d7
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Local stuff for category enrolment plugin.
+ *
+ * @package    enrol_category
+ * @copyright  2010 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Event handler for category enrolment plugin.
+ *
+ * We try to keep everything in sync via listening to events,
+ * it may fail sometimes, so we always do a full sync in cron too.
+ */
+class enrol_category_observer {
+    /**
+     * Triggered when user is assigned a new role.
+     *
+     * @param \core\event\role_assigned $event
+     */
+    public static function role_assigned(\core\event\role_assigned $event) {
+        global $DB;
+
+        if (!enrol_is_enabled('category')) {
+            return;
+        }
+
+        $ra = new stdClass();
+        $ra->roleid = $event->objectid;
+        $ra->userid = $event->relateduserid;
+        $ra->contextid = $event->contextid;
+
+        //only category level roles are interesting
+        $parentcontext = context::instance_by_id($ra->contextid);
+        if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
+            return;
+        }
+
+        // Make sure the role is to be actually synchronised,
+        // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
+        $syscontext = context_system::instance();
+        if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
+            return;
+        }
+
+        // Add necessary enrol instances.
+        $plugin = enrol_get_plugin('category');
+        $sql = "SELECT c.*
+                  FROM {course} c
+                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+             LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+                 WHERE e.id IS NULL";
+        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
+        $rs = $DB->get_recordset_sql($sql, $params);
+        foreach ($rs as $course) {
+            $plugin->add_instance($course);
+        }
+        $rs->close();
+
+        // Now look for missing enrolments.
+        $sql = "SELECT e.*
+                  FROM {course} c
+                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+                  JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+             LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
+                 WHERE ue.id IS NULL";
+        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
+        $rs = $DB->get_recordset_sql($sql, $params);
+        foreach ($rs as $instance) {
+            $plugin->enrol_user($instance, $ra->userid, null, time());
+        }
+        $rs->close();
+    }
+
+    /**
+     * Triggered when user role is unassigned.
+     *
+     * @param \core\event\role_unassigned $event
+     */
+    public static function role_unassigned(\core\event\role_unassigned $event) {
+        global $DB;
+
+        if (!enrol_is_enabled('category')) {
+            return;
+        }
+
+        $ra = new stdClass();
+        $ra->userid = $event->relateduserid;
+        $ra->contextid = $event->contextid;
+
+        // only category level roles are interesting
+        $parentcontext = context::instance_by_id($ra->contextid);
+        if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
+            return;
+        }
+
+        // Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
+        $syscontext = context_system::instance();
+        if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
+            return;
+        }
+
+        $plugin = enrol_get_plugin('category');
+
+        $sql = "SELECT e.*
+                  FROM {course} c
+                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+                  JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
+        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
+        $rs = $DB->get_recordset_sql($sql, $params);
+
+        list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
+        $params['userid'] = $ra->userid;
+
+        foreach ($rs as $instance) {
+            $coursecontext = context_course::instance($instance->courseid);
+            $contextids = $coursecontext->get_parent_context_ids();
+            array_pop($contextids); // Remove system context, we are interested in categories only.
+
+            list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
+            $params = array_merge($params, $contextparams);
+
+            $sql = "SELECT ra.id
+                      FROM {role_assignments} ra
+                     WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
+            if (!$DB->record_exists_sql($sql, $params)) {
+                // User does not have any interesting role in any parent context, let's unenrol.
+                $plugin->unenrol_user($instance, $ra->userid);
+            }
+        }
+        $rs->close();
+    }
+}
index 3a67866..1fea12c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-/* List of handlers */
-$handlers = array (
-    'role_assigned' => array (
-        'handlerfile'      => '/enrol/category/locallib.php',
-        'handlerfunction'  => array('enrol_category_handler', 'role_assigned'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+$observers = array (
+
+    array (
+        'eventname' => '\core\event\role_assigned',
+        'callback'  => 'enrol_category_observer::role_assigned',
     ),
 
-    'role_unassigned' => array (
-        'handlerfile'      => '/enrol/category/locallib.php',
-        'handlerfunction'  => array('enrol_category_handler', 'role_unassigned'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array (
+        'eventname' => '\core\event\role_unassigned',
+        'callback'  => 'enrol_category_observer::role_unassigned',
     ),
 
-);
\ No newline at end of file
+);
index 4240276..404ff8f 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-
-/**
- * Event handler for category enrolment plugin.
- *
- * We try to keep everything in sync via listening to events,
- * it may fail sometimes, so we always do a full sync in cron too.
- */
-class enrol_category_handler {
-    /**
-     * Triggered when user is assigned a new role.
-     * @static
-     * @param stdClass $ra
-     * @return bool
-     */
-    public static function role_assigned($ra) {
-        global $DB;
-
-        if (!enrol_is_enabled('category')) {
-            return true;
-        }
-
-        //only category level roles are interesting
-        $parentcontext = context::instance_by_id($ra->contextid);
-        if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
-            return true;
-        }
-
-        // Make sure the role is to be actually synchronised,
-        // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
-        $syscontext = context_system::instance();
-        if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
-            return true;
-        }
-
-        // Add necessary enrol instances.
-        $plugin = enrol_get_plugin('category');
-        $sql = "SELECT c.*
-                  FROM {course} c
-                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
-             LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
-                 WHERE e.id IS NULL";
-        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
-        $rs = $DB->get_recordset_sql($sql, $params);
-        foreach ($rs as $course) {
-            $plugin->add_instance($course);
-        }
-        $rs->close();
-
-        // Now look for missing enrolments.
-        $sql = "SELECT e.*
-                  FROM {course} c
-                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
-                  JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
-             LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
-                 WHERE ue.id IS NULL";
-        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
-        $rs = $DB->get_recordset_sql($sql, $params);
-        foreach ($rs as $instance) {
-            $plugin->enrol_user($instance, $ra->userid, null, $ra->timemodified);
-        }
-        $rs->close();
-
-        return true;
-    }
-
-    /**
-     * Triggered when user role is unassigned.
-     * @static
-     * @param stdClass $ra
-     * @return bool
-     */
-    public static function role_unassigned($ra) {
-        global $DB;
-
-        if (!enrol_is_enabled('category')) {
-            return true;
-        }
-
-        // only category level roles are interesting
-        $parentcontext = context::instance_by_id($ra->contextid);
-        if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
-            return true;
-        }
-
-        // Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
-        $syscontext = context_system::instance();
-        if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
-            return true;
-        }
-
-        $plugin = enrol_get_plugin('category');
-
-        $sql = "SELECT e.*
-                  FROM {course} c
-                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
-                  JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
-                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
-        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
-        $rs = $DB->get_recordset_sql($sql, $params);
-
-        list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
-        $params['userid'] = $ra->userid;
-
-        foreach ($rs as $instance) {
-            $coursecontext = context_course::instance($instance->courseid);
-            $contextids = $coursecontext->get_parent_context_ids();
-            array_pop($contextids); // Remove system context, we are interested in categories only.
-
-            list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
-            $params = array_merge($params, $contextparams);
-
-            $sql = "SELECT ra.id
-                      FROM {role_assignments} ra
-                     WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
-            if (!$DB->record_exists_sql($sql, $params)) {
-                // User does not have any interesting role in any parent context, let's unenrol.
-                $plugin->unenrol_user($instance, $ra->userid);
-            }
-        }
-        $rs->close();
-
-        return true;
-    }
-}
-
 /**
  * Sync all category enrolments in one course
  * @param stdClass $course
similarity index 98%
rename from enrol/category/tests/sync_test.php
rename to enrol/category/tests/plugin_test.php
index ca8d4cd..3d9ab46 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-global $CFG;
-require_once($CFG->dirroot.'/enrol/category/locallib.php');
-
-class enrol_category_testcase extends advanced_testcase {
+class enrol_category_plugin_testcase extends advanced_testcase {
 
     protected function enable_plugin() {
         $enabled = enrol_get_plugins(true);
@@ -107,7 +104,8 @@ class enrol_category_testcase extends advanced_testcase {
     }
 
     public function test_handler_sync() {
-        global $DB;
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/enrol/category/locallib.php');
 
         $this->resetAfterTest();
 
@@ -179,7 +177,8 @@ class enrol_category_testcase extends advanced_testcase {
     }
 
     public function test_sync_course() {
-        global $DB;
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/enrol/category/locallib.php');
 
         $this->resetAfterTest();
 
@@ -272,7 +271,8 @@ class enrol_category_testcase extends advanced_testcase {
     }
 
     public function test_sync_full() {
-        global $DB;
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/enrol/category/locallib.php');
 
         $this->resetAfterTest();
 
index 53e1c13..582828e 100644 (file)
@@ -197,7 +197,7 @@ class core_enrol_external extends external_api {
                     'users' => new external_multiple_structure(
                         new external_single_structure(
                 array(
-                    'id'    => new external_value(PARAM_NUMBER, 'ID of the user'),
+                    'id'    => new external_value(PARAM_INT, 'ID of the user'),
                     'username'    => new external_value(PARAM_RAW, 'Username', VALUE_OPTIONAL),
                     'firstname'   => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL),
                     'lastname'    => new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL),
index 5c5d466..65444bd 100644 (file)
@@ -183,7 +183,11 @@ class enrol_guest_plugin extends enrol_plugin {
         }
 
         $header = $this->get_instance_name($instance);
-        $config = has_capability('enrol/guest:config', $context);
+        if (!$i) {
+            $config = guess_if_creator_will_have_course_capability('enrol/guest:config', $context);
+        } else {
+            $config = has_capability('enrol/guest:config', $context);
+        }
 
         $mform->addElement('header', 'enrol_guest_header_'.$i, $header);
 
@@ -196,12 +200,27 @@ class enrol_guest_plugin extends enrol_plugin {
         $mform->setAdvanced('enrol_guest_status_'.$i, $this->get_config('status_adv'));
         if (!$config) {
             $mform->hardFreeze('enrol_guest_status_'.$i);
+            if (!$i) {
+                $mform->setConstant('enrol_guest_status_'.$i, $this->get_config('status'));
+            } else {
+                $mform->setConstant('enrol_guest_status_'.$i, $instance->status);
+            }
         }
 
         $mform->addElement('passwordunmask', 'enrol_guest_password_'.$i, get_string('password', 'enrol_guest'));
         $mform->addHelpButton('enrol_guest_password_'.$i, 'password', 'enrol_guest');
         if (!$config) {
             $mform->hardFreeze('enrol_guest_password_'.$i);
+            if (!$i) {
+                if ($this->get_config('requirepassword')) {
+                    $password = generate_password(20);
+                } else {
+                    $password = '';
+                }
+                $mform->setConstant('enrol_guest_password_'.$i, $password);
+            } else {
+                $mform->setConstant('enrol_guest_password_'.$i, $instance->password);
+            }
         } else {
             $mform->disabledIf('enrol_guest_password_'.$i, 'enrol_guest_status_'.$i, 'noteq', ENROL_INSTANCE_ENABLED);
         }
@@ -279,57 +298,46 @@ class enrol_guest_plugin extends enrol_plugin {
     public function course_updated($inserted, $course, $data) {
         global $DB;
 
-        $context = context_course::instance($course->id);
-
-        if (has_capability('enrol/guest:config', $context)) {
-            if ($inserted) {
-                if (isset($data->enrol_guest_status_0)) {
-                    $fields = array('status'=>$data->enrol_guest_status_0);
-                    if ($fields['status'] == ENROL_INSTANCE_ENABLED) {
-                        $fields['password'] = $data->enrol_guest_password_0;
-                    } else {
-                        if ($this->get_config('requirepassword')) {
-                            $fields['password'] = generate_password(20);
-                        }
-                    }
-                    $this->add_instance($course, $fields);
+        if ($inserted) {
+            if (isset($data->enrol_guest_status_0)) {
+                $fields = array('status'=>$data->enrol_guest_status_0);
+                if ($fields['status'] == ENROL_INSTANCE_ENABLED) {
+                    $fields['password'] = $data->enrol_guest_password_0;
                 } else {
-                    if ($this->get_config('defaultenrol')) {
-                        $this->add_default_instance($course);
+                    if ($this->get_config('requirepassword')) {
+                        $fields['password'] = generate_password(20);
                     }
                 }
+                $this->add_instance($course, $fields);
             } else {
-                $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'guest'));
-                foreach ($instances as $instance) {
-                    $i = $instance->id;
-
-                    if (isset($data->{'enrol_guest_status_'.$i})) {
-                        $reset = ($instance->status != $data->{'enrol_guest_status_'.$i});
-
-                        $instance->status       = $data->{'enrol_guest_status_'.$i};
-                        $instance->timemodified = time();
-                        if ($instance->status == ENROL_INSTANCE_ENABLED) {
-                            if ($instance->password !== $data->{'enrol_guest_password_'.$i}) {
-                                $reset = true;
-                            }
-                            $instance->password = $data->{'enrol_guest_password_'.$i};
-                        }
-                        $DB->update_record('enrol', $instance);
-
-                        if ($reset) {
-                            $context->mark_dirty();
-                        }
-                    }
+                if ($this->get_config('defaultenrol')) {
+                    $this->add_default_instance($course);
                 }
             }
 
         } else {
-            if ($inserted) {
-                if ($this->get_config('defaultenrol')) {
-                    $this->add_default_instance($course);
+            $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'guest'));
+            foreach ($instances as $instance) {
+                $i = $instance->id;
+
+                if (isset($data->{'enrol_guest_status_'.$i})) {
+                    $reset = ($instance->status != $data->{'enrol_guest_status_'.$i});
+
+                    $instance->status       = $data->{'enrol_guest_status_'.$i};
+                    $instance->timemodified = time();
+                    if ($instance->status == ENROL_INSTANCE_ENABLED) {
+                        if ($instance->password !== $data->{'enrol_guest_password_'.$i}) {
+                            $reset = true;
+                        }
+                        $instance->password = $data->{'enrol_guest_password_'.$i};
+                    }
+                    $DB->update_record('enrol', $instance);
+
+                    if ($reset) {
+                        $context = context_course::instance($course->id);
+                        $context->mark_dirty();
+                    }
                 }
-            } else {
-                // bad luck, user can not change anything
             }
         }
     }
index 0b18d78..7e751d5 100644 (file)
@@ -50,11 +50,11 @@ class behat_enrol extends behat_base {
     public function i_add_enrolment_method_with($enrolmethod, TableNode $table) {
 
         return array(
-            new Given('I expand "Users" node'),
-            new Given('I follow "Enrolment methods"'),
-            new Given('I select "' . $enrolmethod . '" from "Add method"'),
+            new Given('I expand "' . get_string('users', 'admin') . '" node'),
+            new Given('I follow "' . get_string('type_enrol_plural', 'plugin') . '"'),
+            new Given('I select "' . $this->escape($enrolmethod) . '" from "' . get_string('addinstance', 'enrol') . '"'),
             new Given('I fill the moodle form with:', $table),
-            new Given('I press "Add method"')
+            new Given('I press "' . get_string('addinstance', 'enrol') . '"')
         );
     }
 
index 6b606a4..dc6d2c0 100644 (file)
@@ -6,6 +6,9 @@ information provided here is intended especially for developers.
 * Enrolment plugin which supports self enrolment should implement can_self_enrol()
 * Enrolment plugin should implement get_enrol_info() to expose instance information
   with webservice or external interface.
+* Webservice core_enrol_get_enrolled_users_with_capability was incorrectly specifing
+  float as the return type for user id. int is the actual returned type and is now
+  reported as such.
 
 === 2.5 ===
 
index c5292ba..942b19b 100644 (file)
@@ -820,10 +820,9 @@ class gradingform_guide_instance extends gradingform_instance {
     /**
      * Calculates the grade to be pushed to the gradebook
      *
-     * @return int the valid grade from $this->get_controller()->get_grade_range()
+     * @return float|int the valid grade from $this->get_controller()->get_grade_range()
      */
     public function get_grade() {
-        global $DB, $USER;
         $grade = $this->get_guide_filling();
 
         if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) {
@@ -842,8 +841,12 @@ class gradingform_guide_instance extends gradingform_instance {
         foreach ($grade['criteria'] as $record) {
             $curscore += $record['score'];
         }
-        return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*
-            ($maxgrade-$mingrade), 0) + $mingrade;
+        $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*
+            ($maxgrade-$mingrade);
+        if ($this->get_controller()->get_allow_grade_decimals()) {
+            return $gradeoffset + $mingrade;
+        }
+        return round($gradeoffset, 0) + $mingrade;
     }
 
     /**
index 6d92a4f..445452d 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component  = 'gradingform_guide';
-$plugin->version    = 2013050100;
-$plugin->requires   = 2013050100;
+$plugin->version    = 2013071900.02;
+$plugin->requires   = 2013071900.02;
 $plugin->maturity   = MATURITY_STABLE;
\ No newline at end of file
index f28c516..666d0d9 100644 (file)
@@ -73,6 +73,9 @@ abstract class gradingform_controller {
     /** @var array graderange array of valid grades for this area. Use set_grade_range and get_grade_range to access this */
     private $graderange = null;
 
+    /** @var bool if decimal values are allowed as grades. */
+    private $allowgradedecimals = false;
+
     /** @var boolean|null cached result of function has_active_instances() */
     protected $hasactiveinstances = null;
 
@@ -622,13 +625,23 @@ abstract class gradingform_controller {
 
     /**
      * Sets the range of grades used in this area. This is usually either range like 0-100
-     * or the scale where keys start from 1. Typical use:
-     * $controller->set_grade_range(make_grades_menu($gradingtype));
+     * or the scale where keys start from 1.
+     *
+     * Typically modules will call it:
+     * $controller->set_grade_range(make_grades_menu($gradingtype), $gradingtype > 0);
+     * Negative $gradingtype means that scale is used and the grade must be rounded
+     * to the nearest int. Positive $gradingtype means that range 0..$gradingtype
+     * is used for the grades and in this case grade does not have to be rounded.
+     *
+     * Sometimes modules always expect grade to be rounded (like mod_assignment does).
      *
-     * @param array $graderange
+     * @param array $graderange array where first _key_ is the minimum grade and the
+     *     last key is the maximum grade.
+     * @param bool $allowgradedecimals if decimal values are allowed as grades.
      */
-    public final function set_grade_range(array $graderange) {
+    public final function set_grade_range(array $graderange, $allowgradedecimals = false) {
         $this->graderange = $graderange;
+        $this->allowgradedecimals = $allowgradedecimals;
     }
 
     /**
@@ -643,6 +656,15 @@ abstract class gradingform_controller {
         return $this->graderange;
     }
 
+    /**
+     * Returns if decimal values are allowed as grades
+     *
+     * @return bool
+     */
+    public final function get_allow_grade_decimals() {
+        return $this->allowgradedecimals;
+    }
+
     /**
      * Overridden by sub classes that wish to make definition details available to web services.
      * When not overridden, only definition data common to all grading methods is made available.
@@ -866,7 +888,11 @@ abstract class gradingform_instance {
     /**
      * Calculates the grade to be pushed to the gradebook
      *
-     * @return int the valid grade from $this->get_controller()->get_grade_range()
+     * Returned grade must be in range $this->get_controller()->get_grade_range()
+     * Plugins must returned grade converted to int unless
+     * $this->get_controller()->get_allow_grade_decimals() is true.
+     *
+     * @return float|int
      */
     abstract public function get_grade();
 
index ae4f5d5..39e6adc 100644 (file)
@@ -814,10 +814,9 @@ class gradingform_rubric_instance extends gradingform_instance {
     /**
      * Calculates the grade to be pushed to the gradebook
      *
-     * @return int the valid grade from $this->get_controller()->get_grade_range()
+     * @return float|int the valid grade from $this->get_controller()->get_grade_range()
      */
     public function get_grade() {
-        global $DB, $USER;
         $grade = $this->get_rubric_filling();
 
         if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) {
@@ -836,7 +835,11 @@ class gradingform_rubric_instance extends gradingform_instance {
         foreach ($grade['criteria'] as $id => $record) {
             $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score'];
         }
-        return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade), 0) + $mingrade;
+        $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade);
+        if ($this->get_controller()->get_allow_grade_decimals()) {
+            return $gradeoffset + $mingrade;
+        }
+        return round($gradeoffset, 0) + $mingrade;
     }
 
     /**
index 50a6b37..35b22bf 100644 (file)
@@ -80,7 +80,7 @@
 
 .gradingform_rubric .plainvalue.empty {font-style: italic; color: #AAA;}
 
-.gradingform_rubric.editor .criterion .levels .level .delete {position:absolute;right:0;bottom:0;}
+.gradingform_rubric.editor .criterion .levels .level .delete {position:absolute;right:0;}
 .gradingform_rubric .criterion .levels .level .score {font-style:italic;color:#575;font-weight: bold;margin-top:5px;white-space:nowrap;}
 .gradingform_rubric .criterion .levels .level .score .scorevalue {padding-right:5px;}
 
 .gradingform_rubric.editor .addcriterion input,
 .gradingform_rubric.editor .addlevel input {background: transparent url([[pix:t/add]]) no-repeat top left ;display:block;color:#555555;font-weight:bold;text-decoration:none;}
 .gradingform_rubric.editor .addcriterion input {background-position: 5px 8px;height:30px;line-height:29px;margin-bottom:14px;padding-left:20px;padding-right:10px;}
-.gradingform_rubric.editor .addlevel input {background-position: 5px 5px;height:25px;line-height:24px;margin-bottom:10px;padding-left:18px;padding-right:8px;}
+.gradingform_rubric.editor .addlevel input {background-position: 5px 5px;height:25px;line-height:24px;margin-bottom:45px;padding-left:18px;padding-right:8px;}
 
 .gradingform_rubric .options .optionsheading {font-weight:bold;font-size:1.1em;padding-bottom:5px;}
 .gradingform_rubric .options .option {padding-bottom:2px;}
index d695639..e17b074 100644 (file)
@@ -25,8 +25,8 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component  = 'gradingform_rubric';
-$plugin->version    = 2013050100;
+$plugin->version    = 2013071900.02;
 
-$plugin->requires   = 2013050100;
+$plugin->requires   = 2013071900.02;
 
 $plugin->maturity   = MATURITY_STABLE;
diff --git a/grade/grading/form/upgrade.txt b/grade/grading/form/upgrade.txt
new file mode 100644 (file)
index 0000000..21cc631
--- /dev/null
@@ -0,0 +1,9 @@
+This files describes API changes in /grade/grading/form/* - Advanced grading methods
+information provided here is intended especially for developers.
+
+=== 2.5.2 ===
+
+* Grading methods now can return grade with decimals. See API functions
+  gradingform_controller::set_grade_range() and
+  gradingform_controller::get_allow_grade_decimals(), and also examples
+  in gradingform_rubric_instance::get_grade().
index aae171e..5a61de4 100644 (file)
@@ -161,12 +161,16 @@ function grade_import_commit($courseid, $importcode, $importfeedback=true, $verb
  */
 function get_unenrolled_users_in_import($importcode, $courseid) {
     global $CFG, $DB;
-    $relatedctxcondition = get_related_contexts_string(context_course::instance($courseid));
 
-    //users with a gradeable role
+    $coursecontext = context_course::instance($courseid);
+
+    // We want to query both the current context and parent contexts.
+    list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+    // Users with a gradeable role.
     list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
 
-    //enrolled users
+    // Enrolled users.
     $context = context_course::instance($courseid);
     list($enrolledsql, $enrolledparams) = get_enrolled_sql($context);
     list($sort, $sortparams) = users_order_by_sql('u');
@@ -183,11 +187,11 @@ function get_unenrolled_users_in_import($importcode, $courseid) {
               LEFT JOIN ($enrolledsql) je
                         ON je.id = u.id
               LEFT JOIN {role_assignments} ra
-                        ON (giv.userid = ra.userid AND ra.roleid $gradebookrolessql AND ra.contextid $relatedctxcondition)
+                        ON (giv.userid = ra.userid AND ra.roleid $gradebookrolessql AND ra.contextid $relatedctxsql)
              WHERE giv.importcode = :importcode
                    AND (ra.id IS NULL OR je.id IS NULL)
           ORDER BY gradeidnumber, $sort";
-    $params = array_merge($gradebookrolesparams, $enrolledparams, $sortparams);
+    $params = array_merge($gradebookrolesparams, $enrolledparams, $sortparams, $relatedctxparams);
     $params['importcode'] = $importcode;
 
     return $DB->get_records_sql($sql, $params);
index c08ff66..3603ac4 100644 (file)
@@ -138,18 +138,17 @@ class graded_users_iterator {
         export_verify_grades($this->course->id);
         $course_item = grade_item::fetch_course_item($this->course->id);
         if ($course_item->needsupdate) {
-            // can not calculate all final grades - sorry
+            // Can not calculate all final grades - sorry.
             return false;
         }
 
         $coursecontext = context_course::instance($this->course->id);
-        $relatedcontexts = get_related_contexts_string($coursecontext);
 
-        list($gradebookroles_sql, $params) =
-            $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+        list($gradebookroles_sql, $params) = $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
         list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, '', 0, $this->onlyactive);
 
-        $params = array_merge($params, $enrolledparams);
+        $params = array_merge($params, $enrolledparams, $relatedctxparams);
 
         if ($this->groupid) {
             $groupsql = "INNER JOIN {groups_members} gm ON gm.userid = u.id";
@@ -162,7 +161,7 @@ class graded_users_iterator {
         }
 
         if (empty($this->sortfield1)) {
-            // we must do some sorting even if not specified
+            // We must do some sorting even if not specified.
             $ofields = ", u.id AS usrt";
             $order   = "usrt ASC";
 
@@ -174,8 +173,8 @@ class graded_users_iterator {
                 $order   .= ", usrt2 $this->sortorder2";
             }
             if ($this->sortfield1 != 'id' and $this->sortfield2 != 'id') {
-                // user order MUST be the same in both queries,
-                // must include the only unique user->id if not already present
+                // User order MUST be the same in both queries,
+                // must include the only unique user->id if not already present.
                 $ofields .= ", u.id AS usrt";
                 $order   .= ", usrt ASC";
             }
@@ -199,7 +198,6 @@ class graded_users_iterator {
             }
         }
 
-        // $params contents: gradebookroles and groupid (for $groupwheresql)
         $users_sql = "SELECT $userfields $ofields
                         FROM {user} u
                         JOIN ($enrolledsql) je ON je.id = u.id
@@ -208,7 +206,7 @@ class graded_users_iterator {
                                   SELECT DISTINCT ra.userid
                                     FROM {role_assignments} ra
                                    WHERE ra.roleid $gradebookroles_sql
-                                     AND ra.contextid $relatedcontexts
+                                     AND ra.contextid $relatedctxsql
                              ) rainner ON rainner.userid = u.id
                          WHERE u.deleted = 0
                              $groupwheresql
@@ -226,7 +224,6 @@ class graded_users_iterator {
             $itemids = array_keys($this->grade_items);
             list($itemidsql, $grades_params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED, 'items');
             $params = array_merge($params, $grades_params);
-            // $params contents: gradebookroles, enrolledparams, groupid (for $groupwheresql) and itemids
 
             $grades_sql = "SELECT g.* $ofields
                              FROM {grade_grades} g
@@ -237,7 +234,7 @@ class graded_users_iterator {
                                       SELECT DISTINCT ra.userid
                                         FROM {role_assignments} ra
                                        WHERE ra.roleid $gradebookroles_sql
-                                         AND ra.contextid $relatedcontexts
+                                         AND ra.contextid $relatedctxsql
                                   ) rainner ON rainner.userid = u.id
                               WHERE u.deleted = 0
                               AND g.itemid $itemidsql
@@ -418,7 +415,7 @@ function print_graded_users_selector($course, $actionpage, $userid=0, $groupid=0
 }
 
 function grade_get_graded_users_select($report, $course, $userid, $groupid, $includeall) {
-    global $USER;
+    global $USER, $CFG;
 
     if (is_null($userid)) {
         $userid = $USER->id;
index 5782621..1c6fd33 100644 (file)
@@ -226,6 +226,10 @@ class grade_report_grader extends grade_report {
                         $changedgrades = true;
 
                     } else if ($datatype === 'feedback') {
+                        // If quick grading is on, feedback needs to be compared without line breaks.
+                        if ($this->get_pref('quickgrading')) {
+                            $oldvalue->feedback = preg_replace("/\r\n|\r|\n/", "", $oldvalue->feedback);
+                        }
                         if (($oldvalue->feedback === $postedvalue) or ($oldvalue->feedback === NULL and empty($postedvalue))) {
                             continue;
                         }
@@ -394,24 +398,25 @@ class grade_report_grader extends grade_report {
             return;
         }
 
-        //limit to users with a gradeable role
+        // Limit to users with a gradeable role.
         list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
-        //limit to users with an active enrollment
+        // Limit to users with an active enrollment.
         list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
-        //fields we need from the user table
+        // Fields we need from the user table.
         $userfields = user_picture::fields('u', get_extra_user_fields($this->context));
 
-        $sortjoin = $sort = $params = null;
+        // We want to query both the current context and parent contexts.
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 
-        //if the user has clicked one of the sort asc/desc arrows
+        // If the user has clicked one of the sort asc/desc arrows.
         if (is_numeric($this->sortitemid)) {
-            $params = array_merge(array('gitemid'=>$this->sortitemid), $gradebookrolesparams, $this->groupwheresql_params, $enrolledparams);
+            $params = array_merge(array('gitemid' => $this->sortitemid), $gradebookrolesparams, $this->groupwheresql_params, $enrolledparams,
+                $relatedctxparams);
 
             $sortjoin = "LEFT JOIN {grade_grades} g ON g.userid = u.id AND g.itemid = $this->sortitemid";
             $sort = "g.finalgrade $this->sortorder";
-
         } else {
             $sortjoin = '';
             switch($this->sortitemid) {
@@ -430,7 +435,7 @@ class grade_report_grader extends grade_report {
                     break;
             }
 
-            $params = array_merge($gradebookrolesparams, $this->groupwheresql_params, $enrolledparams);
+            $params = array_merge($gradebookrolesparams, $this->groupwheresql_params, $enrolledparams, $relatedctxparams);
         }
 
         $sql = "SELECT $userfields
@@ -442,12 +447,11 @@ class grade_report_grader extends grade_report {
                            SELECT DISTINCT ra.userid
                              FROM {role_assignments} ra
                             WHERE ra.roleid IN ($this->gradebookroles)
-                              AND ra.contextid " . get_related_contexts_string($this->context) . "
+                              AND ra.contextid $relatedctxsql
                        ) rainner ON rainner.userid = u.id
                    AND u.deleted = 0
                    $this->groupwheresql
               ORDER BY $sort";
-
         $studentsperpage = $this->get_students_per_page();
         $this->users = $DB->get_records_sql($sql, $params, $studentsperpage * $this->page, $studentsperpage);
 
@@ -1273,56 +1277,46 @@ class grade_report_grader extends grade_report {
      * @return array Array of rows for the right part of the report
      */
     public function get_right_avg_row($rows=array(), $grouponly=false) {
-        global $CFG, $USER, $DB, $OUTPUT;
+        global $USER, $DB, $OUTPUT;
 
         if (!$this->canviewhidden) {
-            // totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
-            // better not show them at all if user can not see all hidden grades
+            // Totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
+            // better not show them at all if user can not see all hidden grades.
             return $rows;
         }
 
-        $showaverages = $this->get_pref('showaverages');
-        $showaveragesgroup = $this->currentgroup && $showaverages;
-
         $averagesdisplaytype   = $this->get_pref('averagesdisplaytype');
         $averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
         $meanselection         = $this->get_pref('meanselection');
         $shownumberofgrades    = $this->get_pref('shownumberofgrades');
 
-        $avghtml = '';
-        $avgcssclass = 'avg';
-
         if ($grouponly) {
-            $straverage = get_string('groupavg', 'grades');
             $showaverages = $this->currentgroup && $this->get_pref('showaverages');
             $groupsql = $this->groupsql;
             $groupwheresql = $this->groupwheresql;
             $groupwheresqlparams = $this->groupwheresql_params;
-            $avgcssclass = 'groupavg';
         } else {
-            $straverage = get_string('overallaverage', 'grades');
             $showaverages = $this->get_pref('showaverages');
             $groupsql = "";
             $groupwheresql = "";
             $groupwheresqlparams = array();
         }
 
-        if ($shownumberofgrades) {
-            $straverage .= ' (' . get_string('submissions', 'grades') . ') ';
-        }
+        if ($showaverages) {
+            $totalcount = $this->get_numusers($grouponly);
 
-        $totalcount = $this->get_numusers($grouponly);
+            // Limit to users with a gradeable role.
+            list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
-        //limit to users with a gradeable role
-        list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
+            // Limit to users with an active enrollment.
+            list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
-        //limit to users with an active enrollment
-        list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
+            // We want to query both the current context and parent contexts.
+            list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 
-        if ($showaverages) {
-            $params = array_merge(array('courseid'=>$this->courseid), $gradebookrolesparams, $enrolledparams, $groupwheresqlparams);
+            $params = array_merge(array('courseid' => $this->courseid), $gradebookrolesparams, $enrolledparams, $groupwheresqlparams, $relatedctxparams);
 
-            // find sums of all grade items in course
+            // Find sums of all grade items in course.
             $sql = "SELECT g.itemid, SUM(g.finalgrade) AS sum
                       FROM {grade_items} gi
                       JOIN {grade_grades} g ON g.itemid = gi.id
@@ -1332,7 +1326,7 @@ class grade_report_grader extends grade_report {
                                SELECT DISTINCT ra.userid
                                  FROM {role_assignments} ra
                                 WHERE ra.roleid $gradebookrolessql
-                                  AND ra.contextid " . get_related_contexts_string($this->context) . "
+                                  AND ra.contextid $relatedctxsql
                            ) rainner ON rainner.userid = u.id
                       $groupsql
                      WHERE gi.courseid = :courseid
@@ -1361,7 +1355,7 @@ class grade_report_grader extends grade_report {
                       $groupsql
                      WHERE gi.courseid = :courseid
                            AND ra.roleid $gradebookrolessql
-                           AND ra.contextid ".get_related_contexts_string($this->context)."
+                           AND ra.contextid $relatedctxsql
                            AND u.deleted = 0
                            AND g.id IS NULL
                            $groupwheresql
@@ -1399,8 +1393,6 @@ class grade_report_grader extends grade_report {
                     $meancount = $totalcount;
                 }
 
-                $decimalpoints = $item->get_decimals();
-
                 // Determine which display type to use for this average
                 if ($USER->gradeediting[$this->courseid]) {
                     $displaytype = GRADE_DISPLAY_TYPE_REAL;
index 38459d8..512a8c2 100644 (file)
@@ -271,18 +271,21 @@ abstract class grade_report {
      * @return int Count of users
      */
     public function get_numusers($groups=true) {
-        global $CFG, $DB;
+        global $DB;
 
         $groupsql      = "";
         $groupwheresql = "";
 
-        //limit to users with a gradeable role
+        // Limit to users with a gradeable role.
         list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
-        //limit to users with an active enrollment
+        // Limit to users with an active enrollment.
         list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
-        $params = array_merge($gradebookrolesparams, $enrolledparams);
+        // We want to query both the current context and parent contexts.
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+        $params = array_merge($gradebookrolesparams, $enrolledparams, $relatedctxparams);
 
         if ($groups) {
             $groupsql      = $this->groupsql;
@@ -300,7 +303,7 @@ abstract class grade_report {
                       WHERE ra.roleid $gradebookrolessql
                             AND u.deleted = 0
                             $groupwheresql
-                            AND ra.contextid ".get_related_contexts_string($this->context);
+                            AND ra.contextid $relatedctxsql";
         return $DB->count_records_sql($countsql, $params);
     }
 
index d9232ed..c5920ff 100644 (file)
@@ -611,42 +611,34 @@ class grade_report_user extends grade_report {
 
     /**
      * Builds the grade item averages.
-     *
      */
     function calculate_averages() {
         global $USER, $DB;
 
         if ($this->showaverage) {
-            // this settings are actually grader report settings (not user report)
+            // This settings are actually grader report settings (not user report)
             // however we're using them as having two separate but identical settings the
-            // user would have to keep in sync would be annoying
+            // user would have to keep in sync would be annoying.
             $averagesdisplaytype   = $this->get_pref('averagesdisplaytype');
             $averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
             $meanselection         = $this->get_pref('meanselection');
             $shownumberofgrades    = $this->get_pref('shownumberofgrades');
 
             $avghtml = '';
-            $avgcssclass = 'avg';
-
-            $straverage = get_string('overallaverage', 'grades');
-
             $groupsql = $this->groupsql;
             $groupwheresql = $this->groupwheresql;
-            //$groupwheresqlparams = ;
-
-            if ($shownumberofgrades) {
-                $straverage .= ' (' . get_string('submissions', 'grades') . ') ';
-            }
-
             $totalcount = $this->get_numusers(false);
 
-            //limit to users with a gradeable role ie students
+            // We want to query both the current context and parent contexts.
+            list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+            // Limit to users with a gradeable role ie students.
             list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
-            //limit to users with an active enrolment
+            // Limit to users with an active enrolment.
             list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
-            $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams);
+            $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams, $relatedctxparams);
             $params['courseid'] = $this->courseid;
 
             // find sums of all grade items in course
@@ -659,7 +651,7 @@ class grade_report_user extends grade_report {
                                    SELECT DISTINCT ra.userid
                                      FROM {role_assignments} ra
                                     WHERE ra.roleid $gradebookrolessql
-                                      AND ra.contextid " . get_related_contexts_string($this->context) . "
+                                      AND ra.contextid $relatedctxsql
                            ) rainner ON rainner.userid = u.id
                       $groupsql
                      WHERE gi.courseid = :courseid
@@ -690,7 +682,7 @@ class grade_report_user extends grade_report {
                                SELECT DISTINCT ra.userid
                                  FROM {role_assignments} ra
                                 WHERE ra.roleid $gradebookrolessql
-                                  AND ra.contextid " . get_related_contexts_string($this->context) . "
+                                  AND ra.contextid $relatedctxsql
                            ) rainner ON rainner.userid = u.id
                       LEFT JOIN {grade_grades} gg
                              ON (gg.itemid = gi.id AND gg.userid = u.id AND gg.finalgrade IS NOT NULL AND gg.hidden = 0)
@@ -731,8 +723,6 @@ class grade_report_user extends grade_report {
                     $mean_count = $totalcount;
                 }
 
-                $decimalpoints = $item->get_decimals();
-
                 // Determine which display type to use for this average
                 if (!empty($USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
                     $displaytype = GRADE_DISPLAY_TYPE_REAL;
@@ -747,7 +737,6 @@ class grade_report_user extends grade_report {
                 // Override grade_item setting if a display preference (not inherit) was set for the averages
                 if ($averagesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
                     $decimalpoints = $item->get_decimals();
-
                 } else {
                     $decimalpoints = $averagesdecimalpoints;
                 }
index 86d23f7..f3f2f7e 100644 (file)
@@ -684,16 +684,16 @@ function groups_get_potential_members($courseid, $roleid = null, $cohortid = nul
 
     $context = context_course::instance($courseid);
 
-    // we are looking for all users with this role assigned in this context or higher
-    $listofcontexts = get_related_contexts_string($context);
-
     list($esql, $params) = get_enrolled_sql($context);
 
     if ($roleid) {
-        $params['roleid'] = $roleid;
+        // We are looking for all users with this role assigned in this context or higher.
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+        $params = array_merge($params, $relatedctxparams, array('roleid' => $roleid));
         $where = "WHERE u.id IN (SELECT userid
                                    FROM {role_assignments}
-                                  WHERE roleid = :roleid AND contextid $listofcontexts)";
+                                  WHERE roleid = :roleid AND contextid $relatedctxsql)";
     } else {
         $where = "";
     }
@@ -812,12 +812,15 @@ function groups_unassign_grouping($groupingid, $groupid, $invalidatecache = true
  */
 function groups_get_members_by_role($groupid, $courseid, $fields='u.*',
         $sort=null, $extrawheretest='', $whereorsortparams=array()) {
-    global $CFG, $DB;
+    global $DB;
 
     // Retrieve information about all users and their roles on the course or
     // parent ('related') contexts
     $context = context_course::instance($courseid);
 
+    // We are looking for all users with this role assigned in this context or higher.
+    list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
     if ($extrawheretest) {
         $extrawheretest = ' AND ' . $extrawheretest;
     }
@@ -830,12 +833,12 @@ function groups_get_members_by_role($groupid, $courseid, $fields='u.*',
     $sql = "SELECT r.id AS roleid, u.id AS userid, $fields
               FROM {groups_members} gm
               JOIN {user} u ON u.id = gm.userid
-         LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid ".get_related_contexts_string($context).")
+         LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql)
          LEFT JOIN {role} r ON r.id = ra.roleid
              WHERE gm.groupid=:mgroupid
                    ".$extrawheretest."
           ORDER BY r.sortorder, $sort";
-    $whereorsortparams['mgroupid'] = $groupid;
+    $whereorsortparams = array_merge($whereorsortparams, $relatedctxparams, array('mgroupid' => $groupid));
     $rs = $DB->get_recordset_sql($sql, $whereorsortparams);
 
     return groups_calculate_role_people($rs, $context);
index 847727d..2afb6f3 100644 (file)
@@ -51,36 +51,39 @@ class behat_groups extends behat_base {
         global $DB;
 
         $user = $DB->get_record('user', array('username' => $username));
-        $userfullname = fullname($user);
+        $userfullname = $this->getSession()->getSelectorsHandler()->xpathLiteral(fullname($user));
+
+        // Using a xpath liternal to avoid problems with quotes and double quotes.
+        $groupname = $this->getSession()->getSelectorsHandler()->xpathLiteral($groupname);
 
         // We don't know the option text as it contains the number of users in the group.
         $select = $this->find_field('groups');
-        $xpath = "//select[@id='groups']/descendant::option[contains(., '" . $groupname . "')]";
+        $xpath = "//select[@id='groups']/descendant::option[contains(., $groupname)]";
         $groupoption = $this->find('xpath', $xpath);
         $fulloption = $groupoption->getText();
         $select->selectOption($fulloption);
 
         // Here we don't need to wait for the AJAX response.
-        $this->find_button('Add/remove users')->click();
+        $this->find_button(get_string('adduserstogroup', 'group'))->click();
 
         // Wait for add/remove members page to be loaded.
         $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
 
         // Getting the option and selecting it.
         $select = $this->find_field('addselect');
-        $xpath = "//select[@id='addselect']/descendant::option[contains(., '" . $userfullname . "')]";
+        $xpath = "//select[@id='addselect']/descendant::option[contains(., $userfullname)]";
         $memberoption = $this->find('xpath', $xpath);
         $fulloption = $memberoption->getText();
         $select->selectOption($fulloption);
 
         // Click add button.
-        $this->find_button('Add')->click();
+        $this->find_button(get_string('add'))->click();
 
         // Wait for the page to load.
         $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
 
         // Returning to the main groups page.
-        $this->find_button('Back to groups')->click();
+        $this->find_button(get_string('backtogroups', 'group'))->click();
     }
 
 }
index f591b00..412952b 100644 (file)
@@ -170,6 +170,7 @@ $CFG->httpswwwroot         = $CFG->wwwroot;
 $CFG->dataroot             = $config->dataroot;
 $CFG->tempdir              = $CFG->dataroot.'/temp';
 $CFG->cachedir             = $CFG->dataroot.'/cache';
+$CFG->localcachedir        = $CFG->dataroot.'/localcache';
 $CFG->admin                = $config->admin;
 $CFG->docroot              = 'http://docs.moodle.org';
 $CFG->langotherroot        = $CFG->dataroot.'/lang';
index 73d2a20..733a95f 100644 (file)
@@ -81,6 +81,8 @@ $string['errorminpasswordlower'] = 'Passwords must have at least {$a} lower case
 $string['errorminpasswordnonalphanum'] = 'Passwords must have at least {$a} non-alphanumeric character(s).';
 $string['errorminpasswordupper'] = 'Passwords must have at least {$a} upper case letter(s).';
 $string['errorpasswordupdate'] = 'Error updating password, password not changed';
+$string['event_user_loggedin'] = 'User has logged in';
+$string['event_user_loggedin_desc'] = 'User {$a} has logged in';
 $string['forcechangepassword'] = 'Force change password';
 $string['forcechangepasswordfirst_help'] = 'Force users to change password on their first login to Moodle.';
 $string['forcechangepassword_help'] = 'Force users to change password on their next login to Moodle.';
index 437c5dd..9e5b147 100644 (file)
@@ -198,6 +198,7 @@ $string['donotaward'] = 'Currently, this badge is not active, so it cannot be aw
 $string['editsettings'] = 'Edit settings';
 $string['enablebadges'] = 'Enable badges';
 $string['error:backpacknotavailable'] = 'Your site is not accessible from the Internet, so any badges issued from this site cannot be verified by external backpack services.';
+$string['error:backpackproblem'] = 'There was a problem connecting to your backpack service provider. Please try again later.';
 $string['error:cannotact'] = 'Cannot activate the badge. ';
 $string['error:cannotawardbadge'] = 'Cannot award badge to a user.';
 $string['error:clone'] = 'Cannot clone the badge.';
index 72e913d..be7bb40 100644 (file)
@@ -47,6 +47,7 @@ $string['cachedef_eventinvalidation'] = 'Event invalidation';
 $string['cachedef_groupdata'] = 'Course group information';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_locking'] = 'Locking';
+$string['cachedef_observers'] = 'Event observers';
 $string['cachedef_plugininfo_base'] = 'Plugin info - base';
 $string['cachedef_plugininfo_block'] = 'Plugin info - blocks';
 $string['cachedef_plugininfo_filter'] = 'Plugin info - filters';
index ed50168..0d31a88 100644 (file)
@@ -545,6 +545,62 @@ function has_all_capabilities(array $capabilities, context $context, $user = nul
     return true;
 }
 
+/**
+ * Is course creator going to have capability in a new course?
+ *
+ * This is intended to be used in enrolment plugins before or during course creation,
+ * do not use after the course is fully created.
+ *
+ * @category access
+ *
+ * @param string $capability the name of the capability to check.
+ * @param context $context course or category context where is course going to be created
+ * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
+ * @return boolean true if the user will have this capability.
+ *
+ * @throws coding_exception if different type of context submitted
+ */
+function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
+    global $CFG;
+
+    if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
+        throw new coding_exception('Only course or course category context expected');
+    }
+
+    if (has_capability($capability, $context, $user)) {
+        // User already has the capability, it could be only removed if CAP_PROHIBIT
+        // was involved here, but we ignore that.
+        return true;
+    }
+
+    if (!has_capability('moodle/course:create', $context, $user)) {
+        return false;
+    }
+
+    if (!enrol_is_enabled('manual')) {
+        return false;
+    }
+
+    if (empty($CFG->creatornewroleid)) {
+        return false;
+    }
+
+    if ($context->contextlevel == CONTEXT_COURSE) {
+        if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
+            return false;
+        }
+    } else {
+        if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
+            return false;
+        }
+    }
+
+    // Most likely they will be enrolled after the course creation is finished,
+    // does the new role have the required capability?
+    list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
+    return isset($neededroles[$CFG->creatornewroleid]);
+}
+
 /**
  * Check if the user is an admin at the site level.
  *
@@ -1685,7 +1741,11 @@ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0,
         reload_all_capabilities();
     }
 
-    events_trigger('role_assigned', $ra);
+    $event = \core\event\role_assigned::create(
+        array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+            'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+    $event->add_record_snapshot('role_assignments', $ra);
+    $event->trigger();
 
     return $ra->id;
 }
@@ -1769,8 +1829,12 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual =
             if (!empty($USER->id) && $USER->id == $ra->userid) {
                 reload_all_capabilities();
             }
+            $event = \core\event\role_unassigned::create(
+                array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+                    'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+            $event->add_record_snapshot('role_assignments', $ra);
+            $event->trigger();
         }
-        events_trigger('role_unassigned', $ra);
     }
     unset($ras);
 
@@ -1796,7 +1860,11 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual =
                     if (!empty($USER->id) && $USER->id == $ra->userid) {
                         reload_all_capabilities();
                     }
-                    events_trigger('role_unassigned', $ra);
+                    $event = \core\event\role_unassigned::create(
+                        array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+                            'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+                    $event->add_record_snapshot('role_assignments', $ra);
+                    $event->trigger();
                 }
             }
         }
@@ -6901,7 +6969,7 @@ class context_module extends context {
      * Is this context part of any course? If yes return course context.
      *
      * @param bool $strict true means throw exception if not found, false means return false if not found
-     * @return course_context context of the enclosing course, null if not found or exception
+     * @return context_course context of the enclosing course, null if not found or exception
      */
     public function get_course_context($strict = true) {
         return $this->get_parent_context();
@@ -7264,24 +7332,6 @@ function get_sorted_contexts($select, $params = array()) {
             ", $params);
 }
 
-/**
- * Gets a string for sql calls, searching for stuff in this context or above
- *
- * NOTE: use $DB->get_in_or_equal($context->get_parent_context_ids()...
- *
- * @deprecated since 2.2, $context->use get_parent_context_ids() instead
- * @param context $context
- * @return string
- */
-function get_related_contexts_string(context $context) {
-
-    if ($parents = $context->get_parent_context_ids()) {
-        return (' IN ('.$context->id.','.implode(',', $parents).')');
-    } else {
-        return (' ='.$context->id);
-    }
-}
-
 /**
  * Given context and array of users, returns array of users whose enrolment status is suspended,
  * or enrolment has expired or has not started. Also removes those users from the given array
index fd8057e..5c7af4a 100644 (file)
@@ -605,6 +605,11 @@ function login_is_lockedout($user) {
 function login_attempt_valid($user) {
     global $CFG;
 
+    $event = \core_auth\event\user_loggedin::create(array('objectid' => $user->id,
+        'other' => array('username' => $user->username)));
+    $event->add_record_snapshot('user', $user);
+    $event->trigger();
+
     if ($user->mnethostid != $CFG->mnet_localhost_id) {
         return;
     }
index 8d879f7..1cff7fd 100644 (file)
@@ -1345,3 +1345,19 @@ function badges_handle_course_deletion($courseid) {
         $DB->update_record('badge', $toupdate);
     }
 }
+
+/**
+ * Loads JS files required for backpack support.
+ *
+ * @uses   $CFG, $PAGE
+ * @return void
+ */
+function badges_setup_backpack_js() {
+    global $CFG, $PAGE;
+    if (!empty($CFG->badges_allowexternalbackpack)) {
+        $PAGE->requires->string_for_js('error:backpackproblem', 'badges');
+        $protocol = (strpos($CFG->wwwroot, 'https://') === 0) ? 'https://' : 'http://';
+        $PAGE->requires->js(new moodle_url($protocol . 'backpack.openbadges.org/issuer.js'), true);
+        $PAGE->requires->js('/badges/backpack.js', true);
+    }
+}
index 4e8b7d1..9d12a3a 100644 (file)
@@ -64,10 +64,12 @@ class behat_files extends behat_base {
         $exception = new ExpectationException('"' . $filepickerelement . '" filepicker can not be found', $this->getSession());
 
         // Gets the ffilemanager node specified by the locator which contains the filepicker container.
+        $filepickerelement = $this->getSession()->getSelectorsHandler()->xpathLiteral($filepickerelement);
         $filepickercontainer = $this->find(
             'xpath',
-            "//input[./@id = //label[contains(normalize-space(string(.)), '" . $filepickerelement . "')]/@for]
-//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ') or contains(concat(' ', normalize-space(@class), ' '), ' ffilepicker ')]",
+            "//input[./@id = //label[normalize-space(.)=$filepickerelement]/@for]" .
+                "//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ') or " .
+                "contains(concat(' ', normalize-space(@class), ' '), ' ffilepicker ')]",
             $exception
         );
 
@@ -117,17 +119,20 @@ class behat_files extends behat_base {
 
         $exception = new ExpectationException($exceptionmsg, $this->getSession());
 
+        // Avoid quote-related problems.
+        $name = $this->getSession()->getSelectorsHandler()->xpathLiteral($name);
+
         // Get a filepicker element (folder or file).
         try {
 
             // First we look at the folder as we need to click on the contextual menu otherwise it would be opened.
             $node = $this->find(
                 'xpath',
-                "//div[@class='fp-content']
-//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]
-[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')][contains(normalize-space(string(.)), '" . $name . "')]
-//descendant::a[contains(concat(' ', normalize-space(@class), ' '), ' fp-contextmenu ')]
-",
+                "//div[@class='fp-content']" .
+                    "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
+                    "[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')]" .
+                    "[normalize-space(.)=$name]" .
+                    "//descendant::a[contains(concat(' ', normalize-space(@class), ' '), ' fp-contextmenu ')]",
                 $exception,
                 $containernode
             );
@@ -137,10 +142,10 @@ class behat_files extends behat_base {
             // Here the contextual menu is hidden, we click on the thumbnail.
             $node = $this->find(
                 'xpath',
-                "//div[@class='fp-content']
-//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')][contains(normalize-space(string(.)), '" . $name . "')]
-//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]
-",
+                "//div[@class='fp-content']" .
+                "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
+                "[normalize-space(.)=$name]" .
+                "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]",
                 false,
                 $containernode
             );
@@ -176,12 +181,15 @@ class behat_files extends behat_base {
         // Getting the repository link and opening it.
         $repoexception = new ExpectationException('The "' . $repositoryname . '" repository has not been found', $this->getSession());
 
+        // Avoid problems with both double and single quotes in the same string.
+        $repositoryname = $this->getSession()->getSelectorsHandler()->xpathLiteral($repositoryname);
+
         // Here we don't need to look inside the selected filepicker because there can only be one modal window.
         $repositorylink = $this->find(
             'xpath',
-            "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]
-//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]
-[contains(normalize-space(string(.)), '" . $repositoryname . "')]",
+            "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" .
+                "//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]" .
+                "[normalize-space(.)=$repositoryname]",
             $repoexception
         );
 
@@ -226,10 +234,10 @@ class behat_files extends behat_base {
         // only used when accessing the filepicker, there is no filemanager-loading after selecting the file.
         $this->find(
             'xpath',
-            "//div[contains(concat(' ', @class, ' '), ' filemanager ')]" .
-                "[not(contains(concat(' ', @class, ' '), ' fm-updating '))]" .
+            "//div[contains(concat(' ', normalize-space(@class), ' '), ' filemanager ')]" .
+                "[not(contains(concat(' ', normalize-space(@class), ' '), ' fm-updating '))]" .
             "|" .
-            "//div[contains(concat(' ', @class, ' '), ' filemanager-loading ')]" .
+            "//div[contains(concat(' ', normalize-space(@class), ' '), ' filemanager-loading ')]" .
                 "[contains(@style, 'display: none;')]",
             $exception,
             $filepickernode
index fbde571..5be8135 100644 (file)
@@ -58,10 +58,13 @@ class behat_form_select extends behat_form_field {
 
             // Single select needs an extra click in the option.
             if (!$this->field->hasAttribute('multiple')) {
+
+                $value = $this->session->getSelectorsHandler()->xpathLiteral($value);
+
                 // Using the driver direcly because Element methods are messy when dealing
                 // with elements inside containers.
                 $optionxpath = $this->field->getXpath() .
-                    "/descendant::option[(./@value = '" . $value . "' or contains(normalize-space(string(.)), '" . $value . "'))]";
+                    "/descendant::option[(./@value=$value or normalize-space(.)=$value)]";
                 $optionnodes = $this->session->getDriver()->find($optionxpath);
                 if ($optionnodes) {
                     current($optionnodes)->click();
index e35b0e6..559821e 100644 (file)
@@ -141,3 +141,35 @@ function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
     // Also use the internal error handler so we keep the usual behaviour.
     return false;
 }
+
+/**
+ * Restrict the config.php settings allowed.
+ *
+ * When running the behat features the config.php
+ * settings should not affect the results.
+ *
+ * @return void
+ */
+function behat_clean_init_config() {
+    global $CFG;
+
+    $allowed = array_flip(array(
+        'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
+        'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions',
+        'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 'proxybypass',
+        'theme'
+    ));
+
+    // Add extra allowed settings.
+    if (!empty($CFG->behat_extraallowedsettings)) {
+        $allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings));
+    }
+
+    // Also allowing behat_ prefixed attributes.
+    foreach ($CFG as $key => $value) {
+        if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) {
+            unset($CFG->{$key});
+        }
+    }
+
+}
index 195ec41..09a9205 100644 (file)
@@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die();
  */
 class core_component {
     /** @var array list of ignored directories - watch out for auth/db exception */
-    protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true);
+    protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, &