Merge branch 'install_master' of git://git.moodle.cz/moodle-install
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Fri, 10 May 2013 15:17:43 +0000 (17:17 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Fri, 10 May 2013 15:17:43 +0000 (17:17 +0200)
270 files changed:
admin/cli/install.php
admin/cli/upgrade.php
admin/index.php
admin/registration/forms.php
admin/registration/register.php
admin/registration/renderer.php
admin/tests/behat/behat_admin.php
admin/tests/behat/display_short_names.feature
admin/tests/behat/filter_users.feature
admin/tests/behat/upload_users.feature
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/behat/tests/behat/edit_permissions.feature
admin/tool/behat/tests/behat/test_environment.feature
admin/tool/installaddon/classes/installer.php
admin/tool/installaddon/deploy.php
admin/tool/installaddon/tests/installer_test.php
admin/tool/installaddon/version.php
auth/tests/behat/behat_auth.php
auth/tests/behat/login.feature
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.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/ui/tests/behat/duplicate_activities.feature
backup/util/ui/tests/behat/import_course.feature
backup/util/ui/tests/behat/restore_moodle2_courses.feature
badges/criteria/award_criteria_course.php
badges/criteria/award_criteria_courseset.php
badges/criteria_form.php
badges/criteria_settings.php
badges/renderer.php
blocks/comments/tests/behat/add_comment.feature
blocks/comments/tests/behat/behat_block_comments.php
blocks/comments/tests/behat/delete_comment.feature
blocks/tests/behat/configure_block_throughout_site.feature
blog/index.php
blog/lib.php
blog/tests/behat/comment.feature
cache/README.md
cache/admin.php
cache/classes/config.php
cache/classes/definition.php
cache/classes/helper.php
cache/disabledlib.php
cache/forms.php
cache/locallib.php
cache/renderer.php
cache/stores/file/lib.php
cache/stores/memcache/addinstanceform.php
cache/stores/memcache/lang/en/cachestore_memcache.php
cache/stores/memcache/lib.php
cache/stores/memcache/version.php
cache/stores/memcached/lib.php
cache/stores/session/lib.php
cache/stores/static/lib.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cache/tests/locallib_test.php
cohort/externallib.php [new file with mode: 0755]
cohort/tests/behat/add_cohort.feature
cohort/tests/behat/upload_cohort_users.feature
cohort/tests/externallib_test.php [new file with mode: 0755]
completion/criteria/completion_criteria_date.php
completion/criteria/completion_criteria_duration.php
completion/criteria/completion_criteria_grade.php
completion/criteria/completion_criteria_unenrol.php
completion/tests/behat/behat_completion.php
completion/tests/behat/enable_manual_complete_mark.feature
completion/tests/behat/restrict_section_availability.feature
composer.json
config-dist.php
course/completion.php
course/completion_form.php
course/format/lib.php
course/format/renderer.php
course/lib.php
course/manage.php
course/modlib.php
course/moodleform_mod.php
course/renderer.php
course/tests/behat/activities_group_icons.feature
course/tests/behat/activities_indentation.feature
course/tests/behat/activities_visibility_icons.feature
course/tests/behat/behat_course.php
course/tests/behat/course_controls.feature [new file with mode: 0644]
course/tests/behat/edit_settings.feature
course/tests/behat/force_group_mode.feature
course/tests/behat/max_number_sections.feature
course/tests/behat/move_activities.feature [new file with mode: 0644]
course/tests/behat/move_sections.feature [new file with mode: 0644]
course/tests/behat/paged_course_navigation.feature
course/tests/behat/rename_roles.feature
course/tests/behat/restrict_available_activities.feature
course/tests/behat/section_highlighting.feature
course/tests/behat/section_visibility.feature
course/tests/courselib_test.php
enrol/paypal/pix/icon.png [new file with mode: 0644]
enrol/paypal/pix/icon.svg [new file with mode: 0644]
enrol/self/tests/behat/self_enrolment.feature
grade/export/lib.php
grade/export/ods/index.php
grade/export/txt/index.php
grade/export/xls/index.php
grade/export/xml/index.php
grade/lib.php
grade/report/lib.php
grade/report/overview/lib.php
grade/report/upgrade.txt [new file with mode: 0644]
grade/report/user/lib.php
grade/tests/reportlib_test.php
group/tests/behat/create_groups.feature
group/tests/behat/id_uniqueness.feature
install.php
install/stringnames.txt
lang/en/admin.php
lang/en/cache.php
lang/en/completion.php
lang/en/error.php
lang/en/grades.php
lib/adminlib.php
lib/badgeslib.php
lib/behat/behat_base.php
lib/behat/classes/behat_command.php
lib/behat/classes/util.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_select.php
lib/behat/lib.php
lib/conditionlib.php
lib/coursecatlib.php
lib/db/caches.php
lib/db/services.php
lib/db/upgrade.php
lib/editor/tinymce/styles.css
lib/editor/tinymce/tests/behat/edit_available_icons.feature
lib/externallib.php
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/yui/dateselector/dateselector.js
lib/formslib.php
lib/grade/grade_item.php
lib/grade/tests/grade_item_test.php
lib/gradelib.php
lib/installlib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/sessionlib.php
lib/setup.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/conditionlib_test.php
lib/tests/coursecatlib_test.php
lib/tests/formslib_test.php
lib/tests/moodlelib_test.php
lib/upgradelib.php
lib/weblib.php
message/tests/behat/block_users.feature
message/tests/behat/manage_contacts.feature
message/tests/behat/search_history.feature
mod/assign/db/upgrade.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/tests/behat/file_submission.feature
mod/assign/tests/behat/group_submission.feature
mod/assign/tests/behat/prevent_submission_changes.feature
mod/choice/mod_form.php
mod/choice/tests/behat/add_choice.feature
mod/choice/tests/behat/publish_results.feature
mod/choice/tests/behat/publish_results_anonymously.feature
mod/feedback/edit_form.php
mod/feedback/item/feedback_item_form_class.php
mod/feedback/item/numeric/numeric_form.php
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/feedback/mod_form.php
mod/forum/mod_form.php
mod/forum/tests/behat/add_forum.feature
mod/forum/tests/behat/completion_condition_number_discussions.feature
mod/forum/tests/behat/discussion_display.feature
mod/forum/tests/behat/edit_post_student.feature
mod/forum/tests/behat/edit_post_teacher.feature
mod/forum/tests/behat/single_forum_discussion.feature
mod/forum/tests/behat/track_read_posts.feature
mod/glossary/mod_form.php
mod/glossary/tests/behat/entries_always_editable.feature
mod/glossary/tests/behat/prevent_duplicate_entries.feature
mod/glossary/tests/behat/print_friendly_version.feature
mod/glossary/tests/behat/search_entries.feature
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/multichoice.php
mod/lesson/tests/behat/date_availability.feature
mod/lesson/tests/behat/lesson_navigation.feature
mod/lesson/tests/behat/password_protection.feature
mod/lesson/tests/behat/time_limit.feature
mod/lti/locallib.php
mod/scorm/lang/en/scorm.php
mod/scorm/mod_form.php
mod/scorm/settings.php
mod/survey/tests/behat/survey_types.feature
mod/wiki/tests/behat/page_history.feature
mod/wiki/tests/behat/preview_page.feature
mod/wiki/tests/behat/wiki_formats.feature
mod/workshop/allocation/scheduled/version.php
mod/workshop/renderer.php
pix/i/badge.png
pix/i/badge.svg
pix/t/award.png
pix/t/award.svg
pix/t/lock.png
pix/t/lock.svg
pix/t/unlocked.png
pix/t/unlocked.svg
question/behaviour/adaptive/behaviour.php
question/behaviour/adaptive/tests/walkthrough_test.php
question/behaviour/manualgraded/db/install.php [new file with mode: 0644]
question/behaviour/manualgraded/db/upgrade.php [new file with mode: 0644]
question/behaviour/manualgraded/version.php
question/engine/tests/helpers.php
question/tests/behat/behat_question.php
question/type/calculated/edit_calculated_form.php
repository/lib.php
repository/recent/tests/behat/add_recent.feature
repository/tests/behat/behat_filepicker.php
repository/tests/behat/cancel_add_file.feature
repository/tests/behat/create_folders.feature
repository/tests/behat/delete_files.feature
repository/tests/behat/zip_and_unzip.feature
repository/upload/tests/behat/upload_file.feature
theme/afterburner/style/afterburner_styles.css
theme/anomaly/style/general.css
theme/arialist/style/core.css
theme/base/style/core.css
theme/base/style/course.css
theme/base/style/filemanager.css
theme/bootstrapbase/less/README
theme/bootstrapbase/less/editor.less
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/buttons.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/less/moodle/tabs.less [deleted file]
theme/bootstrapbase/less/moodle/undo.less
theme/bootstrapbase/renderers/core.php
theme/bootstrapbase/style/editor.css
theme/bootstrapbase/style/moodle.css
theme/boxxie/style/core.css
theme/brick/style/core.css
theme/canvas/style/course.css
theme/canvas/style/mods.css
theme/clean/style/custom.css
theme/formal_white/style/course.css
theme/formfactor/style/course.css
theme/fusion/style/core.css
theme/magazine/style/core.css
theme/mymobile/style/core.css
theme/splash/style/core.css
theme/standard/style/course.css
user/filters/date.php
version.php
webservice/renderer.php

index 8ea69e3..a84d556 100644 (file)
@@ -149,6 +149,7 @@ $CFG->httpswwwroot         = $CFG->wwwroot;
 $CFG->docroot              = 'http://docs.moodle.org';
 $CFG->running_installer    = true;
 $CFG->early_install_lang   = true;
+$CFG->ostype               = (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) ? 'WINDOWS' : 'UNIX';
 
 $parts = explode('/', str_replace('\\', '/', dirname(dirname(__FILE__))));
 $CFG->admin                = array_pop($parts);
index a6b36ba..ca6ef6a 100644 (file)
@@ -151,6 +151,12 @@ if ($interactive) {
 }
 
 if ($version > $CFG->version) {
+    // We purge all of MUC's caches here.
+    // Caches are disabled for upgrade by CACHE_DISABLE_ALL so we must set the first arg to true.
+    // This ensures a real config object is loaded and the stores will be purged.
+    // This is the only way we can purge custom caches such as memcache or APC.
+    // Note: all other calls to caches will still used the disabled API.
+    cache_helper::purge_all(true);
     upgrade_core($version, true);
 }
 set_config('release', $release);
index 329c79b..c54e838 100644 (file)
@@ -222,6 +222,13 @@ if ($cache) {
 }
 
 if ($version > $CFG->version) {  // upgrade
+    // We purge all of MUC's caches here.
+    // Caches are disabled for upgrade by CACHE_DISABLE_ALL so we must set the first arg to true.
+    // This ensures a real config object is loaded and the stores will be purged.
+    // This is the only way we can purge custom caches such as memcache or APC.
+    // Note: all other calls to caches will still used the disabled API.
+    cache_helper::purge_all(true);
+    // We then purge the regular caches.
     purge_all_caches();
 
     $PAGE->set_pagelayout('maintenance');
index 9e28f03..1f868d0 100644 (file)
@@ -244,9 +244,11 @@ class site_registration_form extends moodleform {
         $postsnumber = get_config('hub', 'site_postsnumber_' . $cleanhuburl);
         $questionsnumber = get_config('hub', 'site_questionsnumber_' . $cleanhuburl);
         $resourcesnumber = get_config('hub', 'site_resourcesnumber_' . $cleanhuburl);
-        $badges = get_config('hub', 'site_badges_' . $cleanhuburl);
-        $issuedbadges = get_config('hub', 'site_issuedbadges_' . $cleanhuburl);
+        $badgesnumber = get_config('hub', 'site_badges_' . $cleanhuburl);
+        $issuedbadgesnumber = get_config('hub', 'site_issuedbadges_' . $cleanhuburl);
         $mediancoursesize = get_config('hub', 'site_mediancoursesize_' . $cleanhuburl);
+        $participantnumberaveragecfg = get_config('hub', 'site_participantnumberaverage_' . $cleanhuburl);
+        $modulenumberaveragecfg = get_config('hub', 'site_modulenumberaverage_' . $cleanhuburl);
 
         //hidden parameters
         $mform->addElement('hidden', 'huburl', $huburl);
@@ -387,53 +389,53 @@ class site_registration_form extends moodleform {
         if (HUB_MOODLEORGHUBURL != $huburl) {
             $mform->addElement('checkbox', 'courses', get_string('sendfollowinginfo', 'hub'),
                     " " . get_string('coursesnumber', 'hub', $coursecount));
-            $mform->setDefault('courses', 1);
+            $mform->setDefault('courses', $coursesnumber != -1);
             $mform->setType('courses', PARAM_INT);
             $mform->addHelpButton('courses', 'sendfollowinginfo', 'hub');
 
             $mform->addElement('checkbox', 'users', '',
                     " " . get_string('usersnumber', 'hub', $usercount));
-            $mform->setDefault('users', 1);
+            $mform->setDefault('users', $usersnumber != -1);
             $mform->setType('users', PARAM_INT);
 
             $mform->addElement('checkbox', 'roleassignments', '',
                     " " . get_string('roleassignmentsnumber', 'hub', $roleassigncount));
-            $mform->setDefault('roleassignments', 1);
+            $mform->setDefault('roleassignments', $roleassignmentsnumber != -1);
             $mform->setType('roleassignments', PARAM_INT);
 
             $mform->addElement('checkbox', 'posts', '',
                     " " . get_string('postsnumber', 'hub', $postcount));
-            $mform->setDefault('posts', 1);
+            $mform->setDefault('posts', $postsnumber != -1);
             $mform->setType('posts', PARAM_INT);
 
             $mform->addElement('checkbox', 'questions', '',
                     " " . get_string('questionsnumber', 'hub', $questioncount));
-            $mform->setDefault('questions', 1);
+            $mform->setDefault('questions', $questionsnumber != -1);
             $mform->setType('questions', PARAM_INT);
 
             $mform->addElement('checkbox', 'resources', '',
                     " " . get_string('resourcesnumber', 'hub', $resourcecount));
-            $mform->setDefault('resources', 1);
+            $mform->setDefault('resources', $resourcesnumber != -1);
             $mform->setType('resources', PARAM_INT);
 
             $mform->addElement('checkbox', 'badges', '',
                     " " . get_string('badgesnumber', 'hub', $badges));
-            $mform->setDefault('badges', 1);
+            $mform->setDefault('badges', $badgesnumber != -1);
             $mform->setType('resources', PARAM_INT);
 
             $mform->addElement('checkbox', 'issuedbadges', '',
                     " " . get_string('issuedbadgesnumber', 'hub', $issuedbadges));
-            $mform->setDefault('issuedbadges', 1);
+            $mform->setDefault('issuedbadges', $issuedbadgesnumber != -1);
             $mform->setType('resources', PARAM_INT);
 
             $mform->addElement('checkbox', 'participantnumberaverage', '',
                     " " . get_string('participantnumberaverage', 'hub', $participantnumberaverage));
-            $mform->setDefault('participantnumberaverage', 1);
+            $mform->setDefault('participantnumberaverage', $participantnumberaveragecfg != -1);
             $mform->setType('participantnumberaverage', PARAM_FLOAT);
 
             $mform->addElement('checkbox', 'modulenumberaverage', '',
                     " " . get_string('modulenumberaverage', 'hub', $modulenumberaverage));
-            $mform->setDefault('modulenumberaverage', 1);
+            $mform->setDefault('modulenumberaverage', $modulenumberaveragecfg != -1);
             $mform->setType('modulenumberaverage', PARAM_FLOAT);
         } else {
             $mform->addElement('static', 'courseslabel', get_string('sendfollowinginfo', 'hub'),
index 760dd1f..1fd0e33 100644 (file)
@@ -62,7 +62,18 @@ $siteregistrationform = new site_registration_form('',
 $fromform = $siteregistrationform->get_data();
 
 if (!empty($fromform) and confirm_sesskey()) {
-    //save the settings
+
+    // Set to -1 all optional data marked as "don't send" by the admin.
+    // The function get_site_info() will not calculate the optional data if config is set to -1.
+    $inputnames = array('courses', 'users', 'roleassignments', 'posts', 'questions', 'resources',
+        'badges', 'issuedbadges', 'modulenumberaverage', 'participantnumberaverage');
+    foreach ($inputnames as $inputname) {
+        if (empty($fromform->{$inputname})) {
+            $fromform->{$inputname} = -1;
+        }
+    }
+
+    // Save the settings.
     $cleanhuburl = clean_param($huburl, PARAM_ALPHANUMEXT);
     set_config('site_name_' . $cleanhuburl, $fromform->name, 'hub');
     set_config('site_description_' . $cleanhuburl, $fromform->description, 'hub');
@@ -115,6 +126,21 @@ if ($update and confirm_sesskey()) {
 if (!empty($fromform) and empty($update) and confirm_sesskey()) {
 
     if (!empty($fromform) and confirm_sesskey()) { // if the register button has been clicked
+
+        // Retrieve the optional info (specially course number, user number, module number average...).
+        $siteinfo = $registrationmanager->get_site_info($huburl);
+        $fromform->courses = $siteinfo['courses'];
+        $fromform->users = $siteinfo['users'];
+        $fromform->enrolments = $siteinfo['enrolments'];
+        $fromform->posts = $siteinfo['posts'];
+        $fromform->questions = $siteinfo['questions'];
+        $fromform->resources = $siteinfo['resources'];
+        $fromform->badges = $siteinfo['badges'];
+        $fromform->issuedbadges = $siteinfo['issuedbadges'];
+        $fromform->modulenumberaverage = $siteinfo['modulenumberaverage'];
+        $fromform->participantnumberaverage = $siteinfo['participantnumberaverage'];
+        $fromform->street = $siteinfo['street'];
+
         $params = (array) $fromform; //we are using the form input as the redirection parameters (token, url and name)
 
         $unconfirmedhub = $registrationmanager->get_unconfirmedhub($huburl);
index d5ca165..3ab5b36 100644 (file)
@@ -38,7 +38,7 @@ class core_register_renderer extends plugin_renderer_base {
     public function moodleorg_registration_message() {
         $moodleorgurl = html_writer::link('http://moodle.org', 'Moodle.org');
         $moodleorgstatsurl = html_writer::link('http://moodle.org/stats', get_string('statsmoodleorg', 'admin'));
-        $moochurl = html_writer::link(HUB_MOODLEORGHUBURL, 'MOOCH');
+        $moochurl = html_writer::link(HUB_MOODLEORGHUBURL, get_string('moodleorghubname', 'admin'));
         $moodleorgregmsg = get_string('registermoodleorg', 'admin', $moodleorgurl);
         $items = array(get_string('registermoodleorgli1', 'admin'),
             get_string('registermoodleorgli2', 'admin', $moodleorgstatsurl),
@@ -92,4 +92,4 @@ class core_register_renderer extends plugin_renderer_base {
         return html_writer::table($table);
     }
 
-}
\ No newline at end of file
+}
index 97a78a4..0cd2306 100644 (file)
@@ -72,11 +72,11 @@ 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 ');
-            $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]
-[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]";
+            $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" .
+                "[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]";
             $fieldnode = $this->find('xpath', $fieldxpath, $exception);
-            $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']
-/child::div[contains(concat(' ', @class, ' '),  ' form-')]/child::*/parent::div");
+            $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" .
+                "/child::div[contains(concat(' ', @class, ' '),  ' form-')]/child::*/parent::div");
 
             // Getting the class which contains the field type.
             $classes = explode(' ', $formfieldtypenode->getAttribute('class'));
index 8f8cdbb..b6c8258 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_admin
 Feature: Display extended course names
   In order to display more info about the courses
-  As a moodle admin
+  As an admin
   I need to display courses short names along with courses full names
 
   Background:
index 5d81113..bb5e657 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_admin
 Feature: An administrator can filter user accounts by role, cohort and other profile fields
   In order to find the user accounts I am looking for
-  As a moodle admin
+  As an admin
   I need to filter the users account list using different filter
 
   Background:
index bc6e29f..9ff0ab6 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_admin @_only_local
 Feature: Upload users
   In order to add users to the system
-  As a moodle admin
+  As an admin
   I need to upload files containing the users data
 
   @javascript
index d09f880..61eb6ea 100644 (file)
@@ -1,7 +1,7 @@
 @tool @tool_behat
 Feature: Set up contextual data for tests
   In order to write tests quickly
-  As a moodle developer
+  As a developer
   I need to fill the database with fixtures
 
   Scenario: Add a bunch of users
index 7389d07..b16b0e0 100644 (file)
@@ -1,7 +1,7 @@
 @tool @tool_behat
 Feature: Edit capabilities
   In order to extend and restrict moodle features
-  As an admin or teacher
+  As an admin or teacher
   I need to allow/deny the existing capabilities at different levels
 
   Background:
index e64ef5d..1fa202b 100644 (file)
@@ -1,7 +1,7 @@
 @tool @tool_behat
 Feature: Set up the testing environment
   In order to execute automated acceptance tests
-  As a moodle developer
+  As a developer
   I need to use the test environment instead of the regular environment
 
   Scenario: Accessing the site
index 903f354..3490eaa 100644 (file)
@@ -388,6 +388,52 @@ class tool_installaddon_installer {
         }
     }
 
+    /**
+     * Moves the given source into a new location recursively
+     *
+     * This is cross-device safe implementation to be used instead of the native rename() function.
+     * See https://bugs.php.net/bug.php?id=54097 for more details.
+     *
+     * @param string $source full path to the existing directory
+     * @param string $target full path to the new location of the directory
+     */
+    public function move_directory($source, $target) {
+
+        if (file_exists($target)) {
+            throw new tool_installaddon_installer_exception('err_folder_already_exists', array('path' => $target));
+        }
+
+        if (is_dir($source)) {
+            $handle = opendir($source);
+        } else {
+            throw new tool_installaddon_installer_exception('err_no_such_folder', array('path' => $source));
+        }
+
+        make_writable_directory($target);
+
+        while ($filename = readdir($handle)) {
+            $sourcepath = $source.'/'.$filename;
+            $targetpath = $target.'/'.$filename;
+
+            if ($filename === '.' or $filename === '..') {
+                continue;
+            }
+
+            if (is_dir($sourcepath)) {
+                $this->move_directory($sourcepath, $targetpath);
+
+            } else {
+                rename($sourcepath, $targetpath);
+            }
+        }
+
+        closedir($handle);
+
+        rmdir($source);
+
+        clearstatcache();
+    }
+
     //// End of external API ///////////////////////////////////////////////////
 
     /**
index 932e9f2..f68ebed 100644 (file)
@@ -71,6 +71,6 @@ if (file_exists($plugintypepath.'/'.$pluginname)) {
         get_string('invaliddata', 'core_error'));
 }
 
-rename($zipcontentpath.'/'.$pluginname, $plugintypepath.'/'.$pluginname);
+$installer->move_directory($zipcontentpath.'/'.$pluginname, $plugintypepath.'/'.$pluginname);
 fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid);
 redirect(new moodle_url('/admin'));
index fd8d1d1..4169719 100644 (file)
@@ -125,6 +125,20 @@ class tool_installaddon_installer_test extends advanced_testcase {
         )));
         $this->assertSame(false, $installer->testable_decode_remote_request($request));
     }
+
+    public function test_move_directory() {
+        $jobid = md5(rand().uniqid('test_', true));
+        $jobroot = make_temp_directory('tool_installaddon/'.$jobid);
+        $contentsdir = make_temp_directory('tool_installaddon/'.$jobid.'/contents/sub/folder');
+        file_put_contents($contentsdir.'/readme.txt', 'Hello world!');
+
+        $installer = tool_installaddon_installer::instance();
+        $installer->move_directory($jobroot.'/contents', $jobroot.'/moved');
+
+        $this->assertFalse(is_dir($jobroot.'/contents'));
+        $this->assertTrue(is_file($jobroot.'/moved/sub/folder/readme.txt'));
+        $this->assertSame('Hello world!', file_get_contents($jobroot.'/moved/sub/folder/readme.txt'));
+    }
 }
 
 
index dd9fc5e..01df572 100644 (file)
@@ -26,4 +26,4 @@ defined('MOODLE_INTERNAL') || die();
 $plugin->component  = 'tool_installaddon';
 $plugin->version    = 2013050100;
 $plugin->requires   = 2013050100;
-$plugin->maturity   = MATURITY_BETA;
+$plugin->maturity   = MATURITY_STABLE;
index eab0062..c6d47aa 100644 (file)
@@ -52,8 +52,8 @@ class behat_auth extends behat_base {
             new Given('I follow "Login"'),
             new Given('I fill in "Username" with "'.$username.'"'),
             new Given('I fill in "Password" with "'.$username.'"'),
-            new When('I press "Login"'),
-            new Given('I should see "You are logged in as"'));
+            new Given('I press "Login"')
+        );
     }
 
     /**
index 7de5405..72e4492 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_auth
 Feature: Authentication
   In order to validate my credentials in the system
-  As a moodle user
+  As a user
   I need to log into the system
 
   Scenario: Login with the predefined admin user
index d109899..ddd38ef 100644 (file)
@@ -1810,6 +1810,10 @@ class backup_questions_structure_step extends backup_structure_step {
         $qhint = new backup_nested_element('question_hint', array('id'), array(
             'hint', 'hintformat', 'shownumcorrect', 'clearwrong', 'options'));
 
+        $tags = new backup_nested_element('tags');
+
+        $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname'));
+
         // Build the tree
 
         $qcategories->add_child($qcategory);
@@ -1818,6 +1822,9 @@ class backup_questions_structure_step extends backup_structure_step {
         $question->add_child($qhints);
         $qhints->add_child($qhint);
 
+        $question->add_child($tags);
+        $tags->add_child($tag);
+
         // Define the sources
 
         $qcategory->set_source_sql("
@@ -1837,6 +1844,12 @@ class backup_questions_structure_step extends backup_structure_step {
                 ORDER BY id',
                 array('questionid' => backup::VAR_PARENTID));
 
+        $tag->set_source_sql("SELECT t.id, t.name, t.rawname
+                              FROM {tag} t
+                              JOIN {tag_instance} ti ON ti.tagid = t.id
+                              WHERE ti.itemid = ?
+                              AND ti.itemtype = 'question'", array(backup::VAR_PARENTID));
+
         // don't need to annotate ids nor files
         // (already done by {@link backup_annotate_all_question_files}
 
index fe2a98f..8b02dc1 100644 (file)
@@ -3063,13 +3063,15 @@ class restore_create_categories_and_questions extends restore_structure_step {
         $hint = new restore_path_element('question_hint',
                 '/question_categories/question_category/questions/question/question_hints/question_hint');
 
+        $tag = new restore_path_element('tag','/question_categories/question_category/questions/question/tags/tag');
+
         // Apply for 'qtype' plugins optional paths at question level
         $this->add_plugin_structure('qtype', $question);
 
         // Apply for 'local' plugins optional paths at question level
         $this->add_plugin_structure('local', $question);
 
-        return array($category, $question, $hint);
+        return array($category, $question, $hint, $tag);
     }
 
     protected function process_question_category($data) {
@@ -3216,6 +3218,29 @@ class restore_create_categories_and_questions extends restore_structure_step {
         $this->set_mapping('question_hint', $oldid, $newitemid);
     }
 
+    protected function process_tag($data) {
+        global $CFG, $DB;
+
+        $data = (object)$data;
+        $newquestion = $this->get_new_parentid('question');
+
+        if (!empty($CFG->usetags)) { // if enabled in server
+            // TODO: This is highly inneficient. Each time we add one tag
+            // we fetch all the existing because tag_set() deletes them
+            // so everything must be reinserted on each call
+            $tags = array();
+            $existingtags = tag_get_tags('question', $newquestion);
+            // Re-add all the existitng tags
+            foreach ($existingtags as $existingtag) {
+                $tags[] = $existingtag->rawname;
+            }
+            // Add the one being restored
+            $tags[] = $data->rawname;
+            // Send all the tags back to the question
+            tag_set('question', $newquestion, $tags);
+        }
+    }
+
     protected function after_execute() {
         global $DB;
 
index 148afa3..518d7ba 100644 (file)
@@ -821,7 +821,7 @@ abstract class restore_dbops {
      * @return array of result object
      */
     public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) {
-        global $DB;
+        global $DB, $CFG;
 
         $results = array();
 
@@ -916,6 +916,12 @@ abstract class restore_dbops {
 
                 // create the file in the filepool if it does not exist yet
                 if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
+
+                    // If no license found, use default.
+                    if ($file->license == null){
+                        $file->license = $CFG->sitedefaultlicense;
+                    }
+
                     $file_record = array(
                         'contextid'   => $newcontextid,
                         'component'   => $component,
index ba0a84c..488ab60 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_backup
 Feature: Backup Moodle courses
   In order to save and store course contents
-  As a moodle admin
+  As an admin
   I need to create backups of courses
 
   Background:
index 79c05e1..4d8f4b1 100644 (file)
@@ -112,9 +112,9 @@ class behat_backup extends behat_base {
         $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 . "')]
-/descendant::input[@type='radio']";
+        $xpath = "//div[contains(concat(' ', @class, ' '), ' ics-results ')]" .
+            "/descendant::tr[contains(., '" . $fromcourse . "')]" .
+            "/descendant::input[@type='radio']";
         $radionode = $this->find('xpath', $xpath, $exception);
         $radionode->check();
         $radionode->click();
@@ -152,10 +152,10 @@ class behat_backup extends behat_base {
 
         // Selecting the specified course (we can not call behat_forms::select_radio here as is in another behat subcontext).
         $existingcourse = str_replace("'", "\'", $existingcourse);
-        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]
-/descendant::div[@class='restore-course-search']
-/descendant::tr[contains(., '" . $existingcourse . "')]
-/descendant::input[@type='radio']");
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]" .
+            "/descendant::div[@class='restore-course-search']" .
+            "/descendant::tr[contains(., '" . $existingcourse . "')]" .
+            "/descendant::input[@type='radio']");
         $radionode->check();
         $radionode->click();
 
@@ -181,9 +181,9 @@ 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')]
-/descendant::div[@class='restore-course-search']
-/descendant::input[@type='radio']");
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]" .
+            "/descendant::div[@class='restore-course-search']" .
+            "/descendant::input[@type='radio']");
         $radionode->check();
         $radionode->click();
 
@@ -209,13 +209,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')]
-/descendant::input[@type='radio'][@name='target'][@value='1']");
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+            "/descendant::input[@type='radio'][@name='target'][@value='1']");
         $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore merging section.
-        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+            "/descendant::input[@type='submit'][@value='Continue']");
         $continuenode->click();
         $this->wait();
 
@@ -236,13 +237,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')]
-/descendant::input[@type='radio'][@name='target'][@value='0']");
+        $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+            "/descendant::input[@type='radio'][@name='target'][@value='0']");
         $radionode->check();
         $radionode->click();
 
         // Pressing the continue button of the restore merging section.
-        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]/descendant::input[@type='submit'][@value='Continue']");
+        $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" .
+            "/descendant::input[@type='submit'][@value='Continue']");
         $continuenode->click();
         $this->wait();
 
index 901bd7d..3af2839 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_backup
 Feature: Duplicate activities
   In order to set up my course contents quickly
-  As a moodle teacher
+  As a teacher
   I need to duplicate activities inside the same course
 
   @javascript
index e087094..b1851d3 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_backup
 Feature: Import course's contents into another course
   In order to move and copy contents between courses
-  As a moodle teacher
+  As a teacher
   I need to import a course contents into another course selecting what I want to import
 
   @javascript
index 4e5d4b8..f415f07 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_backup
 Feature: Restore Moodle 2 course backups
   In order to continue using my stored course contents
-  As a moodle teacher and as a moodle admin
+  As a teacher and an admin
   I need to restore them inside other Moodle courses or in new courses
 
   Background:
@@ -12,7 +12,7 @@ Feature: Restore Moodle 2 course backups
     And I log in as "admin"
     And I follow "Course 1"
     And I turn editing mode on
-    And I add a "forum" to section "1" and I fill the form with:
+    And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name |
       | Description | Test forum description |
     And I add the "Community finder" block
index 1ad58a5..6912813 100644 (file)
@@ -77,12 +77,17 @@ class award_criteria_course extends award_criteria {
         $param = reset($this->params);
 
         $course = $DB->get_record('course', array('id' => $param['course']));
-        $str = '"' . $course->fullname . '"';
-        if (isset($param['bydate'])) {
-            $str .= get_string('criteria_descr_bydate', 'badges', userdate($param['bydate'], get_string('strftimedate', 'core_langconfig')));
-        }
-        if (isset($param['grade'])) {
-            $str .= get_string('criteria_descr_grade', 'badges', $param['grade']);
+        if (!$course) {
+            $str = $OUTPUT->error_text(get_string('error:nosuchcourse', 'badges'));
+        } else {
+            $options = array('context' => context_course::instance($course->id));
+            $str = html_writer::tag('b', '"' . format_string($course->fullname, true, $options) . '"');
+            if (isset($param['bydate'])) {
+                $str .= get_string('criteria_descr_bydate', 'badges', userdate($param['bydate'], get_string('strftimedate', 'core_langconfig')));
+            }
+            if (isset($param['grade'])) {
+                $str .= get_string('criteria_descr_grade', 'badges', $param['grade']);
+            }
         }
         return $str;
     }
@@ -92,9 +97,16 @@ class award_criteria_course extends award_criteria {
      *
      */
     public function get_options(&$mform) {
-        global $PAGE, $DB;
-        $param = array_shift($this->params);
-        $course = $DB->get_record('course', array('id' => $PAGE->course->id));
+        global $DB;
+        $param = array();
+
+        if ($this->id !== 0) {
+            $param = reset($this->params);
+        } else {
+            $param['course'] = $mform->getElementValue('course');
+            $mform->removeElement('course');
+        }
+        $course = $DB->get_record('course', array('id' => $param['course']));
 
         if (!($course->enablecompletion == COMPLETION_ENABLED)) {
             $none = true;
index 6443bd5..8144a38 100644 (file)
@@ -74,16 +74,15 @@ class award_criteria_courseset extends award_criteria {
     }
 
     public function get_courses(&$mform) {
-        global $DB, $CFG, $PAGE;
+        global $DB, $CFG;
         require_once($CFG->dirroot . '/course/lib.php');
         $buttonarray = array();
 
         // Get courses with enabled completion.
         $courses = $DB->get_records('course', array('enablecompletion' => COMPLETION_ENABLED));
         if (!empty($courses)) {
-            $list = array();
-            $parents = array();
-            make_categories_list($list, $parents);
+            require_once($CFG->libdir . '/coursecatlib.php');
+            $list = coursecat::make_categories_list();
 
             $select = array();
             $selected = array();
@@ -100,7 +99,7 @@ class award_criteria_courseset extends award_criteria {
             $mform->addHelpButton('courses', 'addcourse', 'badges');
 
             $buttonarray[] =& $mform->createElement('submit', 'submitcourse', get_string('addcourse', 'badges'));
-            $buttonarray[] =& $mform->createElement('submit', 'back', get_string('cancel'));
+            $buttonarray[] =& $mform->createElement('submit', 'cancel', get_string('cancel'));
             $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
 
             $mform->addElement('hidden', 'addcourse', 'addcourse');
@@ -111,7 +110,7 @@ class award_criteria_courseset extends award_criteria {
             $mform->setType('agg', PARAM_INT);
         } else {
             $mform->addElement('static', 'nocourses', '', get_string('error:nocourses', 'badges'));
-            $buttonarray[] =& $mform->createElement('submit', 'back', get_string('continue'));
+            $buttonarray[] =& $mform->createElement('submit', 'cancel', get_string('continue'));
             $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
         }
     }
index 65cc718..208060d 100644 (file)
@@ -39,11 +39,16 @@ class edit_criteria_form extends moodleform {
         $mform = $this->_form;
         $criteria = $this->_customdata['criteria'];
         $addcourse = $this->_customdata['addcourse'];
+        $course = $this->_customdata['course'];
 
         // Get course selector first if it's a new courseset criteria.
         if (($criteria->id == 0 || $addcourse) && $criteria->criteriatype == BADGE_CRITERIA_TYPE_COURSESET) {
             $criteria->get_courses($mform);
         } else {
+            if ($criteria->id == 0 && $criteria->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
+                $mform->addElement('hidden', 'course', $course);
+                $mform->setType('course', PARAM_INT);
+            }
             list($none, $message) = $criteria->get_options($mform);
 
             if ($none) {
index 943ecb0..486febd 100644 (file)
@@ -75,7 +75,7 @@ if ($edit) {
     $criteria = award_criteria::build($cparams);
 }
 
-$mform = new edit_criteria_form($FULLME, array('criteria' => $criteria, 'addcourse' => $addcourse));
+$mform = new edit_criteria_form($FULLME, array('criteria' => $criteria, 'addcourse' => $addcourse, 'course' => $badge->courseid));
 
 if (!empty($addcourse)) {
     if ($data = $mform->get_data()) {
index 10fc496..8c0381e 100644 (file)
@@ -506,7 +506,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $paging = new paging_bar($badges->totalcount, $badges->page, $badges->perpage, $this->page->url, 'page');
         $htmlpagingbar = $this->render($paging);
         $table = new html_table();
-        $table->attributes['class'] = 'collection boxaligncenter boxwidthwide';
+        $table->attributes['class'] = 'collection';
 
         $sortbyname = $this->helper_sortable_heading(get_string('name'),
                 'name', $badges->sort, $badges->dir);
@@ -637,12 +637,15 @@ class core_badges_renderer extends plugin_renderer_base {
         echo $this->tabtree($row, $current);
     }
 
-    // Prints badge status box.
+    /**
+     * Prints badge status box.
+     * @return Either the status box html as a string or null
+     */
     public function print_badge_status_box(badge $badge) {
-        $table = new html_table();
-        $table->attributes['class'] = 'boxaligncenter statustable';
-
         if (has_capability('moodle/badges:configurecriteria', $badge->get_context())) {
+            $table = new html_table();
+            $table->attributes['class'] = 'boxaligncenter statustable';
+
             if (!$badge->has_criteria()) {
                 $criteriaurl = new moodle_url('/badges/criteria.php', array('id' => $badge->id));
                 $status = get_string('nocriteria', 'badges');
@@ -669,12 +672,13 @@ class core_badges_renderer extends plugin_renderer_base {
                 }
                 $row = array($status . $this->output->help_icon('status', 'badges'), $action);
             }
-        }
+            $table->data[] = $row;
 
-        $table->data[] = $row;
+            $style = $badge->is_active() ? 'generalbox statusbox active' : 'generalbox statusbox inactive';
+            return $this->output->box(html_writer::table($table), $style);
+        }
 
-        $style = $badge->is_active() ? 'generalbox statusbox active' : 'generalbox statusbox inactive';
-        return $this->output->box(html_writer::table($table), $style);
+        return null;
     }
 
     // Prints badge criteria.
@@ -740,7 +744,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $paging = new paging_bar($recipients->totalcount, $recipients->page, $recipients->perpage, $this->page->url, 'page');
         $htmlpagingbar = $this->render($paging);
         $table = new html_table();
-        $table->attributes['class'] = 'generaltable generalbox boxaligncenter boxwidthwide';
+        $table->attributes['class'] = 'generaltable boxaligncenter boxwidthwide';
 
         $sortbyfirstname = $this->helper_sortable_heading(get_string('firstname'),
                 'firstname', $recipients->sort, $recipients->dir);
index af3d396..ec70958 100644 (file)
@@ -1,7 +1,7 @@
 @block @block_comments
 Feature: Add a comment to the comments block
   In order to comment on a conversation or a topic
-  As a moodle user
+  As a user
   In need to add comments to courses
 
   Background:
index 447eb5a..5482460 100644 (file)
@@ -91,8 +91,8 @@ 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 . "')]";
+        $commentxpath = "//div[contains(concat(' ', @class, ' '), ' block_comments ')]" .
+            "/descendant::div[@class='comment-message'][contains(., '" . $comment . "')]";
         $commentnode = $this->find('xpath', $commentxpath, $exception);
 
         // Click on delete icon.
index 0d2a27d..077a0c1 100644 (file)
@@ -1,7 +1,7 @@
 @block @block_comments
 Feature: Delete comment block messages
   In order to refine comment block's contents
-  As a moodle teacher
+  As a teacher
   In need to delete comments from courses
 
   @javascript
index 3876618..fb97f04 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_block
 Feature: Add and configure blocks throughout the site
   In order to maintain some patterns across all the site
-  As a moodle manager
+  As a manager
   I need to set and configure blocks throughout the site
 
   @javascript
index cef0eeb..61ef61d 100644 (file)
@@ -183,13 +183,7 @@ if (!empty($userid)) {
             print_error('donothaveblog', 'blog');
         }
     } else {
-        $personalcontext = context_user::instance($userid);
-
-        if (!has_capability('moodle/blog:view', $sitecontext) && !has_capability('moodle/user:readuserblogs', $personalcontext)) {
-            print_error('cannotviewuserblog', 'blog');
-        }
-
-        if (!blog_user_can_view_user_entry($userid)) {
+        if (!has_capability('moodle/blog:view', $sitecontext) || !blog_user_can_view_user_entry($userid)) {
             print_error('cannotviewcourseblog', 'blog');
         }
 
index 799e0d2..cfe8771 100644 (file)
@@ -65,35 +65,36 @@ function blog_user_can_view_user_entry($targetuserid, $blogentry=null) {
     global $CFG, $USER, $DB;
 
     if (empty($CFG->enableblogs)) {
-        return false; // blog system disabled
+        return false; // Blog system disabled.
     }
 
     if (isloggedin() && $USER->id == $targetuserid) {
-        return true; // can view own entries in any case
+        return true; // Can view own entries in any case.
     }
 
     $sitecontext = context_system::instance();
     if (has_capability('moodle/blog:manageentries', $sitecontext)) {
-        return true; // can manage all entries
+        return true; // Can manage all entries.
     }
 
-    // coming for 1 entry, make sure it's not a draft
+    // If blog is in draft state, then make sure user have proper capability.
     if ($blogentry && $blogentry->publishstate == 'draft' && !has_capability('moodle/blog:viewdrafts', $sitecontext)) {
-        return false;  // can not view draft of others
+        return false;  // Can not view draft of others.
     }
 
-    // coming for 0 entry, make sure user is logged in, if not a public blog
+    // If blog entry is not public, make sure user is logged in.
     if ($blogentry && $blogentry->publishstate != 'public' && !isloggedin()) {
         return false;
     }
 
+    // If blogentry is not passed or all above checks pass, then check capability based on system config.
     switch ($CFG->bloglevel) {
         case BLOG_GLOBAL_LEVEL:
             return true;
         break;
 
         case BLOG_SITE_LEVEL:
-            if (isloggedin()) { // not logged in viewers forbidden
+            if (isloggedin()) { // Not logged in viewers forbidden.
                 return true;
             }
             return false;
@@ -101,6 +102,7 @@ function blog_user_can_view_user_entry($targetuserid, $blogentry=null) {
 
         case BLOG_USER_LEVEL:
         default:
+            // If user is viewing other user blog, then user should have user:readuserblogs capability.
             $personalcontext = context_user::instance($targetuserid);
             return has_capability('moodle/user:readuserblogs', $personalcontext);
         break;
@@ -923,6 +925,8 @@ function blog_get_associated_count($courseid, $cmid=null) {
  * may have switch to turn on/off comments option, this callback will
  * affect UI display, not like pluginname_comment_validate only throw
  * exceptions.
+ * blog_comment_validate will be called before viewing/adding/deleting
+ * comment, so don't repeat checks.
  * Capability check has been done in comment->check_permissions(), we
  * don't need to do it again here.
  *
@@ -939,7 +943,17 @@ function blog_get_associated_count($courseid, $cmid=null) {
  * @return array
  */
 function blog_comment_permissions($comment_param) {
-    return array('post'=>true, 'view'=>true);
+    global $DB;
+
+    // If blog is public and current user is guest, then don't let him post comments.
+    $blogentry = $DB->get_record('post', array('id' => $comment_param->itemid), 'publishstate', MUST_EXIST);
+
+    if ($blogentry->publishstate != 'public') {
+        if (!isloggedin() || isguestuser()) {
+            return array('post' => false, 'view' => true);
+        }
+    }
+    return array('post' => true, 'view' => true);
 }
 
 /**
@@ -958,16 +972,21 @@ function blog_comment_permissions($comment_param) {
  * @return boolean
  */
 function blog_comment_validate($comment_param) {
-    global $DB;
-    // validate comment itemid
-    if (!$entry = $DB->get_record('post', array('id'=>$comment_param->itemid))) {
-        throw new comment_exception('invalidcommentitemid');
+    global $CFG, $DB, $USER;
+
+    // Check if blogs are enabled user can comment.
+    if (empty($CFG->enableblogs) || empty($CFG->blogusecomments)) {
+        throw new comment_exception('nopermissiontocomment');
     }
-    // validate comment area
+
+    // Validate comment area.
     if ($comment_param->commentarea != 'format_blog') {
         throw new comment_exception('invalidcommentarea');
     }
-    // validation for comment deletion
+
+    $blogentry = $DB->get_record('post', array('id' => $comment_param->itemid), '*', MUST_EXIST);
+
+    // Validation for comment deletion.
     if (!empty($comment_param->commentid)) {
         if ($record = $DB->get_record('comments', array('id'=>$comment_param->commentid))) {
             if ($record->commentarea != 'format_blog') {
@@ -983,7 +1002,11 @@ function blog_comment_validate($comment_param) {
             throw new comment_exception('invalidcommentid');
         }
     }
-    return true;
+
+    // Validate if user has blog view permission.
+    $sitecontext = context_system::instance();
+    return has_capability('moodle/blog:view', $sitecontext) &&
+            blog_user_can_view_user_entry($blogentry->userid, $blogentry);
 }
 
 /**
index 0fdea60..7828194 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_blog
 Feature: Comment on a blog entry
   In order to respond to a blog post
-  As a moodle user
+  As a user
   I need to be able to comment on a blog entry
 
   Background:
index 3868c90..c1f67c2 100644 (file)
@@ -31,6 +31,8 @@ A definition:
             'invalidationevents' => array(            // Optional
                 'contextmarkeddirty'
             ),
+            'sharingoptions' => null                  // Optional
+            'defaultsharing' => null                  // Optional
         )
     );
 
@@ -147,11 +149,13 @@ The following optional settings can also be defined:
 * ttl - Can be used to set a ttl value for data being set for this cache.
 * mappingsonly - This definition can only be used if there is a store mapping for it. More on this later.
 * invalidationevents - An array of events that should trigger this cache to invalidate.
+* sharingoptions - The sum of the possible sharing options that are applicable to the definition. An advanced setting.
+* defaultsharing - The default sharing option to use. It's highly recommended that you don't set this unless there is a very specific reason not to use the system default.
 
 It's important to note that internally the definition is also aware of the component. This is picked up when the definition is read, based upon the location of the caches.php file.
 
-The persist option.
-As noted the persist option causes the loader generated for this definition to be stored when first created. Subsequent requests for this definition will be given the original loader instance.
+The persistent option.
+As noted the persistent option causes the loader generated for this definition to be stored when first created. Subsequent requests for this definition will be given the original loader instance.
 Data passed to or retrieved from the loader and its chained loaders gets cached by the instance.
 This option should be used when you know you will require the loader several times and perhaps in different areas of code.
 Because it caches key=>value data it avoids the need to re-fetch things from stores after the first request. Its good for performance, bad for memory.
@@ -162,6 +166,10 @@ The administrator of a site can create mappings between stores and definitions.
 Setting this option to true means that the definition can only be used if a mapping has been made for it.
 Normally if no mappings exist then the default store for the definition mode is used.
 
+Sharing options.
+This controls the options available to the user when configuring the sharing of a definitions cached data.
+By default all sharing options are available to select. This particular option allows the developer to limit the options available to the admin configuring the cache.
+
 ### Data source
 Data sources allow cache _misses_ (requests for a key that doesn't exist) to be handled and loaded internally.
 The loader gets used as the last resort if provided and means that code using the cache doesn't need to handle the situation that information isn't cached.
index 52b09b3..f3e3acb 100644 (file)
@@ -30,6 +30,15 @@ require_once($CFG->dirroot.'/lib/adminlib.php');
 require_once($CFG->dirroot.'/cache/locallib.php');
 require_once($CFG->dirroot.'/cache/forms.php');
 
+// The first time the user visits this page we are going to reparse the definitions.
+// Just ensures that everything is up to date.
+// We flag is session so that this only happens once as people are likely to hit
+// this page several times if making changes.
+if (empty($SESSION->cacheadminreparsedefinitions)) {
+    cache_helper::update_definitions();
+    $SESSION->cacheadminreparsedefinitions = true;
+}
+
 $action = optional_param('action', null, PARAM_ALPHA);
 
 admin_externalpage_setup('cacheconfig');
@@ -101,7 +110,7 @@ if (!empty($action) && confirm_sesskey()) {
 
             if (!array_key_exists($store, $stores)) {
                 $notifysuccess = false;
-                $notification = get_string('invalidstore');
+                $notification = get_string('invalidstore', 'cache');
             } else if ($stores[$store]['mappings'] > 0) {
                 $notifysuccess = false;
                 $notification = get_string('deletestorehasmappings', 'cache');
@@ -131,6 +140,9 @@ if (!empty($action) && confirm_sesskey()) {
             break;
         case 'editdefinitionmapping' : // Edit definition mappings.
             $definition = required_param('definition', PARAM_SAFEPATH);
+            if (!array_key_exists($definition, $definitions)) {
+                throw new cache_exception('Invalid cache definition requested');
+            }
             $title = get_string('editdefinitionmappings', 'cache', $definition);
             $mform = new cache_definition_mappings_form($PAGE->url, array('definition' => $definition));
             if ($mform->is_cancelled()) {
@@ -147,6 +159,33 @@ if (!empty($action) && confirm_sesskey()) {
                 redirect($PAGE->url);
             }
             break;
+        case 'editdefinitionsharing' :
+            $definition = required_param('definition', PARAM_SAFEPATH);
+            if (!array_key_exists($definition, $definitions)) {
+                throw new cache_exception('Invalid cache definition requested');
+            }
+            $title = get_string('editdefinitionsharing', 'cache', $definition);
+            $sharingoptions = $definitions[$definition]['sharingoptions'];
+            $customdata = array('definition' => $definition, 'sharingoptions' => $sharingoptions);
+            $mform = new cache_definition_sharing_form($PAGE->url, $customdata);
+            $mform->set_data(array(
+                'sharing' => $definitions[$definition]['selectedsharingoption'],
+                'userinputsharingkey' => $definitions[$definition]['userinputsharingkey']
+            ));
+            if ($mform->is_cancelled()) {
+                redirect($PAGE->url);
+            } else if ($data = $mform->get_data()) {
+                $component = $definitions[$definition]['component'];
+                $area = $definitions[$definition]['area'];
+                // Purge the stores removing stale data before we alter the sharing option.
+                cache_helper::purge_stores_used_by_definition($component, $area);
+                $writer = cache_config_writer::instance();
+                $sharing = array_sum(array_keys($data->sharing));
+                $userinputsharingkey = $data->userinputsharingkey;
+                $writer->set_definition_sharing($definition, $sharing, $userinputsharingkey);
+                redirect($PAGE->url);
+            }
+            break;
         case 'editmodemappings': // Edit default mode mappings.
             $mform = new cache_mode_mappings_form(null, $stores);
             $mform->set_data(array(
@@ -171,7 +210,15 @@ if (!empty($action) && confirm_sesskey()) {
         case 'purgedefinition': // Purge a specific definition.
             $definition = required_param('definition', PARAM_SAFEPATH);
             list($component, $area) = explode('/', $definition, 2);
-            cache_helper::purge_by_definition($component, $area);
+            $factory = cache_factory::instance();
+            $definition = $factory->create_definition($component, $area);
+            if ($definition->has_required_identifiers()) {
+                // We will have to purge the stores used by this definition.
+                cache_helper::purge_stores_used_by_definition($component, $area);
+            } else {
+                // Alrighty we can purge just the data belonging to this definition.
+                cache_helper::purge_by_definition($component, $area);
+            }
             redirect($PAGE->url, get_string('purgedefinitionsuccess', 'cache'), 5);
             break;
 
@@ -203,7 +250,7 @@ if (!empty($action) && confirm_sesskey()) {
             $confirm = optional_param('confirm', false, PARAM_BOOL);
             if (!array_key_exists($lock, $locks)) {
                 $notifysuccess = false;
-                $notification = get_string('invalidlock');
+                $notification = get_string('invalidlock', 'cache');
             } else if ($locks[$lock]['uses'] > 0) {
                 $notifysuccess = false;
                 $notification = get_string('deletelockhasuses', 'cache');
@@ -249,7 +296,7 @@ if ($mform instanceof moodleform) {
 } else {
     echo $renderer->store_plugin_summaries($plugins);
     echo $renderer->store_instance_summariers($stores, $plugins);
-    echo $renderer->definition_summaries($definitions, cache_administration_helper::get_definition_actions($context));
+    echo $renderer->definition_summaries($definitions, $context);
     echo $renderer->lock_summaries($locks);
 
     $applicationstore = join(', ', $defaultmodestores[cache_store::MODE_APPLICATION]);
index 8d3fbc0..322035f 100644 (file)
@@ -231,6 +231,29 @@ class cache_config {
                 // Invalid cache mode used for the definition.
                 continue;
             }
+            if ($conf['mode'] === cache_store::MODE_SESSION || $conf['mode'] === cache_store::MODE_REQUEST) {
+                // We force this for session and request caches.
+                // They are only allowed to use the default as we don't want people changing them.
+                $conf['sharingoptions'] = cache_definition::SHARING_DEFAULT;
+                $conf['selectedsharingoption'] = cache_definition::SHARING_DEFAULT;
+                $conf['userinputsharingkey'] = '';
+            } else {
+                // Default the sharing option as it was added for 2.5.
+                // This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
+                if (!isset($conf['sharingoptions'])) {
+                    $conf['sharingoptions'] = cache_definition::SHARING_DEFAULTOPTIONS;
+                }
+                // Default the selected sharing option as it was added for 2.5.
+                // This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
+                if (!isset($conf['selectedsharingoption'])) {
+                    $conf['selectedsharingoption'] = cache_definition::SHARING_DEFAULT;
+                }
+                // Default the user input sharing key as it was added for 2.5.
+                // This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
+                if (!isset($conf['userinputsharingkey'])) {
+                    $conf['userinputsharingkey'] = '';
+                }
+            }
             $this->configdefinitions[$id] = $conf;
         }
 
@@ -373,10 +396,18 @@ class cache_config {
      * @param string $storename
      * @return array Associative array of definitions, id=>definition
      */
-    public static function get_definitions_by_store($storename) {
+    public function get_definitions_by_store($storename) {
         $definitions = array();
 
-        $config = cache_config::instance();
+        // This function was accidentally made static at some stage in the past.
+        // It was converted to an instance method but to be backwards compatible
+        // we must step around this in code.
+        if (!isset($this)) {
+            $config = cache_config::instance();
+        } else {
+            $config = $this;
+        }
+
         $stores = $config->get_all_stores();
         if (!array_key_exists($storename, $stores)) {
             // The store does not exist.
index 62732f7..1b19fb2 100644 (file)
@@ -100,6 +100,11 @@ defined('MOODLE_INTERNAL') || die();
  *          reason or another.
  *     + invalidationevents
  *          [array] An array of events that should cause this cache to invalidate some or all of the items within it.
+ *     + sharingoptions
+ *          [int] The sharing options that are appropriate for this definition. Should be the sum of the possible options.
+ *     + defaultsharing
+ *          [int] The default sharing option to use. It's highly recommended that you don't set this unless there is a very
+ *          specific reason not to use the system default.
  *
  * For examples take a look at lib/db/caches.php
  *
@@ -110,6 +115,26 @@ defined('MOODLE_INTERNAL') || die();
  */
 class cache_definition {
 
+    /** The cache can be shared with everyone */
+    const SHARING_ALL = 1;
+    /** The cache can be shared with other sites using the same siteid. */
+    const SHARING_SITEID = 2;
+    /** The cache can be shared with other sites of the same version. */
+    const SHARING_VERSION = 4;
+    /** The cache can be shared with other sites using the same key */
+    const SHARING_INPUT = 8;
+
+    /**
+     * The default sharing options available.
+     * All + SiteID + Version + Input.
+     */
+    const SHARING_DEFAULTOPTIONS = 15;
+    /**
+     * The default sharing option that gets used if none have been selected.
+     * SiteID. It is the most restrictive.
+     */
+    const SHARING_DEFAULT = 2;
+
     /**
      * The identifier for the definition
      * @var string
@@ -281,6 +306,24 @@ class cache_definition {
      */
     protected $definitionhash = null;
 
+    /**
+     * The selected sharing mode for this definition.
+     * @var int
+     */
+    protected $sharingoptions;
+
+    /**
+     * The selected sharing option.
+     * @var int One of self::SHARING_*
+     */
+    protected $selectedsharingoption = self::SHARING_DEFAULT;
+
+    /**
+     * The user input key to use if the SHARING_INPUT option has been selected.
+     * @var string Must be ALPHANUMEXT
+     */
+    protected $userinputsharingkey = '';
+
     /**
      * Creates a cache definition given a definition from the cache configuration or from a caches.php file.
      *
@@ -325,6 +368,9 @@ class cache_definition {
         $ttl = 0;
         $mappingsonly = false;
         $invalidationevents = array();
+        $sharingoptions = self::SHARING_DEFAULT;
+        $selectedsharingoption = self::SHARING_DEFAULT;
+        $userinputsharingkey = '';
 
         if (array_key_exists('simplekeys', $definition)) {
             $simplekeys = (bool)$definition['simplekeys'];
@@ -387,6 +433,26 @@ class cache_definition {
         if (array_key_exists('invalidationevents', $definition)) {
             $invalidationevents = (array)$definition['invalidationevents'];
         }
+        if (array_key_exists('sharingoptions', $definition)) {
+            $sharingoptions = (int)$definition['sharingoptions'];
+        }
+        if (array_key_exists('selectedsharingoption', $definition)) {
+            $selectedsharingoption = (int)$definition['selectedsharingoption'];
+        } else if (array_key_exists('defaultsharing', $definition)) {
+            $selectedsharingoption = (int)$definition['defaultsharing'];
+        } else if ($sharingoptions ^ $selectedsharingoption) {
+            if ($sharingoptions & self::SHARING_SITEID) {
+                $selectedsharingoption = self::SHARING_SITEID;
+            } else if ($sharingoptions & self::SHARING_VERSION) {
+                $selectedsharingoption = self::SHARING_VERSION;
+            } else {
+                $selectedsharingoption = self::SHARING_ALL;
+            }
+        }
+
+        if (array_key_exists('userinputsharingkey', $definition) && !empty($definition['userinputsharingkey'])) {
+            $userinputsharingkey = (string)$definition['userinputsharingkey'];
+        }
 
         if (!is_null($overrideclass)) {
             if (!is_null($overrideclassfile)) {
@@ -457,6 +523,9 @@ class cache_definition {
         $cachedefinition->ttl = $ttl;
         $cachedefinition->mappingsonly = $mappingsonly;
         $cachedefinition->invalidationevents = $invalidationevents;
+        $cachedefinition->sharingoptions = $sharingoptions;
+        $cachedefinition->selectedsharingoption = $selectedsharingoption;
+        $cachedefinition->userinputsharingkey = $userinputsharingkey;
 
         return $cachedefinition;
     }
@@ -496,6 +565,9 @@ class cache_definition {
         if (!empty($options['overrideclass'])) {
             $definition['overrideclass'] = $options['overrideclass'];
         }
+        if (!empty($options['sharingoptions'])) {
+            $definition['sharingoptions'] = $options['sharingoptions'];
+        }
         return self::load($id, $definition, null);
     }
 
@@ -819,6 +891,29 @@ class cache_definition {
      * @return string A string to be used as part of keys.
      */
     protected function get_cache_identifier() {
-        return cache_helper::get_site_identifier();
+        $identifiers = array();
+        if ($this->selectedsharingoption & self::SHARING_ALL) {
+            // Nothing to do here.
+        } else {
+            if ($this->selectedsharingoption & self::SHARING_SITEID) {
+                $identifiers[] = cache_helper::get_site_identifier();
+            }
+            if ($this->selectedsharingoption & self::SHARING_VERSION) {
+                $identifiers[] = cache_helper::get_site_version();
+            }
+            if ($this->selectedsharingoption & self::SHARING_INPUT && !empty($this->userinputsharingkey)) {
+                $identifiers[] = $this->userinputsharingkey;
+            }
+        }
+        return join('/', $identifiers);
+    }
+
+    /**
+     * Returns true if this definition requires identifiers.
+     *
+     * @param bool
+     */
+    public function has_required_identifiers() {
+        return (count($this->requireidentifiers) > 0);
     }
 }
\ No newline at end of file
index be329f7..873dbbe 100644 (file)
@@ -192,8 +192,7 @@ class cache_helper {
             if (in_array($pluginname, $ignored)) {
                 continue;
             }
-            $pluginname = clean_param($pluginname, PARAM_PLUGIN);
-            if (empty($pluginname)) {
+            if (!is_valid_plugin_name($pluginname)) {
                 // Better ignore plugins with problematic names here.
                 continue;
             }
@@ -276,6 +275,11 @@ class cache_helper {
     /**
      * Purges the cache for a specific definition.
      *
+     * If you need to purge a definition that requires identifiers or an aggregate and you don't
+     * know the details of those please use cache_helper::purge_stores_used_by_definition instead.
+     * It is a more aggressive purge and will purge all data within the store, not just the data
+     * belonging to the given definition.
+     *
      * @todo MDL-36660: Change the signature: $aggregate must be added.
      *
      * @param string $component
@@ -410,12 +414,17 @@ class cache_helper {
      * Think twice before calling this method. It will purge **ALL** caches regardless of whether they have been used recently or
      * anything. This will involve full setup of the cache + the purge operation. On a site using caching heavily this WILL be
      * painful.
+     *
+     * @param bool $usewriter If set to true the cache_config_writer class is used. This class is special as it avoids
+     *      it is still usable when caches have been disabled.
+     *      Please use this option only if you really must. It's purpose is to allow the cache to be purged when it would be
+     *      otherwise impossible.
      */
-    public static function purge_all() {
-        $config = cache_config::instance();
-
+    public static function purge_all($usewriter = false) {
+        $factory = cache_factory::instance();
+        $config = $factory->create_config_instance($usewriter);
         foreach ($config->get_all_stores() as $store) {
-            self::purge_store($store['name']);
+            self::purge_store($store['name'], $config);
         }
     }
 
@@ -423,10 +432,13 @@ class cache_helper {
      * Purges a store given its name.
      *
      * @param string $storename
+     * @param cache_config $config
      * @return bool
      */
-    public static function purge_store($storename) {
-        $config = cache_config::instance();
+    public static function purge_store($storename, cache_config $config = null) {
+        if ($config === null) {
+            $config = cache_config::instance();
+        }
 
         $stores = $config->get_all_stores();
         if (!array_key_exists($storename, $stores)) {
@@ -446,15 +458,36 @@ class cache_helper {
 
         foreach ($config->get_definitions_by_store($storename) as $id => $definition) {
             $definition = cache_definition::load($id, $definition);
-            $instance = new $class($store['name'], $store['configuration']);
-            $instance->initialise($definition);
-            $instance->purge();
-            unset($instance);
+            $definitioninstance = clone($instance);
+            $definitioninstance->initialise($definition);
+            $definitioninstance->purge();
+            unset($definitioninstance);
         }
 
         return true;
     }
 
+    /**
+     * Purges all of the stores used by a definition.
+     *
+     * Unlike cache_helper::purge_by_definition this purges all of the data from the stores not
+     * just the data relating to the definition.
+     * This function is useful when you must purge a definition that requires setup but you don't
+     * want to set it up.
+     *
+     * @param string $component
+     * @param string $area
+     */
+    public static function purge_stores_used_by_definition($component, $area) {
+        $factory = cache_factory::instance();
+        $config = $factory->create_config_instance();
+        $definition = $factory->create_definition($component, $area);
+        $stores = $config->get_stores_for_definition($definition);
+        foreach ($stores as $store) {
+            self::purge_store($store['name']);
+        }
+    }
+
     /**
      * Returns the translated name of the definition.
      *
index 18cc11e..e65b766 100644 (file)
@@ -253,13 +253,28 @@ class cache_factory_disabled extends cache_factory {
      * Creates a cache config instance with the ability to write if required.
      *
      * @param bool $writer Unused.
-     * @return cache_config|cache_config_writer
+     * @return cache_config_disabled|cache_config_writer
      */
     public function create_config_instance($writer = false) {
+        // We are always going to use the cache_config_disabled class for all regular request.
+        // However if the code has requested the writer then likely something is changing and
+        // we're going to need to interact with the config.php file.
+        // In this case we will still use the cache_config_writer.
         $class = 'cache_config_disabled';
+        if ($writer) {
+            // If the writer was requested then something is changing.
+            $class = 'cache_config_writer';
+        }
         if (!array_key_exists($class, $this->configs)) {
             self::set_state(self::STATE_INITIALISING);
-            $configuration = $class::create_default_configuration();
+            if ($class === 'cache_config_disabled') {
+                $configuration = $class::create_default_configuration();
+            } else {
+                $configuration = false;
+                if (!cache_config::config_file_exists()) {
+                    cache_config_writer::create_default_configuration(true);
+                }
+            }
             $this->configs[$class] = new $class;
             $this->configs[$class]->load($configuration);
         }
@@ -361,9 +376,10 @@ class cache_config_disabled extends cache_config_writer {
     /**
      * Creates the default configuration and saves it.
      *
+     * @param bool $forcesave Ignored because we are disabled!
      * @return array
      */
-    public static function create_default_configuration() {
+    public static function create_default_configuration($forcesave = false) {
         global $CFG;
 
         // HACK ALERT.
index b509273..2abfabc 100644 (file)
@@ -180,6 +180,85 @@ class cache_definition_mappings_form extends moodleform {
     }
 }
 
+/**
+ * Form to set definition sharing option
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_definition_sharing_form extends moodleform {
+    /**
+     * The definition of the form
+     */
+    protected final function definition() {
+        $definition = $this->_customdata['definition'];
+        $sharingoptions = $this->_customdata['sharingoptions'];
+        $form = $this->_form;
+
+        $form->addElement('hidden', 'definition', $definition);
+        $form->setType('definition', PARAM_SAFEPATH);
+        $form->addElement('hidden', 'action', 'editdefinitionsharing');
+        $form->setType('action', PARAM_ALPHA);
+
+        // We use a group here for validation.
+        $count = 0;
+        $group = array();
+        foreach ($sharingoptions as $value => $text) {
+            $count++;
+            $group[] = $form->createElement('checkbox', $value, null, $text);
+        }
+        $form->addGroup($group, 'sharing', get_string('sharing', 'cache'), '<br />');
+        $form->setType('sharing', PARAM_INT);
+
+        $form->addElement('text', 'userinputsharingkey', get_string('userinputsharingkey', 'cache'));
+        $form->addHelpButton('userinputsharingkey', 'userinputsharingkey', 'cache');
+        $form->disabledIf('userinputsharingkey', 'sharing['.cache_definition::SHARING_INPUT.']', 'notchecked');
+        $form->setType('userinputsharingkey', PARAM_ALPHANUMEXT);
+
+        $values = array_keys($sharingoptions);
+        if (in_array(cache_definition::SHARING_ALL, $values)) {
+            // If you share with all thenthe other options don't really make sense.
+            foreach ($values as $value) {
+                $form->disabledIf('sharing['.$value.']', 'sharing['.cache_definition::SHARING_ALL.']', 'checked');
+            }
+            $form->disabledIf('userinputsharingkey', 'sharing['.cache_definition::SHARING_ALL.']', 'checked');
+        }
+
+        $this->add_action_buttons();
+    }
+
+    /**
+     * Sets the data for this form.
+     *
+     * @param array $data
+     */
+    public function set_data($data) {
+        if (!isset($data['sharing'])) {
+            // Set the default value here. mforms doesn't handle defaults very nicely.
+            $data['sharing'] = cache_administration_helper::get_definition_sharing_options(cache_definition::SHARING_DEFAULT);
+        }
+        parent::set_data($data);
+    }
+
+    /**
+     * Validates this form
+     *
+     * @param array $data
+     * @param array $files
+     * @return array
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        if (count($errors) === 0 && !isset($data['sharing'])) {
+            // They must select at least one sharing option.
+            $errors['sharing'] = get_string('sharingrequired', 'cache');
+        }
+        return $errors;
+    }
+}
+
 /**
  * Form to set the mappings for a mode.
  *
index 617ac7d..8124fdc 100644 (file)
@@ -388,19 +388,14 @@ class cache_config_writer extends cache_config {
      * This function calls config_save, however it is safe to continue using it afterwards as this function should only ever
      * be called when there is no configuration file already.
      *
+     * @param bool $forcesave If set to true then we will forcefully save the default configuration file.
      * @return true|array Returns true if the default configuration was successfully created.
      *     Returns a configuration array if it could not be saved. This is a bad situation. Check your error logs.
      */
-    public static function create_default_configuration() {
-        global $CFG;
-
+    public static function create_default_configuration($forcesave = false) {
         // HACK ALERT.
         // We probably need to come up with a better way to create the default stores, or at least ensure 100% that the
         // default store plugins are protected from deletion.
-        require_once($CFG->dirroot.'/cache/stores/file/lib.php');
-        require_once($CFG->dirroot.'/cache/stores/session/lib.php');
-        require_once($CFG->dirroot.'/cache/stores/static/lib.php');
-
         $writer = new self;
         $writer->configstores = self::get_default_stores();
         $writer->configdefinitions = self::locate_definitions();
@@ -433,7 +428,7 @@ class cache_config_writer extends cache_config {
         $factory = cache_factory::instance();
         // We expect the cache to be initialising presently. If its not then something has gone wrong and likely
         // we are now in a loop.
-        if ($factory->get_state() !== cache_factory::STATE_INITIALISING) {
+        if (!$forcesave && $factory->get_state() !== cache_factory::STATE_INITIALISING) {
             return $writer->generate_configuration_array();
         }
         $factory->set_state(cache_factory::STATE_SAVING);
@@ -447,6 +442,12 @@ class cache_config_writer extends cache_config {
      * @return array
      */
     protected static function get_default_stores() {
+        global $CFG;
+
+        require_once($CFG->dirroot.'/cache/stores/file/lib.php');
+        require_once($CFG->dirroot.'/cache/stores/session/lib.php');
+        require_once($CFG->dirroot.'/cache/stores/static/lib.php');
+
         return array(
             'default_application' => array(
                 'name' => 'default_application',
@@ -551,6 +552,20 @@ class cache_config_writer extends cache_config {
      * @param array $definitions
      */
     private function write_definitions_to_cache(array $definitions) {
+
+        // Preserve the selected sharing option when updating the definitions.
+        // This is set by the user and should never come from caches.php.
+        foreach ($definitions as $key => $definition) {
+            unset($definitions[$key]['selectedsharingoption']);
+            unset($definitions[$key]['userinputsharingkey']);
+            if (isset($this->configdefinitions[$key]) && isset($this->configdefinitions[$key]['selectedsharingoption'])) {
+                $definitions[$key]['selectedsharingoption'] = $this->configdefinitions[$key]['selectedsharingoption'];
+            }
+            if (isset($this->configdefinitions[$key]) && isset($this->configdefinitions[$key]['userinputsharingkey'])) {
+                $definitions[$key]['userinputsharingkey'] = $this->configdefinitions[$key]['userinputsharingkey'];
+            }
+        }
+
         $this->configdefinitions = $definitions;
         foreach ($this->configdefinitionmappings as $key => $mapping) {
             if (!array_key_exists($mapping['definition'], $definitions)) {
@@ -619,6 +634,29 @@ class cache_config_writer extends cache_config {
         $this->config_save();
         return $this->siteidentifier;
     }
+
+    /**
+     * Sets the selected sharing options and key for a definition.
+     *
+     * @param string $definition The name of the definition to set for.
+     * @param int $sharingoption The sharing option to set.
+     * @param string|null $userinputsharingkey The user input key or null.
+     * @throws coding_exception
+     */
+    public function set_definition_sharing($definition, $sharingoption, $userinputsharingkey = null) {
+        if (!array_key_exists($definition, $this->configdefinitions)) {
+            throw new coding_exception('Invalid definition name passed when updating sharing options.');
+        }
+        if (!($this->configdefinitions[$definition]['sharingoptions'] & $sharingoption)) {
+            throw new coding_exception('Invalid sharing option passed when updating definition.');
+        }
+        $this->configdefinitions[$definition]['selectedsharingoption'] = (int)$sharingoption;
+        if (!empty($userinputsharingkey)) {
+            $this->configdefinitions[$definition]['userinputsharingkey'] = (string)$userinputsharingkey;
+        }
+        $this->config_save();
+    }
+
 }
 
 /**
@@ -790,29 +828,66 @@ abstract class cache_administration_helper extends cache_helper {
                 'mode' => $definition['mode'],
                 'component' => $definition['component'],
                 'area' => $definition['area'],
-                'mappings' => $mappings
+                'mappings' => $mappings,
+                'sharingoptions' => self::get_definition_sharing_options($definition['sharingoptions'], false),
+                'selectedsharingoption' => self::get_definition_sharing_options($definition['selectedsharingoption'], true),
+                'userinputsharingkey' => $definition['userinputsharingkey']
             );
         }
         return $return;
     }
 
+    /**
+     * Given a sharing option hash this function returns an array of strings that can be used to describe it.
+     *
+     * @param int $sharingoption The sharing option hash to get strings for.
+     * @param bool $isselectedoptions Set to true if the strings will be used to view the selected options.
+     * @return array An array of lang_string's.
+     */
+    public static function get_definition_sharing_options($sharingoption, $isselectedoptions = true) {
+        $options = array();
+        $prefix = ($isselectedoptions) ? 'sharingselected' : 'sharing';
+        if ($sharingoption & cache_definition::SHARING_ALL) {
+            $options[cache_definition::SHARING_ALL] = new lang_string($prefix.'_all', 'cache');
+        }
+        if ($sharingoption & cache_definition::SHARING_SITEID) {
+            $options[cache_definition::SHARING_SITEID] = new lang_string($prefix.'_siteid', 'cache');
+        }
+        if ($sharingoption & cache_definition::SHARING_VERSION) {
+            $options[cache_definition::SHARING_VERSION] = new lang_string($prefix.'_version', 'cache');
+        }
+        if ($sharingoption & cache_definition::SHARING_INPUT) {
+            $options[cache_definition::SHARING_INPUT] = new lang_string($prefix.'_input', 'cache');
+        }
+        return $options;
+    }
+
     /**
      * Returns all of the actions that can be performed on a definition.
      * @param context $context
      * @return array
      */
-    public static function get_definition_actions(context $context) {
+    public static function get_definition_actions(context $context, array $definition) {
         if (has_capability('moodle/site:config', $context)) {
-            return array(
-                array(
-                    'text' => get_string('editmappings', 'cache'),
-                    'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping', 'sesskey' => sesskey()))
-                ),
-                array(
-                    'text' => get_string('purge', 'cache'),
-                    'url' => new moodle_url('/cache/admin.php', array('action' => 'purgedefinition', 'sesskey' => sesskey()))
-                )
+            $actions = array();
+            // Edit mappings.
+            $actions[] = array(
+                'text' => get_string('editmappings', 'cache'),
+                'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping', 'sesskey' => sesskey()))
+            );
+            // Edit sharing.
+            if (count($definition['sharingoptions']) > 1) {
+                $actions[] = array(
+                    'text' => get_string('editsharing', 'cache'),
+                    'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionsharing', 'sesskey' => sesskey()))
+                );
+            }
+            // Purge.
+            $actions[] = array(
+                'text' => get_string('purge', 'cache'),
+                'url' => new moodle_url('/cache/admin.php', array('action' => 'purgedefinition', 'sesskey' => sesskey()))
             );
+            return $actions;
         }
         return array();
     }
index 98a20c5..685b838 100644 (file)
@@ -211,10 +211,9 @@ class core_cache_renderer extends plugin_renderer_base {
      * Displays definition summaries
      *
      * @param array $definitions
-     * @param array $actions
      * @return string HTML
      */
-    public function definition_summaries(array $definitions, array $actions) {
+    public function definition_summaries(array $definitions, context $context) {
         $table = new html_table();
         $table->head = array(
             get_string('definition', 'cache'),
@@ -222,6 +221,7 @@ class core_cache_renderer extends plugin_renderer_base {
             get_string('component', 'cache'),
             get_string('area', 'cache'),
             get_string('mappings', 'cache'),
+            get_string('sharing', 'cache'),
             get_string('actions', 'cache'),
         );
         $table->colclasses = array(
@@ -230,12 +230,14 @@ class core_cache_renderer extends plugin_renderer_base {
             'component',
             'area',
             'mappings',
+            'sharing',
             'actions'
         );
         $table->data = array();
 
         $none = new lang_string('none', 'cache');
         foreach ($definitions as $id => $definition) {
+            $actions = cache_administration_helper::get_definition_actions($context, $definition);
             $htmlactions = array();
             foreach ($actions as $action) {
                 $action['url']->param('definition', $id);
@@ -253,6 +255,7 @@ class core_cache_renderer extends plugin_renderer_base {
                 $definition['component'],
                 $definition['area'],
                 $mapping,
+                join(', ', $definition['selectedsharingoption']),
                 join(', ', $htmlactions)
             ));
             $row->attributes['class'] = 'definition-'.$definition['component'].'-'.$definition['area'];
index 54dfe93..607a4d2 100644 (file)
@@ -195,6 +195,17 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
         return $supported;
     }
 
+    /**
+     * Returns false as this store does not support multiple identifiers.
+     * (This optional function is a performance optimisation; it must be
+     * consistent with the value from get_supported_features.)
+     *
+     * @return bool False
+     */
+    public function supports_multiple_identifiers() {
+        return false;
+    }
+
     /**
      * Returns the supported modes as a combined int.
      *
index deb4c9b..edd55d7 100644 (file)
@@ -46,5 +46,11 @@ class cachestore_memcache_addinstance_form extends cachestore_addinstance_form {
         $form->addHelpButton('servers', 'servers', 'cachestore_memcache');
         $form->addRule('servers', get_string('required'), 'required');
         $form->setType('servers', PARAM_RAW);
+
+        $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->setDefault('prefix', 'mdl_');
     }
 }
\ No newline at end of file
index 585fd53..5cba887 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 $string['pluginname'] = 'Memcache';
+$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['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 906b355..882ab37 100644 (file)
@@ -51,6 +51,12 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      */
     protected $connection;
 
+    /**
+     * Key prefix for this memcache.
+     * @var string
+     */
+    protected $prefix;
+
     /**
      * An array of servers to use in the connection args.
      * @var array
@@ -81,6 +87,12 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      */
     protected $definition;
 
+    /**
+     * Default prefix for key names.
+     * @var string
+     */
+    const DEFAULT_PREFIX = 'mdl_';
+
     /**
      * Constructs the store instance.
      *
@@ -111,6 +123,11 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
             }
             $this->servers[] = $server;
         }
+        if (empty($configuration['prefix'])) {
+            $this->prefix = self::DEFAULT_PREFIX;
+        } else {
+            $this->prefix = $configuration['prefix'];
+        }
 
         $this->connection = new Memcache;
         foreach ($this->servers as $server) {
@@ -181,6 +198,17 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         return self::SUPPORTS_NATIVE_TTL;
     }
 
+    /**
+     * Returns false as this store does not support multiple identifiers.
+     * (This optional function is a performance optimisation; it must be
+     * consistent with the value from get_supported_features.)
+     *
+     * @return bool False
+     */
+    public function supports_multiple_identifiers() {
+        return false;
+    }
+
     /**
      * Returns the supported modes as a combined int.
      *
@@ -201,7 +229,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         if (strlen($key) > 245) {
             $key = '_sha1_'.sha1($key);
         }
-        $key = 'mdl_'.$key;
+        $key = $this->prefix . $key;
         return $key;
     }
 
@@ -327,6 +355,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         }
         return array(
             'servers' => $servers,
+            'prefix' => $data->prefix,
         );
     }
 
@@ -345,6 +374,12 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
             }
             $data['servers'] = join("\n", $servers);
         }
+        if (!empty($config['prefix'])) {
+            $data['prefix'] = $config['prefix'];
+        } else {
+            $data['prefix'] = self::DEFAULT_PREFIX;
+        }
+
         $editform->set_data($data);
     }
 
index f986c2a..b11b08a 100644 (file)
@@ -26,6 +26,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version = 2013050100;    // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2013050700;    // The current module version (Date: YYYYMMDDXX)
 $plugin->requires = 2013050100;    // Requires this Moodle version.
-$plugin->component = 'cachestore_memcache';  // Full name of the plugin.
\ No newline at end of file
+$plugin->component = 'cachestore_memcache';  // Full name of the plugin.
index 98d3955..6e3834c 100644 (file)
@@ -205,6 +205,17 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
         return self::SUPPORTS_NATIVE_TTL;
     }
 
+    /**
+     * Returns false as this store does not support multiple identifiers.
+     * (This optional function is a performance optimisation; it must be
+     * consistent with the value from get_supported_features.)
+     *
+     * @return bool False
+     */
+    public function supports_multiple_identifiers() {
+        return false;
+    }
+
     /**
      * Returns the supported modes as a combined int.
      *
index 39ebbf3..b2f7778 100644 (file)
@@ -141,6 +141,17 @@ class cachestore_session extends session_data_store implements cache_is_key_awar
                self::IS_SEARCHABLE;
     }
 
+    /**
+     * Returns false as this store does not support multiple identifiers.
+     * (This optional function is a performance optimisation; it must be
+     * consistent with the value from get_supported_features.)
+     *
+     * @return bool False
+     */
+    public function supports_multiple_identifiers() {
+        return false;
+    }
+
     /**
      * Returns the supported modes as a combined int.
      *
index cc74f74..405474a 100644 (file)
@@ -136,6 +136,17 @@ class cachestore_static extends static_data_store implements cache_is_key_aware
                self::SUPPORTS_NATIVE_TTL;
     }
 
+    /**
+     * Returns false as this store does not support multiple identifiers.
+     * (This optional function is a performance optimisation; it must be
+     * consistent with the value from get_supported_features.)
+     *
+     * @return bool False
+     */
+    public function supports_multiple_identifiers() {
+        return false;
+    }
+
     /**
      * Returns the supported modes as a combined int.
      *
index b13a4b5..5293eec 100644 (file)
@@ -1305,4 +1305,73 @@ class cache_phpunit_tests extends advanced_testcase {
         $this->assertTrue($cache->delete('a'));
         $this->assertFalse($cache->has('a'));
     }
+
+    /**
+     * Test the static cache_helper method purge_stores_used_by_definition.
+     */
+    public function test_purge_stores_used_by_definition() {
+        $instance = cache_config_phpunittest::instance(true);
+        $instance->phpunit_add_definition('phpunit/test_purge_stores_used_by_definition', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'test_purge_stores_used_by_definition'
+        ));
+        $cache = cache::make('phpunit', 'test_purge_stores_used_by_definition');
+        $this->assertInstanceOf('cache_application', $cache);
+        $this->assertTrue($cache->set('test', 'test'));
+        unset($cache);
+
+        cache_helper::purge_stores_used_by_definition('phpunit', 'test_purge_stores_used_by_definition');
+
+        $cache = cache::make('phpunit', 'test_purge_stores_used_by_definition');
+        $this->assertInstanceOf('cache_application', $cache);
+        $this->assertFalse($cache->get('test'));
+    }
+
+    /**
+     * Test purge routines.
+     */
+    public function test_purge_routines() {
+        $instance = cache_config_phpunittest::instance(true);
+        $instance->phpunit_add_definition('phpunit/purge1', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'purge1'
+        ));
+        $instance->phpunit_add_definition('phpunit/purge2', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'purge2',
+            'requireidentifiers' => array(
+                'id'
+            )
+        ));
+
+        $factory = cache_factory::instance();
+        $definition = $factory->create_definition('phpunit', 'purge1');
+        $this->assertFalse($definition->has_required_identifiers());
+        $cache = $factory->create_cache($definition);
+        $this->assertInstanceOf('cache_application', $cache);
+        $this->assertTrue($cache->set('test', 'test'));
+        $this->assertTrue($cache->has('test'));
+        cache_helper::purge_by_definition('phpunit', 'purge1');
+        $this->assertFalse($cache->has('test'));
+
+        $factory = cache_factory::instance();
+        $definition = $factory->create_definition('phpunit', 'purge2');
+        $this->assertTrue($definition->has_required_identifiers());
+        $cache = $factory->create_cache($definition);
+        $this->assertInstanceOf('cache_application', $cache);
+        $this->assertTrue($cache->set('test', 'test'));
+        $this->assertTrue($cache->has('test'));
+        cache_helper::purge_stores_used_by_definition('phpunit', 'purge2');
+        $this->assertFalse($cache->has('test'));
+
+        try {
+            cache_helper::purge_by_definition('phpunit', 'purge2');
+            $this->fail('Should not be able to purge a definition required identifiers without providing them.');
+        } catch (coding_exception $ex) {
+            $this->assertContains('Identifier required for cache has not been provided', $ex->getMessage());
+        }
+    }
 }
index b4de9d0..bdb2291 100644 (file)
@@ -58,6 +58,14 @@ class cache_config_phpunittest extends cache_config_writer {
         $this->configdefinitions[$area] = $properties;
     }
 
+    /**
+     * Removes a definition.
+     * @param string $name
+     */
+    public function phpunit_remove_definition($name) {
+        unset($this->configdefinitions[$name]);
+    }
+
     /**
      * Removes the configured stores so that there are none available.
      */
index 004a537..56397f2 100644 (file)
@@ -154,15 +154,16 @@ class cache_config_writer_phpunit_tests extends advanced_testcase {
      */
     public function test_update_definitions() {
         $config = cache_config_writer::instance();
-        $earlydefinitions = $config->get_definitions();
-        unset($config);
-        cache_factory::reset();
+        // Remove the definition.
+        $config->phpunit_remove_definition('core/string');
+        $definitions = $config->get_definitions();
+        // Check it is gone.
+        $this->assertFalse(array_key_exists('core/string', $definitions));
+        // Update definitions. This should re-add it.
         cache_config_writer::update_definitions();
-
-        $config = cache_config_writer::instance();
-        $latedefinitions = $config->get_definitions();
-
-        $this->assertSame($latedefinitions, $earlydefinitions);
+        $definitions = $config->get_definitions();
+        // Check it is back again.
+        $this->assertTrue(array_key_exists('core/string', $definitions));
     }
 
     /**
diff --git a/cohort/externallib.php b/cohort/externallib.php
new file mode 100755 (executable)
index 0000000..6821459
--- /dev/null
@@ -0,0 +1,642 @@
+<?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/>.
+
+/**
+ * External cohort API
+ *
+ * @package    core_cohort
+ * @category   external
+ * @copyright  MediaTouch 2000 srl
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once("$CFG->libdir/externallib.php");
+
+class core_cohort_external extends external_api {
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function create_cohorts_parameters() {
+        return new external_function_parameters(
+            array(
+                'cohorts' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'categorytype' => new external_single_structure(
+                                array(
+                                    'type' => new external_value(PARAM_TEXT, 'the name of the field: id (numeric value
+                                        of course category id) or idnumber (alphanumeric value of idnumber course category)
+                                        or system (value ignored)'),
+                                    'value' => new external_value(PARAM_RAW, 'the value of the categorytype')
+                                )
+                            ),
+                            'name' => new external_value(PARAM_RAW, 'cohort name'),
+                            'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
+                            'description' => new external_value(PARAM_RAW, 'cohort description', VALUE_OPTIONAL),
+                            'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Create one or more cohorts
+     *
+     * @param array $cohorts An array of cohorts to create.
+     * @return array An array of arrays
+     * @since Moodle 2.5
+     */
+    public static function create_cohorts($cohorts) {
+        global $CFG, $DB;
+        require_once("$CFG->dirroot/cohort/lib.php");
+
+        $params = self::validate_parameters(self::create_cohorts_parameters(), array('cohorts' => $cohorts));
+
+        $transaction = $DB->start_delegated_transaction();
+
+        $syscontext = context_system::instance();
+        $cohortids = array();
+
+        foreach ($params['cohorts'] as $cohort) {
+            $cohort = (object)$cohort;
+
+            // Category type (context id).
+            $categorytype = $cohort->categorytype;
+            if (!in_array($categorytype['type'], array('idnumber', 'id', 'system'))) {
+                throw new invalid_parameter_exception('category type must be id, idnumber or system:' . $categorytype['type']);
+            }
+            if ($categorytype['type'] === 'system') {
+                $cohort->contextid = $syscontext->id;
+            } else if ($catid = $DB->get_field('course_categories', 'id', array($categorytype['type'] => $categorytype['value']))) {
+                $catcontext = context_coursecat::instance($catid);
+                $cohort->contextid = $catcontext->id;
+            } else {
+                throw new invalid_parameter_exception('category not exists: category '
+                    .$categorytype['type'].' = '.$categorytype['value']);
+            }
+            // Make sure that the idnumber doesn't already exist.
+            if ($DB->record_exists('cohort', array('idnumber' => $cohort->idnumber))) {
+                throw new invalid_parameter_exception('record already exists: idnumber='.$cohort->idnumber);
+            }
+            $context = context::instance_by_id($cohort->contextid, MUST_EXIST);
+            if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) {
+                throw new invalid_parameter_exception('Invalid context');
+            }
+            self::validate_context($context);
+            require_capability('moodle/cohort:manage', $context);
+
+            // Validate format.
+            $cohort->descriptionformat = external_validate_format($cohort->descriptionformat);
+            $cohort->id = cohort_add_cohort($cohort);
+
+            list($cohort->description, $cohort->descriptionformat) =
+                external_format_text($cohort->description, $cohort->descriptionformat,
+                        $context->id, 'cohort', 'description', $cohort->id);
+            $cohortids[] = (array)$cohort;
+        }
+        $transaction->allow_commit();
+
+        return $cohortids;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.5
+     */
+    public static function create_cohorts_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_INT, 'cohort id'),
+                    'name' => new external_value(PARAM_RAW, 'cohort name'),
+                    'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
+                    'description' => new external_value(PARAM_RAW, 'cohort description'),
+                    'descriptionformat' => new external_format_value('description'),
+                )
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function delete_cohorts_parameters() {
+        return new external_function_parameters(
+            array(
+                'cohortids' => new external_multiple_structure(new external_value(PARAM_INT, 'cohort ID')),
+            )
+        );
+    }
+
+    /**
+     * Delete cohorts
+     *
+     * @param array $cohortids
+     * @return null
+     * @since Moodle 2.5
+     */
+    public static function delete_cohorts($cohortids) {
+        global $CFG, $DB;
+        require_once("$CFG->dirroot/cohort/lib.php");
+
+        $params = self::validate_parameters(self::delete_cohorts_parameters(), array('cohortids' => $cohortids));
+
+        $transaction = $DB->start_delegated_transaction();
+
+        foreach ($params['cohortids'] as $cohortid) {
+            // Validate params.
+            $cohortid = validate_param($cohortid, PARAM_INT);
+            $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+
+            // Now security checks.
+            $context = context::instance_by_id($cohort->contextid, MUST_EXIST);
+            if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) {
+                throw new invalid_parameter_exception('Invalid context');
+            }
+            self::validate_context($context);
+            require_capability('moodle/cohort:manage', $context);
+            cohort_delete_cohort($cohort);
+        }
+        $transaction->allow_commit();
+
+        return null;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return null
+     * @since Moodle 2.5
+     */
+    public static function delete_cohorts_returns() {
+        return null;
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function get_cohorts_parameters() {
+        return new external_function_parameters(
+            array(
+                'cohortids' => new external_multiple_structure(new external_value(PARAM_INT, 'Cohort ID')
+                    , 'List of cohort id. A cohort id is an integer.'),
+            )
+        );
+    }
+
+    /**
+     * Get cohorts definition specified by ids
+     *
+     * @param array $cohortids array of cohort ids
+     * @return array of cohort objects (id, courseid, name)
+     * @since Moodle 2.5
+     */
+    public static function get_cohorts($cohortids) {
+        global $DB;
+
+        $params = self::validate_parameters(self::get_cohorts_parameters(), array('cohortids' => $cohortids));
+
+        $cohorts = array();
+        foreach ($params['cohortids'] as $cohortid) {
+            // Validate params.
+            $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+
+            // Now security checks.
+            $context = context::instance_by_id($cohort->contextid, MUST_EXIST);
+            if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) {
+                throw new invalid_parameter_exception('Invalid context');
+            }
+            self::validate_context($context);
+            if (!has_any_capability(array('moodle/cohort:manage', 'moodle/cohort:view'), $context)) {
+                throw new required_capability_exception($context, 'moodle/cohort:view', 'nopermissions', '');
+            }
+
+            list($cohort->description, $cohort->descriptionformat) =
+                external_format_text($cohort->description, $cohort->descriptionformat,
+                        $context->id, 'cohort', 'description', $cohort->id);
+
+            $cohorts[] = (array) $cohort;
+        }
+
+        return $cohorts;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.5
+     */
+    public static function get_cohorts_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_NUMBER, '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'),
+                    'descriptionformat' => new external_format_value('description'),
+                )
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function update_cohorts_parameters() {
+        return new external_function_parameters(
+            array(
+                'cohorts' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_NUMBER, 'ID of the cohort'),
+                            'categorytype' => new external_single_structure(
+                                array(
+                                    'type' => new external_value(PARAM_TEXT, 'the name of the field: id (numeric value
+                                        of course category id) or idnumber (alphanumeric value of idnumber course category)
+                                        or system (value ignored)'),
+                                    'value' => new external_value(PARAM_RAW, 'the value of the categorytype')
+                                )
+                            ),
+                            'name' => new external_value(PARAM_RAW, 'cohort name'),
+                            'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
+                            'description' => new external_value(PARAM_RAW, 'cohort description', VALUE_OPTIONAL),
+                            'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Update cohorts
+     *
+     * @param array $cohorts
+     * @return null
+     * @since Moodle 2.5
+     */
+    public static function update_cohorts($cohorts) {
+        global $CFG, $DB;
+        require_once("$CFG->dirroot/cohort/lib.php");
+
+        $params = self::validate_parameters(self::update_cohorts_parameters(), array('cohorts' => $cohorts));
+
+        $transaction = $DB->start_delegated_transaction();
+        $syscontext = context_system::instance();
+
+        foreach ($params['cohorts'] as $cohort) {
+            $cohort = (object) $cohort;
+
+            if (trim($cohort->name) == '') {
+                throw new invalid_parameter_exception('Invalid cohort name');
+            }
+
+            $oldcohort = $DB->get_record('cohort', array('id' => $cohort->id), '*', MUST_EXIST);
+            $oldcontext = context::instance_by_id($oldcohort->contextid, MUST_EXIST);
+            require_capability('moodle/cohort:manage', $oldcontext);
+
+            // Category type (context id).
+            $categorytype = $cohort->categorytype;
+            if (!in_array($categorytype['type'], array('idnumber', 'id', 'system'))) {
+                throw new invalid_parameter_exception('category type must be id, idnumber or system:' . $categorytype['type']);
+            }
+            if ($categorytype['type'] === 'system') {
+                $cohort->contextid = $syscontext->id;
+            } else if ($catid = $DB->get_field('course_categories', 'id', array($categorytype['type'] => $categorytype['value']))) {
+                $cohort->contextid = $DB->get_field('context', 'id', array('instanceid' => $catid,
+                    'contextlevel' => CONTEXT_COURSECAT));
+            } else {
+                throw new invalid_parameter_exception('category not exists: category='.$categorytype['value']);
+            }
+
+            if ($cohort->contextid != $oldcohort->contextid) {
+                $context = context::instance_by_id($cohort->contextid, MUST_EXIST);
+                if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) {
+                    throw new invalid_parameter_exception('Invalid context');
+                }
+
+                self::validate_context($context);
+                require_capability('moodle/cohort:manage', $context);
+            }
+
+            if (!empty($cohort->description)) {
+                $cohort->descriptionformat = external_validate_format($cohort->descriptionformat);
+            }
+
+            cohort_update_cohort($cohort);
+        }
+
+        $transaction->allow_commit();
+
+        return null;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return null
+     * @since Moodle 2.5
+     */
+    public static function update_cohorts_returns() {
+        return null;
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function add_cohort_members_parameters() {
+        return new external_function_parameters (
+            array(
+                'members' => new external_multiple_structure (
+                    new external_single_structure (
+                        array (
+                            'cohorttype' => new external_single_structure (
+                                array(
+                                    'type' => new external_value(PARAM_ALPHANUMEXT, 'The name of the field: id
+                                        (numeric value of cohortid) or idnumber (alphanumeric value of idnumber) '),
+                                    'value' => new external_value(PARAM_RAW, 'The value of the cohort')
+                                )
+                            ),
+                            'usertype' => new external_single_structure (
+                                array(
+                                    'type' => new external_value(PARAM_ALPHANUMEXT, 'The name of the field: id
+                                        (numeric value of id) or username (alphanumeric value of username) '),
+                                    'value' => new external_value(PARAM_RAW, 'The value of the cohort')
+                                )
+                            )
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Add cohort members
+     *
+     * @param array $members of arrays with keys userid, cohortid
+     * @since Moodle 2.5
+     */
+    public static function add_cohort_members($members) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot."/cohort/lib.php");
+
+        $params = self::validate_parameters(self::add_cohort_members_parameters(), array('members' => $members));
+
+        $transaction = $DB->start_delegated_transaction();
+        $warnings = array();
+        foreach ($params['members'] as $member) {
+            // Cohort parameters.
+            $cohorttype = $member['cohorttype'];
+            $cohortparam = array($cohorttype['type'] => $cohorttype['value']);
+            // User parameters.
+            $usertype = $member['usertype'];
+            $userparam = array($usertype['type'] => $usertype['value']);
+            try {
+                // Check parameters.
+                if ($cohorttype['type'] != 'id' && $cohorttype['type'] != 'idnumber') {
+                    $warning = array();
+                    $warning['warningcode'] = '1';
+                    $warning['message'] = 'invalid parameter: cohortype='.$cohorttype['type'];
+                    $warnings[] = $warning;
+                    continue;
+                }
+                if ($usertype['type'] != 'id' && $usertype['type'] != 'username') {
+                    $warning = array();
+                    $warning['warningcode'] = '1';
+                    $warning['message'] = 'invalid parameter: usertype='.$usertype['type'];
+                    $warnings[] = $warning;
+                    continue;
+                }
+                // Extract parameters.
+                if (!$cohortid = $DB->get_field('cohort', 'id', $cohortparam)) {
+                    $warning = array();
+                    $warning['warningcode'] = '2';
+                    $warning['message'] = 'cohort '.$cohorttype['type'].'='.$cohorttype['value'].' not exists';
+                    $warnings[] = $warning;
+                    continue;
+                }
+                if (!$userid = $DB->get_field('user', 'id', array_merge($userparam, array('deleted' => 0,
+                    'mnethostid' => $CFG->mnet_localhost_id)))) {
+                    $warning = array();
+                    $warning['warningcode'] = '2';
+                    $warning['message'] = 'user '.$usertype['type'].'='.$usertype['value'].' not exists';
+                    $warnings[] = $warning;
+                    continue;
+                }
+                if ($DB->record_exists('cohort_members', array('cohortid' => $cohortid, 'userid' => $userid))) {
+                    $warning = array();
+                    $warning['warningcode'] = '3';
+                    $warning['message'] = 'record already exists: cohort('.$cohorttype['type'].'='.$cohorttype['value'].' '.
+                        $usertype['type'].'='.$usertype['value'].')';
+                    $warnings[] = $warning;
+                    continue;
+                }
+                $cohort = $DB->get_record('cohort', array('id'=>$cohortid), '*', MUST_EXIST);
+                $context = context::instance_by_id($cohort->contextid, MUST_EXIST);
+                if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) {
+                    $warning = array();
+                    $warning['warningcode'] = '1';
+                    $warning['message'] = 'Invalid context: '.$context->contextlevel;
+                    $warnings[] = $warning;
+                    continue;
+                }
+                self::validate_context($context);
+            } catch (Exception $e) {
+                throw new moodle_exception('Error', 'cohort', '', $e->getMessage());
+            }
+            if (!has_any_capability(array('moodle/cohort:manage', 'moodle/cohort:assign'), $context)) {
+                throw new required_capability_exception($context, 'moodle/cohort:assign', 'nopermissions', '');
+            }
+            cohort_add_member($cohortid, $userid);
+        }
+        $transaction->allow_commit();
+        // Return.
+        $result = array();
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return null
+     * @since Moodle 2.5
+     */
+    public static function add_cohort_members_returns() {
+        return new external_single_structure(
+            array(
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function delete_cohort_members_parameters() {
+        return new external_function_parameters(
+            array(
+                'members' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'cohortid' => new external_value(PARAM_INT, 'cohort record id'),
+                            'userid' => new external_value(PARAM_INT, 'user id'),
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Delete cohort members
+     *
+     * @param array $members of arrays with keys userid, cohortid
+     * @since Moodle 2.5
+     */
+    public static function delete_cohort_members($members) {
+        global $CFG, $DB;
+        require_once("$CFG->dirroot/cohort/lib.php");
+
+        // Validate parameters.
+        $params = self::validate_parameters(self::delete_cohort_members_parameters(), array('members' => $members));
+
+        $transaction = $DB->start_delegated_transaction();
+
+        foreach ($params['members'] as $member) {
+            $cohortid = $member['cohortid'];
+            $userid = $member['userid'];
+
+            $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+            $user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0, 'mnethostid' => $CFG->mnet_localhost_id),
+                '*', MUST_EXIST);
+
+            // Now security checks.
+            $context = context::instance_by_id($cohort->contextid, MUST_EXIST);
+            if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) {
+                throw new invalid_parameter_exception('Invalid context');
+            }
+            self::validate_context($context);
+            if (!has_any_capability(array('moodle/cohort:manage', 'moodle/cohort:assign'), $context)) {
+                throw new required_capability_exception($context, 'moodle/cohort:assign', 'nopermissions', '');
+            }
+
+            cohort_remove_member($cohort->id, $user->id);
+        }
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return null
+     * @since Moodle 2.5
+     */
+    public static function delete_cohort_members_returns() {
+        return null;
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function get_cohort_members_parameters() {
+        return new external_function_parameters(
+            array(
+                'cohortids' => new external_multiple_structure(new external_value(PARAM_INT, 'Cohort ID')),
+            )
+        );
+    }
+
+    /**
+     * Return all members for a cohort
+     *
+     * @param array $cohortids array of cohort ids
+     * @return array with cohort id keys containing arrays of user ids
+     * @since Moodle 2.5
+     */
+    public static function get_cohort_members($cohortids) {
+        global $DB;
+        $params = self::validate_parameters(self::get_cohort_members_parameters(), array('cohortids' => $cohortids));
+
+        $members = array();
+
+        foreach ($params['cohortids'] as $cohortid) {
+            // Validate params.
+            $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+            // Now security checks.
+            $context = context::instance_by_id($cohort->contextid, MUST_EXIST);
+            if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) {
+                throw new invalid_parameter_exception('Invalid context');
+            }
+            self::validate_context($context);
+            if (!has_any_capability(array('moodle/cohort:manage', 'moodle/cohort:view'), $context)) {
+                throw new required_capability_exception($context, 'moodle/cohort:view', 'nopermissions', '');
+            }
+
+            $cohortmembers = $DB->get_records_sql("SELECT u.id FROM {user} u, {cohort_members} cm
+                WHERE u.id = cm.userid AND cm.cohortid = ?
+                ORDER BY lastname ASC, firstname ASC", array($cohort->id));
+            $members[] = array('cohortid' => $cohortid, 'userids' => array_keys($cohortmembers));
+        }
+        return $members;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.5
+     */
+    public static function get_cohort_members_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'cohortid' => new external_value(PARAM_INT, 'cohort record id'),
+                    'userids' => new external_multiple_structure(new external_value(PARAM_INT, 'user id')),
+                )
+            )
+        );
+    }
+}
index cb7554f..82dd518 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_cohort
 Feature: Add cohorts of users
   In order to create site-wide groups
-  As a moodle admin
+  As an admin
   I need to create cohorts and add users on them
 
   Background:
index 300feaf..e62a13f 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_cohort @_only_local
 Feature: Upload users to a cohort
   In order to quickly fill site-wide groups with users
-  As a moodle admin
+  As an admin
   I need to upload a file with users data containing cohort assigns
 
   @javascript
diff --git a/cohort/tests/externallib_test.php b/cohort/tests/externallib_test.php
new file mode 100755 (executable)
index 0000000..39c2ef7
--- /dev/null
@@ -0,0 +1,395 @@
+<?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/>.
+
+/**
+ * External cohort API
+ *
+ * @package    core_cohort
+ * @category   external
+ * @copyright  MediaTouch 2000 srl
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once($CFG->dirroot . '/cohort/externallib.php');
+
+class core_cohort_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test create_cohorts
+     */
+    public function test_create_cohorts() {
+        global $USER, $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $contextid = context_system::instance()->id;
+
+        $cohort1 = array(
+            'categorytype' => array('type' => 'id', 'value' => '1'),
+            'name' => 'cohort test 1',
+            'idnumber' => 'cohorttest1',
+            'description' => 'This is a description for cohorttest1'
+            );
+
+        $cohort2 = array(
+            'categorytype' => array('type' => 'system', 'value' => ''),
+            'name' => 'cohort test 2',
+            'idnumber' => 'cohorttest2',
+            'description' => 'This is a description for cohorttest2'
+            );
+
+        $cohort3 = array(
+            'categorytype' => array('type' => 'id', 'value' => '1'),
+            'name' => 'cohort test 3',
+            'idnumber' => 'cohorttest3',
+            'description' => 'This is a description for cohorttest3'
+            );
+        $roleid = $this->assignUserCapability('moodle/cohort:manage', $contextid);
+
+        // Call the external function.
+        $createdcohorts = core_cohort_external::create_cohorts(array($cohort1, $cohort2));
+
+        // Check we retrieve the good total number of created cohorts + no error on capability.
+        $this->assertEquals(2, count($createdcohorts));
+
+        foreach ($createdcohorts as $createdcohort) {
+            if ($createdcohort['idnumber'] == $cohort1['idnumber']) {
+                $dbcohort = $DB->get_record('cohort', array('id' => $createdcohort['id']));
+                $conid = $DB->get_field('context', 'id', array('instanceid' => $cohort1['categorytype']['value'],
+                        'contextlevel' => CONTEXT_COURSECAT));
+                $this->assertEquals($dbcohort->contextid, $conid);
+                $this->assertEquals($dbcohort->name, $cohort1['name']);
+                $this->assertEquals($dbcohort->idnumber, $cohort1['idnumber']);
+                $this->assertEquals($dbcohort->description, $cohort1['description']);
+            }
+        }
+
+        // Call without required capability.
+        $this->unassignUserCapability('moodle/cohort:manage', $contextid, $roleid);
+        $this->setExpectedException('required_capability_exception');
+        $createdcohorts = core_cohort_external::create_cohorts(array($cohort3));
+    }
+
+    /**
+     * Test delete_cohorts
+     */
+    public function test_delete_cohorts() {
+        global $USER, $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $cohort1 = self::getDataGenerator()->create_cohort();
+        $cohort2 = self::getDataGenerator()->create_cohort();
+        // Check the cohorts were correctly created.
+        $this->assertEquals(2, $DB->count_records_select('cohort', ' (id = :cohortid1 OR id = :cohortid2)',
+                array('cohortid1' => $cohort1->id, 'cohortid2' => $cohort2->id)));
+
+        $contextid = $cohort1->contextid;
+        $roleid = $this->assignUserCapability('moodle/cohort:manage', $contextid);
+
+        // Call the external function.
+        core_cohort_external::delete_cohorts(array($cohort1->id, $cohort2->id));
+
+        // Check we retrieve no cohorts + no error on capability.
+        $this->assertEquals(0, $DB->count_records_select('cohort', ' (id = :cohortid1 OR id = :cohortid2)',
+                array('cohortid1' => $cohort1->id, 'cohortid2' => $cohort2->id)));
+
+        // Call without required capability.
+        $cohort1 = self::getDataGenerator()->create_cohort();
+        $cohort2 = self::getDataGenerator()->create_cohort();
+        $this->unassignUserCapability('moodle/cohort:manage', $contextid, $roleid);
+        $this->setExpectedException('required_capability_exception');
+        core_cohort_external::delete_cohorts(array($cohort1->id, $cohort2->id));
+    }
+
+    /**
+     * Test get_cohorts
+     */
+    public function test_get_cohorts() {
+        global $USER, $CFG;
+
+        $this->resetAfterTest(true);
+
+        $cohort1 = array(
+            'contextid' => 1,
+            'name' => 'cohortnametest1',
+            'idnumber' => 'idnumbertest1',
+            'description' => 'This is a description for cohort 1'
+            );
+        $cohort1 = self::getDataGenerator()->create_cohort($cohort1);
+        $cohort2 = self::getDataGenerator()->create_cohort();
+
+        $context = context_system::instance();
+        $roleid = $this->assignUserCapability('moodle/cohort:view', $context->id);
+
+        // Call the external function.
+        $returnedcohorts = core_cohort_external::get_cohorts(array(
+            $cohort1->id, $cohort2->id));
+
+        // 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']);
+                $this->assertEquals($cohort1->description, $enrolledcohort['description']);
+            }
+        }
+
+        // Check that a user with cohort:manage can see the cohort.
+        $this->unassignUserCapability('moodle/cohort:view', $context->id, $roleid);
+        $roleid = $this->assignUserCapability('moodle/cohort:manage', $context->id, $roleid);
+        // Call the external function.
+        $returnedcohorts = core_cohort_external::get_cohorts(array(
+            $cohort1->id, $cohort2->id));
+
+        // Check we retrieve the good total number of enrolled cohorts + no error on capability.
+        $this->assertEquals(2, count($returnedcohorts));
+    }
+
+    /**
+     * Test update_cohorts
+     */
+    public function test_update_cohorts() {
+        global $USER, $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $cohort1 = self::getDataGenerator()->create_cohort();
+
+        $cohort1 = array(
+            'id' => $cohort1->id,
+            'categorytype' => array('type' => 'id', 'value' => '1'),
+            'name' => 'cohortnametest1',
+            'idnumber' => 'idnumbertest1',
+            'description' => 'This is a description for cohort 1'
+            );
+
+        $context = context_system::instance();
+        $roleid = $this->assignUserCapability('moodle/cohort:manage', $context->id);
+
+        // Call the external function.
+        core_cohort_external::update_cohorts(array($cohort1));
+
+        $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
+        $contextid = $DB->get_field('context', 'id', array('instanceid' => $cohort1['categorytype']['value'],
+        'contextlevel' => CONTEXT_COURSECAT));
+        $this->assertEquals($dbcohort->contextid, $contextid);
+        $this->assertEquals($dbcohort->name, $cohort1['name']);
+        $this->assertEquals($dbcohort->idnumber, $cohort1['idnumber']);
+        $this->assertEquals($dbcohort->description, $cohort1['description']);
+
+        // Call without required capability.
+        $this->unassignUserCapability('moodle/cohort:manage', $context->id, $roleid);
+        $this->setExpectedException('required_capability_exception');
+        core_cohort_external::update_cohorts(array($cohort1));
+    }
+
+    /**
+     * Test update_cohorts without permission on the dest category.
+     */
+    public function test_update_cohorts_missing_dest() {
+        global $USER, $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $category1 = self::getDataGenerator()->create_category(array(
+            'name' => 'Test category 1'
+        ));
+        $category2 = self::getDataGenerator()->create_category(array(
+            'name' => 'Test category 2'
+        ));
+        $context1 = context_coursecat::instance($category1->id);
+        $context2 = context_coursecat::instance($category2->id);
+
+        $cohort = array(
+            'contextid' => $context1->id,
+            'name' => 'cohortnametest1',
+            'idnumber' => 'idnumbertest1',
+            'description' => 'This is a description for cohort 1'
+            );
+        $cohort1 = self::getDataGenerator()->create_cohort($cohort);
+
+        $roleid = $this->assignUserCapability('moodle/cohort:manage', $context1->id);
+
+        $cohortupdate = array(
+            'id' => $cohort1->id,
+            'categorytype' => array('type' => 'id', 'value' => $category2->id),
+            'name' => 'cohort update',
+            'idnumber' => 'idnumber update',
+            'description' => 'This is a description update'
+            );
+
+        // Call the external function.
+        // Should fail because we don't have permission on the dest category
+        $this->setExpectedException('required_capability_exception');
+        core_cohort_external::update_cohorts(array($cohortupdate));
+    }
+
+    /**
+     * Test update_cohorts without permission on the src category.
+     */
+    public function test_update_cohorts_missing_src() {
+        global $USER, $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $category1 = self::getDataGenerator()->create_category(array(
+            'name' => 'Test category 1'
+        ));
+        $category2 = self::getDataGenerator()->create_category(array(
+            'name' => 'Test category 2'
+        ));
+        $context1 = context_coursecat::instance($category1->id);
+        $context2 = context_coursecat::instance($category2->id);
+
+        $cohort = array(
+            'contextid' => $context1->id,
+            'name' => 'cohortnametest1',
+            'idnumber' => 'idnumbertest1',
+            'description' => 'This is a description for cohort 1'
+            );
+        $cohort1 = self::getDataGenerator()->create_cohort($cohort);
+
+        $roleid = $this->assignUserCapability('moodle/cohort:manage', $context2->id);
+
+        $cohortupdate = array(
+            'id' => $cohort1->id,
+            'categorytype' => array('type' => 'id', 'value' => $category2->id),
+            'name' => 'cohort update',
+            'idnumber' => 'idnumber update',
+            'description' => 'This is a description update'
+            );
+
+        // Call the external function.
+        // Should fail because we don't have permission on the src category
+        $this->setExpectedException('required_capability_exception');
+        core_cohort_external::update_cohorts(array($cohortupdate));
+    }
+
+    /**
+     * Test add_cohort_members
+     */
+    public function test_add_cohort_members() {
+        global $DB;
+
+        $this->resetAfterTest(true); // Reset all changes automatically after this test.
+
+        $contextid = context_system::instance()->id;
+
+        $cohort = array(
+            'contextid' => $contextid,
+            'name' => 'cohortnametest1',
+            'idnumber' => 'idnumbertest1',
+            'description' => 'This is a description for cohort 1'
+            );
+        $cohort0 = self::getDataGenerator()->create_cohort($cohort);
+        // Check the cohorts were correctly created.
+        $this->assertEquals(1, $DB->count_records_select('cohort', ' (id = :cohortid0)',
+            array('cohortid0' => $cohort0->id)));
+
+        $cohort1 = array(
+            'cohorttype' => array('type' => 'id', 'value' => $cohort0->id),
+            'usertype' => array('type' => 'id', 'value' => '1')
+            );
+
+        $roleid = $this->assignUserCapability('moodle/cohort:assign', $contextid);
+
+        // Call the external function.
+        $addcohortmembers = core_cohort_external::add_cohort_members(array($cohort1));
+
+        // Check we retrieve the good total number of created cohorts + no error on capability.
+        $this->assertEquals(1, count($addcohortmembers));
+
+        foreach ($addcohortmembers as $addcohortmember) {
+            $dbcohort = $DB->get_record('cohort_members', array('cohortid' => $cohort0->id));
+            $this->assertEquals($dbcohort->cohortid, $cohort1['cohorttype']['value']);
+            $this->assertEquals($dbcohort->userid, $cohort1['usertype']['value']);
+        }
+
+        // Call without required capability.
+        $cohort2 = array(
+            'cohorttype' => array('type' => 'id', 'value' => $cohort0->id),
+            'usertype' => array('type' => 'id', 'value' => '2')
+            );
+        $this->unassignUserCapability('moodle/cohort:assign', $contextid, $roleid);
+        $this->setExpectedException('required_capability_exception');
+        $addcohortmembers = core_cohort_external::add_cohort_members(array($cohort2));
+    }
+
+    /**
+     * Test delete_cohort_members
+     */
+    public function test_delete_cohort_members() {
+        global $DB;
+
+        $this->resetAfterTest(true); // Reset all changes automatically after this test.
+
+        $cohort1 = self::getDataGenerator()->create_cohort();
+        $user1 = self::getDataGenerator()->create_user();
+        $cohort2 = self::getDataGenerator()->create_cohort();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $context = context_system::instance();
+        $roleid = $this->assignUserCapability('moodle/cohort:assign', $context->id);
+
+        $cohortaddmember1 = array(
+            'cohorttype' => array('type' => 'id', 'value' => $cohort1->id),
+            'usertype' => array('type' => 'id', 'value' => $user1->id)
+            );
+        $cohortmembers1 = core_cohort_external::add_cohort_members(array($cohortaddmember1));
+        $cohortaddmember2 = array(
+            'cohorttype' => array('type' => 'id', 'value' => $cohort2->id),
+            'usertype' => array('type' => 'id', 'value' => $user2->id)
+            );
+        $cohortmembers2 = core_cohort_external::add_cohort_members(array($cohortaddmember2));
+
+        // Check we retrieve no cohorts + no error on capability.
+        $this->assertEquals(2, $DB->count_records_select('cohort_members', ' ((cohortid = :idcohort1 AND userid = :iduser1)
+            OR (cohortid = :idcohort2 AND userid = :iduser2))',
+            array('idcohort1' => $cohort1->id, 'iduser1' => $user1->id, 'idcohort2' => $cohort2->id, 'iduser2' => $user2->id)));
+
+        // Call the external function.
+         $cohortdel1 = array(
+            'cohortid' => $cohort1->id,
+            'userid' => $user1->id
+            );
+         $cohortdel2 = array(
+            'cohortid' => $cohort2->id,
+            'userid' => $user2->id
+            );
+        core_cohort_external::delete_cohort_members(array($cohortdel1, $cohortdel2));
+
+        // Check we retrieve no cohorts + no error on capability.
+        $this->assertEquals(0, $DB->count_records_select('cohort_members', ' ((cohortid = :idcohort1 AND userid = :iduser1)
+            OR (cohortid = :idcohort2 AND userid = :iduser2))',
+            array('idcohort1' => $cohort1->id, 'iduser1' => $user1->id, 'idcohort2' => $cohort2->id, 'iduser2' => $user2->id)));
+
+        // Call without required capability.
+        $this->unassignUserCapability('moodle/cohort:assign', $context->id, $roleid);
+        $this->setExpectedException('required_capability_exception');
+        core_cohort_external::delete_cohort_members(array($cohortdel1, $cohortdel2));
+    }
+}
index baecc20..883fa5a 100644 (file)
@@ -59,7 +59,8 @@ class completion_criteria_date extends completion_criteria {
      */
     public function config_form_display(&$mform, $data = null) {
         $mform->addElement('checkbox', 'criteria_date', get_string('enable'));
-        $mform->addElement('date_selector', 'criteria_date_value', get_string('afterspecifieddate', 'completion'));
+        $mform->addElement('date_selector', 'criteria_date_value', get_string('completionondatevalue', 'core_completion'));
+        $mform->disabledIf('criteria_date_value', 'criteria_date');
 
         // If instance of criteria exists
         if ($this->id) {
index ead1a55..aa34c60 100644 (file)
@@ -61,12 +61,32 @@ class completion_criteria_duration extends completion_criteria {
 
         $mform->addElement('checkbox', 'criteria_duration', get_string('enable'));
 
-        $thresholdmenu=array();
-        for ($i=1; $i<=30; $i++) {
-            $seconds = $i * 86400;
-            $thresholdmenu[$seconds] = get_string('numdays', '', $i);
+        // Populate the duration length drop down.
+        $thresholdmenu = array(
+            // We have strings for 1 - 6 days in the core.
+            86400 => get_string('secondstotime86400', 'core'),
+            172800 => get_string('secondstotime172800', 'core'),
+            259200 => get_string('secondstotime259200', 'core'),
+            345600 => get_string('secondstotime345600', 'core'),
+            432000 => get_string('secondstotime432000', 'core'),
+            518400 => get_string('secondstotime518400', 'core'),
+            518400 => get_string('secondstotime518400', 'core'),
+        );
+        // Append strings for 7 - 30 days (step by 1 day).
+        for ($i = 7; $i <= 30; $i++) {
+            $seconds = $i * DAYSECS;
+            $thresholdmenu[$seconds] = get_string('numdays', 'core', $i);
         }
-        $mform->addElement('select', 'criteria_duration_days', get_string('daysafterenrolment', 'completion'), $thresholdmenu);
+        // Append strings for 40 - 180 days (step by 10 days).
+        for ($i = 40; $i <= 180; $i = $i + 10) {
+            $seconds = $i * DAYSECS;
+            $thresholdmenu[$seconds] = get_string('numdays', 'core', $i);
+        }
+        // Append string for 1 year.
+        $thresholdmenu[365 * DAYSECS] = get_string('numdays', 'core', 365);
+
+        $mform->addElement('select', 'criteria_duration_days', get_string('enrolmentdurationlength', 'core_completion'), $thresholdmenu);
+        $mform->disabledIf('criteria_duration_days', 'criteria_duration');
 
         if ($this->id) {
             $mform->setDefault('criteria_duration', 1);
index c7847c5..2e9655f 100644 (file)
@@ -63,9 +63,9 @@ class completion_criteria_grade extends completion_criteria {
     public function config_form_display(&$mform, $data = null) {
         $mform->addElement('checkbox', 'criteria_grade', get_string('enable'));
         $mform->addElement('text', 'criteria_grade_value', get_string('graderequired', 'completion'));
+        $mform->disabledIf('criteria_grade_value', 'criteria_grade');
         $mform->setType('criteria_grade_value', PARAM_RAW); // Uses unformat_float.
         $mform->setDefault('criteria_grade_value', format_float($data));
-        $mform->addElement('static', 'criteria_grade_value_note', '', get_string('criteriagradenote', 'completion'));
 
         if ($this->id) {
             $mform->setDefault('criteria_grade', 1);
@@ -80,12 +80,14 @@ class completion_criteria_grade extends completion_criteria {
      */
     public function update_config(&$data) {
 
-        $formatedgrade = unformat_float($data->criteria_grade_value);
-        // TODO validation
-        if (!empty($formatedgrade) && is_numeric($formatedgrade)) {
-            $this->course = $data->id;
-            $this->gradepass = $formatedgrade;
-            $this->insert();
+        if (!empty($data->criteria_grade)) {
+            $formatedgrade = unformat_float($data->criteria_grade_value);
+            // TODO validation
+            if (!empty($formatedgrade) && is_numeric($formatedgrade)) {
+                $this->course = $data->id;
+                $this->gradepass = $formatedgrade;
+                $this->insert();
+            }
         }
     }
 
index 62f3072..6eaab0e 100644 (file)
@@ -58,7 +58,7 @@ class completion_criteria_unenrol extends completion_criteria {
      * @param stdClass $data Form data
      */
     public function config_form_display(&$mform, $data = null) {
-        $mform->addElement('checkbox', 'criteria_unenrol', get_string('completiononunenrolment','completion'));
+        $mform->addElement('checkbox', 'criteria_unenrol', get_string('enable'));
 
         if ($this->id) {
             $mform->setDefault('criteria_unenrol', 1);
index 7bf57fb..ef05aeb 100644 (file)
@@ -50,7 +50,8 @@ 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.
-        $xpath = "//table[@id='completion-progress']/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Completed')]";
+        $xpath = "//table[@id='completion-progress']" .
+            "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Completed')]";
 
         return array(
             new Given('I go to the current course activity completion report'),
@@ -67,7 +68,8 @@ class behat_completion extends behat_base {
      */
     public function user_has_not_completed_activity($userfullname, $activityname) {
 
-        $xpath = "//table[@id='completion-progress']/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Not completed')]";
+        $xpath = "//table[@id='completion-progress']" .
+            "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Not completed')]";
         return array(
             new Given('I go to the current course activity completion report'),
             new Given('I hover "' . $xpath . '" "xpath_element"')
@@ -87,7 +89,10 @@ class behat_completion extends behat_base {
 
         // Expand reports node if we can't see the link.
         try {
-            $this->find('xpath', "//*[@id='settingsnav']/descendant::li/descendant::li[not(contains(@class,'collapsed'))]/descendant::p[contains(., 'Activity completion')]");
+            $this->find('xpath', "//*[@id='settingsnav']" .
+                "/descendant::li" .
+                "/descendant::li[not(contains(@class,'collapsed'))]" .
+                "/descendant::p[contains(., 'Activity completion')]");
         } catch (ElementNotFoundException $e) {
             $steps[] = new Given('I expand "Reports" node');
         }
index f483985..5d9eee1 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_completion
 Feature: Allow students to manually mark an activity as complete
   In order to let students decide when an activity is completed
-  As a moodle teacher
+  As a teacher
   I need to allow students to mark activities as completed
 
   @javascript
index 595a974..35b7f09 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_completion
 Feature: Restrict sections availability through completion conditions
   In order to control section's contents access through activities completion
-  As a moodle teacher
+  As a teacher
   I need to restrict sections availability using different conditions
 
   @javascript
index cb017d3..472d05e 100644 (file)
@@ -8,6 +8,6 @@
     "require-dev": {
         "phpunit/phpunit": "3.7.*",
         "phpunit/dbUnit": "1.2.*",
-        "moodlehq/behat-extension": "1.0.*"
+        "moodlehq/behat-extension": "1.25.6"
     }
 }
index 0dd7032..378d341 100644 (file)
@@ -456,6 +456,12 @@ $CFG->admin = 'admin';
 // To ensure they are never used even when available:
 //      $CFG->svgicons = false;
 //
+// Some administration options allow setting the path to executable files. This can
+// potentially cause a security risk. Set this option to true to disable editing
+// those config settings via the web. They will need to be set explicitly in the
+// config.php file
+//      $CFG->preventexecpath = true;
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index 559ccdb..8c16a87 100644 (file)
@@ -1,32 +1,32 @@
 <?php
 
-///////////////////////////////////////////////////////////////////////////
-//                                                                       //
-// NOTICE OF COPYRIGHT                                                   //
-//                                                                       //
-// Moodle - Modular Object-Oriented Dynamic Learning Environment         //
-//          http://moodle.com                                            //
-//                                                                       //
-// Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com     //
-//                                                                       //
-// This program 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 2 of the License, or     //
-// (at your option) any later version.                                   //
-//                                                                       //
-// This program 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:                          //
-//                                                                       //
-//          http://www.gnu.org/copyleft/gpl.html                         //
-//                                                                       //
-///////////////////////////////////////////////////////////////////////////
-
-// Edit course completion settings
-
-require_once('../config.php');
-require_once('lib.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/>.
+
+/**
+ * Edit course completion settings
+ *
+ * @package     core_completion
+ * @category    completion
+ * @copyright   2009 Catalyst IT Ltd
+ * @author      Aaron Barnes <aaronb@catalyst.net.nz>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__.'/../config.php');
+require_once($CFG->dirroot.'/course/lib.php');
 require_once($CFG->libdir.'/completionlib.php');
 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
@@ -37,15 +37,15 @@ require_once($CFG->dirroot.'/completion/criteria/completion_criteria_grade.php')
 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_role.php');
 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_course.php');
 require_once $CFG->libdir.'/gradelib.php';
-require_once('completion_form.php');
+require_once($CFG->dirroot.'/course/completion_form.php');
 
 $id = required_param('id', PARAM_INT);       // course id
 
-/// basic access control checks
-if ($id) { // editing course
+// Perform some basic access control checks.
+if ($id) {
 
     if($id == SITEID){
-        // don't allow editing of  'site course' using this from
+        // Don't allow editing of 'site course' using this form.
         print_error('cannoteditsiteform');
     }
 
@@ -60,40 +60,34 @@ if ($id) { // editing course
     print_error('needcourseid');
 }
 
-/// Set up the page
-$streditcompletionsettings = get_string("editcoursecompletionsettings", 'completion');
+// Set up the page.
 $PAGE->set_course($course);
 $PAGE->set_url('/course/completion.php', array('id' => $course->id));
-//$PAGE->navbar->add($streditcompletionsettings);
 $PAGE->set_title($course->shortname);
 $PAGE->set_heading($course->fullname);
 $PAGE->set_pagelayout('standard');
 
-/// first create the form
-$form = new course_completion_form('completion.php?id='.$id, compact('course'));
+// Create the settings form instance.
+$form = new course_completion_form('completion.php?id='.$id, array('course' => $course));
 
-// now override defaults if course already exists
 if ($form->is_cancelled()){
     redirect($CFG->wwwroot.'/course/view.php?id='.$course->id);
 
 } else if ($data = $form->get_data()) {
-
     $completion = new completion_info($course);
 
-/// process criteria unlocking if requested
+    // Process criteria unlocking if requested.
     if (!empty($data->settingsunlock)) {
-
         $completion->delete_course_completion_data();
 
-        // Return to form (now unlocked)
-        redirect($CFG->wwwroot."/course/completion.php?id=$course->id");
+        // Return to form (now unlocked).
+        redirect($PAGE->url);
     }
 
-/// process data if submitted
-    // Delete old criteria
+    // Delete old criteria.
     $completion->clear_criteria();
 
-    // Loop through each criteria type and run update_config
+    // Loop through each criteria type and run its update_config() method.
     global $COMPLETION_CRITERIA_TYPES;
     foreach ($COMPLETION_CRITERIA_TYPES as $type) {
         $class = 'completion_criteria_'.$type;
@@ -101,8 +95,7 @@ if ($form->is_cancelled()){
         $criterion->update_config($data);
     }
 
-    // Handle aggregation methods
-    // Overall aggregation
+    // Handle overall aggregation.
     $aggdata = array(
         'course'        => $data->id,
         'criteriatype'  => null
@@ -111,7 +104,7 @@ if ($form->is_cancelled()){
     $aggregation->setMethod($data->overall_aggregation);
     $aggregation->save();
 
-    // Activity aggregation
+    // Handle activity aggregation.
     if (empty($data->activity_aggregation)) {
         $data->activity_aggregation = 0;
     }
@@ -121,7 +114,7 @@ if ($form->is_cancelled()){
     $aggregation->setMethod($data->activity_aggregation);
     $aggregation->save();
 
-    // Course aggregation
+    // Handle course aggregation.
     if (empty($data->course_aggregation)) {
         $data->course_aggregation = 0;
     }
@@ -131,7 +124,7 @@ if ($form->is_cancelled()){
     $aggregation->setMethod($data->course_aggregation);
     $aggregation->save();
 
-    // Role aggregation
+    // Handle role aggregation.
     if (empty($data->role_aggregation)) {
         $data->role_aggregation = 0;
     }
@@ -141,18 +134,17 @@ if ($form->is_cancelled()){
     $aggregation->setMethod($data->role_aggregation);
     $aggregation->save();
 
+    // Log changes.
     add_to_log($course->id, 'course', 'completion updated', 'completion.php?id='.$course->id);
 
+    // Redirect to the course main page.
     $url = new moodle_url('/course/view.php', array('id' => $course->id));
     redirect($url);
 }
 
-
-/// Print the form
-
-
+// Print the form.
 echo $OUTPUT->header();
-echo $OUTPUT->heading($streditcompletionsettings);
+echo $OUTPUT->heading(get_string('editcoursecompletionsettings', 'core_completion'));
 
 $form->display();
 
index 7dab70a..ce4880a 100644 (file)
 <?php
 
-///////////////////////////////////////////////////////////////////////////
-//                                                                       //
-// NOTICE OF COPYRIGHT                                                   //
-//                                                                       //
-// Moodle - Modular Object-Oriented Dynamic Learning Environment         //
-//          http://moodle.com                                            //
-//                                                                       //
-// Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com     //
-//                                                                       //
-// This program 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 2 of the License, or     //
-// (at your option) any later version.                                   //
-//                                                                       //
-// This program 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:                          //
-//                                                                       //
-//          http://www.gnu.org/copyleft/gpl.html                         //
-//                                                                       //
-///////////////////////////////////////////////////////////////////////////
-
-if (!defined('MOODLE_INTERNAL')) {
-    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
-}
+// 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/>.
+
+/**
+ * Edit course completion settings - the form definition.
+ *
+ * @package     core_completion
+ * @category    completion
+ * @copyright   2009 Catalyst IT Ltd
+ * @author      Aaron Barnes <aaronb@catalyst.net.nz>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir.'/formslib.php');
 require_once($CFG->libdir.'/completionlib.php');
 
+/**
+ * Defines the course completion settings form.
+ */
 class course_completion_form extends moodleform {
 
-    function definition() {
-        global $USER, $CFG, $DB, $js_enabled;
+    /**
+     * Defines the form fields.
+     */
+    public function definition() {
+        global $USER, $CFG, $DB;
 
         $courseconfig = get_config('moodlecourse');
-        $mform    =& $this->_form;
-
-        $course   = $this->_customdata['course'];
+        $mform = $this->_form;
+        $course = $this->_customdata['course'];
         $completion = new completion_info($course);
 
         $params = array(
             'course'  => $course->id
         );
 
-
-/// form definition
-//--------------------------------------------------------------------------------
-
-        // Check if there is existing criteria completions
+        // Check if there are existing criteria completions.
         if ($completion->is_course_locked()) {
             $mform->addElement('header', 'completionsettingslocked', get_string('completionsettingslocked', 'completion'));
             $mform->addElement('static', '', '', get_string('err_settingslocked', 'completion'));
             $mform->addElement('submit', 'settingsunlock', get_string('unlockcompletiondelete', 'completion'));
         }
 
-        // Get array of all available aggregation methods
+        // Get array of all available aggregation methods.
         $aggregation_methods = $completion->get_aggregation_methods();
 
-        // Overall criteria aggregation
-        $mform->addElement('header', 'overallcriteria', get_string('overallcriteriaaggregation', 'completion'));
-        $mform->addElement('select', 'overall_aggregation', get_string('aggregationmethod', 'completion'), $aggregation_methods);
+        // Overall criteria aggregation.
+        $mform->addElement('header', 'overallcriteria', get_string('general', 'core_form'));
+        // Map aggregation methods to context-sensitive human readable dropdown menu.
+        $overallaggregationmenu = array();
+        foreach ($aggregation_methods as $methodcode => $methodname) {
+            if ($methodcode === COMPLETION_AGGREGATION_ALL) {
+                $overallaggregationmenu[COMPLETION_AGGREGATION_ALL] = get_string('overallaggregation_all', 'core_completion');
+            } else if ($methodcode === COMPLETION_AGGREGATION_ANY) {
+                $overallaggregationmenu[COMPLETION_AGGREGATION_ANY] = get_string('overallaggregation_any', 'core_completion');
+            } else {
+                $overallaggregationmenu[$methodcode] = $methodname;
+            }
+        }
+        $mform->addElement('select', 'overall_aggregation', get_string('overallaggregation', 'core_completion'), $overallaggregationmenu);
         $mform->setDefault('overall_aggregation', $completion->get_aggregation_method());
 
-        // Course prerequisite completion criteria
-        $mform->addElement('header', 'courseprerequisites', get_string('completiondependencies', 'completion'));
-
-        // Get applicable courses
-        $courses = $DB->get_records_sql(
-            "
-                SELECT DISTINCT
-                    c.id,
-                    c.category,
-                    c.fullname,
-                    cc.id AS selected
-                FROM
-                    {course} c
-                LEFT JOIN
-                    {course_completion_criteria} cc
-                 ON cc.courseinstance = c.id
-                AND cc.course = {$course->id}
-                INNER JOIN
-                    {course_completion_criteria} ccc
-                 ON ccc.course = c.id
-                WHERE
-                    c.enablecompletion = ".COMPLETION_ENABLED."
-                AND c.id <> {$course->id}
-            "
-        );
+        // Activity completion criteria
+        $label = get_string('coursecompletioncondition', 'core_completion', get_string('activitiescompleted', 'core_completion'));
+        $mform->addElement('header', 'activitiescompleted', $label);
+        // Get the list of currently specified conditions and expand the section if some are found.
+        $current = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY);
+        if (!empty($current)) {
+            $mform->setExpanded('activitiescompleted');
+        }
 
-        if (!empty($courses)) {
-            if (count($courses) > 1) {
-                $mform->addElement('select', 'course_aggregation', get_string('aggregationmethod', 'completion'), $aggregation_methods);
-                $mform->setDefault('course_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE));
+        $activities = $completion->get_activities();
+        if (!empty($activities)) {
+
+            foreach ($activities as $activity) {
+                $params_a = array('moduleinstance' => $activity->id);
+                $criteria = new completion_criteria_activity(array_merge($params, $params_a));
+                $criteria->config_form_display($mform, $activity);
+            }
+            $mform->addElement('static', 'criteria_role_note', '', get_string('activitiescompletednote', 'core_completion'));
+
+            if (count($activities) > 1) {
+                // Map aggregation methods to context-sensitive human readable dropdown menu.
+                $activityaggregationmenu = array();
+                foreach ($aggregation_methods as $methodcode => $methodname) {
+                    if ($methodcode === COMPLETION_AGGREGATION_ALL) {
+                        $activityaggregationmenu[COMPLETION_AGGREGATION_ALL] = get_string('activityaggregation_all', 'core_completion');
+                    } else if ($methodcode === COMPLETION_AGGREGATION_ANY) {
+                        $activityaggregationmenu[COMPLETION_AGGREGATION_ANY] = get_string('activityaggregation_any', 'core_completion');
+                    } else {
+                        $activityaggregationmenu[$methodcode] = $methodname;
+                    }
+                }
+                $mform->addElement('select', 'activity_aggregation', get_string('activityaggregation', 'core_completion'), $activityaggregationmenu);
+                $mform->setDefault('activity_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY));
             }
 
-            // Get category list
+        } else {
+            $mform->addElement('static', 'noactivities', '', get_string('err_noactivities', 'completion'));
+        }
+
+        // Course prerequisite completion criteria.
+        $label = get_string('coursecompletioncondition', 'core_completion', get_string('dependenciescompleted', 'core_completion'));
+        $mform->addElement('header', 'courseprerequisites', $label);
+        // Get the list of currently specified conditions and expand the section if some are found.
+        $current = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_COURSE);
+        if (!empty($current)) {
+            $mform->setExpanded('courseprerequisites');
+        }
+
+        // Get applicable courses (prerequisites).
+        $courses = $DB->get_records_sql("
+                SELECT DISTINCT c.id, c.category, c.fullname, cc.id AS selected
+                  FROM {course} c
+             LEFT JOIN {course_completion_criteria} cc ON cc.courseinstance = c.id AND cc.course = {$course->id}
+            INNER JOIN {course_completion_criteria} ccc ON ccc.course = c.id
+                 WHERE c.enablecompletion = ".COMPLETION_ENABLED."
+                       AND c.id <> {$course->id}");
+
+        if (!empty($courses)) {
+            // Get category list.
             require_once($CFG->libdir. '/coursecatlib.php');
             $list = coursecat::make_categories_list();
 
-            // Get course list for select box
+            // Get course list for select box.
             $selectbox = array();
             $selected = array();
             foreach ($courses as $c) {
-                $selectbox[$c->id] = $list[$c->category] . ' / ' . format_string($c->fullname, true, array('context' => context_course::instance($c->id)));
+                $selectbox[$c->id] = $list[$c->category] . ' / ' . format_string($c->fullname, true,
+                    array('context' => context_course::instance($c->id)));
 
-                // If already selected
+                // If already selected ...
                 if ($c->selected) {
                     $selected[] = $c->id;
                 }
             }
 
-            // Show multiselect box
-            $mform->addElement('select', 'criteria_course', get_string('coursesavailable', 'completion'), $selectbox, array('multiple' => 'multiple', 'size' => 6));
+            // Show multiselect box.
+            $mform->addElement('select', 'criteria_course', get_string('coursesavailable', 'completion'), $selectbox,
+                array('multiple' => 'multiple', 'size' => 6));
 
-            // Select current criteria
+            // Select current criteria.
             $mform->setDefault('criteria_course', $selected);
 
-            // Explain list
+            // Explain list.
             $mform->addElement('static', 'criteria_courses_explaination', '', get_string('coursesavailableexplaination', 'completion'));
 
-        } else {
-            $mform->addElement('static', 'nocourses', '', get_string('err_nocourses', 'completion'));
-        }
-
-        // Manual self completion
-        $mform->addElement('header', 'manualselfcompletion', get_string('manualselfcompletion', 'completion'));
-        $criteria = new completion_criteria_self($params);
-        $criteria->config_form_display($mform);
-
-        // Role completion criteria
-        $mform->addElement('header', 'roles', get_string('manualcompletionby', 'completion'));
-
-        $roles = get_roles_with_capability('moodle/course:markcomplete', CAP_ALLOW, context_course::instance($course->id, IGNORE_MISSING));
-
-        if (!empty($roles)) {
-            $mform->addElement('select', 'role_aggregation', get_string('aggregationmethod', 'completion'), $aggregation_methods);
-            $mform->setDefault('role_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE));
-
-            foreach ($roles as $role) {
-                $params_a = array('role' => $role->id);
-                $criteria = new completion_criteria_role(array_merge($params, $params_a));
-                $criteria->config_form_display($mform, $role);
-            }
-        } else {
-            $mform->addElement('static', 'noroles', '', get_string('err_noroles', 'completion'));
-        }
-
-        // Activity completion criteria
-        $mform->addElement('header', 'activitiescompleted', get_string('activitiescompleted', 'completion'));
-
-        $activities = $completion->get_activities();
-        if (!empty($activities)) {
-            if (count($activities) > 1) {
-                $mform->addElement('select', 'activity_aggregation', get_string('aggregationmethod', 'completion'), $aggregation_methods);
-                $mform->setDefault('activity_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY));
+            if (count($courses) > 1) {
+                // Map aggregation methods to context-sensitive human readable dropdown menu.
+                $courseaggregationmenu = array();
+                foreach ($aggregation_methods as $methodcode => $methodname) {
+                    if ($methodcode === COMPLETION_AGGREGATION_ALL) {
+                        $courseaggregationmenu[COMPLETION_AGGREGATION_ALL] = get_string('courseaggregation_all', 'core_completion');
+                    } else if ($methodcode === COMPLETION_AGGREGATION_ANY) {
+                        $courseaggregationmenu[COMPLETION_AGGREGATION_ANY] = get_string('courseaggregation_any', 'core_completion');
+                    } else {
+                        $courseaggregationmenu[$methodcode] = $methodname;
+                    }
+                }
+                $mform->addElement('select', 'course_aggregation', get_string('courseaggregation', 'core_completion'), $courseaggregationmenu);
+                $mform->setDefault('course_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE));
             }
 
-            foreach ($activities as $activity) {
-                $params_a = array('moduleinstance' => $activity->id);
-                $criteria = new completion_criteria_activity(array_merge($params, $params_a));
-                $criteria->config_form_display($mform, $activity);
-            }
         } else {
-            $mform->addElement('static', 'noactivities', '', get_string('err_noactivities', 'completion'));
+            $mform->addElement('static', 'nocourses', '', get_string('err_nocourses', 'completion'));
         }
 
         // Completion on date
-        $mform->addElement('header', 'date', get_string('date'));
+        $label = get_string('coursecompletioncondition', 'core_completion', get_string('completionondate', 'core_completion'));
+        $mform->addElement('header', 'date', $label);
+        // Expand the condition section if it is currently enabled.
+        $current = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_DATE);
+        if (!empty($current)) {
+            $mform->setExpanded('date');
+        }
         $criteria = new completion_criteria_date($params);
         $criteria->config_form_display($mform);
 
         // Completion after enrolment duration
-        $mform->addElement('header', 'duration', get_string('durationafterenrolment', 'completion'));
+        $label = get_string('coursecompletioncondition', 'core_completion', get_string('enrolmentduration', 'core_completion'));
+        $mform->addElement('header', 'duration', $label);
+        // Expand the condition section if it is currently enabled.
+        $current = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_DURATION);
+        if (!empty($current)) {
+            $mform->setExpanded('duration');
+        }
         $criteria = new completion_criteria_duration($params);
         $criteria->config_form_display($mform);
 
-        // Completion on course grade
-        $mform->addElement('header', 'grade', get_string('coursegrade', 'completion'));
+        // Completion on unenrolment
+        $label = get_string('coursecompletioncondition', 'core_completion', get_string('unenrolment', 'core_completion'));
+        $mform->addElement('header', 'unenrolment', $label);
+        // Expand the condition section if it is currently enabled.
+        $current = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_UNENROL);
+        if (!empty($current)) {
+            $mform->setExpanded('unenrolment');
+        }
+        $criteria = new completion_criteria_unenrol($params);
+        $criteria->config_form_display($mform);
 
-        // Grade enable and passing grade
+        // Completion on course grade
+        $label = get_string('coursecompletioncondition', 'core_completion', get_string('coursegrade', 'core_completion'));
+        $mform->addElement('header', 'grade', $label);
+        // Expand the condition section if it is currently enabled.
+        $current = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_GRADE);
+        if (!empty($current)) {
+            $mform->setExpanded('grade');
+        }
         $course_grade = $DB->get_field('grade_items', 'gradepass', array('courseid' => $course->id, 'itemtype' => 'course'));
         if (!$course_grade) {
             $course_grade = '0.00000';
@@ -188,34 +229,65 @@ class course_completion_form extends moodleform {
         $criteria = new completion_criteria_grade($params);
         $criteria->config_form_display($mform, $course_grade);
 
-        // Completion on unenrolment
-        $mform->addElement('header', 'unenrolment', get_string('unenrolment', 'completion'));
-        $criteria = new completion_criteria_unenrol($params);
+        // Manual self completion
+        $label = get_string('coursecompletioncondition', 'core_completion', get_string('manualselfcompletion', 'core_completion'));
+        $mform->addElement('header', 'manualselfcompletion', $label);
+        // Expand the condition section if it is currently enabled.
+        $current = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_SELF);
+        if (!empty($current)) {
+            $mform->setExpanded('manualselfcompletion');
+        }
+        $criteria = new completion_criteria_self($params);
         $criteria->config_form_display($mform);
+        $mform->addElement('static', 'criteria_self_note', '', get_string('manualselfcompletionnote', 'core_completion'));
 
+        // Role completion criteria
+        $label = get_string('coursecompletioncondition', 'core_completion', get_string('manualcompletionby', 'core_completion'));
+        $mform->addElement('header', 'roles', $label);
+        // Expand the condition section if it is currently enabled.
+        $current = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_ROLE);
+        if (!empty($current)) {
+            $mform->setExpanded('roles');
+        }
+        $roles = get_roles_with_capability('moodle/course:markcomplete', CAP_ALLOW, context_course::instance($course->id, IGNORE_MISSING));
 
-//--------------------------------------------------------------------------------
+        if (!empty($roles)) {
+            foreach ($roles as $role) {
+                $params_a = array('role' => $role->id);
+                $criteria = new completion_criteria_role(array_merge($params, $params_a));
+                $criteria->config_form_display($mform, $role);
+            }
+            $mform->addElement('static', 'criteria_role_note', '', get_string('manualcompletionbynote', 'core_completion'));
+            // Map aggregation methods to context-sensitive human readable dropdown menu.
+            $roleaggregationmenu = array();
+            foreach ($aggregation_methods as $methodcode => $methodname) {
+                if ($methodcode === COMPLETION_AGGREGATION_ALL) {
+                    $roleaggregationmenu[COMPLETION_AGGREGATION_ALL] = get_string('roleaggregation_all', 'core_completion');
+                } else if ($methodcode === COMPLETION_AGGREGATION_ANY) {
+                    $roleaggregationmenu[COMPLETION_AGGREGATION_ANY] = get_string('roleaggregation_any', 'core_completion');
+                } else {
+                    $roleaggregationmenu[$methodcode] = $methodname;
+                }
+            }
+            $mform->addElement('select', 'role_aggregation', get_string('roleaggregation', 'core_completion'), $roleaggregationmenu);
+            $mform->setDefault('role_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE));
+
+        } else {
+            $mform->addElement('static', 'noroles', '', get_string('err_noroles', 'completion'));
+        }
+
+        // Add common action buttons.
         $this->add_action_buttons();
-//--------------------------------------------------------------------------------
+
+        // Add hidden fields.
         $mform->addElement('hidden', 'id', $course->id);
         $mform->setType('id', PARAM_INT);
 
-        // If the criteria are locked, freeze values and submit button
+        // If the criteria are locked, freeze values and submit button.
         if ($completion->is_course_locked()) {
             $except = array('settingsunlock');
             $mform->hardFreezeAllVisibleExcept($except);
             $mform->addElement('cancel');
         }
     }
-
-
-/// perform some extra moodle validation
-    function validation($data, $files) {
-        global $DB, $CFG;
-
-        $errors = parent::validation($data, $files);
-
-        return $errors;
-    }
 }
-?>
index f0ccf9b..a12f187 100644 (file)
@@ -72,6 +72,8 @@ abstract class format_base {
     protected $formatoptions = array();
     /** @var array cached instances */
     private static $instances = array();
+    /** @var array plugin name => class name. */
+    private static $classesforformat = array('site' => 'site');
 
     /**
      * Creates a new instance of class
@@ -94,24 +96,28 @@ abstract class format_base {
      * @return string
      */
     protected static final function get_format_or_default($format) {
-        if ($format === 'site') {
-            return $format;
+        if (array_key_exists($format, self::$classesforformat)) {
+            return self::$classesforformat[$format];
         }
+
         $plugins = get_sorted_course_formats();
-        if (in_array($format, $plugins)) {
-            return $format;
+        foreach ($plugins as $plugin) {
+            self::$classesforformat[$plugin] = $plugin;
+        }
+
+        if (array_key_exists($format, self::$classesforformat)) {
+            return self::$classesforformat[$format];
         }
+
         // Else return default format
         $defaultformat = get_config('moodlecourse', 'format');
         if (!in_array($defaultformat, $plugins)) {
             // when default format is not set correctly, use the first available format
             $defaultformat = reset($plugins);
         }
-        static $warningprinted = array();
-        if (empty($warningprinted[$format])) {
-            debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER);
-            $warningprinted[$format] = true;
-        }
+        debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER);
+
+        self::$classesforformat[$format] = $defaultformat;
         return $defaultformat;
     }
 
index 7586505..fe69315 100644 (file)
@@ -720,8 +720,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 // 0-section is displayed a little different then the others
                 if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) {
                     echo $this->section_header($thissection, $course, false, 0);
-                    echo $this->courserenderer->course_section_cm_list($course, $thissection);
-                    echo $this->courserenderer->course_section_add_cm_control($course, 0);
+                    echo $this->courserenderer->course_section_cm_list($course, $thissection, 0);
+                    echo $this->courserenderer->course_section_add_cm_control($course, 0, 0);
                     echo $this->section_footer();
                 }
                 continue;
@@ -751,8 +751,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             } else {
                 echo $this->section_header($thissection, $course, false, 0);
                 if ($thissection->uservisible) {
-                    echo $this->courserenderer->course_section_cm_list($course, $thissection);
-                    echo $this->courserenderer->course_section_add_cm_control($course, $section);
+                    echo $this->courserenderer->course_section_cm_list($course, $thissection, 0);
+                    echo $this->courserenderer->course_section_add_cm_control($course, $section, 0);
                 }
                 echo $this->section_footer();
             }
@@ -766,7 +766,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                     continue;
                 }
                 echo $this->stealth_section_header($section);
-                echo $this->courserenderer->course_section_cm_list($course, $thissection);
+                echo $this->courserenderer->course_section_cm_list($course, $thissection, 0);
                 echo $this->stealth_section_footer();
             }
 
index 526e184..f1663f7 100644 (file)
@@ -2349,7 +2349,7 @@ function update_course($data, $editoroptions = NULL) {
         // prevent nulls and 0 in category field
         unset($data->category);
     }
-    $movecat = (isset($data->category) and $oldcourse->category != $data->category);
+    $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category);
 
     if (!isset($data->visible)) {
         // data not from form, add missing visibility info
@@ -2359,6 +2359,7 @@ function update_course($data, $editoroptions = NULL) {
     if ($data->visible != $oldcourse->visible) {
         // reset the visibleold flag when manually hiding/unhiding course
         $data->visibleold = $data->visible;
+        $changesincoursecat = true;
     } else {
         if ($movecat) {
             $newcategory = $DB->get_record('course_categories', array('id'=>$data->category));
@@ -2387,6 +2388,9 @@ function update_course($data, $editoroptions = NULL) {
     fix_course_sortorder();
     // purge appropriate caches in case fix_course_sortorder() did not change anything
     cache_helper::purge_by_event('changesincourse');
+    if ($changesincoursecat) {
+        cache_helper::purge_by_event('changesincoursecat');
+    }
 
     // Test for and remove blocks which aren't appropriate anymore
     blocks_remove_inappropriate($course);
index 5817b32..fbc384d 100644 (file)
@@ -314,8 +314,6 @@ if (can_edit_in_category()) {
     $PAGE->set_button($courserenderer->course_search_form('', 'navbar'));
 }
 
-$displaylist[0] = get_string('top');
-
 // Start output.
 echo $OUTPUT->header();
 
@@ -345,6 +343,7 @@ if (!empty($searchcriteria)) {
     echo html_writer::table($table);
 } else {
     // Print the category selector.
+    $displaylist = coursecat::make_categories_list();
     $select = new single_select(new moodle_url('/course/manage.php'), 'categoryid', $displaylist, $coursecat->id, null, 'switchcategory');
     $select->set_label(get_string('categories').':');
 
index 614ac8a..daf6d9c 100644 (file)
@@ -438,7 +438,7 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) {
     }
 
     $completion = new completion_info($course);
-    if ($completion->is_enabled()) {
+    if ($completion->is_enabled() && !empty($moduleinfo->completionunlocked)) {
         // Update completion settings.
         $cm->completion                = $moduleinfo->completion;
         $cm->completiongradeitemnumber = $moduleinfo->completiongradeitemnumber;
index dd5652a..f83bf79 100644 (file)
@@ -312,8 +312,11 @@ abstract class moodleform_mod extends moodleform {
         }
 
         // Completion: Don't let them choose automatic completion without turning
-        // on some conditions
-        if (array_key_exists('completion', $data) && $data['completion']==COMPLETION_TRACKING_AUTOMATIC) {
+        // on some conditions. Ignore this check when completion settings are
+        // locked, as the options are then disabled.
+        if (array_key_exists('completion', $data) &&
+                $data['completion'] == COMPLETION_TRACKING_AUTOMATIC &&
+                !empty($data['completionunlocked'])) {
             if (empty($data['completionview']) && empty($data['completionusegrade']) &&
                 !$this->completion_rule_enabled($data)) {
                 $errors['completion'] = get_string('badautocompletion', 'completion');
index 05f285a..0e3da60 100644 (file)
@@ -1165,13 +1165,15 @@ class core_course_renderer extends plugin_renderer_base {
         }
 
         // display course category if necessary (for example in search results)
-        if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT
-                && ($cat = coursecat::get($course->category, IGNORE_MISSING))) {
-            $content .= html_writer::start_tag('div', array('class' => 'coursecat'));
-            $content .= get_string('category').': '.
-                    html_writer::link(new moodle_url('/course/index.php', array('categoryid' => $cat->id)),
-                            $cat->get_formatted_name(), array('class' => $cat->visible ? '' : 'dimmed'));
-            $content .= html_writer::end_tag('div'); // .coursecat
+        if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT) {
+            require_once($CFG->libdir. '/coursecatlib.php');
+            if ($cat = coursecat::get($course->category, IGNORE_MISSING)) {
+                $content .= html_writer::start_tag('div', array('class' => 'coursecat'));
+                $content .= get_string('category').': '.
+                        html_writer::link(new moodle_url('/course/index.php', array('categoryid' => $cat->id)),
+                                $cat->get_formatted_name(), array('class' => $cat->visible ? '' : 'dimmed'));
+                $content .= html_writer::end_tag('div'); // .coursecat
+            }
         }
 
         return $content;
@@ -1621,6 +1623,7 @@ class core_course_renderer extends plugin_renderer_base {
         $content = '';
         if (!empty($searchcriteria)) {
             // print search results
+            require_once($CFG->libdir. '/coursecatlib.php');
 
             $displayoptions = array('sort' => array('displayname' => 1));
             // take the current page and number of results per page from query
@@ -1685,6 +1688,7 @@ class core_course_renderer extends plugin_renderer_base {
      */
     public function tagged_courses($tagid) {
         global $CFG;
+        require_once($CFG->libdir. '/coursecatlib.php');
         $displayoptions = array('limit' => $CFG->coursesperpage);
         $displayoptions['viewmoreurl'] = new moodle_url('/course/search.php',
                 array('tagid' => $tagid, 'page' => 1, 'perpage' => $CFG->coursesperpage));
index 82cd3f2..78c342a 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_course @_cross_browser
 Feature: Toggle activities groups mode from the course page
   In order to split activities in groups
-  As a moodle teacher
+  As a teacher
   I need to change quickly the group mode of an activity
 
   @javascript
@@ -18,7 +18,7 @@ Feature: Toggle activities groups mode from the course page
     And I log in as "teacher1"
     And I follow "Course 1"
     And I turn editing mode on
-    And I add a "forum" to section "1" and I fill the form with:
+    And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name |
       | Description | Test forum description |
     And I follow "Edit settings"
@@ -28,20 +28,20 @@ Feature: Toggle activities groups mode from the course page
     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 I click on "No groups (Click to change)" "link" in the "li.activity.forum" "css_element"
+    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 I reload the page
     And "Separate groups (Click to change)" "link" should exists
     And ".//a//img[contains(@src, 'groups')]" "xpath_element" should exists
-    And I click on "Separate groups (Click to change)" "link" in the "li.activity.forum" "css_element"
+    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 I reload the page
     And "Visible groups (Click to change)" "link" should exists
     And ".//a//img[contains(@src, 'groupv')]" "xpath_element" should exists
-    And I click on "Visible groups (Click to change)" "link" in the "li.activity.forum" "css_element"
+    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
index 5f960d8..5621e49 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_course @_cross_browser
 Feature: Indent items on the course page
   In order to create a structured view of activities
-  As a moodle teacher
+  As a teacher
   I need to move activities and resources to left and right
 
   @javascript
@@ -23,19 +23,15 @@ Feature: Indent items on the course page
     And I add a "Glossary" to section "1" and I fill the form with:
       | Name | Test glossary name |
       | Description | Test glossary description |
-    When I click on "Move right" "link" in the "#section-1 li.glossary" "css_element"
-    And I wait "2" seconds
+    When I indent right "Test glossary name" activity
     Then "#section-1 li.glossary div.mod-indent-1" "css_element" should exists
-    And I click on "Move right" "link" in the "#section-1 li.glossary" "css_element"
-    And I wait "2" seconds
+    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 "#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
-    And I click on "Move left" "link" in the "#section-1 li.glossary" "css_element"
-    And I wait "2" seconds
-    And I click on "Move left" "link" in the "#section-1 li.glossary" "css_element"
-    And I wait "2" seconds
+    And I indent left "Test glossary name" activity
+    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
index 6f60299..6ef447f 100644 (file)
@@ -1,7 +1,7 @@
 @core @core_course @_cross_browser
 Feature: Toggle activities visibility from the course page
   In order to delay activities availability
-  As a moodle teacher
+  As a teacher
   I need to quickly change the visibility of an activity
 
   @javascript
@@ -20,15 +20,15 @@ Feature: Toggle activities visibility from the course page
     And I log in as "teacher1"
     And I follow "Course 1"
     And I turn editing mode on
-    And I add a "forum" to section "1" and I fill the form with:
+    And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name |
       | Description | Test forum description |
       | Visible | Show |
-    When I click on "Hide" "link" in the "li.activity.forum" "css_element"
+    When I click on "Hide" "link" in the "Test forum name" activity
     Then "Test forum name" activity should be hidden
-    And I click on "Show" "link" in the "li.activity.forum" "css_element"
+    And I click on "Show" "link" in the "Test forum name" activity
     And "Test forum name" activity should be visible
-    And I click on "Hide" "link" in the "li.activity.forum" "css_element"
+    And I click on "Hide" "link" in the "Test forum name" activity
     And "Test forum name" activity should be hidden
     And I reload the page
     And "Test forum name" activity should be hidden
index 71a59b9..2bc6c26 100644 (file)
@@ -30,6 +30,7 @@ require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
 use Behat\Behat\Context\Step\Given as Given,
     Behat\Gherkin\Node\TableNode as TableNode,
     Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\DriverException as DriverException,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
 
 /**
@@ -93,7 +94,7 @@ class behat_course extends behat_base {
      *
      * @When /^I add a "(?P<activity_or_resource_name_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)" and I fill the form with:$/
      * @param string $activity The activity name
-     * @param string $section The section number
+     * @param int $section The section number
      * @param TableNode $data The activity field/value data
      */
     public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
@@ -111,7 +112,7 @@ class behat_course extends behat_base {
      * @Given /^I add a "(?P<activity_or_resource_name_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)"$/
      * @throws ElementNotFoundException Thrown by behat_base::find
      * @param string $activity
-     * @param string $section
+     * @param int $section
      */
     public function i_add_to_section($activity, $section) {
 
@@ -126,9 +127,9 @@ class behat_course extends behat_base {
 
             // Clicks the selected activity if it exists.
             $activity = ucfirst($activity);
-            $activityxpath = "//div[@id='chooseform']/descendant::label
-/descendant::span[contains(concat(' ', @class, ' '), ' typename ')][contains(.,'" . $activity . "')]
-/parent::label/child::input";
+            $activityxpath = "//div[@id='chooseform']/descendant::label" .
+                "/descendant::span[contains(concat(' ', @class, ' '), ' typename ')][contains(.,'" . $activity . "')]" .
+                "/parent::label/child::input";
             $activitynode = $this->find('xpath', $activityxpath);
             $activitynode->doubleClick();
 
@@ -136,8 +137,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(' ', @class, ' '), ' section_add_menus ')]" .
+                "/descendant::select[contains(., '" . $activity . "')]";
             $selectnode = $this->find('xpath', $selectxpath);
             $selectnode->selectOption($activity);
 
@@ -190,11 +191,13 @@ class behat_course extends behat_base {
      * @param int $sectionnumber
      */
     public function i_show_section($sectionnumber) {
-        $showicon = $this->show_section_icon_exists($sectionnumber);
-        $showicon->click();
+        $showlink = $this->show_section_icon_exists($sectionnumber);
+        $showlink->click();
 
         // It requires time.
-        $this->getSession()->wait(5000, false);
+        if ($this->running_javascript()) {
+            $this->getSession()->wait(5000, false);
+        }
     }
 
     /**
@@ -204,11 +207,13 @@ class behat_course extends behat_base {
      * @param int $sectionnumber
      */
     public function i_hide_section($sectionnumber) {
-        $hideicon = $this->hide_section_icon_exists($sectionnumber);
-        $hideicon->click();
+        $hidelink = $this->hide_section_icon_exists($sectionnumber);
+        $hidelink->click();
 
         // It requires time.
-        $this->getSession()->wait(5000, false);
+        if ($this->running_javascript()) {
+            $this->getSession()->wait(5000, false);
+        }
     }
 
     /**
@@ -279,12 +284,15 @@ 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(' ', @class, ' '), ' activityinstance ')]" .
+                        "/a[contains(concat(' ', @class, ' '), ' dimmed ')]", $dimmedexception, $activity);
 
-                    // To check that the visibility is not clickable we check the funcionality rather than the applied style.
-                    $visibilityiconnode = $this->find('css', 'a.editing_show img', false, $activity);
-                    $visibilityiconnode->click();
+                    // Non-JS browsers can not click on img elements.
+                    if ($this->running_javascript()) {
+                        // To check that the visibility is not clickable we check the funcionality rather than the applied style.
+                        $visibilityiconnode = $this->find('css', 'a.editing_show img', false, $activity);
+                        $visibilityiconnode->click();
+                    }
 
                     // We ensure that we still see the show icon.
                     $visibilityiconnode = $this->find('css', 'a.editing_show img', $visibilityexception, $activity);
@@ -321,6 +329,48 @@ class behat_course extends behat_base {
         }
     }
 
+    /**
+     * Moves up the specified section, this step only works with Javascript disabled. Editing mode should be on.
+     *
+     * @Given /^I move up section "(?P<section_number>\d+)"$/
+     * @throws DriverException Step not available when Javascript is enabled
+     * @param int $sectionnumber
+     */
+    public function i_move_up_section($sectionnumber) {
+
+        if ($this->running_javascript()) {
+            throw new DriverException('Move a section up step is not available with Javascript enabled');
+        }
+
+        // Ensures the section exists.
+        $sectionxpath = $this->section_exists($sectionnumber);
+
+        // Follows the link
+        $moveuplink = $this->get_node_in_container('link', get_string('moveup'), 'xpath_element', $sectionxpath);
+        $moveuplink->click();
+    }
+
+    /**
+     * Moves down the specified section, this step only works with Javascript disabled. Editing mode should be on.
+     *
+     * @Given /^I move down section "(?P<section_number>\d+)"$/
+     * @throws DriverException Step not available when Javascript is enabled
+     * @param int $sectionnumber
+     */
+    public function i_move_down_section($sectionnumber) {
+
+        if ($this->running_javascript()) {
+            throw new DriverException('Move a section down step is not available with Javascript enabled');
+        }
+
+        // Ensures the section exists.
+        $sectionxpath = $this->section_exists($sectionnumber);
+
+        // Follows the link
+        $movedownlink = $this->get_node_in_container('link', get_string('movedown'), 'xpath_element', $sectionxpath);
+        $movedownlink->click();
+    }
+
     /**
      * Checks that the specified activity is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
      *
@@ -382,6 +432,198 @@ class behat_course extends behat_base {
 
     }
 
+    /**
+     * Moves the specified activity to the first slot of a section. Editing mode should be on.
+     *
+     * @Given /^I move "(?P<activity_name_string>(?:[^"]|\\")*)" activity to section "(?P<section_number>\d+)"$/
+     * @param string $activityname The activity name
+     * @param int $sectionnumber The number of section
+     */
+    public function i_move_activity_to_section($activityname, $sectionnumber) {
+
+        // Ensure the destination is valid.
+        $sectionxpath = $this->section_exists($sectionnumber);
+
+        $activitynode = $this->get_activity_element('.editing_move img', 'css_element', $activityname);
+
+        // JS enabled.
+        if ($this->running_javascript()) {
+
+            $destinationxpath = $sectionxpath . "/descendant::ul[contains(@class, 'yui3-dd-drop')]";
+
+            return array(
+                new Given('I drag "' . $activitynode->getXpath() . '" "xpath_element" and I drop it in "' . $destinationxpath . '" "xpath_element"'),
+            );
+
+        } else {
+            // Following links with no-JS.
+
+            // 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"'),
+            );
+        }
+    }
+
+    /**
+     * Edits the activity name through the edit activity; this step only works with Javascript enabled. Editing mode should be on.
+     *
+     * @Given /^I change "(?P<activity_name_string>(?:[^"]|\\")*)" activity name to "(?P<new_name_string>(?:[^"]|\\")*)"$/
+     * @throws DriverException Step not available when Javascript is disabled
+     * @param string $activityname
+     * @param string $newactivityname
+     */
+    public function i_change_activity_name_to($activityname, $newactivityname) {
+
+        if (!$this->running_javascript()) {
+            throw new DriverException('Change activity name step is not available with Javascript disabled');
+        }
+
+        // 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 wait "2" seconds')
+        );
+    }
+
+    /**
+     * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
+     *
+     * @Given /^I indent right "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
+     * @param string $activityname
+     */
+    public function i_indent_right_activity($activityname) {
+
+        $steps = array(
+            new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activityname . '" activity')
+        );
+
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I wait "2" seconds');
+        }
+
+        return $steps;
+    }
+
+    /**
+     * Indents to the left the activity or resource specified by it's name. Editing mode should be on.
+     *
+     * @Given /^I indent left "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
+     * @param string $activityname
+     */
+    public function i_indent_left_activity($activityname) {
+
+        $steps = array(
+            new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activityname . '" activity')
+        );
+
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I wait "2" seconds');
+        }
+
+        return $steps;
+
+    }
+
+    /**
+     * Deletes the activity or resource specified by it's name. You should be in the course page with editing mode on.
+     *
+     * @Given /^I delete "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
+     * @param string $activityname
+     */
+    public function i_delete_activity($activityname) {
+
+        $deletestring = get_string('delete');
+
+        // JS enabled.
+        // Not using chain steps here because the exceptions catcher have problems detecting
+        // JS modal windows and avoiding interacting them at the same time.
+        if ($this->running_javascript()) {
+
+            $element = $this->get_activity_element($deletestring, 'link', $activityname);
+            $element->click();
+
+            $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
+
+            $this->getSession()->wait(2 * 1000, false);
+
+        } else {
+
+            // With JS disabled.
+            $steps = array(
+                new Given('I click on "' . $deletestring . '" "link" in the "' . $activityname . '" activity'),
+                new Given('I press "' . get_string('yes') . '"')
+            );
+
+            return $steps;
+        }
+    }
+
+    /**
+     * Duplicates the activity or resource specified by it's name. You should be in the course page with editing mode on.
+     *
+     * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
+     * @param string $activityname
+     */
+    public function i_duplicate_activity($activityname) {
+        return array(
+            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'),
+            new Given('I press "' . get_string('continue') .'"'),
+            new Given('I press "' . get_string('duplicatecontcourse') .'"')
+        );
+    }
+
+    /**
+     * Duplicates the activity or resource and modifies the new activity with the provided data. You should be in the course page with editing mode on.
+     *
+     * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity editing the new copy with:$/
+     * @param string $activityname
+     * @param TableNode $data
+     */
+    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 press "' . get_string('continue') .'"'),
+            new Given('I press "' . get_string('duplicatecontedit') . '"'),
+            new Given('I fill the moodle form with:', $data),
+            new Given('I press "' . get_string('savechangesandreturntocourse') . '"')
+        );
+    }
+
+    /**
+     * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
+     *
+     * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<activity_name_string>[^"]*)" activity$/
+     * @param string $element
+     * @param string $selectortype
+     * @param string $activityname
+     */
+    public function i_click_on_in_the_activity($element, $selectortype, $activityname) {
+        $element = $this->get_activity_element($element, $selectortype, $activityname);
+        $element->click();
+    }
+
+    /**
+     * Clicks on the specified element inside the activity container.
+     *
+     * @throws ElementNotFoundException
+     * @param string $element
+     * @param string $selectortype
+     * @param string $activityname
+     * @return NodeElement
+     */
+    protected function get_activity_element($element, $selectortype, $activityname) {
+        $activitynode = $this->get_activity_node($activityname);
+
+        // Transforming to Behat selector/locator.
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+        $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' . $selectortype . '" in "' . $activityname . '" ');
+
+        return $this->find($selector, $locator, $exception, $activitynode);
+    }
+
     /**
      * Checks if the course section exists.
      *
@@ -415,10 +657,15 @@ class behat_course extends behat_base {
         $courseformat = $this->get_course_format();
 
         // Checking the show button alt text and show icon.
-        $xpath = $xpath . "/descendant::a/descendant::img[@alt='". get_string('showfromothers', $courseformat) ."'][contains(@src, 'show')]";
+        $showtext = 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 ');
-        return $this->find('xpath', $xpath, $exception);
+        $this->find('xpath', $imgxpath, $exception);
+
+        // Returing the link so both Non-JS and JS browsers can interact with it.
+        return $this->find('xpath', $linkxpath, $exception);
     }
 
     /**
@@ -437,10 +684,15 @@ class behat_course extends behat_base {
         $courseformat = $this->get_course_format();
 
         // Checking the hide button alt text and hide icon.
-        $xpath = $xpath . "/descendant::a/descendant::img[@alt='". get_string('hidefromothers', $courseformat) ."'][contains(@src, 'hide')]";
+        $hidetext = 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 ');
-        return $this->find('xpath', $xpath, $exception);
+        $this->find('xpath', $imgxpath, $exception);
+
+        // Returing the link so both Non-JS and JS browsers can interact with it.
+        return $this->find('xpath', $linkxpath, $exception);
     }
 
     /**
diff --git a/course/tests/behat/course_controls.feature b/course/tests/behat/course_controls.feature
new file mode 100644 (file)
index 0000000..a243ace
--- /dev/null
@@ -0,0 +1,165 @@
+@core @core_course
+Feature: Course activity controls works as expected
+  In order to manage my course's activities
+  As a teacher
+  I need to edit, hide, show and indent activities inside course sections
+
+  # This two scenario outlines contains exactly the same steps, the
+  # only difference is whether JS is enabled or not; we can not use
+  # Background sections when using Scenario Outlines because of Behat
+  # framework restrictions.
+
+  # We are testing:
+  # * Javascript on and off
+  # * Topics and weeks course formats
+  # * Course controls without paged mode
+  # * Course controls with paged mode in the course home page
+  # * Course controls with paged mode in a section's page
+
+  @javascript @_cross_browser
+  Scenario Outline: General activities course controls using topics and weeks formats, and paged mode and not paged mode works as expected
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | format | coursedisplay | numsections |
+      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    When I follow <targetpage>
+    And I press "Turn editing on"
+    Then I should see "Turn editing off"
+    And I press "Turn editing off"
+    And "Turn editing on" "button" should exists
+    And I follow "Turn editing on"
+    And "Turn editing off" "button" should exists
+    And I follow "Turn editing off"
+    And I should see "Turn editing on"
+    And "Turn editing on" "button" should exists
+    And I turn editing mode on
+    And I click on "Delete Recent activity block" "link"
+    And I press "Yes"
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name 1 |
+      | Description | Test forum description 1 |
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name 2 |
+      | Description | Test forum description 2 |
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I indent right "Test forum name 1" activity
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I indent left "Test forum name 1" activity
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Update" "link" in the "Test forum name 1" activity
+    And I should see "Updating Forum"
+    And I should see "Display description on course page"
+    And I press "Save and return to course"
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Hide" "link" in the "Test forum name 1" activity
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I delete "Test forum name 1" activity
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I should not see "Test forum name 1" in the ".region-content" "css_element"
+    And I duplicate "Test forum name 2" activity editing the new copy with:
+      | Forum name | Edited test forum name 2 |
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I should see "Test forum name 2"
+    And I should see "Edited test forum name 2"
+    And I hide section "1"
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And section "1" should be hidden
+    And I show section "1"
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And section "1" should be visible
+    And I add the "Section links" block
+    And "#section-2" "css_element" <should_see_other_sections> exists
+    And I should see "1 2 3 4 5" in the ".block_section_links" "css_element"
+    And I click on "2" "link" in the ".block_section_links" "css_element"
+    And I <should_see_other_sections_following_block_sections_links> see "Test forum name 2"
+
+    Examples:
+      | courseformat | coursedisplay | targetpage              | should_see_other_sections | should_see_other_sections_following_block_sections_links |
+      | topics       | 0             | "Course 1"              | should                    | should                                                   |
+      | topics       | 1             | "Topic 1"               | should not                | should not                                               |
+      | topics       | 1             | "Course 1"              | should                    | should not                                               |
+      | weeks        | 0             | "Course 1"              | should                    | should                                                   |
+      | weeks        | 1             | "1 January - 7 January" | should not