Merge branch 'MDL-48243_master' of git://github.com/lazydaisy/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 17 Dec 2014 03:48:42 +0000 (11:48 +0800)
committerDavid Monllao <davidm@moodle.com>
Wed, 17 Dec 2014 03:48:42 +0000 (11:48 +0800)
Conflicts:
theme/bootstrapbase/style/moodle.css

423 files changed:
admin/mnet/peer_forms.php
admin/mnet/peers.php
admin/mnet/testclient.php
admin/roles/module.js
admin/roles/usersroles.php
admin/settings/appearance.php
admin/settings/security.php
admin/settings/server.php
admin/tool/assignmentupgrade/module.js
admin/tool/health/index.php
admin/tool/health/locallib.php [new file with mode: 0644]
admin/tool/health/tests/healthlib_test.php [new file with mode: 0644]
admin/tool/log/store/legacy/classes/log/store.php
admin/tool/log/store/standard/classes/log/store.php
admin/tool/messageinbound/classes/manager.php
admin/tool/monitor/tests/eventobservers_test.php
admin/tool/profiling/styles.css
admin/tool/spamcleaner/module.js
admin/tool/xmldb/styles_bootstrapbase.css [new file with mode: 0644]
auth/email/auth.php
auth/ldap/auth.php
auth/shibboleth/README.txt
auth/shibboleth/logout.php
auth/upgrade.txt
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js
availability/condition/completion/yui/src/form/js/form.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js
availability/condition/date/yui/src/form/js/form.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js
availability/condition/grade/yui/src/form/js/form.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js
availability/condition/group/yui/src/form/js/form.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js
availability/condition/grouping/yui/src/form/js/form.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js
availability/condition/profile/yui/src/form/js/form.js
availability/tests/behat/edit_availability.feature
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js
availability/yui/src/form/js/form.js
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_enrol_plugin.class.php [new file with mode: 0644]
backup/moodle2/backup_plan_builder.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_enrol_plugin.class.php [new file with mode: 0644]
backup/moodle2/restore_plan_builder.class.php
backup/moodle2/restore_stepslib.php
backup/util/factories/backup_factory.class.php
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/backup_helper.class.php
backup/util/ui/tests/behat/restore_moodle2_courses.feature
badges/renderer.php
badges/tests/behat/award_badge.feature
blocks/calendar_month/block_calendar_month.php
blocks/comments/tests/behat/add_comment.feature
blocks/comments/tests/behat/behat_block_comments.php
blocks/completionstatus/block_completionstatus.php
blocks/course_summary/block_course_summary.php
blocks/edit_form.php
blocks/moodleblock.class.php
blocks/navigation/tests/behat/expand_courses_node.feature
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js
blocks/navigation/yui/src/navigation/js/navigation.js
blocks/news_items/tests/behat/display_news.feature
blocks/rss_client/block_rss_client.php
blocks/rss_client/editfeed.php
blocks/rss_client/lang/en/block_rss_client.php
blocks/site_main_menu/block_site_main_menu.php
blocks/site_main_menu/styles.css
blocks/tag_flickr/block_tag_flickr.php
blocks/tag_youtube/block_tag_youtube.php
blocks/tests/behat/configure_block_throughout_site.feature
blocks/upgrade.txt
cache/forms.php
cache/renderer.php
calendar/externallib.php
calendar/tests/externallib_test.php
calendar/upgrade.txt
comment/lib.php
comment/locallib.php
completion/tests/behat/behat_completion.php
completion/tests/behat/enable_manual_complete_mark.feature
completion/tests/behat/restrict_section_availability.feature
completion/tests/behat/teacher_manual_completion.feature [new file with mode: 0644]
course/classes/management/helper.php
course/completion.js
course/edit.php
course/edit_form.php
course/editsection.php
course/format/lib.php
course/format/topics/lib.php
course/format/weeks/lib.php
course/lib.php
course/modedit.php
course/modlib.php
course/moodleform_mod.php
course/request.php
course/reset_form.php
course/tests/behat/activities_edit_completion.feature [new file with mode: 0644]
course/tests/behat/activities_group_icons.feature
course/tests/behat/behat_course.php
course/tests/behat/category_change_visibility.feature
course/tests/behat/category_resort.feature
course/tests/behat/course_category_management_listing.feature
course/tests/behat/course_change_visibility.feature
course/tests/behat/course_creation.feature
course/tests/behat/course_resort.feature
course/tests/behat/create_delete_course.feature
course/tests/behat/edit_settings.feature
course/tests/behat/force_group_mode.feature
course/tests/behat/move_activities.feature
course/tests/behat/move_sections.feature
course/tests/behat/rename_roles.feature
course/tests/courselib_test.php
course/tests/externallib_test.php
course/togglecompletion.php
course/view.php
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js
course/yui/src/toolboxes/js/resource.js
enrol/cohort/edit_form.php
enrol/cohort/lib.php
enrol/guest/tests/behat/guest_access.feature
enrol/manual/lib.php
enrol/manual/yui/quickenrolment/quickenrolment.js
enrol/meta/addinstance_form.php
enrol/meta/lang/en/enrol_meta.php
enrol/meta/settings.php
enrol/meta/version.php
enrol/self/db/access.php
enrol/self/edit_form.php
enrol/self/lang/en/enrol_self.php
enrol/self/locallib.php
enrol/self/version.php
enrol/yui/otherusersmanager/otherusersmanager.js
enrol/yui/rolemanager/rolemanager.js
files/renderer.php
grade/edit/tree/lib.php
grade/grading/form/guide/edit.php
grade/grading/form/guide/js/guideeditor.js
grade/grading/form/rubric/js/rubriceditor.js
grade/report/grader/index.php
grade/report/grader/module.js
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/screen/screen.php
grade/report/singleview/classes/local/screen/user.php
grade/report/singleview/classes/local/ui/finalgrade.php
grade/report/singleview/index.php
grade/report/singleview/lib.php
grade/report/singleview/tests/behat/singleview.feature
grade/report/singleview/tests/fixtures/screen.php [new file with mode: 0644]
grade/report/singleview/tests/screen_test.php [new file with mode: 0644]
grade/report/upgrade.txt
grade/tests/behat/grade_UI_settings.feature
group/autogroup_form.php
group/index.php
help.php
index.php
install/lang/es/error.php
install/lang/es/install.php
install/lang/mk/admin.php
install/lang/nl/install.php
install/lang/pt/install.php
lang/en/access.php
lang/en/admin.php
lang/en/auth.php
lang/en/availability.php
lang/en/backup.php
lang/en/badges.php
lang/en/block.php
lang/en/blog.php
lang/en/bulkusers.php
lang/en/cache.php
lang/en/calendar.php
lang/en/cohort.php
lang/en/completion.php
lang/en/countries.php
lang/en/currencies.php
lang/en/dbtransfer.php
lang/en/debug.php
lang/en/deprecated.txt
lang/en/editor.php
lang/en/edufields.php
lang/en/enrol.php
lang/en/error.php
lang/en/filters.php
lang/en/form.php
lang/en/grades.php
lang/en/grading.php
lang/en/group.php
lang/en/hub.php
lang/en/imscc.php
lang/en/install.php
lang/en/iso6392.php
lang/en/langconfig.php
lang/en/license.php
lang/en/mathslib.php
lang/en/message.php
lang/en/mimetypes.php
lang/en/mnet.php
lang/en/moodle.php
lang/en/my.php
lang/en/notes.php
lang/en/pagetype.php
lang/en/pix.php
lang/en/plagiarism.php
lang/en/portfolio.php
lang/en/question.php
lang/en/rating.php
lang/en/repository.php
lang/en/role.php
lang/en/search.php
lang/en/table.php
lang/en/tag.php
lang/en/timezones.php
lang/en/userkey.php
lang/en/webservice.php
lib/adminlib.php
lib/behat/classes/behat_selectors.php
lib/behat/classes/util.php
lib/blocklib.php
lib/classes/plugininfo/theme.php
lib/classes/session/manager.php
lib/classes/task/file_temp_cleanup_task.php
lib/conditionlib.php
lib/coursecatlib.php
lib/db/install.xml
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/atto/plugins/image/lang/en/atto_image.php
lib/editor/atto/plugins/image/lib.php
lib/editor/atto/plugins/image/version.php
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
lib/editor/atto/plugins/image/yui/src/button/js/button.js
lib/editor/atto/upgrade.txt [new file with mode: 0644]
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js
lib/editor/atto/yui/src/editor/js/editor-plugin-buttons.js
lib/editor/tinymce/plugins/managefiles/module.js
lib/enrollib.php
lib/filelib.php
lib/form/dndupload.js
lib/form/filemanager.js
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js
lib/form/yui/src/showadvanced/js/showadvanced.js
lib/formslib.php
lib/javascript-static.js
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/sessionkeepalive_ajax.php [new file with mode: 0644]
lib/tablelib.php
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/blocklib_test.php
lib/tests/conditionlib_test.php [deleted file]
lib/tests/filelib_test.php
lib/tests/html_writer_test.php
lib/tests/modinfolib_test.php
lib/tests/tablelib_test.php
lib/thirdpartylibs.xml
lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js
lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js
lib/yui/build/moodle-core-checknet/moodle-core-checknet.js
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js
lib/yui/build/moodle-core-dock/moodle-core-dock.js
lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-debug.js
lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-min.js
lib/yui/build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer.js
lib/yui/src/checknet/js/checknet.js
lib/yui/src/dock/js/block.js
lib/yui/src/dock/js/dock.js
lib/yui/src/dock/js/dockeditem.js
lib/yui/src/maintenancemodetimer/js/maintenancemodetimer.js
login/change_password.php
login/change_password_form.php
login/lib.php
login/set_password_form.php
message/module.js
mnet/peer.php
mnet/xmlrpc/client.php
mod/assign/batchsetmarkingworkflowstateform.php
mod/assign/feedback/editpdf/ajax.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/page_editor.php
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/editpdf/tests/behat/group_annotations.feature [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationstamp.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/feedback/editpdf/yui/src/editor/js/drawable.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/gradeform.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/tests/behat/group_submission.feature
mod/assign/tests/behat/quickgrading.feature
mod/assign/tests/locallib_test.php
mod/book/tests/behat/log_entries.feature [new file with mode: 0644]
mod/chat/gui_ajax/module.js
mod/feedback/backup/moodle1/lib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/subscribe.php
mod/forum/tests/behat/completion_condition_number_discussions.feature
mod/forum/tests/behat/discussion_subscriptions.feature
mod/forum/tests/mail_test.php
mod/imscp/backup/moodle1/lib.php
mod/imscp/backup/moodle2/backup_imscp_activity_task.class.php
mod/imscp/backup/moodle2/backup_imscp_stepslib.php
mod/imscp/backup/moodle2/restore_imscp_activity_task.class.php
mod/imscp/backup/moodle2/restore_imscp_stepslib.php
mod/imscp/db/install.php
mod/imscp/db/log.php
mod/imscp/db/upgrade.php
mod/imscp/index.php
mod/imscp/lang/en/imscp.php
mod/imscp/lib.php
mod/imscp/locallib.php
mod/imscp/mod_form.php
mod/imscp/module.js
mod/imscp/settings.php
mod/imscp/styles.css
mod/imscp/tests/generator_test.php
mod/imscp/version.php
mod/imscp/view.php
mod/lesson/essay.php
mod/lesson/format.php
mod/lesson/pagetypes/shortanswer.php
mod/lesson/renderer.php
mod/lesson/styles.css
mod/lesson/tests/behat/import_fillintheblank_question.feature [new file with mode: 0644]
mod/lesson/tests/fixtures/sample_blackboard_fib_qti.dat [new file with mode: 0644]
mod/lesson/view.php
mod/lti/mod_form.js
mod/quiz/lang/en/quiz.php
mod/quiz/module.js
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js
mod/quiz/yui/src/dragdrop/js/resource.js
mod/scorm/module.js
mod/scorm/view.js
mod/survey/survey.js
mod/wiki/tests/behat/wiki_comments.feature
mod/workshop/allocation/manual/styles.css
pix/t/viewdetails.png [new file with mode: 0644]
pix/t/viewdetails.svg [new file with mode: 0644]
question/engine/tests/helpers.php
question/format.php
question/format/gift/tests/behat/import_export.feature
question/format/xml/tests/behat/import_export.feature
question/tests/behat/copy_questions.feature
question/tests/behat/delete_questions.feature
question/tests/behat/edit_questions.feature
question/tests/behat/preview_question.feature
question/tests/behat/question_categories.feature
question/tests/behat/sort_questions.feature
question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php
report/backups/index.php
report/backups/lang/en/report_backups.php
report/completion/index.php
report/completion/lang/en/report_completion.php
report/log/classes/table_log.php
report/outline/tests/behat/outline.feature
report/progress/index.php
repository/equella/callback.php
repository/filepicker.js
repository/url/lib.php
repository/url/tests/lib_test.php [new file with mode: 0644]
tag/tag.js
theme/base/style/core.css
theme/base/style/filemanager.css
theme/base/style/user.css
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
theme/upgrade.txt
user/edit_form.php
user/lib.php
user/selector/module.js
user/tests/behat/view_full_profile.feature [new file with mode: 0644]
user/tests/userlib_test.php
user/view.php
version.php
webservice/renderer.php

index ce1c1c0..a9339b0 100644 (file)
@@ -94,6 +94,15 @@ class mnet_review_host_form extends moodleform {
         $mform->setType('wwwroot', PARAM_URL);
         $mform->addRule('wwwroot', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
 
+        $options = array(
+            mnet_peer::SSL_NONE => get_string('none'),
+            mnet_peer::SSL_HOST => get_string('verifyhostonly', 'core_mnet'),
+            mnet_peer::SSL_HOST_AND_PEER => get_string('verifyhostandpeer', 'core_mnet')
+        );
+        $mform->addElement('select', 'sslverification', get_string('sslverification', 'core_mnet'), $options);
+        $mform->setDefault('sslverification', mnet_peer::SSL_HOST_AND_PEER);
+        $mform->addHelpButton('sslverification', 'sslverification', 'core_mnet');
+
         $themes = array('' => get_string('forceno'));
         foreach (array_keys(core_component::get_plugin_list('theme')) as $themename) {
             $themes[$themename] = get_string('pluginname', 'theme_'.$themename);
index 40759db..24ef9b6 100644 (file)
@@ -172,6 +172,7 @@ if ($formdata = $reviewform->get_data()) {
     $mnet_peer->public_key          = $formdata->public_key;
     $credentials                    = $mnet_peer->check_credentials($mnet_peer->public_key);
     $mnet_peer->public_key_expires  = $credentials['validTo_time_t'];
+    $mnet_peer->sslverification     = $formdata->sslverification;
 
     if ($mnet_peer->commit()) {
         redirect(new moodle_url('/admin/mnet/peers.php', array('hostid' => $mnet_peer->id)), get_string('changessaved'));
index 89734e3..f43225c 100644 (file)
@@ -66,12 +66,19 @@ if (!empty($hostid) && array_key_exists($hostid, $hosts)) {
 
     $mnet_request->set_method('system/listServices');
     $mnet_request->send($mnet_peer);
+
     $services = $mnet_request->response;
     $yesno = array('No', 'Yes');
     $servicenames = array();
 
     echo $OUTPUT->heading(get_string('servicesavailableonhost', 'mnet', $host->wwwroot));
 
+    if (!empty($mnet_request->error)) {
+        echo $OUTPUT->heading(get_string('error'), 3);
+        echo html_writer::alist($mnet_request->error);
+        $services = array();
+    }
+
     $table = new html_table();
     $table->head = array(
         get_string('serviceid', 'mnet'),
@@ -127,6 +134,7 @@ if (!empty($hostid) && array_key_exists($hostid, $hosts)) {
     echo html_writer::table($table);
 
 
+    $mnet_request = new mnet_xmlrpc_client();
     $mnet_request->set_method('system/listMethods');
     if (isset($servicename) && array_key_exists($servicename, $serviceinfo)) {
         echo $OUTPUT->heading(get_string('methodsavailableonhostinservice', 'mnet', (object)array('host' => $host->wwwroot, 'service' => $servicename)));
@@ -139,6 +147,11 @@ if (!empty($hostid) && array_key_exists($hostid, $hosts)) {
     $mnet_request->send($mnet_peer);
     $methods = $mnet_request->response;
 
+    if (!empty($mnet_request->error)) {
+        echo $OUTPUT->heading(get_string('error'), 3);
+        echo html_writer::alist($mnet_request->error);
+        $methods = array();
+    }
 
     $table = new html_table();
     $table->head = array(
@@ -171,6 +184,12 @@ if (!empty($hostid) && array_key_exists($hostid, $hosts)) {
 
         echo $OUTPUT->heading(get_string('methodsignature', 'mnet', $method));
 
+        if (!empty($mnet_request->error)) {
+            echo $OUTPUT->heading(get_string('error'), 3);
+            echo html_writer::alist($mnet_request->error);
+            $signature = array();
+        }
+
         $table = new html_table();
         $table->head = array(
             get_string('position', 'mnet'),
index 0cac01a..9c331f1 100644 (file)
@@ -53,9 +53,9 @@ M.core_role.init_cap_table_filter = function(Y, tableid, contextid) {
             // Create the capability search input.
             this.input = Y.Node.create('<input type="text" id="'+this.table.get('id')+'capabilitysearch" value="'+Y.Escape.html(filtervalue)+'" />');
             // Create a label for the search input.
-            this.label = Y.Node.create('<label for="'+this.input.get('id')+'">'+M.str.moodle.filter+' </label>');
+            this.label = Y.Node.create('<label for="'+this.input.get('id')+'">'+M.util.get_string('filter', 'moodle')+' </label>');
             // Create a clear button to clear the input.
-            this.button = Y.Node.create('<input type="button" value="'+M.str.moodle.clear+'" />').set('disabled', filtervalue=='');
+            this.button = Y.Node.create('<input type="button" value="'+M.util.get_string('clear', 'moodle')+'" />').set('disabled', filtervalue=='');
 
             // Tie it all together
             this.div.append(this.label).append(this.input).append(this.button);
index 99ded8f..4a301b1 100644 (file)
@@ -132,7 +132,7 @@ if ($courseid == SITEID) {
     $PAGE->set_heading($course->fullname.': '.$fullname);
 }
 echo $OUTPUT->header();
-echo $OUTPUT->heading($title, 3);
+echo $OUTPUT->heading($title);
 echo $OUTPUT->box_start('generalbox boxaligncenter boxwidthnormal');
 
 // Display them.
index b904da2..062df28 100644 (file)
@@ -144,6 +144,7 @@ mybadges,badges|/badges/mybadges.php|award',
     $temp->add(new admin_setting_configtext('navcourselimit',new lang_string('navcourselimit','admin'),new lang_string('confignavcourselimit', 'admin'),20,PARAM_INT));
     $temp->add(new admin_setting_configcheckbox('usesitenameforsitepages', new lang_string('usesitenameforsitepages', 'admin'), new lang_string('configusesitenameforsitepages', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('linkadmincategories', new lang_string('linkadmincategories', 'admin'), new lang_string('linkadmincategories_help', 'admin'), 0));
+    $temp->add(new admin_setting_configcheckbox('linkcoursesections', new lang_string('linkcoursesections', 'admin'), new lang_string('linkcoursesections_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('navshowfrontpagemods', new lang_string('navshowfrontpagemods', 'admin'), new lang_string('navshowfrontpagemods_help', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('navadduserpostslinks', new lang_string('navadduserpostslinks', 'admin'), new lang_string('navadduserpostslinks_help', 'admin'), 1));
 
index b455946..af1e6c8 100644 (file)
@@ -70,6 +70,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configtext('minpasswordupper', new lang_string('minpasswordupper', 'admin'), new lang_string('configminpasswordupper', 'admin'), 1, PARAM_INT));
     $temp->add(new admin_setting_configtext('minpasswordnonalphanum', new lang_string('minpasswordnonalphanum', 'admin'), new lang_string('configminpasswordnonalphanum', 'admin'), 1, PARAM_INT));
     $temp->add(new admin_setting_configtext('maxconsecutiveidentchars', new lang_string('maxconsecutiveidentchars', 'admin'), new lang_string('configmaxconsecutiveidentchars', 'admin'), 0, PARAM_INT));
+
+    $temp->add(new admin_setting_configtext('passwordreuselimit',
+        new lang_string('passwordreuselimit', 'admin'),
+        new lang_string('passwordreuselimit_desc', 'admin'), 0, PARAM_INT));
+
     $pwresetoptions = array(
         300 => new lang_string('numminutes', '', 5),
         900 => new lang_string('numminutes', '', 15),
index 29a5966..4b3b11e 100644 (file)
@@ -153,6 +153,19 @@ $temp->add(new admin_setting_configselect('gradehistorylifetime', new lang_strin
                                                                                                      60 => new lang_string('numdays', '', 60),
                                                                                                      30 => new lang_string('numdays', '', 30))));
 
+$temp->add(new admin_setting_configselect('tempdatafoldercleanup', new lang_string('tempdatafoldercleanup', 'admin'),
+        new lang_string('configtempdatafoldercleanup', 'admin'), 168, array(
+            1 => new lang_string('numhours', '', 1),
+            3 => new lang_string('numhours', '', 3),
+            6 => new lang_string('numhours', '', 6),
+            9 => new lang_string('numhours', '', 9),
+            12 => new lang_string('numhours', '', 12),
+            18 => new lang_string('numhours', '', 18),
+            24 => new lang_string('numhours', '', 24),
+            48 => new lang_string('numdays', '', 2),
+            168 => new lang_string('numdays', '', 7),
+)));
+
 $ADMIN->add('server', $temp);
 
 
index 933408a..ab7f670 100644 (file)
@@ -55,7 +55,7 @@ M.tool_assignmentupgrade = {
             assignmentsinput = Y.one('input.selectedassignments');
             assignmentsinput.set('value', selectedassignments.join(','));
             if (selectedassignments.length == 0) {
-                alert(M.str.tool_assignmentupgrade.noassignmentsselected);
+                alert(M.util.get_string('noassignmentsselected', 'tool_assignmentupgrade'));
                 e.preventDefault();
             }
         });
index f0fe6c8..1e5b147 100644 (file)
@@ -28,6 +28,7 @@
     $extraws = ob_get_clean();
 
     require_once($CFG->libdir.'/adminlib.php');
+    require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/health/locallib.php');
 
     admin_externalpage_setup('toolhealth');
 
@@ -603,34 +604,10 @@ class problem_000017 extends problem_base {
             $categories = $DB->get_records('question_categories', array(), 'id');
 
             // Look for missing parents.
-            $missingparent = array();
-            foreach ($categories as $category) {
-                if ($category->parent != 0 && !array_key_exists($category->parent, $categories)) {
-                    $missingparent[$category->id] = $category;
-                }
-            }
+            $missingparent = tool_health_category_find_missing_parents($categories);
 
             // Look for loops.
-            $loops = array();
-            while (!empty($categories)) {
-                $current = array_pop($categories);
-                $thisloop = array($current->id => $current);
-                while (true) {
-                    if (isset($thisloop[$current->parent])) {
-                        // Loop detected
-                        $loops[$current->id] = $thisloop;
-                        break;
-                    } else if (!isset($categories[$current->parent])) {
-                        // Got to the top level, or a category we already know is OK.
-                        break;
-                    } else {
-                        // Continue following the path.
-                        $current = $categories[$current->parent];
-                        $thisloop[$current->id] = $current;
-                        unset($categories[$current->id]);
-                    }
-                }
-            }
+            $loops = tool_health_category_find_loops($categories);
 
             $answer = array($missingparent, $loops);
         }
@@ -651,29 +628,126 @@ class problem_000017 extends problem_base {
                 ' structures by the question_categories.parent field. Sometimes ' .
                 ' this tree structure gets messed up.</p>';
 
+        $description .= tool_health_category_list_missing_parents($missingparent);
+        $description .= tool_health_category_list_loops($loops);
+
+        return $description;
+    }
+
+    /**
+     * Outputs resolutions to problems outlined in MDL-34684 with items having themselves as parent
+     *
+     * @link https://tracker.moodle.org/browse/MDL-34684
+     * @return string Formatted html to be output to the browser with instructions and sql statements to run
+     */
+    public function solution() {
+        global $CFG;
+        list($missingparent, $loops) = $this->find_problems();
+
+        $solution = '<p>Consider executing the following SQL queries. These fix ' .
+                'the problem by moving some categories to the top level.</p>';
+
         if (!empty($missingparent)) {
-            $description .= '<p>The following categories are missing their parents:</p><ul>';
-            foreach ($missingparent as $cat) {
-                $description .= "<li>Category $cat->id: " . s($cat->name) . "</li>\n";
-            }
-            $description .= "</ul>\n";
+            $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
+                    "        SET parent = 0\n" .
+                    "        WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
         }
 
         if (!empty($loops)) {
-            $description .= '<p>The following categories form a loop of parents:</p><ul>';
-            foreach ($loops as $loop) {
-                $description .= "<li><ul>\n";
-                foreach ($loop as $cat) {
-                    $description .= "<li>Category $cat->id: " . s($cat->name) . " has parent $cat->parent</li>\n";
-                }
-                $description .= "</ul></li>\n";
-            }
-            $description .= "</ul>\n";
+            $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
+                    "        SET parent = 0\n" .
+                    "        WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
+        }
+
+        return $solution;
+    }
+}
+
+/**
+ * Check course categories tree structure for problems.
+ *
+ * @copyright  2013 Marko Vidberg
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class problem_000018 extends problem_base {
+    /**
+     * Generate title for this problem.
+     *
+     * @return string Title of problem.
+     */
+    public function title() {
+        return 'Course categories tree structure';
+    }
+
+    /**
+     * Search for problems in the course categories.
+     *
+     * @uses $DB
+     * @return array List of categories that contain missing parents or loops.
+     */
+    public function find_problems() {
+        global $DB;
+        static $answer = null;
+
+        if (is_null($answer)) {
+            $categories = $DB->get_records('course_categories', array(), 'id');
+
+            // Look for missing parents.
+            $missingparent = tool_health_category_find_missing_parents($categories);
+
+            // Look for loops.
+            $loops = tool_health_category_find_loops($categories);
+
+            $answer = array($missingparent, $loops);
         }
 
+        return $answer;
+    }
+
+    /**
+     * Check if the problem exists.
+     *
+     * @return boolean True if either missing parents or loops found
+     */
+    public function exists() {
+        list($missingparent, $loops) = $this->find_problems();
+        return !empty($missingparent) || !empty($loops);
+    }
+
+    /**
+     * Set problem severity.
+     *
+     * @return constant Problem severity.
+     */
+    public function severity() {
+        return SEVERITY_SIGNIFICANT;
+    }
+
+    /**
+     * Generate problem description.
+     *
+     * @return string HTML containing details of the problem.
+     */
+    public function description() {
+        list($missingparent, $loops) = $this->find_problems();
+
+        $description = '<p>The course categories should be arranged into tree ' .
+                ' structures by the course_categories.parent field. Sometimes ' .
+                ' this tree structure gets messed up.</p>';
+
+        $description .= tool_health_category_list_missing_parents($missingparent);
+        $description .= tool_health_category_list_loops($loops);
+
         return $description;
     }
-    function solution() {
+
+    /**
+     * Generate solution text.
+     *
+     * @uses $CFG
+     * @return string HTML containing the suggested solution.
+     */
+    public function solution() {
         global $CFG;
         list($missingparent, $loops) = $this->find_problems();
 
@@ -681,14 +755,14 @@ class problem_000017 extends problem_base {
                 'the problem by moving some categories to the top level.</p>';
 
         if (!empty($missingparent)) {
-            $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
-                    "        SET parent = 0\n" .
+            $solution .= "<pre>UPDATE " . $CFG->prefix . "course_categories\n" .
+                    "        SET parent = 0, depth = 1, path = CONCAT('/', id)\n" .
                     "        WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
         }
 
         if (!empty($loops)) {
-            $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
-                    "        SET parent = 0\n" .
+            $solution .= "<pre>UPDATE " . $CFG->prefix . "course_categories\n" .
+                    "        SET parent = 0, depth = 1, path = CONCAT('/', id)\n" .
                     "        WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
         }
 
diff --git a/admin/tool/health/locallib.php b/admin/tool/health/locallib.php
new file mode 100644 (file)
index 0000000..9eb28f9
--- /dev/null
@@ -0,0 +1,128 @@
+<?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/>.
+
+/**
+ * Functions used by the health tool.
+ *
+ * @package    tool_health
+ * @copyright  2013 Marko Vidberg
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Given a list of categories, this function searches for ones
+ * that have a missing parent category.
+ *
+ * @param array $categories List of categories.
+ * @return array List of categories with missing parents.
+ */
+function tool_health_category_find_missing_parents($categories) {
+    $missingparent = array();
+
+    foreach ($categories as $category) {
+        if ($category->parent != 0 && !array_key_exists($category->parent, $categories)) {
+            $missingparent[$category->id] = $category;
+        }
+    }
+
+    return $missingparent;
+}
+
+/**
+ * Generates a list of categories with missing parents.
+ *
+ * @param array $missingparent List of categories with missing parents.
+ * @return string Bullet point list of categories with missing parents.
+ */
+function tool_health_category_list_missing_parents($missingparent) {
+    $description = '';
+
+    if (!empty($missingparent)) {
+        $description .= '<p>The following categories are missing their parents:</p><ul>';
+        foreach ($missingparent as $cat) {
+            $description .= "<li>Category $cat->id: " . s($cat->name) . "</li>\n";
+        }
+        $description .= "</ul>\n";
+    }
+
+    return $description;
+}
+
+/**
+ * Given a list of categories, this function searches for ones
+ * that have loops to previous parent categories.
+ *
+ * @param array $categories List of categories.
+ * @return array List of categories with loops.
+ */
+function tool_health_category_find_loops($categories) {
+    $loops = array();
+
+    while (!empty($categories)) {
+
+        $current = array_pop($categories);
+        $thisloop = array($current->id => $current);
+
+        while (true) {
+            if (isset($thisloop[$current->parent])) {
+                // Loop detected.
+                $loops = $loops + $thisloop;
+                break;
+            } else if ($current->parent === 0) {
+                // Top level.
+                break;
+            } else if (isset($loops[$current->parent])) {
+                // If the parent is in a loop we should also update this category.
+                $loops = $loops + $thisloop;
+                break;
+            } else if (!isset($categories[$current->parent])) {
+                // We already checked this category and is correct.
+                break;
+            } else {
+                // Continue following the path.
+                $current = $categories[$current->parent];
+                $thisloop[$current->id] = $current;
+                unset($categories[$current->id]);
+            }
+        }
+    }
+
+    return $loops;
+}
+
+/**
+ * Generates a list of categories with loops.
+ *
+ * @param array $loops List of categories with loops.
+ * @return string Bullet point list of categories with loops.
+ */
+function tool_health_category_list_loops($loops) {
+    $description = '';
+
+    if (!empty($loops)) {
+        $description .= '<p>The following categories form a loop of parents:</p><ul>';
+        foreach ($loops as $loop) {
+            $description .= "<li>\n";
+            $description .= "Category $loop->id: " . s($loop->name) . " has parent $loop->parent\n";
+            $description .= "</li>\n";
+        }
+        $description .= "</ul>\n";
+    }
+
+    return $description;
+}
diff --git a/admin/tool/health/tests/healthlib_test.php b/admin/tool/health/tests/healthlib_test.php
new file mode 100644 (file)
index 0000000..0190af9
--- /dev/null
@@ -0,0 +1,218 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for tool_health.
+ *
+ * @package    tool_health
+ * @copyright  2013 Marko Vidberg
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/health/locallib.php');
+
+/**
+ * Health lib testcase.
+ *
+ * @package    tool_health
+ * @copyright  2013 Marko Vidberg
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class healthlib_testcase extends advanced_testcase {
+
+    /**
+     * Data provider for test_tool_health_category_find_loops.
+     */
+    public static function provider_loop_categories() {
+        return array(
+            // One item loop including root.
+            0 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 1)
+                ),
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 1)
+                ),
+            ),
+            // One item loop not including root.
+            1 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 0),
+                    '2' => (object) array('id' => 2, 'parent' => 2)
+                ),
+                array(
+                    '2' => (object) array('id' => 2, 'parent' => 2)
+                ),
+            ),
+            // Two item loop including root.
+            2 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1)
+                ),
+                array(
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                )
+            ),
+            // Two item loop not including root.
+            3 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 0),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 2),
+                ),
+                array(
+                    '3' => (object) array('id' => 3, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                )
+            ),
+            // Three item loop including root.
+            4 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 1),
+                ),
+                array(
+                    '3' => (object) array('id' => 3, 'parent' => 1),
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                )
+            ),
+            // Three item loop not including root.
+            5 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 0),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 4),
+                    '4' => (object) array('id' => 4, 'parent' => 2)
+                ),
+                array(
+                    '4' => (object) array('id' => 4, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 4),
+                )
+            ),
+            // Multi-loop.
+            6 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '3' => (object) array('id' => 3, 'parent' => 4),
+                    '4' => (object) array('id' => 4, 'parent' => 5),
+                    '5' => (object) array('id' => 5, 'parent' => 3),
+                    '6' => (object) array('id' => 6, 'parent' => 6),
+                    '7' => (object) array('id' => 7, 'parent' => 1),
+                    '8' => (object) array('id' => 8, 'parent' => 7),
+                ),
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '8' => (object) array('id' => 8, 'parent' => 7),
+                    '7' => (object) array('id' => 7, 'parent' => 1),
+                    '6' => (object) array('id' => 6, 'parent' => 6),
+                    '5' => (object) array('id' => 5, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 4),
+                    '4' => (object) array('id' => 4, 'parent' => 5),
+                )
+            ),
+            // Double-loop
+            7 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '3' => (object) array('id' => 3, 'parent' => 2),
+                    '4' => (object) array('id' => 4, 'parent' => 2),
+                ),
+                array(
+                    '4' => (object) array('id' => 4, 'parent' => 2),
+                    '3' => (object) array('id' => 3, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                )
+            )
+        );
+    }
+
+    /**
+     * Data provider for test_tool_health_category_find_missing_parents.
+     */
+    public static function provider_missing_parent_categories() {
+        return array(
+           // Test for two items, both with direct ancestor (parent) missing.
+            0 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 0),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '4' => (object) array('id' => 4, 'parent' => 5),
+                    '6' => (object) array('id' => 6, 'parent' => 2)
+                ),
+                array(
+                    '4' => (object) array('id' => 4, 'parent' => 5),
+                    '2' => (object) array('id' => 2, 'parent' => 3)
+                ),
+            )
+        );
+    }
+
+    /**
+     * Test finding loops between two items referring to each other.
+     *
+     * @param array $categories
+     * @param array $expected
+     * @dataProvider provider_loop_categories
+     */
+    public function test_tool_health_category_find_loops($categories, $expected) {
+        $loops = tool_health_category_find_loops($categories);
+        $this->assertEquals($expected, $loops);
+    }
+
+    /**
+     * Test finding missing parent categories.
+     *
+     * @param array $categories
+     * @param array $expected
+     * @dataProvider provider_missing_parent_categories
+     */
+    public function test_tool_health_category_find_missing_parents($categories, $expected) {
+        $missingparent = tool_health_category_find_missing_parents($categories);
+        $this->assertEquals($expected, $missingparent);
+    }
+
+    /**
+     * Test listing missing parent categories.
+     */
+    public function test_tool_health_category_list_missing_parents() {
+        $missingparent = array((object) array('id' => 2, 'parent' => 3, 'name' => 'test'),
+                               (object) array('id' => 4, 'parent' => 5, 'name' => 'test2'));
+        $result = tool_health_category_list_missing_parents($missingparent);
+        $this->assertRegExp('/Category 2: test/', $result);
+        $this->assertRegExp('/Category 4: test2/', $result);
+    }
+
+    /**
+     * Test listing loop categories.
+     */
+    public function test_tool_health_category_list_loops() {
+        $loops = array((object) array('id' => 2, 'parent' => 3, 'name' => 'test'));
+        $result = tool_health_category_list_loops($loops);
+        $this->assertRegExp('/Category 2: test/', $result);
+    }
+}
index 48d561e..faabf71 100644 (file)
@@ -95,7 +95,7 @@ class store implements \tool_log\log\store, \core\log\sql_select_reader {
         $records = array();
 
         try {
-            $records = $DB->get_records_select('log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
+            $records = $DB->get_recordset_select('log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
         } catch (\moodle_exception $ex) {
             debugging("error converting legacy event data " . $ex->getMessage() . $ex->debuginfo, DEBUG_DEVELOPER);
         }
@@ -104,6 +104,8 @@ class store implements \tool_log\log\store, \core\log\sql_select_reader {
             $events[$data->id] = \logstore_legacy\event\legacy_logged::restore_legacy($data);
         }
 
+        $records->close();
+
         return $events;
     }
 
index 992d4cf..44c66de 100644 (file)
@@ -72,7 +72,7 @@ class store implements \tool_log\log\writer, \core\log\sql_internal_reader {
         $sort = self::tweak_sort_by_id($sort);
 
         $events = array();
-        $records = $DB->get_records_select('logstore_standard_log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
+        $records = $DB->get_recordset_select('logstore_standard_log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
 
         foreach ($records as $data) {
             $extra = array('origin' => $data->origin, 'ip' => $data->ip, 'realuserid' => $data->realuserid);
@@ -94,6 +94,8 @@ class store implements \tool_log\log\writer, \core\log\sql_internal_reader {
             }
         }
 
+        $records->close();
+
         return $events;
     }
 
index dcb4e7b..7e12367 100644 (file)
@@ -295,6 +295,11 @@ class manager {
         // First flag this message to prevent another running hitting this message while we look at the headers.
         $this->add_flag_to_message($messageid, self::MESSAGE_FLAGGED);
 
+        if ($this->is_bulk_message($message, $messageid)) {
+            mtrace("- The message has a bulk header set. This is likely an auto-generated reply - discarding.");
+            return;
+        }
+
         // Record the user that this script is currently being run as.  This is important when re-processing existing
         // messages, as cron_setup_user is called multiple times.
         $originaluser = $USER;
@@ -741,6 +746,43 @@ class manager {
         return in_array($flag, $flags);
     }
 
+    /**
+     * Attempt to determine whether this message is a bulk message (e.g. automated reply).
+     *
+     * @param \Horde_Imap_Client_Data_Fetch $message The message to process
+     * @param string|\Horde_Imap_Client_Ids $messageid The Hore message Uid
+     * @return boolean
+     */
+    private function is_bulk_message(
+            \Horde_Imap_Client_Data_Fetch $message,
+            $messageid) {
+        $query = new \Horde_Imap_Client_Fetch_Query();
+        $query->headerText(array('peek' => true));
+
+        $messagedata = $this->client->fetch($this->get_mailbox(), $query, array('ids' => $messageid))->first();
+
+        // Assume that this message is not bulk to begin with.
+        $isbulk = false;
+
+        // An auto-reply may itself include the Bulk Precedence.
+        $precedence = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Precedence');
+        $isbulk = $isbulk || strtolower($precedence) == 'bulk';
+
+        // If the X-Autoreply header is set, and not 'no', then this is an automatic reply.
+        $autoreply = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('X-Autoreply');
+        $isbulk = $isbulk || ($autoreply && $autoreply != 'no');
+
+        // If the X-Autorespond header is set, and not 'no', then this is an automatic response.
+        $autorespond = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('X-Autorespond');
+        $isbulk = $isbulk || ($autorespond && $autorespond != 'no');
+
+        // If the Auto-Submitted header is set, and not 'no', then this is a non-human response.
+        $autosubmitted = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Auto-Submitted');
+        $isbulk = $isbulk || ($autosubmitted && $autosubmitted != 'no');
+
+        return $isbulk;
+    }
+
     /**
      * Send the message to the appropriate handler.
      *
index 73b684f..79356c6 100644 (file)
@@ -404,12 +404,11 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
      * Run adhoc tasks.
      */
     protected function run_adhock_tasks() {
-        ob_start();
         while ($task = \core\task\manager::get_next_adhoc_task(time())) {
             $task->execute();
             \core\task\manager::adhoc_task_complete($task);
         }
-        ob_clean(); // Suppress mtrace debugging info.
+        $this->expectOutputRegex("/^Sending message to the user with id \d+ for the subscription with id \d+\.\.\..Sent./ms");
     }
 
     /**
index 03ec2af..4025312 100644 (file)
@@ -16,9 +16,6 @@
 .path-admin-tool-profiling .flexible .referencerun {
     font-weight: bold;
 }
-.path-admin-tool-profiling .flexible .r1 .cell {
-    background-color: whitesmoke;
-}
 .path-admin-tool-profiling .flexible {
     margin-left:auto;
     margin-right:auto
index d7cd018..166d5cd 100644 (file)
@@ -6,7 +6,7 @@ M.tool_spamcleaner = {
     del_all: function() {
         var context = M.tool_spamcleaner;
 
-        var yes = confirm(M.str.tool_spamcleaner.spamdeleteallconfirm);
+        var yes = confirm(M.util.get_string('spamdeleteallconfirm', 'tool_spamcleaner'));
         if (yes) {
             var cfg = {
                 method: "POST",
@@ -15,7 +15,7 @@ M.tool_spamcleaner = {
                         try {
                             var resp = context.Y.JSON.parse(o.responseText);
                         } catch(e) {
-                            alert(M.str.tool_spamcleaner.spaminvalidresult);
+                            alert(M.util.get_string('spaminvalidresult', 'tool_spamcleaner'));
                             return;
                         }
                         if (resp == true) {
@@ -36,7 +36,7 @@ M.tool_spamcleaner = {
             return;
         }
 
-        var yes = confirm(M.str.tool_spamcleaner.spamdeleteconfirm);
+        var yes = confirm(M.util.get_string('spamdeleteconfirm', 'tool_spamcleaner'));
         if (yes) {
             context.row = obj;
             var cfg = {
@@ -46,7 +46,7 @@ M.tool_spamcleaner = {
                         try {
                             var resp = context.Y.JSON.parse(o.responseText);
                         } catch(e) {
-                            alert(M.str.tool_spamcleaner.spaminvalidresult);
+                            alert(M.util.get_string('spaminvalidresult', 'tool_spamcleaner'));
                             return;
                         }
                         if (context.row) {
@@ -57,7 +57,7 @@ M.tool_spamcleaner = {
                                 context.row.parentNode.removeChild(context.row);
                                 context.row = null;
                             } else {
-                                alert(M.str.tool_spamcleaner.spamcannotdelete);
+                                alert(M.util.get_string('spamcannotdelete', 'tool_spamcleaner'));
                             }
                         }
                     }
@@ -83,7 +83,7 @@ M.tool_spamcleaner = {
                     try {
                         var resp = context.Y.JSON.parse(o.responseText);
                     } catch(e) {
-                        alert(M.str.tool_spamcleaner.spaminvalidresult);
+                        alert(M.util.get_string('spaminvalidresult', 'tool_spamcleaner'));
                         return;
                     }
                     if (context.row) {
diff --git a/admin/tool/xmldb/styles_bootstrapbase.css b/admin/tool/xmldb/styles_bootstrapbase.css
new file mode 100644 (file)
index 0000000..ae530cb
--- /dev/null
@@ -0,0 +1,3 @@
+.path-admin-tool-xmldb a[name="lastused"] {
+    padding-top: 50px;
+}
index 8ab97ae..f244c45 100644 (file)
@@ -89,6 +89,7 @@ class auth_plugin_email extends auth_plugin_base {
         require_once($CFG->dirroot.'/user/profile/lib.php');
         require_once($CFG->dirroot.'/user/lib.php');
 
+        $plainpassword = $user->password;
         $user->password = hash_internal_user_password($user->password);
         if (empty($user->calendartype)) {
             $user->calendartype = $CFG->calendartype;
@@ -96,6 +97,8 @@ class auth_plugin_email extends auth_plugin_base {
 
         $user->id = user_create_user($user, false, false);
 
+        user_add_password_history($user->id, $plainpassword);
+
         // Save any custom profile field information.
         profile_save_data($user);
 
index f80ab19..1da1b66 100644 (file)
@@ -539,6 +539,7 @@ class auth_plugin_ldap extends auth_plugin_base {
         global $CFG, $DB, $PAGE, $OUTPUT;
 
         require_once($CFG->dirroot.'/user/profile/lib.php');
+        require_once($CFG->dirroot.'/user/lib.php');
 
         if ($this->user_exists($user->username)) {
             print_error('auth_ldap_user_exists', 'auth_ldap');
@@ -553,6 +554,8 @@ class auth_plugin_ldap extends auth_plugin_base {
 
         $user->id = user_create_user($user, false, false);
 
+        user_add_password_history($user->id, $plainslashedpassword);
+
         // Save any custom profile field information
         profile_save_data($user);
 
@@ -739,7 +742,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                     } while ($entry = ldap_next_entry($ldapconnection, $entry));
                 }
                 unset($ldap_result); // Free mem.
-            } while ($ldap_pagedresults && !empty($ldap_cookie));
+            } while ($ldap_pagedresults && $ldap_cookie !== null && $ldap_cookie != '');
         }
 
         // If LDAP paged results were used, the current connection must be completely
index 849dad2..d1adb68 100644 (file)
@@ -61,8 +61,8 @@ Moodle Configuration with Dual login
 --
 
    Also see:
-   https://spaces.internet2.edu/display/SHIB2/NativeSPRequestMapper and
-   https://spaces.internet2.edu/display/SHIB2/NativeSPAccessControl
+   https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPRequestMapper and
+   https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPAccessControl
 
 2. As Moodle admin, go to the 'Administrations >> Users >> Authentication' and
    click on the the 'Shibboleth' settings.
@@ -101,8 +101,8 @@ Moodle Configuration with Dual login
     to the same host. If no SessionInitiator URL is given, the default one
     '/Shibboleth.sso' (only works for Shibboleth 1.3.x) will be used. For
     Shibboleth 2.x you have to add '/Shibboleth.sso/DS' as a SessionInitiator.
-    Also see https://spaces.internet2.edu/display/SHIB/SessionInitiator
-    and https://spaces.internet2.edu/display/SHIB2/NativeSPSessionInitiator
+    Also see https://wiki.shibboleth.net/confluence/display/SHIB/SessionInitiator
+    and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSessionInitiator
 
     Important Note: If you upgraded from a previous version of Moodle and now
                     want to use the integrated WAYF, you have to make sure that
@@ -324,8 +324,8 @@ logout. Hopefully, the Moodle logout helps to motivate the developers to
 implement SLO. On the other hand, the easiest and safest way to log out
 still is to tell users to quit their web browsers :)
 
-Also see https://spaces.internet2.edu/display/SHIB2/SLOIssues and
-https://spaces.internet2.edu/display/SHIB2/NativeSPLogoutInitiator for some
+Also see https://wiki.shibboleth.net/confluence/display/SHIB2/SLOIssues and
+https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLogoutInitiator for some
 background information on this topic.
 
 --------------------------------------------------------------------------------
index bd16aaa..d5181fd 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 
 // Implements logout for Shibboleth authenticated users according to:
-// - https://spaces.internet2.edu/display/SHIB2/NativeSPLogoutInitiator
-// - https://spaces.internet2.edu/display/SHIB2/NativeSPNotify
+// - https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLogoutInitiator
+// - https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPNotify
 
 require_once("../../config.php");
 
@@ -66,8 +66,8 @@ Because neither of these two variants seems to be the case, the WSDL file for
 the web service is returned.
 
 For more information see:
-- https://spaces.internet2.edu/display/SHIB2/NativeSPLogoutInitiator
-- https://spaces.internet2.edu/display/SHIB2/NativeSPNotify
+- https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLogoutInitiator
+- https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPNotify
 -->
 
     <types>
index 0c6d453..a2acc9f 100644 (file)
@@ -5,6 +5,8 @@ information provided here is intended especially for developers.
 
 * Do not update user->firstaccess from any auth plugin, the complete_user_login() does it automatically.
 
+* Add user_add_password_history() to user_signup() method.
+
 === 2.8 ===
 
 * \core\session\manager::session_exists() now verifies the session is active
index 27350c4..803f565 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js differ
index eb98c96..17f1813 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js differ
index 27350c4..803f565 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js differ
index 926a9e4..1416260 100644 (file)
@@ -23,22 +23,21 @@ M.availability_completion.form.initInner = function(cms) {
 
 M.availability_completion.form.getNode = function(json) {
     // Create HTML structure.
-    var strings = M.str.availability_completion;
-    var html = strings.title + ' <span class="availability-group"><label>' +
-            '<span class="accesshide">' + strings.label_cm + ' </span>' +
-            '<select name="cm" title="' + strings.label_cm + '">' +
-            '<option value="0">' + M.str.moodle.choosedots + '</option>';
+    var html = M.util.get_string('title', 'availability_completion') + ' <span class="availability-group"><label>' +
+            '<span class="accesshide">' + M.util.get_string('label_cm', 'availability_completion') + ' </span>' +
+            '<select name="cm" title="' + M.util.get_string('label_cm', 'availability_completion') + '">' +
+            '<option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     for (var i = 0; i < this.cms.length; i++) {
         var cm = this.cms[i];
         // String has already been escaped using format_string.
         html += '<option value="' + cm.id + '">' + cm.name + '</option>';
     }
-    html += '</select></label> <label><span class="accesshide">' + strings.label_completion +
-            ' </span><select name="e" title="' + strings.label_completion + '">' +
-            '<option value="1">' + strings.option_complete + '</option>' +
-            '<option value="0">' + strings.option_incomplete + '</option>' +
-            '<option value="2">' + strings.option_pass + '</option>' +
-            '<option value="3">' + strings.option_fail + '</option>' +
+    html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_completion', 'availability_completion') +
+            ' </span><select name="e" title="' + M.util.get_string('label_completion', 'availability_completion') + '">' +
+            '<option value="1">' + M.util.get_string('option_complete', 'availability_completion') + '</option>' +
+            '<option value="0">' + M.util.get_string('option_incomplete', 'availability_completion') + '</option>' +
+            '<option value="2">' + M.util.get_string('option_pass', 'availability_completion') + '</option>' +
+            '<option value="3">' + M.util.get_string('option_fail', 'availability_completion') + '</option>' +
             '</select></label></span>';
     var node = Y.Node.create('<span>' + html + '</span>');
 
index d84da8c..684d208 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js differ
index 7447c99..eeb12a3 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js differ
index d84da8c..684d208 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js differ
index 580c6eb..ca45ffc 100644 (file)
@@ -27,12 +27,11 @@ M.availability_date.form.initInner = function(html, defaultTime) {
 };
 
 M.availability_date.form.getNode = function(json) {
-    var strings = M.str.availability_date;
-    var html = strings.direction_before + ' <span class="availability-group">' +
-            '<label><span class="accesshide">' + strings.direction_label + ' </span>' +
+    var html = M.util.get_string('direction_before', 'availability_date') + ' <span class="availability-group">' +
+            '<label><span class="accesshide">' + M.util.get_string('direction_label', 'availability_date') + ' </span>' +
             '<select name="direction">' +
-            '<option value="&gt;=">' + strings.direction_from + '</option>' +
-            '<option value="&lt;">' + strings.direction_until + '</option>' +
+            '<option value="&gt;=">' + M.util.get_string('direction_from', 'availability_date') + '</option>' +
+            '<option value="&lt;">' + M.util.get_string('direction_until', 'availability_date') + '</option>' +
             '</select></label></span> ' + this.html;
     var node = Y.Node.create('<span>' + html + '</span>');
 
@@ -56,7 +55,7 @@ M.availability_date.form.getNode = function(json) {
                 }
             },
             failure : function() {
-                window.alert(M.str.availability_date.ajaxerror);
+                window.alert(M.util.get_string('ajaxerror', 'availability_date'));
             }
         }});
     } else {
@@ -130,7 +129,7 @@ M.availability_date.form.updateTime = function(node) {
             M.core_availability.form.update();
         },
         failure : function() {
-            window.alert(M.str.availability_date.ajaxerror);
+            window.alert(M.util.get_string('ajaxerror', 'availability_date'));
         }
     }});
 };
index 28f5f81..ad24176 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js differ
index 83c5e5a..57654e4 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js differ
index 28f5f81..ad24176 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js differ
index e0b0fe9..92108aa 100644 (file)
@@ -35,24 +35,23 @@ M.availability_grade.form.getNode = function(json) {
     this.nodesSoFar++;
 
     // Create HTML structure.
-    var strings = M.str.availability_grade;
-    var html = '<label>' + strings.title + ' <span class="availability-group">' +
-            '<select name="id"><option value="0">' + M.str.moodle.choosedots + '</option>';
+    var html = '<label>' + M.util.get_string('title', 'availability_grade') + ' <span class="availability-group">' +
+            '<select name="id"><option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     for (var i = 0; i < this.grades.length; i++) {
         var grade = this.grades[i];
         // String has already been escaped using format_string.
         html += '<option value="' + grade.id + '">' + grade.name + '</option>';
     }
     html += '</select></span></label> <span class="availability-group">' +
-            '<label><input type="checkbox" name="min"/>' + strings.option_min +
-            '</label> <label><span class="accesshide">' + strings.label_min +
+            '<label><input type="checkbox" name="min"/>' + M.util.get_string('option_min', 'availability_grade') +
+            '</label> <label><span class="accesshide">' + M.util.get_string('label_min', 'availability_grade') +
             '</span><input type="text" name="minval" title="' +
-            strings.label_min + '"/></label>%</span>' +
+            M.util.get_string('label_min', 'availability_grade') + '"/></label>%</span>' +
             '<span class="availability-group">' +
-            '<label><input type="checkbox" name="max"/>' + strings.option_max +
-            '</label> <label><span class="accesshide">' + strings.label_max +
+            '<label><input type="checkbox" name="max"/>' + M.util.get_string('option_max', 'availability_grade') +
+            '</label> <label><span class="accesshide">' + M.util.get_string('label_max', 'availability_grade') +
             '</span><input type="text" name="maxval" title="' +
-            strings.label_max + '"/></label>%</span>';
+            M.util.get_string('label_max', 'availability_grade') + '"/></label>%</span>';
     var node = Y.Node.create('<span>' + html + '</span>');
 
     // Set initial values.
index a968d4e..a1b8a65 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js differ
index a5fe42f..3949118 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js differ
index a968d4e..a1b8a65 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js differ
index e68a703..f9ec44f 100644 (file)
@@ -31,11 +31,10 @@ M.availability_group.form.initInner = function(groups) {
 
 M.availability_group.form.getNode = function(json) {
     // Create HTML structure.
-    var strings = M.str.availability_group;
-    var html = '<label>' + strings.title + ' <span class="availability-group">' +
+    var html = '<label>' + M.util.get_string('title', 'availability_group') + ' <span class="availability-group">' +
             '<select name="id">' +
-            '<option value="choose">' + M.str.moodle.choosedots + '</option>' +
-            '<option value="any">' + strings.anygroup + '</option>';
+            '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>' +
+            '<option value="any">' + M.util.get_string('anygroup', 'availability_group') + '</option>';
     for (var i = 0; i < this.groups.length; i++) {
         var group = this.groups[i];
         // String has already been escaped using format_string.
index 4c0a4ab..f3d616a 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js differ
index 305d8d5..34c9759 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js differ
index 4c0a4ab..f3d616a 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js differ
index dc09e83..b0fd7c9 100644 (file)
@@ -31,10 +31,9 @@ M.availability_grouping.form.initInner = function(groupings) {
 
 M.availability_grouping.form.getNode = function(json) {
     // Create HTML structure.
-    var strings = M.str.availability_grouping;
-    var html = '<label>' + strings.title + ' <span class="availability-group">' +
+    var html = '<label>' + M.util.get_string('title', 'availability_grouping') + ' <span class="availability-group">' +
             '<select name="id">' +
-            '<option value="choose">' + M.str.moodle.choosedots + '</option>';
+            '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     for (var i = 0; i < this.groupings.length; i++) {
         var grouping = this.groupings[i];
         // String has already been escaped using format_string.
index c4b906d..be46a7e 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js differ
index 6251004..63a87d3 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js differ
index c4b906d..be46a7e 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js differ
index aa01d6a..5336589 100644 (file)
@@ -33,10 +33,9 @@ M.availability_profile.form.initInner = function(standardFields, customFields) {
 
 M.availability_profile.form.getNode = function(json) {
     // Create HTML structure.
-    var strings = M.str.availability_profile;
-    var html = '<span class="availability-group"><label>' + strings.conditiontitle + ' ' +
+    var html = '<span class="availability-group"><label>' + M.util.get_string('conditiontitle', 'availability_profile') + ' ' +
             '<select name="field">' +
-            '<option value="choose">' + M.str.moodle.choosedots + '</option>';
+            '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     var fieldInfo;
     for (var i = 0; i < this.standardFields.length; i++) {
         fieldInfo = this.standardFields[i];
@@ -48,17 +47,17 @@ M.availability_profile.form.getNode = function(json) {
         // String has already been escaped using format_string.
         html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
     }
-    html += '</select></label> <label><span class="accesshide">' + strings.label_operator +
-            ' </span><select name="op" title="' + strings.label_operator + '">';
+    html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_operator', 'availability_profile') +
+            ' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '">';
     var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith',
             'isempty', 'isnotempty'];
     for (i = 0; i < operators.length; i++) {
         html += '<option value="' + operators[i] + '">' +
-                strings['op_' + operators[i]] + '</option>';
+                M.util.get_string('op_' + operators[i], 'availability_profile') + '</option>';
     }
-    html += '</select></label> <label><span class="accesshide">' + strings.label_value +
+    html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_value', 'availability_profile') +
             '</span><input name="value" type="text" style="width: 10em" title="' +
-            strings.label_value + '"/></label></span>';
+            M.util.get_string('label_value', 'availability_profile') + '"/></label></span>';
     var node = Y.Node.create('<span>' + html + '</span>');
 
     // Set initial values if specified.
index 3f2e3a2..82f3662 100644 (file)
@@ -172,3 +172,62 @@ Feature: edit_availability
     And I should not see "None" in the "Restrict access" "fieldset"
     And "Restriction type" "select" should be visible
     And I should see "Date" in the "Restrict access" "fieldset"
+
+  @javascript
+  Scenario: 'Add group/grouping access restriction' button unavailable
+    # Button does not exist when conditional access restrictions are turned off.
+    Given I log in as "admin"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Forum" to section "1"
+    When I expand all fieldsets
+    Then "Add group/grouping access restriction" "button" should not exist
+
+  @javascript
+  Scenario: Use the 'Add group/grouping access restriction' button
+    # Button should initially be disabled.
+    Given I log in as "admin"
+    And I set the following administration settings values:
+      | Enable conditional access | 1 |
+    And the following "groupings" exist:
+      | name | course | idnumber |
+      | GX1  | C1     | GXI1     |
+    And I am on homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Forum" to section "1"
+    And I set the following fields to these values:
+      | Forum name  | MyForum |
+      | Description | x       |
+    When I expand all fieldsets
+    Then the "Add group/grouping access restriction" "button" should be disabled
+
+    # Turn on separate groups.
+    And I set the field "Group mode" to "Separate groups"
+    And the "Add group/grouping access restriction" "button" should be enabled
+
+    # Press the button and check it adds a restriction and disables itself.
+    And I should see "None" in the "Restrict access" "fieldset"
+    And I press "Add group/grouping access restriction"
+    And I should see "Group" in the "Restrict access" "fieldset"
+    And the "Add group/grouping access restriction" "button" should be disabled
+
+    # Delete the restriction and check it is enabled again.
+    And I click on "Delete" "link" in the "Restrict access" "fieldset"
+    And the "Add group/grouping access restriction" "button" should be enabled
+
+    # Try a grouping instead.
+    And I set the field "Grouping" to "GX1"
+    And I press "Add group/grouping access restriction"
+    And I should see "Grouping" in the "Restrict access" "fieldset"
+
+    # Check the button still works after saving and editing.
+    And I press "Save and display"
+    And I navigate to "Edit settings" node in "Forum administration"
+    And I expand all fieldsets
+    And the "Add group/grouping access restriction" "button" should be disabled
+    And I should see "Grouping" in the "Restrict access" "fieldset"
+
+    # And check it's still active if I delete the condition.
+    And I click on "Delete" "link" in the "Restrict access" "fieldset"
+    And the "Add group/grouping access restriction" "button" should be enabled
index 185800c..61b4781 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js differ
index 9c578a0..92740e7 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js differ
index 185800c..61b4781 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js differ
index 099fe5e..c247441 100644 (file)
@@ -64,6 +64,14 @@ M.core_availability.form = {
      */
     idCounter : 0,
 
+    /**
+     * The 'Restrict by group' button if present.
+     *
+     * @property restrictByGroup
+     * @type Y.Node
+     */
+    restrictByGroup : null,
+
     /**
      * Called to initialise the system when the page loads. This method will
      * also call the init method for each plugin.
@@ -119,6 +127,22 @@ M.core_availability.form = {
         this.field.ancestor('form').on('submit', function() {
             this.mainDiv.all('input,textarea,select').set('disabled', true);
         }, this);
+
+        // If the form has group mode and/or grouping options, there is a
+        // 'add restriction' button there.
+        this.restrictByGroup = Y.one('#restrictbygroup');
+        if (this.restrictByGroup) {
+            this.restrictByGroup.on('click', this.addRestrictByGroup, this);
+            var groupmode = Y.one('#id_groupmode');
+            var groupingid = Y.one('#id_groupingid');
+            if (groupmode) {
+                groupmode.on('change', this.updateRestrictByGroup, this);
+            }
+            if (groupingid) {
+                groupingid.on('change', this.updateRestrictByGroup, this);
+            }
+            this.updateRestrictByGroup();
+        }
     },
 
     /**
@@ -141,6 +165,75 @@ M.core_availability.form = {
 
         // Set into hidden form field, JS-encoded.
         this.field.set('value', Y.JSON.stringify(jsValue));
+
+        // Also update the restrict by group button if present.
+        this.updateRestrictByGroup();
+    },
+
+    /**
+     * Updates the status of the 'restrict by group' button (enables or disables
+     * it) based on current availability restrictions and group/grouping settings.
+     */
+    updateRestrictByGroup : function() {
+        if (!this.restrictByGroup) {
+            return;
+        }
+
+        // If the root list is anything other than the default 'and' type, disable.
+        if (this.rootList.getValue().op !== '&') {
+            this.restrictByGroup.set('disabled', true);
+            return;
+        }
+
+        // If there's already a group restriction, disable it.
+        var alreadyGot = this.rootList.hasItemOfType('group') ||
+                this.rootList.hasItemOfType('grouping');
+        if (alreadyGot) {
+            this.restrictByGroup.set('disabled', true);
+            return;
+        }
+
+        // If the groupmode and grouping id aren't set, disable it.
+        var groupmode = Y.one('#id_groupmode');
+        var groupingid = Y.one('#id_groupingid');
+        if ((!groupmode || Number(groupmode.get('value')) === 0) &&
+                (!groupingid || Number(groupingid.get('value')) === 0)) {
+            this.restrictByGroup.set('disabled', true);
+            return;
+        }
+
+        this.restrictByGroup.set('disabled', false);
+    },
+
+    /**
+     * Called when the user clicks on the 'restrict by group' button. This is
+     * a special case that adds a group or grouping restriction.
+     *
+     * By default this restriction is not shown which makes it similar to the
+     *
+     * @param e Button click event
+     */
+    addRestrictByGroup : function(e) {
+        // If you don't prevent default, it submits the form for some reason.
+        e.preventDefault();
+
+        // Add the condition.
+        var groupingid = Y.one('#id_groupingid');
+        var newChild;
+        if (groupingid && Number(groupingid.get('value')) !== 0) {
+            // Add a grouping restriction if one is specified.
+            newChild = new M.core_availability.Item(
+                    {type : 'grouping', id : Number(groupingid.get('value'))}, true);
+        } else {
+            // Otherwise just add a group restriction.
+            newChild = new M.core_availability.Item({type : 'group'}, true);
+        }
+
+        // Refresh HTML.
+        this.rootList.addChild(newChild);
+        this.update();
+        this.rootList.renumber();
+        this.rootList.updateHtml();
     }
 };
 
@@ -258,24 +351,23 @@ M.core_availability.List = function(json, root, parentRoot) {
     if (root !== undefined) {
         this.root = root;
     }
-    var strings = M.str.availability;
     // Create DIV structure (without kids).
     this.node = Y.Node.create('<div class="availability-list"><h3 class="accesshide"></h3>' +
             '<div class="availability-inner">' +
-            '<div class="availability-header">' + strings.listheader_sign_before +
-            ' <label><span class="accesshide">' + strings.label_sign +
-            ' </span><select class="availability-neg" title="' + strings.label_sign + '">' +
-            '<option value="">' + strings.listheader_sign_pos + '</option>' +
-            '<option value="!">' + strings.listheader_sign_neg + '</option></select></label> ' +
-            '<span class="availability-single">' + strings.listheader_single + '</span>' +
-            '<span class="availability-multi">' + strings.listheader_multi_before +
-            ' <label><span class="accesshide">' + strings.label_multi + ' </span>' +
-            '<select class="availability-op" title="' + strings.label_multi + '"><option value="&">' +
-            strings.listheader_multi_and + '</option>' +
-            '<option value="|">' + strings.listheader_multi_or + '</option></select></label> ' +
-            strings.listheader_multi_after + '</span></div>' +
+            '<div class="availability-header">' + M.util.get_string('listheader_sign_before', 'availability') +
+            ' <label><span class="accesshide">' + M.util.get_string('label_sign', 'availability') +
+            ' </span><select class="availability-neg" title="' + M.util.get_string('label_sign', 'availability') + '">' +
+            '<option value="">' + M.util.get_string('listheader_sign_pos', 'availability') + '</option>' +
+            '<option value="!">' + M.util.get_string('listheader_sign_neg', 'availability') + '</option></select></label> ' +
+            '<span class="availability-single">' + M.util.get_string('listheader_single', 'availability') + '</span>' +
+            '<span class="availability-multi">' + M.util.get_string('listheader_multi_before', 'availability') +
+            ' <label><span class="accesshide">' + M.util.get_string('label_multi', 'availability') + ' </span>' +
+            '<select class="availability-op" title="' + M.util.get_string('label_multi', 'availability') + '"><option value="&">' +
+            M.util.get_string('listheader_multi_and', 'availability') + '</option>' +
+            '<option value="|">' + M.util.get_string('listheader_multi_or', 'availability') + '</option></select></label> ' +
+            M.util.get_string('listheader_multi_after', 'availability') + '</span></div>' +
             '<div class="availability-children"></div>' +
-            '<div class="availability-none">' + M.str.moodle.none + '</div>' +
+            '<div class="availability-none">' + M.util.get_string('none', 'moodle') + '</div>' +
             '<div class="availability-button"></div></div></div>');
     if (!root) {
         this.node.addClass('availability-childlist');
@@ -311,12 +403,12 @@ M.core_availability.List = function(json, root, parentRoot) {
 
         // Also if it's not the root, none is actually invalid, so add a label.
         noneNode.appendChild(Y.Node.create('<span class="label label-warning">' +
-                M.str.availability.invalid + '</span>'));
+                M.util.get_string('invalid', 'availability') + '</span>'));
     }
 
     // Create the button and add it.
     var button = Y.Node.create('<button type="button" class="btn btn-default">' +
-            M.str.availability.addrestriction + '</button>');
+            M.util.get_string('addrestriction', 'availability') + '</button>');
     button.on("click", function() { this.clickAdd(); }, this);
     this.node.one('div.availability-button').appendChild(button);
 
@@ -507,9 +599,9 @@ M.core_availability.List.prototype.updateHtml = function() {
     // Update connector text.
     var connectorText;
     if (this.inner.one('.availability-op').get('value') === '&') {
-        connectorText = M.str.availability.and;
+        connectorText = M.util.get_string('and', 'availability');
     } else {
-        connectorText = M.str.availability.or;
+        connectorText = M.util.get_string('or', 'availability');
     }
     this.inner.all('> .availability-children > .availability-connector span.label').each(function(span) {
         span.set('innerHTML', connectorText);
@@ -571,7 +663,7 @@ M.core_availability.List.prototype.clickAdd = function() {
     var content = Y.Node.create('<div>' +
             '<ul class="list-unstyled"></ul>' +
             '<div class="availability-buttons mdl-align">' +
-            '<button type="button" class="btn btn-default">' + M.str.moodle.cancel +
+            '<button type="button" class="btn btn-default">' + M.util.get_string('cancel', 'moodle') +
             '</button></div></div>');
     var cancel = content.one('button');
 
@@ -587,13 +679,12 @@ M.core_availability.List.prototype.clickAdd = function() {
         // Add entry for plugin.
         li = Y.Node.create('<li class="clearfix"></li>');
         id = 'availability_addrestriction_' + type;
-        var pluginStrings = M.str['availability_' + type];
         button = Y.Node.create('<button type="button" class="btn btn-default"' +
-                'id="' + id + '">' + pluginStrings.title + '</button>');
+                'id="' + id + '">' + M.util.get_string('title', 'availability_' + type) + '</button>');
         button.on('click', this.getAddHandler(type, dialogRef), this);
         li.appendChild(button);
         label = Y.Node.create('<label for="' + id + '">' +
-                pluginStrings.description + '</label>');
+                M.util.get_string('description', 'availability_' + type) + '</label>');
         li.appendChild(label);
         ul.appendChild(li);
     }
@@ -601,16 +692,16 @@ M.core_availability.List.prototype.clickAdd = function() {
     li = Y.Node.create('<li class="clearfix"></li>');
     id = 'availability_addrestriction_list_';
     button = Y.Node.create('<button type="button" class="btn btn-default"' +
-            'id="' + id + '">' + M.str.availability.condition_group + '</button>');
+            'id="' + id + '">' + M.util.get_string('condition_group', 'availability') + '</button>');
     button.on('click', this.getAddHandler(null, dialogRef), this);
     li.appendChild(button);
     label = Y.Node.create('<label for="' + id + '">' +
-            M.str.availability.condition_group_info + '</label>');
+            M.util.get_string('condition_group_info', 'availability') + '</label>');
     li.appendChild(label);
     ul.appendChild(li);
 
     var config = {
-        headerContent : M.str.availability.addrestriction,
+        headerContent : M.util.get_string('addrestriction', 'availability'),
         bodyContent : content,
         additionalBaseClass : 'availability-dialogue',
         draggable : true,
@@ -709,6 +800,31 @@ M.core_availability.List.prototype.fillErrors = function(errors) {
     }
 };
 
+/**
+ * Checks whether the list contains any items of the given type name.
+ *
+ * @method hasItemOfType
+ * @param {String} pluginType Required plugin type (name)
+ * @return {Boolean} True if there is one
+ */
+M.core_availability.List.prototype.hasItemOfType = function(pluginType) {
+    // Check each item.
+    for (var i = 0; i < this.children.length; i++) {
+        var child = this.children[i];
+        if (child instanceof M.core_availability.List) {
+            // Recursive call.
+            if (child.hasItemOfType(pluginType)) {
+                return true;
+            }
+        } else {
+            if (child.pluginType === pluginType) {
+                return true;
+            }
+        }
+    }
+    return false;
+};
+
 /**
  * Eye icon for this list (null if none).
  *
@@ -764,7 +880,7 @@ M.core_availability.Item = function(json, root) {
         // Handle undefined plugins.
         this.plugin = null;
         this.pluginNode = Y.Node.create('<div class="availability-warning">' +
-                M.str.availability.missingplugin + '</div>');
+                M.util.get_string('missingplugin', 'availability') + '</div>');
     } else {
         // Plugin is known.
         this.plugin = M.core_availability.form.plugins[json.type];
@@ -835,7 +951,7 @@ M.core_availability.Item.prototype.fillErrors = function(errors) {
     // If any errors were added, add the marker to this item.
     var errorLabel = this.node.one('> .label-warning');
     if (errors.length !== before && !errorLabel.get('firstChild')) {
-        errorLabel.appendChild(document.createTextNode(M.str.availability.invalid));
+        errorLabel.appendChild(document.createTextNode(M.util.get_string('invalid', 'availability')));
     } else if (errors.length === before && errorLabel.get('firstChild')) {
         errorLabel.get('firstChild').remove();
     }
@@ -851,7 +967,7 @@ M.core_availability.Item.prototype.renumber = function(number) {
     // Update heading for item.
     var headingParams = { number: number };
     if (this.plugin) {
-        headingParams.type = M.str['availability_' + this.pluginType].title;
+        headingParams.type = M.util.get_string('title', 'availability_' + this.pluginType);
     } else {
         headingParams.type = '[' + this.pluginType + ']';
     }
@@ -930,19 +1046,21 @@ M.core_availability.EyeIcon = function(individual, shown) {
     this.span.appendChild(icon);
 
     // Set up button text and icon.
-    var suffix = individual ? '_individual' : '_all';
-    var setHidden = function() {
-        icon.set('src', M.util.image_url('i/show', 'core'));
-        icon.set('alt', M.str.availability['hidden' + suffix]);
-        this.span.set('title', M.str.availability['hidden' + suffix] + ' \u2022 ' +
-                M.str.availability.show_verb);
-    };
-    var setShown = function() {
-        icon.set('src', M.util.image_url('i/hide', 'core'));
-        icon.set('alt', M.str.availability['shown' + suffix]);
-        this.span.set('title', M.str.availability['shown' + suffix] + ' \u2022 ' +
-                M.str.availability.hide_verb);
-    };
+    var suffix = individual ? '_individual' : '_all',
+        setHidden = function() {
+            var hiddenStr = M.util.get_string('hidden' + suffix, 'availability');
+            icon.set('src', M.util.image_url('i/show', 'core'));
+            icon.set('alt', hiddenStr);
+            this.span.set('title', hiddenStr + ' \u2022 ' +
+                    M.util.get_string('show_verb', 'availability'));
+        },
+        setShown = function() {
+            var shownStr = M.util.get_string('shown' + suffix, 'availability');
+            icon.set('src', M.util.image_url('i/hide', 'core'));
+            icon.set('alt', shownStr);
+            this.span.set('title', shownStr + ' \u2022 ' +
+                    M.util.get_string('hide_verb', 'availability'));
+        };
     if(shown) {
         setShown.call(this);
     } else {
@@ -987,8 +1105,8 @@ M.core_availability.EyeIcon.prototype.span = null;
  * @return {Boolean} True if this icon is set to 'hidden'
  */
 M.core_availability.EyeIcon.prototype.isHidden = function() {
-    var suffix = this.individual ? '_individual' : '_all';
-    var compare = M.str.availability['hidden' + suffix];
+    var suffix = this.individual ? '_individual' : '_all',
+        compare = M.util.get_string('hidden' + suffix, 'availability');
     return this.span.one('img').get('alt') === compare;
 };
 
@@ -1002,9 +1120,9 @@ M.core_availability.EyeIcon.prototype.isHidden = function() {
  */
 M.core_availability.DeleteIcon = function(toDelete) {
     this.span = Y.Node.create('<a class="availability-delete" href="#" title="' +
-            M.str.moodle['delete'] + '" role="button">');
+            M.util.get_string('delete', 'moodle') + '" role="button">');
     var img = Y.Node.create('<img src="' + M.util.image_url('t/delete', 'core') +
-            '" alt="' + M.str.moodle['delete'] + '" />');
+            '" alt="' + M.util.get_string('delete', 'moodle') + '" />');
     this.span.appendChild(img);
     var click = function(e) {
         e.preventDefault();
index 11f245f..5ed54e9 100644 (file)
@@ -80,6 +80,9 @@ class backup_course_task extends backup_task {
             $this->add_step(new backup_enrolments_structure_step('course_enrolments', 'enrolments.xml'));
         }
 
+        // Annotate enrolment custom fields.
+        $this->add_step(new backup_enrolments_execution_step('annotate_enrol_custom_fields'));
+
         // Annotate all the groups and groupings belonging to the course
         $this->add_step(new backup_annotate_course_groups_and_groupings('annotate_course_groups'));
 
diff --git a/backup/moodle2/backup_enrol_plugin.class.php b/backup/moodle2/backup_enrol_plugin.class.php
new file mode 100644 (file)
index 0000000..a9ede54
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Defines backup_enrol_plugin class.
+ *
+ * @package     core_backup
+ * @subpackage  moodle2
+ * @category    backup
+ * @copyright   2014 University of Wisconsin
+ * @author      Matt petro
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for enrol backup plugins.
+ *
+ * @package   core_backup
+ * @copyright 2014 University of Wisconsin
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class backup_enrol_plugin extends backup_plugin {
+    // Use default parent behaviour.
+}
index b78fbad..c359096 100644 (file)
@@ -44,6 +44,7 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_theme_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_report_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_coursereport_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plagiarism_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/backup_enrol_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_subplugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_settingslib.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_stepslib.php');
index de35cc3..7830271 100644 (file)
@@ -555,7 +555,8 @@ class backup_enrolments_structure_step extends backup_structure_step {
 
         $enrol->annotate_ids('role', 'roleid');
 
-        //TODO: let plugins annotate custom fields too and add more children
+        // Add enrol plugin structure.
+        $this->add_plugin_structure('enrol', $enrol, false);
 
         return $enrolments;
     }
@@ -1875,6 +1876,47 @@ class backup_activity_grade_items_to_ids extends backup_execution_step {
     }
 }
 
+
+/**
+ * This step allows enrol plugins to annotate custom fields.
+ *
+ * @package   core_backup
+ * @copyright 2014 University of Wisconsin
+ * @author    Matt Petro
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_enrolments_execution_step extends backup_execution_step {
+
+    /**
+     * Function that will contain all the code to be executed.
+     */
+    protected function define_execution() {
+        global $DB;
+
+        $plugins = enrol_get_plugins(true);
+        $enrols = $DB->get_records('enrol', array(
+                'courseid' => $this->task->get_courseid()));
+
+        // Allow each enrol plugin to add annotations.
+        foreach ($enrols as $enrol) {
+            if (isset($plugins[$enrol->enrol])) {
+                $plugins[$enrol->enrol]->backup_annotate_custom_fields($this, $enrol);
+            }
+        }
+    }
+
+    /**
+     * Annotate a single name/id pair.
+     * This can be called from {@link enrol_plugin::backup_annotate_custom_fields()}.
+     *
+     * @param string $itemname
+     * @param int $itemid
+     */
+    public function annotate_id($itemname, $itemid) {
+        backup_structure_dbops::insert_backup_ids_record($this->get_backupid(), $itemname, $itemid);
+    }
+}
+
 /**
  * This step will annotate all the groups and groupings belonging to the course
  */
diff --git a/backup/moodle2/restore_enrol_plugin.class.php b/backup/moodle2/restore_enrol_plugin.class.php
new file mode 100644 (file)
index 0000000..92d945e
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Defines restore_enrol_plugin class.
+ *
+ * @package     core_backup
+ * @subpackage  moodle2
+ * @category    backup
+ * @copyright   2014 University of Wisconsin
+ * @author      Matt petro
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for enrol backup plugins.
+ *
+ * @package   core_backup
+ * @copyright 2014 University of Wisconsin
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class restore_enrol_plugin extends restore_plugin {
+    // Use default parent behaviour.
+}
index b7c65ef..ae48ec9 100644 (file)
@@ -43,6 +43,7 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_report_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_coursereport_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_plagiarism_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_gradingform_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/restore_enrol_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php');
@@ -52,6 +53,7 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_report_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_coursereport_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plagiarism_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_gradingform_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/backup_enrol_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_subplugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_settingslib.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_stepslib.php');
index ee4fa9d..1380f20 100644 (file)
@@ -1884,12 +1884,12 @@ class restore_enrolments_structure_step extends restore_structure_step {
 
     protected function define_structure() {
 
-        $paths = array();
-
-        $paths[] = new restore_path_element('enrol', '/enrolments/enrols/enrol');
-        $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
+        $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
+        $enrolment = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
+        // Attach local plugin stucture to enrol element.
+        $this->add_plugin_structure('enrol', $enrol);
 
-        return $paths;
+        return array($enrol, $enrolment);
     }
 
     /**
@@ -2630,7 +2630,21 @@ class restore_course_completion_structure_step extends restore_structure_step {
                 'timecompleted' => $this->apply_date_offset($data->timecompleted),
                 'reaggregate' => $data->reaggregate
             );
-            $DB->insert_record('course_completions', $params);
+
+            $existing = $DB->get_record('course_completions', array(
+                'userid' => $data->userid,
+                'course' => $data->course
+            ));
+
+            // MDL-46651 - If cron writes out a new record before we get to it
+            // then we should replace it with the Truth data from the backup.
+            // This may be obsolete after MDL-48518 is resolved
+            if ($existing) {
+                $params['id'] = $existing->id;
+                $DB->update_record('course_completions', $params);
+            } else {
+                $DB->insert_record('course_completions', $params);
+            }
         }
     }
 
@@ -3187,11 +3201,52 @@ class restore_block_instance_structure_step extends restore_structure_step {
         }
 
         if (!$bi->instance_allow_multiple()) {
-            if ($DB->record_exists_sql("SELECT bi.id
-                                          FROM {block_instances} bi
-                                          JOIN {block} b ON b.name = bi.blockname
-                                         WHERE bi.parentcontextid = ?
-                                           AND bi.blockname = ?", array($data->parentcontextid, $data->blockname))) {
+            // The block cannot be added twice, so we will check if the same block is already being
+            // displayed on the same page. For this, rather than mocking a page and using the block_manager
+            // we use a similar query to the one in block_manager::load_blocks(), this will give us
+            // a very good idea of the blocks already displayed in the context.
+            $params =  array(
+                'blockname' => $data->blockname
+            );
+
+            // Context matching test.
+            $context = context::instance_by_id($data->parentcontextid);
+            $contextsql = 'bi.parentcontextid = :contextid';
+            $params['contextid'] = $context->id;
+
+            $parentcontextids = $context->get_parent_context_ids();
+            if ($parentcontextids) {
+                list($parentcontextsql, $parentcontextparams) =
+                        $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED);
+                $contextsql = "($contextsql OR (bi.showinsubcontexts = 1 AND bi.parentcontextid $parentcontextsql))";
+                $params = array_merge($params, $parentcontextparams);
+            }
+
+            // Page type pattern test.
+            $pagetypepatterns = matching_page_type_patterns_from_pattern($data->pagetypepattern);
+            list($pagetypepatternsql, $pagetypepatternparams) =
+                $DB->get_in_or_equal($pagetypepatterns, SQL_PARAMS_NAMED);
+            $params = array_merge($params, $pagetypepatternparams);
+
+            // Sub page pattern test.
+            $subpagepatternsql = 'bi.subpagepattern IS NULL';
+            if ($data->subpagepattern !== null) {
+                $subpagepatternsql = "($subpagepatternsql OR bi.subpagepattern = :subpagepattern)";
+                $params['subpagepattern'] = $data->subpagepattern;
+            }
+
+            $exists = $DB->record_exists_sql("SELECT bi.id
+                                                FROM {block_instances} bi
+                                                JOIN {block} b ON b.name = bi.blockname
+                                               WHERE bi.blockname = :blockname
+                                                 AND $contextsql
+                                                 AND bi.pagetypepattern $pagetypepatternsql
+                                                 AND $subpagepatternsql", $params);
+            if ($exists) {
+                // There is at least one very similar block visible on the page where we
+                // are trying to restore the block. In these circumstances the block API
+                // would not allow the user to add another instance of the block, so we
+                // apply the same rule here.
                 return false;
             }
         }
index 6f8ba31..38c72a8 100644 (file)
@@ -66,7 +66,7 @@ abstract class backup_factory {
 
         // Create database_logger, observing $CFG->backup_database_logger_level and defaulting to LOG_WARNING
         // and pointing to the backup_logs table
-        $dllevel = isset($CFG->backup_database_logger_level) ? $CFG->backup_database_logger_level : backup::LOG_WARNING;
+        $dllevel = isset($CFG->backup_database_logger_level) ? $CFG->backup_database_logger_level : $dfltloglevel;
         $columns = array('backupid' => $backupid);
         $enabledloggers[] = new database_logger($dllevel, 'timecreated', 'loglevel', 'message', 'backup_logs', $columns);
 
index 7f76cff..671a8b5 100644 (file)
@@ -396,18 +396,36 @@ abstract class backup_cron_automated_helper {
             $results = $bc->get_results();
             $outcome = self::outcome_from_results($results);
             $file = $results['backup_destination']; // May be empty if file already moved to target location.
-            if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
+
+            if (empty($dir) && $storage !== 0) {
+                // This is intentionally left as a warning instead of an error because of the current behaviour of backup settings.
+                // See MDL-48266 for details.
+                $bc->log('No directory specified for automated backups',
+                        backup::LOG_WARNING);
+                $outcome = self::BACKUP_STATUS_WARNING;
+            } else if ($storage !== 0 && (!file_exists($dir) || !is_dir($dir) || !is_writable($dir))) {
+                // If we need to copy the backup file to an external dir and it is not writable, change status to error.
+                $bc->log('Specified backup directory is not writable - ',
+                        backup::LOG_ERROR, $dir);
                 $dir = null;
+                $outcome = self::BACKUP_STATUS_ERROR;
             }
+
             // Copy file only if there was no error.
             if ($file && !empty($dir) && $storage !== 0 && $outcome != self::BACKUP_STATUS_ERROR) {
                 $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised,
                         !$config->backup_shortname);
                 if (!$file->copy_content_to($dir.'/'.$filename)) {
+                    $bc->log('Attempt to copy backup file to the specified directory failed - ',
+                            backup::LOG_ERROR, $dir);
                     $outcome = self::BACKUP_STATUS_ERROR;
                 }
                 if ($outcome != self::BACKUP_STATUS_ERROR && $storage === 1) {
-                    $file->delete();
+                    if (!$file->delete()) {
+                        $outcome = self::BACKUP_STATUS_WARNING;
+                        $bc->log('Attempt to delete the backup file from course automated backup area failed - ',
+                                backup::LOG_WARNING, $file->get_filename());
+                    }
                 }
             }
 
index 963e471..4096371 100644 (file)
@@ -298,6 +298,10 @@ abstract class backup_helper {
                     @chmod($filedest, $CFG->filepermissions); // may fail because the permissions may not make sense outside of dataroot
                     unlink($filepath);
                     return null;
+                } else {
+                    $bc = backup_controller::load_controller($backupid);
+                    $bc->log('Attempt to copy backup file to the specified directory using filesystem failed - ',
+                            backup::LOG_WARNING, $dir);
                 }
                 // bad luck, try to deal with the file the old way - keep backup in file area if we can not copy to ext system
             }
index e5476ca..934e011 100644 (file)
@@ -90,7 +90,7 @@ Feature: Restore Moodle 2 course backups
       | id_startdate_month | January |
       | id_startdate_year | 2020 |
       | id_format | Weekly format |
-    And I press "Save changes"
+    And I press "Save and display"
     And I should see "1 January - 7 January"
     And I should see "Test forum name"
     And I click on "Edit settings" "link" in the "Administration" "block"
@@ -98,7 +98,7 @@ Feature: Restore Moodle 2 course backups
     And the field "id_format" matches value "Weekly format"
     And I set the following fields to these values:
       | id_format | Social format |
-    And I press "Save changes"
+    And I press "Save and display"
     And I should see "An open forum for chatting about anything you want to"
     And I click on "Edit settings" "link" in the "Administration" "block"
     And I expand all fieldsets
index 99c099e..3632182 100644 (file)
@@ -140,7 +140,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $dl = array();
         $dl[get_string('name')] = $badge->name;
         $dl[get_string('description', 'badges')] = $badge->description;
-        $dl[get_string('createdon', 'search')] = $badge->timecreated;
+        $dl[get_string('createdon', 'search')] = userdate($badge->timecreated);
         $dl[get_string('badgeimage', 'badges')] = print_badge_image($badge, $context, 'large');
         $display .= $this->definition_list($dl);
 
index 209e992..bf504b0 100644 (file)
@@ -132,7 +132,7 @@ Feature: Award badges
     And I follow "Edit settings"
     And I set the following fields to these values:
       | Enable completion tracking | Yes |
-    And I press "Save changes"
+    And I press "Save and display"
     And I turn editing mode on
     And I add a "Assignment" to section "1" and I fill the form with:
       | Assignment name | Test assignment name |
@@ -185,7 +185,7 @@ Feature: Award badges
     And I follow "Edit settings"
     And I set the following fields to these values:
       | Enable completion tracking | Yes |
-    And I press "Save changes"
+    And I press "Save and display"
     And I turn editing mode on
     And I add a "Assignment" to section "1" and I fill the form with:
       | Assignment name | Test assignment name |
index 41cd98e..ace839e 100644 (file)
@@ -30,15 +30,6 @@ class block_calendar_month extends block_base {
         $this->title = get_string('pluginname', 'block_calendar_month');
     }
 
-    /**
-     * Return preferred_width.
-     *
-     * @return int
-     */
-    public function preferred_width() {
-        return 210;
-    }
-
     /**
      * Return the content of this block.
      *
index 1fd16c6..3619914 100644 (file)
@@ -33,3 +33,67 @@ Feature: Add a comment to the comments block
     When I follow "Show comments"
     And I add "I'm a comment from student1" comment to comments block
     Then I should see "I'm a comment from student1"
+
+  @javascript
+  Scenario: Test comment block pagination
+    When I add "Super test comment 01" comment to comments block
+    And I add "Super test comment 02" comment to comments block
+    And I add "Super test comment 03" comment to comments block
+    And I add "Super test comment 04" comment to comments block
+    And I add "Super test comment 05" comment to comments block
+    And I add "Super test comment 06" comment to comments block
+    And I add "Super test comment 07" comment to comments block
+    And I add "Super test comment 08" comment to comments block
+    And I add "Super test comment 09" comment to comments block
+    And I add "Super test comment 10" comment to comments block
+    And I add "Super test comment 11" comment to comments block
+    And I add "Super test comment 12" comment to comments block
+    And I add "Super test comment 13" comment to comments block
+    And I add "Super test comment 14" comment to comments block
+    And I add "Super test comment 15" comment to comments block
+    And I add "Super test comment 16" comment to comments block
+    And I add "Super test comment 17" comment to comments block
+    And I add "Super test comment 18" comment to comments block
+    And I add "Super test comment 19" comment to comments block
+    And I add "Super test comment 20" comment to comments block
+    And I add "Super test comment 21" comment to comments block
+    And I add "Super test comment 22" comment to comments block
+    And I add "Super test comment 23" comment to comments block
+    And I add "Super test comment 24" comment to comments block
+    And I add "Super test comment 25" comment to comments block
+    And I add "Super test comment 26" comment to comments block
+    And I add "Super test comment 27" comment to comments block
+    And I add "Super test comment 28" comment to comments block
+    And I add "Super test comment 29" comment to comments block
+    And I add "Super test comment 30" comment to comments block
+    And I add "Super test comment 31" comment to comments block
+    Then I should see "Super test comment 01"
+    And I should see "Super test comment 31"
+    And I follow "Course 1"
+    And I should not see "Super test comment 01"
+    And I should not see "Super test comment 02"
+    And I should not see "Super test comment 16"
+    And I should see "Super test comment 17"
+    And I should see "Super test comment 31"
+    And I should see "1" in the ".block_comments .comment-paging" "css_element"
+    And I should see "2" in the ".block_comments .comment-paging" "css_element"
+    And I should see "3" in the ".block_comments .comment-paging" "css_element"
+    And I should not see "4" in the ".block_comments .comment-paging" "css_element"
+    And I click on "2" "link" in the ".block_comments .comment-paging" "css_element"
+    And I should not see "Super test comment 01"
+    And I should see "Super test comment 02"
+    And I should see "Super test comment 16"
+    And I should not see "Super test comment 17"
+    And I should not see "Super test comment 31"
+    And I click on "3" "link" in the ".block_comments .comment-paging" "css_element"
+    And I should see "Super test comment 01"
+    And I should not see "Super test comment 02"
+    And I should not see "Super test comment 16"
+    And I should not see "Super test comment 17"
+    And I should not see "Super test comment 31"
+    And I click on "1" "link" in the ".block_comments .comment-paging" "css_element"
+    And I should not see "Super test comment 01"
+    And I should not see "Super test comment 02"
+    And I should not see "Super test comment 16"
+    And I should see "Super test comment 17"
+    And I should see "Super test comment 31"
index 094c668..491c342 100644 (file)
@@ -64,6 +64,9 @@ class behat_block_comments extends behat_base {
             $commentstextarea->setValue($comment);
 
             $this->find_link(get_string('savecomment'))->click();
+            // Delay after clicking so that additional comments will have unique time stamps.
+            // We delay 1 second which is all we need.
+            $this->getSession()->wait(1000, false);
 
         } else {
 
index c27067b..041bfe8 100644 (file)
@@ -57,6 +57,8 @@ class block_completionstatus extends block_base {
 
         // Create empty content.
         $this->content = new stdClass();
+        $this->content->text = '';
+        $this->content->footer = '';
 
         // Can edit settings?
         $can_edit = has_capability('moodle/course:update', $context);
@@ -67,13 +69,13 @@ class block_completionstatus extends block_base {
         // Don't display if completion isn't enabled!
         if (!completion_info::is_enabled_for_site()) {
             if ($can_edit) {
-                $this->content->text = get_string('completionnotenabledforsite', 'completion');
+                $this->content->text .= get_string('completionnotenabledforsite', 'completion');
             }
             return $this->content;
 
         } else if (!$info->is_enabled()) {
             if ($can_edit) {
-                $this->content->text = get_string('completionnotenabledforcourse', 'completion');
+                $this->content->text .= get_string('completionnotenabledforcourse', 'completion');
             }
             return $this->content;
         }
@@ -84,7 +86,7 @@ class block_completionstatus extends block_base {
         // Check if this course has any criteria.
         if (empty($completions)) {
             if ($can_edit) {
-                $this->content->text = get_string('nocriteriaset', 'completion');
+                $this->content->text .= get_string('nocriteriaset', 'completion');
             }
             return $this->content;
         }
@@ -230,11 +232,11 @@ class block_completionstatus extends block_base {
             $rows = array_merge($rows, $srows);
 
             $table->data = $rows;
-            $this->content->text = html_writer::table($table);
+            $this->content->text .= html_writer::table($table);
 
             // Display link to detailed view.
             $details = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id));
-            $this->content->footer = html_writer::link($details, get_string('moredetails', 'completion'));
+            $this->content->footer .= html_writer::link($details, get_string('moredetails', 'completion'));
         } else {
             // If user is not enrolled, show error.
             $this->content->text = get_string('nottracked', 'completion');
index 281aba3..c2163fc 100644 (file)
@@ -74,10 +74,6 @@ class block_course_summary extends block_base {
         return true;
     }
 
-    function preferred_width() {
-        return 210;
-    }
-
 }
 
 
index 19624eb..ad32113 100644 (file)
@@ -84,6 +84,12 @@ class block_edit_form extends moodleform {
         $weightoptions[$last] = get_string('bracketlast', 'block', $last);
 
         $regionoptions = $this->page->theme->get_all_block_regions();
+        foreach ($this->page->blocks->get_regions() as $region) {
+            // Make sure to add all custom regions of this particular page too.
+            if (!isset($regionoptions[$region])) {
+                $regionoptions[$region] = $region;
+            }
+        }
 
         $parentcontext = context::instance_by_id($this->block->instance->parentcontextid);
         $mform->addElement('hidden', 'bui_parentcontextid', $parentcontext->id);
index bbbd72b..0061a9b 100644 (file)
@@ -326,11 +326,6 @@ class block_base {
             $correct = false;
         }
 
-        $width = $this->preferred_width();
-        if (!is_int($width) || $width <= 0) {
-            $errors[] = 'invalid_width';
-            $correct = false;
-        }
         return $correct;
     }
 
@@ -594,17 +589,6 @@ class block_base {
         return array('moodle/block:view', 'moodle/block:edit');
     }
 
-    // Methods deprecated in Moodle 2.0 ========================================
-
-    /**
-     * Default case: the block wants to be 180 pixels wide
-     * @deprecated since Moodle 2.0.
-     * @return int
-     */
-    function preferred_width() {
-        return 180;
-    }
-
     /**
      * Can be overridden by the block to prevent the block from being dockable.
      *
index 2fd28ca..4aa815f 100644 (file)
@@ -40,7 +40,7 @@ Feature: Expand the courses nodes within the navigation block
     And I click on "Edit settings" "link" in the "Administration" "block"
     And I set the following fields to these values:
       | Allow guest access | Yes |
-    And I press "Save changes"
+    And I press "Save and display"
     And I set the following administration settings values:
       | Show all courses | 1 |
     And I log out
index e02b874..b483ee2 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js differ
index 9431ffc..463c78c 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js differ
index 18bccb6..37b4dd9 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js differ
index 52f2f08..9544816 100644 (file)
@@ -748,8 +748,8 @@ BRANCH.prototype = {
             url = M.cfg.wwwroot+'/course/index.php?categoryid=' + branch.get('key');
         }
         branch.addChild({
-            name : M.str.moodle.viewallcourses,
-            title : M.str.moodle.viewallcourses,
+            name : M.util.get_string('viewallcourses', 'moodle'),
+            title : M.util.get_string('viewallcourses', 'moodle'),
             link : url,
             haschildren : false,
             icon : {'pix':"i/navigationitem",'component':'moodle'}
index 1768980..1509125 100644 (file)
@@ -34,12 +34,12 @@ Feature: Latest news block displays the course latest news
     And I follow "Edit settings"
     And I set the following fields to these values:
       | News items to show | 2 |
-    And I press "Save changes"
+    And I press "Save and display"
     And I should not see "Discussion One" in the "Latest news" "block"
     And I should see "Discussion Two" in the "Latest news" "block"
     And I should see "Discussion Three" in the "Latest news" "block"
     And I follow "Edit settings"
     And I set the following fields to these values:
       | News items to show | 0 |
-    And I press "Save changes"
+    And I press "Save and display"
     And "Latest news" "block" should not exist
index 30b623c..2838651 100644 (file)
         $this->title = get_string('pluginname', 'block_rss_client');
     }
 
-    function preferred_width() {
-        return 210;
-    }
-
     function applicable_formats() {
         return array('all' => true, 'tag' => false);   // Needs work to make it work on tags MDL-11960
     }
             $feed->set_cache_duration($CFG->block_rss_client_timeout*60);
         }
 
-        if(debugging() && $feed->error()){
+        if ($CFG->debugdeveloper && $feed->error()) {
             return '<p>'. $feedrecord->url .' Failed with code: '.$feed->error().'</p>';
         }
 
             $feed->init();
 
             if ($feed->error()) {
-                mtrace ('error');
-                mtrace ('SimplePie failed with error:'.$feed->error());
+                mtrace('Error: could not load/find the RSS feed');
                 $status = false;
             } else {
                 mtrace ('ok');
index a0193af..77e3097 100644 (file)
@@ -89,7 +89,7 @@ class feed_edit_form extends moodleform {
         $rss->init();
 
         if ($rss->error()) {
-            $errors['url'] = get_string('errorloadingfeed', 'block_rss_client', $rss->error());
+            $errors['url'] = get_string('couldnotfindloadrssfeed', 'block_rss_client');
         } else {
             $this->title = $rss->get_title();
             $this->description = $rss->get_description();
index cb2db5c..cdd8291 100644 (file)
@@ -33,6 +33,7 @@ $string['clientshowchannellinklabel'] = 'Should a link to the original site (cha
 $string['clientshowimagelabel'] = 'Show channel image if available :';
 $string['configblock'] = 'Configure this block';
 $string['couldnotfindfeed'] = 'Could not find feed with id';
+$string['couldnotfindloadrssfeed'] = 'Could not find or load the RSS feed.';
 $string['customtitlelabel'] = 'Custom title (leave blank to use title supplied by feed):';
 $string['deletefeedconfirm'] = 'Are you sure you want to delete this feed?';
 $string['disabledrssfeeds'] = 'RSS feeds are disabled';
@@ -43,7 +44,6 @@ $string['editnewsfeeds'] = 'Edit news feeds';
 $string['editrssblock'] = 'Edit RSS headline block';
 $string['enableautodiscovery'] = 'Enable auto-discovery of feeds?';
 $string['enableautodiscovery_help'] = 'If enabled, feeds on web pages are found automatically. For example, if http://docs.moodle.org is entered, then http://docs.moodle.org/en/index.php?title=Special:RecentChanges&feed=rss would be found.';
-$string['errorloadingfeed'] = 'Error loading this RSS feed ({$a})';
 $string['feed'] = 'Feed';
 $string['feedadded'] = 'News feed added';
 $string['feeddeleted'] = 'News feed deleted';
index a84e3d1..e28c75b 100644 (file)
@@ -56,26 +56,36 @@ class block_site_main_menu extends block_list {
         if (!$isediting) {
             $modinfo = get_fast_modinfo($course);
             if (!empty($modinfo->sections[0])) {
-                $options = array('overflowdiv'=>true);
                 foreach($modinfo->sections[0] as $cmid) {
                     $cm = $modinfo->cms[$cmid];
                     if (!$cm->uservisible) {
                         continue;
                     }
 
-                    $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
-                    $instancename = $cm->get_formatted_name();
-
-                    if (!($url = $cm->url)) {
-                        $this->content->items[] = $content;
-                        $this->content->icons[] = '';
+                    if ($cm->indent > 0) {
+                        $indent = '<div class="mod-indent mod-indent-'.$cm->indent.'"></div>';
                     } else {
-                        $linkcss = $cm->visible ? '' : ' class="dimmed" ';
-                        //Accessibility: incidental image - should be empty Alt text
+                        $indent = '';
+                    }
+
+                    if (!empty($cm->url)) {
+                        $attrs = array();
+                        $attrs['title'] = $cm->modfullname;
+                        $attrs['class'] = $cm->extraclasses . ' activity-action';
+                        if ($cm->onclick) {
+                            $attrs['id'] = html_writer::random_id('onclick');
+                            $OUTPUT->add_action_handler(new component_action('click', $cm->onclick), $attrs['id']);
+                        }
+                        if (!$cm->visible) {
+                            $attrs['class'] .= ' dimmed';
+                        }
                         $icon = '<img src="' . $cm->get_icon_url() . '" class="icon" alt="" />';
-                        $this->content->items[] = '<a title="'.$cm->modplural.'" '.$linkcss.' '.$cm->extra.
-                                ' href="' . $url . '">' . $icon . $instancename . '</a>';
+                        $content = html_writer::link($cm->url, $icon . $cm->get_formatted_name(), $attrs);
+                    } else {
+                        $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
                     }
+
+                    $this->content->items[] = $indent.html_writer::div($content, 'main-menu-content');
                 }
             }
             return $this->content;
@@ -112,7 +122,7 @@ class block_site_main_menu extends block_list {
                     continue;
                 }
                 if (!$ismoving) {
-                    $actions = course_get_cm_edit_actions($mod, -1);
+                    $actions = course_get_cm_edit_actions($mod, $mod->indent);
 
                     // Prepend list of actions with the 'move' action.
                     $actions = array('move' => new action_menu_link_primary(
@@ -137,19 +147,31 @@ class block_site_main_menu extends block_list {
                             '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->pix_url('movehere') . '" alt="'.$strmovehere.'" /></a>';
                         $this->content->icons[] = '';
                     }
-                    $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
-                    $instancename = $mod->get_formatted_name();
-                    $linkcss = $mod->visible ? '' : ' class="dimmed" ';
-
-                    if (!($url = $mod->url)) {
-                        $this->content->items[] = $content . $editbuttons;
-                        $this->content->icons[] = '';
+                    if ($mod->indent > 0) {
+                        $indent = '<div class="mod-indent mod-indent-'.$mod->indent.'"></div>';
+                    } else {
+                        $indent = '';
+                    }
+                    $url = $mod->url;
+                    if (!$url) {
+                        $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
                     } else {
                         //Accessibility: incidental image - should be empty Alt text
+                        $attrs = array();
+                        $attrs['title'] = $mod->modfullname;
+                        $attrs['class'] = $mod->extraclasses . ' activity-action';
+                        if ($mod->onclick) {
+                            $attrs['id'] = html_writer::random_id('onclick');
+                            $OUTPUT->add_action_handler(new component_action('click', $mod->onclick), $attrs['id']);
+                        }
+                        if (!$mod->visible) {
+                            $attrs['class'] .= ' dimmed';
+                        }
+
                         $icon = '<img src="' . $mod->get_icon_url() . '" class="icon" alt="" />';
-                        $this->content->items[] = '<a title="' . $mod->modfullname . '" ' . $linkcss . ' ' . $mod->extra .
-                            ' href="' . $url . '">' . $icon . $instancename . '</a>' . $editbuttons;
+                        $content = html_writer::link($url, $icon . $mod->get_formatted_name(), $attrs);
                     }
+                    $this->content->items[] = $indent.html_writer::div($content . $editbuttons, 'main-menu-content');
                 }
             }
         }
index dd8f92f..0e15ab7 100644 (file)
@@ -5,3 +5,6 @@
 .block_site_main_menu li .buttons a img{ vertical-align: text-bottom;}
 .block_site_main_menu .footer { margin-top: 1em; }
 .block_site_main_menu .section_add_menus noscript div { display: inline;}
+.block_site_main_menu .mod-indent,
+.block_site_main_menu .main-menu-content { display: table-cell; }
+.block_site_main_menu .main-menu-content > .activity-action { display: block; }
index 46f8c19..06f691d 100644 (file)
@@ -43,10 +43,6 @@ class block_tag_flickr extends block_base {
         return true;
     }
 
-    function preferred_width() {
-        return 170;
-    }
-
     function get_content() {
         global $CFG, $USER;
 
index 3779d42..a06588e 100644 (file)
@@ -45,10 +45,6 @@ class block_tag_youtube extends block_base {
         return true;
     }
 
-    function preferred_width() {
-        return 140;
-    }
-
     function get_content() {
         global $CFG;
 
index b51143a..b71303c 100644 (file)
@@ -32,3 +32,27 @@ Feature: Add and configure blocks throughout the site
     And I follow "Course 1"
     # The first block matching the pattern should be top-left block
     And I should see "Comments" in the "//*[@id='region-pre' or @id='block-region-side-pre']/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element"
+
+  Scenario: Blocks on the my home page cannot have roles assigned to them
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | manager1 | Manager | 1 | manager1@asd.com |
+    And I log in as "manager1"
+    And I click on "My home" "link" in the "Navigation" "block"
+    When I press "Customise this page"
+    Then I should not see "Assign roles in Navigation block"
+
+  Scenario: Blocks on courses can have roles assigned to them
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email               |
+      | teacher1 | teacher   | 1        | teacher@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Turn editing on"
+    Then I should see "Assign roles in Search forums block"
index abd99d2..288eff3 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /blocks/* - activity modules,
 information provided here is intended especially for developers.
 
+=== 2.9 ===
+
+* The obsolete method preferred_width() was removed (it was not doing anything)
+
 === 2.8 ===
 
 * The instance_config_print() function was removed. It was deprecated in
index 2abfabc..5a2ef3b 100644 (file)
@@ -65,13 +65,13 @@ class cachestore_addinstance_form extends moodleform {
         }
 
         if (is_array($locks)) {
-            $form->addElement('select', 'lock', get_string('lockmethod', 'cache'), $locks);
-            $form->addHelpButton('lock', 'lockmethod', 'cache');
+            $form->addElement('select', 'lock', get_string('locking', 'cache'), $locks);
+            $form->addHelpButton('lock', 'locking', 'cache');
             $form->setType('lock', PARAM_ALPHANUMEXT);
         } else {
             $form->addElement('hidden', 'lock', '');
             $form->setType('lock', PARAM_ALPHANUMEXT);
-            $form->addElement('static', 'lock-value', get_string('lockmethod', 'cache'),
+            $form->addElement('static', 'lock-value', get_string('locking', 'cache'),
                     '<em>'.get_string('nativelocking', 'cache').'</em>');
         }
 
index 1ccb4bb..f6ceb1d 100644 (file)
@@ -53,7 +53,7 @@ class core_cache_renderer extends plugin_renderer_base {
             get_string('mappings', 'cache'),
             get_string('modes', 'cache'),
             get_string('supports', 'cache'),
-            get_string('lockingmeans', 'cache'),
+            get_string('locking', 'cache') . ' ' . $this->output->help_icon('locking', 'cache'),
             get_string('actions', 'cache'),
         );
         $table->colclasses = array(
index ac5917e..902693d 100644 (file)
@@ -142,8 +142,8 @@ class core_calendar_external extends external_api {
                                              "Time from which events should be returned",
                                              VALUE_DEFAULT, 0, NULL_ALLOWED),
                                     'timeend' => new external_value(PARAM_INT,
-                                             "Time to which the events should be returned",
-                                             VALUE_DEFAULT, time(), NULL_ALLOWED),
+                                             "Time to which the events should be returned. We treat 0 and null as no end",
+                                             VALUE_DEFAULT, 0, NULL_ALLOWED),
                                     'ignorehidden' => new external_value(PARAM_BOOL,
                                              "Ignore hidden events or not",
                                              VALUE_DEFAULT, true, NULL_ALLOWED),
@@ -215,6 +215,11 @@ class core_calendar_external extends external_api {
             $funcparam['courses'][] = $SITE->id;
         }
 
+        // We treat 0 and null as no end.
+        if (empty($params['options']['timeend'])) {
+            $params['options']['timeend'] = PHP_INT_MAX;
+        }
+
         $eventlist = calendar_get_events($params['options']['timestart'], $params['options']['timeend'], $funcparam['users'], $funcparam['groups'],
                 $funcparam['courses'], true, $params['options']['ignorehidden']);
         // WS expects arrays.
index b8582ab..4fb8764 100644 (file)
@@ -292,7 +292,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
 
         // Check to see if we got all events.
-        $this->assertEquals(4, count($events['events']));
+        $this->assertEquals(5, count($events['events']));
         $this->assertEquals(0, count($events['warnings']));
         $options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + 7*WEEKSECS);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
@@ -314,7 +314,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(4, count($events['events'])); // site, user, both course events.
         $this->assertEquals(1, count($events['warnings'])); // group.
 
-        $options = array ('siteevents' => true, 'userevents' => true);
+        $options = array ('siteevents' => true, 'userevents' => true, 'timeend' => time() + HOURSECS);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
         $this->assertEquals(3, count($events['events'])); // site, user, one course event.
index 72a666a..a5d31af 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /calendar/* ,
 information provided here is intended especially for developers.
 
+=== 2.9 ===
+default values changes in code:
+* core_calendar_external::get_calendar_events_parameters() 'timeend' default option changed; now, by default,
+  all events are returned, not only the past ones.
+
 === 2.5 ===
 required changes in code:
 * calendar_add_icalendar_event() now requires a valid subscriptionid
index cea8c66..17e9736 100644 (file)
@@ -48,7 +48,14 @@ class comment {
     private $courseid;
     /** @var stdClass course module object, only be used to help find pluginname automatically */
     private $cm;
-    /** @var string The component that this comment is for. It is STRONGLY recommended to set this. */
+    /**
+     * The component that this comment is for.
+     *
+     * It is STRONGLY recommended to set this.
+     * Added as a database field in 2.9, old comments will have a null component.
+     *
+     * @var string
+     */
     private $component;
     /** @var string This is calculated by normalising the component */
     private $pluginname;
@@ -241,10 +248,11 @@ class comment {
         }
         // setup variables for non-js interface
         self::$nonjs = optional_param('nonjscomment', '', PARAM_ALPHANUM);
-        self::$comment_itemid  = optional_param('comment_itemid',  '', PARAM_INT);
+        self::$comment_itemid = optional_param('comment_itemid',  '', PARAM_INT);
+        self::$comment_component = optional_param('comment_component', '', PARAM_COMPONENT);
         self::$comment_context = optional_param('comment_context', '', PARAM_INT);
-        self::$comment_page    = optional_param('comment_page',    '', PARAM_INT);
-        self::$comment_area    = optional_param('comment_area',    '', PARAM_AREA);
+        self::$comment_page = optional_param('comment_page',    '', PARAM_INT);
+        self::$comment_area = optional_param('comment_area',    '', PARAM_AREA);
 
         $page->requires->strings_for_js(array(
                 'addcomment',
@@ -264,6 +272,7 @@ class comment {
      * invalidates permission checks.
      * A coding_error is now thrown if code attempts to change the component.
      *
+     * @throws coding_exception if you try to change the component after it has been set.
      * @param string $component
      */
     public function set_component($component) {
@@ -325,6 +334,7 @@ class comment {
             'nonjscomment'    => true,
             'comment_itemid'  => $this->itemid,
             'comment_context' => $this->context->id,
+            'comment_component' => $this->get_component(),
             'comment_area'    => $this->commentarea,
         ));
         $link->remove_params(array('comment_page'));
@@ -530,10 +540,19 @@ class comment {
         $perpage = (!empty($CFG->commentsperpage))?$CFG->commentsperpage:15;
         $start = $page * $perpage;
         $ufields = user_picture::fields('u');
+
+        list($componentwhere, $component) = $this->get_component_select_sql('c');
+        if ($component) {
+            $params['component'] = $component;
+        }
+
         $sql = "SELECT $ufields, c.id AS cid, c.content AS ccontent, c.format AS cformat, c.timecreated AS ctimecreated
                   FROM {comments} c
                   JOIN {user} u ON u.id = c.userid
-                 WHERE c.contextid = :contextid AND c.commentarea = :commentarea AND c.itemid = :itemid
+                 WHERE c.contextid = :contextid AND
+                       c.commentarea = :commentarea AND
+                       c.itemid = :itemid AND
+                       $componentwhere
               ORDER BY c.timecreated DESC";
         $params['contextid'] = $this->contextid;
         $params['commentarea'] = $this->commentarea;
@@ -573,6 +592,25 @@ class comment {
         return $comments;
     }
 
+    /**
+     * Returns an SQL fragment and param for selecting on component.
+     * @param string $alias
+     * @return array
+     */
+    protected function get_component_select_sql($alias = '') {
+        $component = $this->get_component();
+        if ($alias) {
+            $alias = $alias.'.';
+        }
+        if (empty($component)) {
+            $componentwhere = "{$alias}component IS NULL";
+            $component = null;
+        } else {
+            $componentwhere = "({$alias}component IS NULL OR {$alias}component = :component)";
+        }
+        return array($componentwhere, $component);
+    }
+
     /**
      * Returns the number of comments associated with the details of this object
      *
@@ -582,7 +620,18 @@ class comment {
     public function count() {
         global $DB;
         if ($this->totalcommentcount === null) {
-            $this->totalcommentcount = $DB->count_records('comments', array('itemid' => $this->itemid, 'commentarea' => $this->commentarea, 'contextid' => $this->context->id));
+            list($where, $component) = $this->get_component_select_sql();
+            $where .= ' AND itemid = :itemid AND commentarea = :commentarea AND contextid = :contextid';
+            $params = array(
+                'itemid' => $this->itemid,
+                'commentarea' => $this->commentarea,
+                'contextid' => $this->context->id,
+            );
+            if ($component) {
+                $params['component'] = $component;
+            }
+
+            $this->totalcommentcount = $DB->count_records_select('comments', $where, $params);
         }
         return $this->totalcommentcount;
     }
@@ -641,6 +690,7 @@ class comment {
         $newcmt->contextid    = $this->contextid;
         $newcmt->commentarea  = $this->commentarea;
         $newcmt->itemid       = $this->itemid;
+        $newcmt->component    = !empty($this->component) ? $this->component : null;
         $newcmt->content      = $content;
         $newcmt->format       = $format;
         $newcmt->userid       = $USER->id;
@@ -785,10 +835,11 @@ class comment {
             return '';
         }
 
-        $html = '';
         if (!(self::$comment_itemid == $this->itemid &&
             self::$comment_context == $this->context->id &&
-            self::$comment_area == $this->commentarea)) {
+            self::$comment_area == $this->commentarea &&
+            self::$comment_component == $this->component
+        )) {
             $page = 0;
         }
         $comments = $this->get_comments($page);
@@ -918,13 +969,24 @@ class comment {
     }
 
     /**
-     * Returns the component associated with the comment
+     * Returns the component associated with the comment.
+     *
      * @return string
      */
-    public function get_compontent() {
+    public function get_component() {
         return $this->component;
     }
 
+    /**
+     * Do not call! I am a deprecated method because of the typo in my name.
+     * @deprecated since 2.9
+     * @see comment::get_component()
+     * @return string
+     */
+    public function get_compontent() {
+        return $this->get_component();
+    }
+
     /**
      * Returns the context associated with the comment
      * @return stdClass
index 606d335..14a4965 100644 (file)
@@ -62,7 +62,7 @@ class comment_manager {
         $comments = array();
 
         $usernamefields = get_all_user_name_fields(true, 'u');
-        $sql = "SELECT c.id, c.contextid, c.itemid, c.commentarea, c.userid, c.content, $usernamefields, c.timecreated
+        $sql = "SELECT c.id, c.contextid, c.itemid, c.component, c.commentarea, c.userid, c.content, $usernamefields, c.timecreated
                   FROM {comments} c
                   JOIN {user} u
                        ON u.id=c.userid
index 89b6344..5125a88 100644 (file)
@@ -105,4 +105,20 @@ class behat_completion extends behat_base {
         return $steps;
     }
 
+    /**
+     * Toggles completion tracking for course being in the course page.
+     *
+     * @When /^completion tracking is "(?P<completion_status_string>Enabled|Disabled)" in current course$/
+     * @param string $completionstatus The status, enabled or disabled.
+     */
+    public function completion_is_toggled_in_course($completionstatus) {
+
+        $toggle = strtolower($completionstatus) == 'enabled' ? get_string('yes') : get_string('no');
+
+        return array(
+            new Given('I follow "'.get_string('editsettings').'"'),
+            new Given('I set the field "'.get_string('enablecompletion', 'completion').'" to "'.$toggle.'"'),
+            new Given('I press "'.get_string('savechangesanddisplay').'"')
+        );
+    }
 }
index 7c29e65..303d79f 100644 (file)
@@ -28,7 +28,7 @@ Feature: Allow students to manually mark an activity as complete
     And I click on "Edit settings" "link" in the "Administration" "block"
     And I set the following fields to these values:
       | Enable completion tracking | Yes |
-    And I press "Save changes"
+    And I press "Save and display"
     When I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name |
       | Description | Test forum description |
index 584506a..69239a4 100644 (file)
@@ -30,7 +30,7 @@ Feature: Restrict sections availability through completion or grade conditions
     And I click on "Edit settings" "link" in the "Administration" "block"
     And I set the following fields to these values:
       | Enable completion tracking | Yes |
-    And I press "Save changes"
+    And I press "Save and display"
     And I add a "Label" to section "1" and I fill the form with:
       | Label text | Test label |
       | Completion tracking | Students can manually mark the activity as completed |
diff --git a/completion/tests/behat/teacher_manual_completion.feature b/completion/tests/behat/teacher_manual_completion.feature
new file mode 100644 (file)
index 0000000..d15691a
--- /dev/null
@@ -0,0 +1,45 @@
+@core @core_completion
+Feature: Allow teachers to manually mark users as complete when configured
+  In order for teachers to mark students as complete
+  As a teacher
+  I need to be able to use the completion report mark complete functionality
+
+  Scenario: Mark a student as complete using the completion report
+    Given the following "courses" exist:
+      | fullname          | shortname | category |
+      | Completion course | CC1       | 0        |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | First    | student1@example.com |
+      | teacher1 | Teacher   | First    | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | CC1    | student        |
+      | teacher1 | CC1    | editingteacher |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | Enable completion tracking | 1 |
+    And I am on homepage
+    And I follow "Completion course"
+    And completion tracking is "Enabled" in current course
+    And I follow "Course completion"
+    And I set the field "Teacher" to "1"
+    And I press "Save changes"
+    And I turn editing mode on
+    And I add the "Course completion status" block
+    And I log out
+    And I log in as "student1"
+    And I follow "Completion course"
+    And I should see "Status: Not yet started"
+    And I log out
+    When I log in as "teacher1"
+    And I follow "Completion course"
+    And I follow "View course report"
+    And I should see "Student First"
+    And I follow "Click to mark user complete"
+    And I trigger cron
+    And I am on homepage
+    And I log out
+    Then I log in as "student1"
+    And I follow "Completion course"
+    And I should see "Status: Complete"
index f13757b..b553923 100644 (file)
@@ -315,7 +315,7 @@ class helper {
         // Edit.
         if ($course->can_edit()) {
             $actions[] = array(
-                'url' => new \moodle_url('/course/edit.php', array('id' => $course->id)),
+                'url' => new \moodle_url('/course/edit.php', array('id' => $course->id, 'returnto' => 'catmanage')),
                 'icon' => new \pix_icon('t/edit', \get_string('edit')),
                 'attributes' => array('class' => 'action-edit')
             );
index 4d9542a..eb548f3 100644 (file)
@@ -18,17 +18,19 @@ M.core_completion.init = function(Y) {
 
         } else {
             var current = args.state.get('value');
-            var modulename = args.modulename.get('value');
+            var modulename = args.modulename.get('value'),
+                altstr,
+                titlestr;
             if (current == 1) {
-                var altstr = M.str.completion['completion-alt-manual-y'].replace('{$a}', modulename);
-                var titlestr = M.str.completion['completion-title-manual-y'].replace('{$a}', modulename);
+                altstr = M.util.get_string('completion-alt-manual-y', 'completion', modulename);
+                titlestr = M.util.get_string('completion-title-manual-y', 'completion', modulename);
                 args.state.set('value', 0);
                 args.image.set('src', M.util.image_url('i/completion-manual-y', 'moodle'));
                 args.image.set('alt', altstr);
                 args.image.set('title', titlestr);
             } else {
-                var altstr = M.str.completion['completion-alt-manual-n'].replace('{$a}', modulename);
-                var titlestr = M.str.completion['completion-title-manual-n'].replace('{$a}', modulename);
+                altstr = M.util.get_string('completion-alt-manual-n', 'completion', modulename);
+                titlestr = M.util.get_string('completion-title-manual-n', 'completion', modulename);
                 args.state.set('value', 1);
                 args.image.set('src', M.util.image_url('i/completion-manual-n', 'moodle'));
                 args.image.set('alt', altstr);
index f21ca88..bbe8992 100644 (file)
@@ -29,6 +29,35 @@ require_once('edit_form.php');
 $id = optional_param('id', 0, PARAM_INT); // Course id.
 $categoryid = optional_param('category', 0, PARAM_INT); // Course category - can be changed in edit form.
 $returnto = optional_param('returnto', 0, PARAM_ALPHANUM); // Generic navigation return page switch.
+$returnurl = optional_param('returnurl', '', PARAM_LOCALURL); // A return URL. returnto must also be set to 'url'.
+
+if ($returnto === 'url' && confirm_sesskey() && $returnurl) {
+    // If returnto is 'url' then $returnurl may be used as the destination to return to after saving or cancelling.
+    // Sesskey must be specified, and would be set by the form anyway.
+    $returnurl = new moodle_url($returnurl);
+} else {
+    if (!empty($id)) {
+        $returnurl = new moodle_url($CFG->wwwroot . '/course/view.php', array('id' => $id));
+    } else {
+        $returnurl = new moodle_url($CFG->wwwroot . '/course/');
+    }
+    if ($returnto !== 0) {
+        switch ($returnto) {
+            case 'category':
+                $returnurl = new moodle_url($CFG->wwwroot . '/course/index.php', array('categoryid' => $categoryid));
+                break;
+            case 'catmanage':
+                $returnurl = new moodle_url($CFG->wwwroot . '/course/management.php', array('categoryid' => $categoryid));
+                break;
+            case 'topcatmanage':
+                $returnurl = new moodle_url($CFG->wwwroot . '/course/management.php');
+                break;
+            case 'topcat':
+                $returnurl = new moodle_url($CFG->wwwroot . '/course/');
+                break;
+        }
+    }
+}
 
 $PAGE->set_pagelayout('admin');
 if ($id) {
@@ -36,6 +65,12 @@ if ($id) {
 } else {
     $pageparams = array('category' => $categoryid);
 }
+if ($returnto !== 0) {
+    $pageparams['returnto'] = $returnto;
+    if ($returnto === 'url' && $returnurl) {
+        $pageparams['returnurl'] = $returnurl;
+    }
+}
 $PAGE->set_url('/course/edit.php', $pageparams);
 
 // Basic access control checks.
@@ -98,31 +133,17 @@ if (!empty($course)) {
 }
 
 // First create the form.
-$editform = new course_edit_form(NULL, array('course'=>$course, 'category'=>$category, 'editoroptions'=>$editoroptions, 'returnto'=>$returnto));
+$args = array(
+    'course' => $course,
+    'category' => $category,
+    'editoroptions' => $editoroptions,
+    'returnto' => $returnto,
+    'returnurl' => $returnurl
+);
+$editform = new course_edit_form(null, $args);
 if ($editform->is_cancelled()) {
-        switch ($returnto) {
-            case 'category':
-                $url = new moodle_url($CFG->wwwroot.'/course/index.php', array('categoryid' => $categoryid));
-                break;
-            case 'catmanage':
-                $url = new moodle_url($CFG->wwwroot.'/course/management.php', array('categoryid' => $categoryid));
-                break;
-            case 'topcatmanage':
-                $url = new moodle_url($CFG->wwwroot.'/course/management.php');
-                break;
-            case 'topcat':
-                $url = new moodle_url($CFG->wwwroot.'/course/');
-                break;
-            default:
-                if (!empty($course->id)) {
-                    $url = new moodle_url($CFG->wwwroot.'/course/view.php', array('id'=>$course->id));
-                } else {
-                    $url = new moodle_url($CFG->wwwroot.'/course/');
-                }
-                break;
-        }
-        redirect($url);
-
+    // The form has been cancelled, take them back to what ever the return to is.
+    redirect($returnurl);
 } else if ($data = $editform->get_data()) {
     // Process data if submitted.
     if (empty($course->id)) {
@@ -136,14 +157,20 @@ if ($editform->is_cancelled()) {
             // Deal with course creators - enrol them internally with default role.
             enrol_try_internal_enrol($course->id, $USER->id, $CFG->creatornewroleid);
         }
-        if (!is_enrolled($context)) {
+
+        // The URL to take them to if they chose save and display.
+        $courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
+
+        // If they choose to save and display, and they are not enrolled take them to the enrolments page instead.
+        if (!is_enrolled($context) && isset($data->saveanddisplay)) {
             // Redirect to manual enrolment page if possible.
             $instances = enrol_get_instances($course->id, true);
             foreach($instances as $instance) {
                 if ($plugin = enrol_get_plugin($instance->enrol)) {
                     if ($plugin->get_manual_enrol_link($instance)) {
                         // We know that the ajax enrol UI will have an option to enrol.
-                        redirect(new moodle_url('/enrol/users.php', array('id'=>$course->id)));
+                        $courseurl = new moodle_url('/enrol/users.php', array('id' => $course->id));
+                        break;
                     }
                 }
             }
@@ -151,10 +178,17 @@ if ($editform->is_cancelled()) {
     } else {
         // Save any changes to the files used in the editor.
         update_course($data, $editoroptions);
+        // Set the URL to take them too if they choose save and display.
+        $courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
     }
 
-    // Redirect user to newly created/updated course.
-    redirect(new moodle_url('/course/view.php', array('id' => $course->id)));
+    if (isset($data->saveanddisplay)) {
+        // Redirect user to newly created/updated course.
+        redirect($courseurl);
+    } else {
+        // Save and return. Take them back to wherever.
+        redirect($returnurl);
+    }
 }
 
 // Print the form.
index 1de01c9..54359aa 100644 (file)
@@ -27,6 +27,7 @@ class course_edit_form extends moodleform {
         $category      = $this->_customdata['category'];
         $editoroptions = $this->_customdata['editoroptions'];
         $returnto = $this->_customdata['returnto'];
+        $returnurl = $this->_customdata['returnurl'];
 
         $systemcontext   = context_system::instance();
         $categorycontext = context_coursecat::instance($category->id);
@@ -51,6 +52,10 @@ class course_edit_form extends moodleform {
         $mform->setType('returnto', PARAM_ALPHANUM);
         $mform->setConstant('returnto', $returnto);
 
+        $mform->addElement('hidden', 'returnurl', null);
+        $mform->setType('returnurl', PARAM_LOCALURL);
+        $mform->setConstant('returnurl', $returnurl);
+
         $mform->addElement('text','fullname', get_string('fullnamecourse'),'maxlength="254" size="50"');
         $mform->addHelpButton('fullname', 'fullnamecourse');
         $mform->addRule('fullname', get_string('missingfullname'), 'required', null, 'client');
@@ -296,7 +301,15 @@ class course_edit_form extends moodleform {
             }
         }
 
-        $this->add_action_buttons();
+        // When two elements we need a group.
+        $buttonarray = array();
+        if ($returnto !== 0) {
+            $buttonarray[] = &$mform->createElement('submit', 'saveandreturn', get_string('savechangesandreturn'));
+        }
+        $buttonarray[] = &$mform->createElement('submit', 'saveanddisplay', get_string('savechangesanddisplay'));
+        $buttonarray[] = &$mform->createElement('cancel');
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
 
         $mform->addElement('hidden', 'id', null);
         $mform->setType('id', PARAM_INT);
index f3765a4..6b8ce0a 100644 (file)
@@ -26,7 +26,6 @@
 require_once("../config.php");
 require_once("lib.php");
 require_once($CFG->libdir . '/formslib.php');
-require_once($CFG->libdir . '/conditionlib.php');
 
 $id = required_param('id', PARAM_INT);    // course_sections.id
 $sectionreturn = optional_param('sr', 0, PARAM_INT);
index 33b9f55..eaeaff7 100644 (file)
@@ -395,6 +395,7 @@ abstract class format_base {
      * @return null|moodle_url
      */
     public function get_view_url($section, $options = array()) {
+        global $CFG;
         $course = $this->get_course();
         $url = new moodle_url('/course/view.php', array('id' => $course->id));
 
@@ -405,7 +406,7 @@ abstract class format_base {
         } else {
             $sectionno = $section;
         }
-        if (!empty($options['navigation']) && $sectionno !== null) {
+        if (empty($CFG->linkcoursesections) && !empty($options['navigation']) && $sectionno !== null) {
             // by default assume that sections are never displayed on separate pages
             return null;
         }
index f7e91bb..cb0a7ed 100644 (file)
@@ -75,6 +75,7 @@ class format_topics extends format_base {
      * @return null|moodle_url
      */
     public function get_view_url($section, $options = array()) {
+        global $CFG;
         $course = $this->get_course();
         $url = new moodle_url('/course/view.php', array('id' => $course->id));
 
@@ -101,7 +102,7 @@ class format_topics extends format_base {
             if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) {
                 $url->param('section', $sectionno);
             } else {
-                if (!empty($options['navigation'])) {
+                if (empty($CFG->linkcoursesections) && !empty($options['navigation'])) {
                     return null;
                 }
                 $url->set_anchor('section-'.$sectionno);
index 73d97c2..66152d2 100644 (file)
@@ -82,6 +82,7 @@ class format_weeks extends format_base {
      * @return null|moodle_url
      */
     public function get_view_url($section, $options = array()) {
+        global $CFG;
         $course = $this->get_course();
         $url = new moodle_url('/course/view.php', array('id' => $course->id));
 
@@ -108,7 +109,7 @@ class format_weeks extends format_base {
             if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) {
                 $url->param('section', $sectionno);
             } else {
-                if (!empty($options['navigation'])) {
+                if (empty($CFG->linkcoursesections) && !empty($options['navigation'])) {
                     return null;
                 }
                 $url->set_anchor('section-'.$sectionno);
index 07cd6ed..b8212d5 100644 (file)
@@ -998,9 +998,6 @@ function get_array_of_activities($courseid) {
 //  groupingid - grouping id
 //  extra - contains extra string to include in any link
     global $CFG, $DB;
-    if(!empty($CFG->enableavailability)) {
-        require_once($CFG->libdir.'/conditionlib.php');
-    }
 
     $course = $DB->get_record('course', array('id'=>$courseid));
 
index b2d8ede..234e3ad 100644 (file)
@@ -28,7 +28,6 @@ require_once("lib.php");
 require_once($CFG->libdir.'/filelib.php');
 require_once($CFG->libdir.'/gradelib.php');
 require_once($CFG->libdir.'/completionlib.php');
-require_once($CFG->libdir.'/conditionlib.php');
 require_once($CFG->libdir.'/plagiarismlib.php');
 require_once($CFG->dirroot . '/course/modlib.php');
 
index fa1ac35..9a44714 100644 (file)
@@ -483,12 +483,17 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) {
     }
 
     $completion = new completion_info($course);
-    if ($completion->is_enabled() && !empty($moduleinfo->completionunlocked)) {
-        // Update completion settings.
-        $cm->completion                = $moduleinfo->completion;
-        $cm->completiongradeitemnumber = $moduleinfo->completiongradeitemnumber;
-        $cm->completionview            = $moduleinfo->completionview;
-        $cm->completionexpected        = $moduleinfo->completionexpected;
+    if ($completion->is_enabled()) {
+        // Completion settings that would affect users who have already completed
+        // the activity may be locked; if so, these should not be updated.
+        if (!empty($moduleinfo->completionunlocked)) {
+            $cm->completion = $moduleinfo->completion;
+            $cm->completiongradeitemnumber = $moduleinfo->completiongradeitemnumber;
+            $cm->completionview = $moduleinfo->completionview;
+        }
+        // The expected date does not affect users who have completed the activity,
+        // so it is safe to update it regardless of the lock status.
+        $cm->completionexpected = $moduleinfo->completionexpected;
     }
     if (!empty($CFG->enableavailability)) {
         // This code is used both when submitting the form, which uses a long
index 33014d6..6a3a845 100644 (file)
@@ -436,6 +436,14 @@ abstract class moodleform_mod extends moodleform {
         }
 
         if (!empty($CFG->enableavailability)) {
+            // Add special button to end of previous section if groups/groupings
+            // are enabled.
+            if ($this->_features->groups || $this->_features->groupings) {
+                $mform->addElement('static', 'restrictgroupbutton', '',
+                        html_writer::tag('button', get_string('restrictbygroup', 'availability'),
+                        array('id' => 'restrictbygroup', 'disabled' => 'disabled')));
+            }
+
             // Availability field. This is just a textarea; the user interface
             // interaction is all implemented in JavaScript.
             $mform->addElement('header', 'availabilityconditionsheader',
index 75ce9c9..170e308 100644 (file)
@@ -40,7 +40,7 @@ if ($return === 'management') {
 $PAGE->set_url($url);
 
 // Check permissions.
-require_login();
+require_login(null, false);
 if (isguestuser()) {
     print_error('guestsarenotallowed', '', $returnurl);
 }
index 52ba8fa..1662307 100644 (file)
@@ -65,7 +65,9 @@ class course_reset_form extends moodleform {
         $mform->addElement('header', 'gradebookheader', get_string('gradebook', 'grades'));
 
         $mform->addElement('checkbox', 'reset_gradebook_items', get_string('removeallcourseitems', 'grades'));
+        $mform->addHelpButton('reset_gradebook_items', 'removeallcourseitems', 'grades');
         $mform->addElement('checkbox', 'reset_gradebook_grades', get_string('removeallcoursegrades', 'grades'));
+        $mform->addHelpButton('reset_gradebook_grades', 'removeallcoursegrades', 'grades');
         $mform->disabledIf('reset_gradebook_grades', 'reset_gradebook_items', 'checked');
 
 
diff --git a/course/tests/behat/activities_edit_completion.feature b/course/tests/behat/activities_edit_completion.feature
new file mode 100644 (file)
index 0000000..e099938
--- /dev/null
@@ -0,0 +1,57 @@
+@core @core_course
+Feature: Edit completion settings of an activity
+  In order to edit completion settings without accidentally breaking user data
+  As a teacher
+  I need to edit the activity and use the unlock button if required
+
+  Background:
+    Given I log in as "admin"
+    And I set the following administration settings values:
+      | Enable completion tracking | 1 |
+    And I log out
+    And the following "courses" exist:
+      | fullname | shortname | enablecompletion |
+      | Course 1 | C1        | 1                |
+    And the following "activities" exist:
+      | activity | course | idnumber | name     | intro | content | completion | completionview |
+      | page     | C1     | x        | TestPage | x     | x       | 2          | 1              |
+    And I log in as "admin"
+    And I follow "Course 1"
+
+  Scenario: Completion is not locked when the activity has not yet been viewed
+    Given I turn editing mode on
+    And I click on "Edit settings" "link" in the "TestPage" activity
+    When I expand all fieldsets
+    Then I should see "Completion tracking"
+    And I should not see "Completion options locked"
+
+  Scenario: Completion is locked after the activity has been viewed
+    Given I follow "TestPage"
+    When I follow "Edit settings"
+    And I expand all fieldsets
+    Then I should see "Completion options locked"
+
+  @javascript
+  Scenario: Pressing the unlock button allows the user to edit completion settings
+    Given I follow "TestPage"
+    When I follow "Edit settings"
+    And I expand all fieldsets
+    And I press "Unlock completion options"
+    Then I should see "Completion options unlocked"
+    And I set the field "Completion tracking" to "Students can manually mark the activity as completed"
+    And I press "Save and display"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    Then the field "Completion tracking" matches value "Students can manually mark the activity as completed"
+
+  @javascript
+  Scenario: Even when completion is locked, the user can still set the date
+    Given I follow "TestPage"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    When I click on "id_completionexpected_enabled" "checkbox"
+    And I set the field "id_completionexpected_year" to "2013"
+    And I press "Save and display"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    Then the field "id_completionexpected_year" matches value "2013"