Merge branch 'MDL-39191_master' of git://github.com/kordan/moodle
authorDamyon Wiese <damyon@moodle.com>
Tue, 14 May 2013 06:16:05 +0000 (14:16 +0800)
committerDamyon Wiese <damyon@moodle.com>
Tue, 14 May 2013 06:16:05 +0000 (14:16 +0800)
322 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/lang/en/tool_installaddon.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/award.php
badges/backpack.js
badges/criteria.php
badges/criteria/award_criteria_course.php
badges/criteria/award_criteria_courseset.php
badges/criteria_form.php
badges/criteria_settings.php
badges/edit.php
badges/index.php
badges/newbadge.php
badges/overview.php
badges/recipients.php
badges/renderer.php
badges/view.php
blocks/badges/lang/en/block_badges.php
blocks/comments/tests/behat/add_comment.feature
blocks/comments/tests/behat/behat_block_comments.php
blocks/comments/tests/behat/delete_comment.feature
blocks/glossary_random/lang/en/block_glossary_random.php
blocks/mentees/lang/en/block_mentees.php
blocks/news_items/lang/en/block_news_items.php
blocks/online_users/lang/en/block_online_users.php
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/mongodb/lang/en/cachestore_mongodb.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: 0644]
cohort/tests/behat/add_cohort.feature
cohort/tests/behat/upload_cohort_users.feature
cohort/tests/externallib_test.php [new file with mode: 0644]
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/editsection_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/edit/tree/calculation.php
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/grading/form/rubric/lang/en/gradingform_rubric.php
grade/import/xml/grade_import_form.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/lang/gl/error.php
install/lang/he/moodle.php
install/lang/zh_tw/admin.php
install/stringnames.txt
lang/en/admin.php
lang/en/backup.php
lang/en/badges.php
lang/en/cache.php
lang/en/completion.php
lang/en/condition.php
lang/en/error.php
lang/en/grades.php
lang/en/hub.php
lang/en/moodle.php
lang/en/plugin.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
mdeploy.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/assignment/lang/en/assignment.php
mod/chat/lang/en/chat.php
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/lang/en/forum.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/lang/en/lti.php
mod/lti/locallib.php
mod/page/lang/en/page.php
mod/resource/lang/en/resource.php
mod/scorm/lang/en/scorm.php
mod/scorm/mod_form.php
mod/scorm/report/basic/lang/en/scormreport_basic.php
mod/scorm/report/interactions/lang/en/scormreport_interactions.php
mod/scorm/settings.php
mod/survey/tests/behat/survey_types.feature
mod/url/lang/en/url.php
mod/url/mod_form.php
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/datasetitems_form.php
question/type/calculated/edit_calculated_form.php
question/type/numerical/edit_numerical_form.php
repository/boxnet/lang/en/repository_boxnet.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/layout/general.php
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.php
theme/bootstrapbase/renderers/core_renderer.php [moved from theme/bootstrapbase/renderers/core.php with 70% similarity]
theme/bootstrapbase/style/editor.css
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/version.php
theme/boxxie/style/core.css
theme/brick/style/core.css
theme/canvas/style/course.css
theme/canvas/style/mods.css
theme/clean/layout/general.php
theme/clean/style/custom.css
theme/formal_white/lang/en/theme_formal_white.php
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/cohort.php
user/filters/courserole.php
user/filters/date.php
user/filters/profilefield.php
user/filters/text.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 bca494f..a55199a 100644 (file)
@@ -29,36 +29,36 @@ defined('MOODLE_INTERNAL') || die();
 $string['acknowledgement'] = 'Acknowledgement';
 $string['acknowledgementmust'] = 'You must acknowledge this';
 $string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing add-ons. I accept and understand that add-ons (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
-$string['featuredisabled'] = 'Add-on installer is disabled at this site.';
+$string['featuredisabled'] = 'The add-on installer is disabled on this site.';
 $string['installaddon'] = 'Install add-on!';
 $string['installaddons'] = 'Install add-ons';
-$string['installexception'] = 'Oops... An error occured while trying to install the add-on. Turn debugging mode on to see more details about the error.';
-$string['installfromrepo'] = 'Install add-ons from Moodle plugins directory';
-$string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install an add-on. Note that your site fullname, URL and major version will be sent as well, to make the installation process easier for you.';
-$string['installfromzip'] = 'Install add-on from the ZIP file';
-$string['installfromzip_help'] = 'Alternatively to installing add-ons directly from the Moodle plugins directory, you can install add-ons from manually uploaded ZIP packages. Such ZIP packages are expected to have same structure as the ones available in the Moodle plugins directory.';
+$string['installexception'] = 'Oops... An error occurred while trying to install the add-on. Turn debugging mode on to see details of the error.';
+$string['installfromrepo'] = 'Install add-ons from the Moodle plugins directory';
+$string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install an add-on. Note that your site full name, URL and Moodle version will be sent as well, to make the installation process easier for you.';
+$string['installfromzip'] = 'Install add-on from ZIP file';
+$string['installfromzip_help'] = 'An alternative to installing an add-on directly from the Moodle plugins directory is to upload a ZIP package of the add-on. The ZIP package should have the same structure as a package downloaded from the Moodle plugins directory.';
 $string['installfromzipfile'] = 'ZIP package';
-$string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory with the name of the plugin. The ZIP will be extracted into the appropriate location for the given plugin type. Packages downloaded from the Moodle plugins directory have this format.';
+$string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory, named to match the plugin. The ZIP will be extracted into an appropriate location for the plugin type. If the package has been downloaded from the Moodle plugins directory then it will have this structure.';
 $string['installfromziprootdir'] = 'Rename the root directory';
-$string['installfromziprootdir_help'] = 'Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You can rename the root directory of the extracted package to the correct value defined in this field.';
+$string['installfromziprootdir_help'] = 'Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. If so, the correct name may be entered here.';
 $string['installfromzipsubmit'] = 'Install add-on from the ZIP file';
 $string['installfromziptype'] = 'Plugin type';
-$string['installfromziptype_help'] = 'Choose the correct type of plugin you are about to install. The installation procedure may fail badly when incorrect plugin type is provided.';
-$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process';
+$string['installfromziptype_help'] = 'Choose the correct type of plugin you are about to install. Warning: The installation procedure can fail badly if an incorrect plugin type is specified.';
+$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process.';
 $string['permcheckerror'] = 'Error while checking for write permission';
 $string['permcheckprogress'] = 'Checking for write permission ...';
-$string['permcheckresultno'] = 'Plugin type location <em>{$a->path}</em> not writable';
+$string['permcheckresultno'] = 'Plugin type location <em>{$a->path}</em> is not writable';
 $string['permcheckresultyes'] = 'Plugin type location <em>{$a->path}</em> is writable';
 $string['pluginname'] = 'Add-on installer';
-$string['remoterequestalreadyinstalled'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. This plugin is <strong>already installed</strong> at this site.';
-$string['remoterequestconfirm'] = 'There is a request to install add-on <strong>{$a->name}</strong> ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. If you continue, the add-on ZIP package will be downloaded for validation. Nothing will be installed yet.';
-$string['remoterequestinvalid'] = 'There is a request to install add-on from the Moodle plugins directory to this site. Unfortunately, the request is not valid. The add-on cannot be installed.';
-$string['remoterequestpermcheck'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. The plugin type location <strong>{$a->typepath}</strong> is <strong>not writable</strong> though. You need to give the write access for the web server user to the plugin type location now. Once the write access is granted, press the continue button to repeat the check.';
-$string['remoterequestpluginfoexception'] = 'Oops... An error occured while trying to obtain information about the add-on {$a->name} ({$a->component}) version {$a->version}. The add-on cannot be installed. Turn debugging mode on to see more details about the error.';
+$string['remoterequestalreadyinstalled'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. However, this plugin is <strong>already installed</strong> on the site.';
+$string['remoterequestconfirm'] = 'There is a request to install add-on <strong>{$a->name}</strong> ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. If you continue, the add-on ZIP package will be downloaded for validation. Nothing will be installed yet.';
+$string['remoterequestinvalid'] = 'There is a request to install an add-on from the Moodle plugins directory on this site. Unfortunately the request is not valid and so the add-on cannot be installed.';
+$string['remoterequestpermcheck'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. However, the plugin type location <strong>{$a->typepath}</strong> is <strong>not writable</strong>. You need to give write access for the web server user to the plugin type location, then press the continue button to repeat the check.';
+$string['remoterequestpluginfoexception'] = 'Oops... An error occurred while trying to obtain information about the add-on {$a->name} ({$a->component}) version {$a->version}. The add-on cannot be installed. Turn debugging mode on to see details of the error.';
 $string['validation'] = 'Add-on package validation';
 $string['validationmsg_componentmatch'] = 'Full component name';
 $string['validationmsg_componentmismatchname'] = 'Add-on name mismatch';
-$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You have to fix the name of the root directory to match the declared add-on name.';
+$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the declared add-on name.';
 $string['validationmsg_componentmismatchname_info'] = 'The add-on declares its name is \'{$a}\' but that does not match the name of the root directory.';
 $string['validationmsg_componentmismatchtype'] = 'Add-on type mismatch';
 $string['validationmsg_componentmismatchtype_info'] = 'You have selected the type \'{$a->expected}\' but the add-on declares its type is \'{$a->found}\'.';
@@ -69,7 +69,7 @@ $string['validationmsg_filestatus_info'] = 'Attempting to extract file {$a->file
 $string['validationmsg_maturity'] = 'Declared maturity level';
 $string['validationmsg_maturity_help'] = 'The add-on can declare its maturity level. If the maintainer considers the add-on stable, the declared maturity level will read MATURITY_STABLE. All other maturity levels (such as alpha or beta) should be considered unstable and a warning is raised.';
 $string['validationmsg_missingexpectedlangenfile'] = 'English language file name mismatch';
-$string['validationmsg_missingexpectedlangenfile_info'] = 'The given add-on type would need to provide the English language file {$a}.';
+$string['validationmsg_missingexpectedlangenfile_info'] = 'The given add-on type is missing the expected English language file {$a}.';
 $string['validationmsg_missinglangenfile'] = 'No English language file found';
 $string['validationmsg_missinglangenfolder'] = 'Missing English language folder';
 $string['validationmsg_missingversion'] = 'Add-on does not declare its version';
@@ -83,8 +83,8 @@ $string['validationmsg_release'] = 'Add-on release';
 $string['validationmsg_requiresmoodle'] = 'Required Moodle version';
 $string['validationmsg_rootdir'] = 'Name of the add-on to be installed';
 $string['validationmsg_rootdir_help'] = 'The name of the root directory in the ZIP package forms the name of the add-on to be installed. If the name is not correct, you may wish to rename the root directory in the ZIP prior to installing the add-on.';
-$string['validationmsg_rootdirinvalid'] = 'Invalid name of the add-on';
-$string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory in the ZIP package violates formal syntax requirements. Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You have to fix the name of the root directory to match the add-on name.';
+$string['validationmsg_rootdirinvalid'] = 'Invalid add-on name';
+$string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory in the ZIP package violates formal syntax requirements. Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the add-on name.';
 $string['validationmsg_targetexists'] = 'Target location already exists';
 $string['validationmsg_targetexists_help'] = 'The directory that the add-on is to be installed to, must not exist yet.';
 $string['validationmsg_unknowntype'] = 'Unknown plugin type';
@@ -93,7 +93,7 @@ $string['validationmsglevel_error'] = 'Error';
 $string['validationmsglevel_info'] = 'OK';
 $string['validationmsglevel_warning'] = 'Warning';
 $string['validationresult0'] = 'Validation failed!';
-$string['validationresult0_help'] = 'Some serious problem was detected. It is not safe to install the add-on. See the validation log messages for more details.';
+$string['validationresult0_help'] = 'A serious problem was detected and so it is not safe to install the add-on. See the validation log messages for details.';
 $string['validationresult1'] = 'Validation passed!';
 $string['validationresult1_help'] = 'No serious problems were detected. You can continue with the add-on installation. See the validation log messages for more details and eventual warnings.';
 $string['validationresult1_help'] = 'The add-on package has been validated and no serious problems were detected.';
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 bf75d0d..3d3aba7 100644 (file)
@@ -30,6 +30,7 @@ require_once($CFG->dirroot . '/badges/lib/awardlib.php');
 
 $badgeid = required_param('id', PARAM_INT);
 $role = optional_param('role', 0, PARAM_INT);
+$award = optional_param('award', false, PARAM_BOOL);
 
 require_login();
 
@@ -44,6 +45,9 @@ $isadmin = is_siteadmin($USER);
 $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
 
 if ($badge->type == BADGE_TYPE_COURSE) {
+    if (empty($CFG->badges_allowcoursebadges)) {
+        print_error('coursebadgesdisabled', 'badges');
+    }
     require_login($badge->courseid);
     $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
 }
@@ -72,37 +76,52 @@ if (!$badge->is_active()) {
 $output = $PAGE->get_renderer('core', 'badges');
 
 // Roles that can award this badge.
-$accepted_roles = array_keys($badge->criteria[BADGE_CRITERIA_TYPE_MANUAL]->params);
+$acceptedroles = array_keys($badge->criteria[BADGE_CRITERIA_TYPE_MANUAL]->params);
 
-// If site admin, select a role to award a badge.
-if ($isadmin) {
-    list($usertest, $userparams) = $DB->get_in_or_equal($accepted_roles, SQL_PARAMS_NAMED, 'existing', true);
+if (count($acceptedroles) > 1) {
+    // If there is more than one role that can award a badge, prompt user to make a selection.
+    // If it is an admin, include all accepted roles, otherwise only the ones that current user has in this context.
+    if ($isadmin) {
+        $selection = $acceptedroles;
+    } else {
+        // Get all the roles that user has and use the ones required by this badge.
+        $roles = get_user_roles($context, $USER->id);
+        $roleids = array_map(create_function('$o', 'return $o->roleid;'), $roles);
+        $selection = array_intersect($acceptedroles, $roleids);
+    }
+    list($usertest, $userparams) = $DB->get_in_or_equal($selection, SQL_PARAMS_NAMED, 'existing', true);
     $options = $DB->get_records_sql('SELECT * FROM {role} WHERE id ' . $usertest, $userparams);
     foreach ($options as $p) {
         $select[$p->id] = role_get_name($p);
     }
     if (!$role) {
+        $pageurl = new moodle_url('/badges/award.php', array('id' => $badgeid));
         echo $OUTPUT->header();
-        echo $OUTPUT->box(get_string('adminaward', 'badges') . $OUTPUT->single_select(new moodle_url($PAGE->url), 'role', $select));
+        echo $OUTPUT->box(get_string('selectaward', 'badges') . $OUTPUT->single_select(new moodle_url($pageurl), 'role', $select));
         echo $OUTPUT->footer();
         die();
     } else {
+        $pageurl = new moodle_url('/badges/award.php', array('id' => $badgeid));
         $issuerrole = new stdClass();
         $issuerrole->roleid = $role;
-        $roleselect = get_string('adminaward', 'badges') . $OUTPUT->single_select(new moodle_url($PAGE->url), 'role', $select, $role);
+        $roleselect = get_string('selectaward', 'badges') . $OUTPUT->single_select(new moodle_url($pageurl), 'role', $select, $role);
     }
 } else {
-    // Current user's role.
-    $roles = get_user_roles($context, $USER->id);
-    $issuerrole = array_shift($roles);
-    if (!isset($issuerrole->roleid) || !in_array($issuerrole->roleid, $accepted_roles)) {
+    // User has to be an admin or the one with the required role.
+    $users = get_role_users($acceptedroles[0], $context, false, 'u.id', 'u.id ASC');
+    $usersids = array_keys($users);
+    if (!$isadmin && !in_array($USER->id, $usersids)) {
         echo $OUTPUT->header();
         $rlink = html_writer::link(new moodle_url('recipients.php', array('id' => $badge->id)), get_string('recipients', 'badges'));
         echo $OUTPUT->notification(get_string('notacceptedrole', 'badges', $rlink));
         echo $OUTPUT->footer();
         die();
+    } else {
+        $issuerrole = new stdClass();
+        $issuerrole->roleid = $acceptedroles[0];
     }
 }
+
 $options = array(
         'badgeid' => $badge->id,
         'context' => $context,
@@ -113,7 +132,7 @@ $existingselector = new badge_existing_users_selector('existingrecipients', $opt
 $recipientselector = new badge_potential_users_selector('potentialrecipients', $options);
 $recipientselector->set_existing_recipients($existingselector->find_users(''));
 
-if (optional_param('award', false, PARAM_BOOL) && data_submitted() && has_capability('moodle/badges:awardbadge', $context)) {
+if ($award && data_submitted() && has_capability('moodle/badges:awardbadge', $context)) {
     require_sesskey();
     $users = $recipientselector->get_selected_users();
     foreach ($users as $user) {
@@ -136,7 +155,7 @@ if (optional_param('award', false, PARAM_BOOL) && data_submitted() && has_capabi
 echo $OUTPUT->header();
 echo $OUTPUT->heading($strrecipients);
 
-if ($isadmin) {
+if (count($acceptedroles) > 1) {
     echo $OUTPUT->box($roleselect);
 }
 
index b0caec4..68e736b 100644 (file)
@@ -10,19 +10,23 @@ function addtobackpack(event, args) {
  */
 function check_site_access() {
     var add = Y.one('#check_connection');
+
     var callback = {
-        success: function(o) {
-            var data = Y.JSON.parse(o.responseText);
-            if (data.code == 'http-unreachable') {
-                add.setHTML(data.response);
-                add.removeClass('hide');
+            method: "GET",
+            on: {
+                success: function(id, o, args) {
+                            var data = Y.JSON.parse(o.responseText);
+                            if (data.code == 'http-unreachable') {
+                                add.setHTML(data.response);
+                                add.removeClass('hide');
+                            }
+                        },
+                failure: function(o) { }
             }
-        },
-        failure: function(o) { }
-    };
+        };
 
-    YUI().use('yui2-connection', function (Y) {
-        Y.YUI2.util.Connect.asyncRequest('GET', 'ajax.php', callback, null);
+    Y.use('io-base', function(Y) {
+        Y.io('ajax.php', callback);
     });
 
     return false;
index 7844499..9bfd389 100644 (file)
@@ -41,6 +41,9 @@ $context = $badge->get_context();
 $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
 
 if ($badge->type == BADGE_TYPE_COURSE) {
+    if (empty($CFG->badges_allowcoursebadges)) {
+        print_error('coursebadgesdisabled', 'badges');
+    }
     require_login($badge->courseid);
     $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
 }
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 9093040..1c91562 100644 (file)
@@ -44,6 +44,9 @@ $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
 require_capability('moodle/badges:configuredetails', $context);
 
 if ($badge->type == BADGE_TYPE_COURSE) {
+    if (empty($CFG->badges_allowcoursebadges)) {
+        print_error('coursebadgesdisabled', 'badges');
+    }
     require_login($badge->courseid);
     $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
 }
index 56176fb..8a30471 100644 (file)
@@ -56,6 +56,10 @@ if (empty($CFG->enablebadges)) {
     print_error('badgesdisabled', 'badges');
 }
 
+if (empty($CFG->badges_allowcoursebadges) && ($type == BADGE_TYPE_COURSE)) {
+    print_error('coursebadgesdisabled', 'badges');
+}
+
 $err = '';
 $urlparams = array('sort' => $sortby, 'dir' => $sorthow, 'page' => $page);
 
index fa3c2c6..413e56c 100644 (file)
@@ -37,6 +37,10 @@ if (empty($CFG->enablebadges)) {
     print_error('badgesdisabled', 'badges');
 }
 
+if (empty($CFG->badges_allowcoursebadges) && ($type == BADGE_TYPE_COURSE)) {
+    print_error('coursebadgesdisabled', 'badges');
+}
+
 $title = get_string('create', 'badges');
 
 if (($type == BADGE_TYPE_COURSE) && ($course = $DB->get_record('course', array('id' => $courseid)))) {
index b658558..e2dec7f 100644 (file)
@@ -41,6 +41,9 @@ $context = $badge->get_context();
 $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
 
 if ($badge->type == BADGE_TYPE_COURSE) {
+    if (empty($CFG->badges_allowcoursebadges)) {
+        print_error('coursebadgesdisabled', 'badges');
+    }
     require_login($badge->courseid);
     $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
 }
index 8105863..539696b 100644 (file)
@@ -56,6 +56,9 @@ $context = $badge->get_context();
 $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
 
 if ($badge->type == BADGE_TYPE_COURSE) {
+    if (empty($CFG->badges_allowcoursebadges)) {
+        print_error('coursebadgesdisabled', 'badges');
+    }
     require_login($badge->courseid);
     $navurl = new moodle_url('/badges/index.php', array('type' => $badge->type, 'id' => $badge->courseid));
 }
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 b4a5c2a..33f2b91 100644 (file)
@@ -39,6 +39,10 @@ if (empty($CFG->enablebadges)) {
     print_error('badgesdisabled', 'badges');
 }
 
+if (empty($CFG->badges_allowcoursebadges) && $courseid != 0) {
+    print_error('coursebadgesdisabled', 'badges');
+}
+
 if (!in_array($sortby, array('name', 'dateissued'))) {
     $sortby = 'name';
 }
index 93c71ed..cd73e3d 100644 (file)
@@ -27,4 +27,4 @@ $string['pluginname'] = 'My latest badges';
 $string['numbadgestodisplay'] = 'Number of latest badges to display';
 $string['nothingtodisplay'] = 'You have no badges to display';
 $string['badges:addinstance'] = 'Add a new My latest badges block';
-$string['badges:myaddinstance'] = 'Add a new My latest badges block to the My Moodle page';
\ No newline at end of file
+$string['badges:myaddinstance'] = 'Add a new My latest badges block to My home';
\ No newline at end of file
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 6d1b75a..7aa54b6 100644 (file)
@@ -28,7 +28,7 @@ $string['askaddentry'] = 'When users can add entries to the glossary, show a lin
 $string['askinvisible'] = 'When users cannot edit or view the glossary, show this text (without link)';
 $string['askviewglossary'] = 'When users can view the glossary but not add entries, show a link with this text';
 $string['glossary_random:addinstance'] = 'Add a new random glossary entry block';
-$string['glossary_random:myaddinstance'] = 'Add a new random glossary entry block to the My Moodle page';
+$string['glossary_random:myaddinstance'] = 'Add a new random glossary entry block to My home';
 $string['intro'] = 'Make sure you have at least one glossary with at least one entry added to this course. Then you can adjust the following settings';
 $string['invisible'] = '(to be continued)';
 $string['lastmodified'] = 'Last modified entry';
index 3a22057..46e312b 100644 (file)
@@ -27,6 +27,6 @@ $string['configtitle'] = 'Block title';
 $string['configtitleblankhides'] = 'Block title (no title if blank)';
 $string['leaveblanktohide'] = 'leave blank to hide the title';
 $string['mentees:addinstance'] = 'Add a new mentees block';
-$string['mentees:myaddinstance'] = 'Add a new mentees block to the My Moodle page';
+$string['mentees:myaddinstance'] = 'Add a new mentees block to My home';
 $string['newmenteesblock'] = '(new Mentees block)';
 $string['pluginname'] = 'Mentees';
index 7d76b9c..ba638ad 100644 (file)
@@ -24,5 +24,5 @@
  */
 
 $string['news_items:addinstance'] = 'Add a new latest news block';
-$string['news_items:myaddinstance'] = 'Add a new latest news block to the My Moodle page';
+$string['news_items:myaddinstance'] = 'Add a new latest news block to My home';
 $string['pluginname'] = 'Latest news';
index 034fce5..1dc5aeb 100644 (file)
@@ -25,7 +25,7 @@
 
 $string['configtimetosee'] = 'Number of minutes determining the period of inactivity after which a user is no longer considered to be online.';
 $string['online_users:addinstance'] = 'Add a new online users block';
-$string['online_users:myaddinstance'] = 'Add a new online users block to the My Moodle page';
+$string['online_users:myaddinstance'] = 'Add a new online users block to My home';
 $string['online_users:viewlist'] = 'View list of online users';
 $string['periodnminutes'] = 'last {$a} minutes';
 $string['pluginname'] = 'Online users';
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 cb1d765..d787d97 100644 (file)
@@ -32,7 +32,7 @@ $string['pluginname'] = 'MongoDB';
 $string['replicaset'] = 'Replica set';
 $string['replicaset_help'] = 'The name of the replica set to connect to. If this is given the master will be determined by using the ismaster database command on the seeds, so the driver may end up connecting to a server that was not even listed.';
 $string['server'] = 'Server';
-$string['server_help'] = 'This is the connection string for the server you want to use. Multiple servers can be specified by separating them with comma\'s';
+$string['server_help'] = 'This is the connection string for the server you want to use. Multiple servers can be specified using a comma-separated list.';
 $string['testserver'] = 'Test server';
 $string['testserver_desc'] = 'This is the connection string for the test server you want to use. Test servers are entirely optional, by specifiying a test server you can run PHPunit tests for this store and can run the performance tests.';
 $string['username'] = 'Username';
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 100644 (file)
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 100644 (file)
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 e892045..928a4a8 100644 (file)
@@ -21,6 +21,8 @@ class editsection_form extends moodleform {
         $mform  = $this->_form;
         $course = $this->_customdata['course'];
 
+        $mform->addElement('header', 'generalhdr', get_string('general'));
+
         $elementgroup = array();
         $elementgroup[] = $mform->createElement('text', 'name', '', array('size' => '30'));
         $elementgroup[] = $mform->createElement('checkbox', 'usedefaultname', '', get_string('sectionusedefaultname'));
@@ -58,6 +60,7 @@ class editsection_form extends moodleform {
 
         if (!empty($CFG->enableavailability)) {
             $mform->addElement('header', 'availabilityconditions', get_string('availabilityconditions', 'condition'));
+            $mform->setExpanded('availabilityconditions', false);
             // String used by conditions more than once
             $strcondnone = get_string('none', 'condition');
             // Grouping conditions - only if grouping is enabled at site level
@@ -116,8 +119,10 @@ class editsection_form extends moodleform {
             $count = count($fullcs->conditionsgrade) + 1;
 
             // Grade conditions
-            $this->repeat_elements(array($group), $count, array(), 'conditiongraderepeats',
-                    'conditiongradeadds', 2, get_string('addgrades', 'condition'),