Merge branch 'MDL-37993-quiz-completion-pass-attempts' of git://github.com/MorrisR2...
authorSam Hemelryk <sam@moodle.com>
Mon, 14 Jul 2014 20:48:55 +0000 (08:48 +1200)
committerSam Hemelryk <sam@moodle.com>
Mon, 14 Jul 2014 20:48:55 +0000 (08:48 +1200)
618 files changed:
admin/cli/check_database_schema.php [new file with mode: 0644]
admin/cli/install.php
admin/mnet/index.php
admin/roles/classes/capability_table_base.php
admin/roles/module.js
admin/roles/override.php
admin/tests/behat/behat_admin.php
admin/tests/behat/set_admin_settings_value.feature [new file with mode: 0644]
admin/tool/installaddon/lang/en/tool_installaddon.php
admin/tool/log/classes/helper/buffered_writer.php
admin/tool/log/store/database/tests/store_test.php
admin/tool/log/store/standard/tests/store_test.php
admin/tool/task/scheduledtasks.php
admin/tool/uploaduser/index.php
admin/user.php
auth/cas/auth.php
auth/email/auth.php
auth/ldap/auth.php
auth/ldap/tests/plugin_test.php
auth/shibboleth/README.txt
auth/shibboleth/index.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/dbops/tests/backup_dbops_test.php
backup/util/helper/backup_cron_helper.class.php
backup/util/plan/restore_plan.class.php
backup/util/structure/tests/structure_test.php
backup/util/ui/renderer.php
backup/util/ui/tests/behat/backup_courses.feature
backup/util/ui/tests/behat/restore_moodle2_courses.feature
badges/badge.php
badges/edit_form.php
badges/external.php
badges/renderer.php
badges/tests/behat/add_badge.feature
badges/tests/behat/award_badge.feature
blocks/community/block_community.php
blocks/navigation/tests/behat/expand_my_courses_setting.feature
blog/edit.php
blog/external_blog_edit.php
blog/external_blog_edit_form.php
blog/index.php
blog/tests/bloglib_test.php
cache/README.md
cache/classes/store.php
cache/stores/memcache/addinstanceform.php
cache/stores/memcache/lang/en/cachestore_memcache.php
cache/stores/memcache/lib.php
cache/stores/memcache/settings.php
cache/stores/memcache/tests/memcache_test.php
cache/stores/memcached/addinstanceform.php
cache/stores/memcached/lang/en/cachestore_memcached.php
cache/stores/memcached/lib.php
cache/stores/memcached/settings.php
cache/stores/memcached/tests/memcached_test.php
cache/stores/mongodb/lib.php
cache/stores/mongodb/tests/mongodb_test.php
cache/tests/fixtures/stores.php
calendar/event_form.php
calendar/lib.php
calendar/renderer.php
calendar/set.php
calendar/tests/lib_test.php
calendar/yui/build/moodle-calendar-eventmanager/moodle-calendar-eventmanager-debug.js
calendar/yui/build/moodle-calendar-eventmanager/moodle-calendar-eventmanager-min.js
calendar/yui/build/moodle-calendar-eventmanager/moodle-calendar-eventmanager.js
calendar/yui/src/eventmanager/js/eventmanager.js
completion/tests/behat/restrict_activity_by_grade.feature
completion/tests/behat/restrict_section_availability.feature
config-dist.php
course/delete_category_form.php [deleted file]
course/dnduploadlib.php
course/edit.php
course/editcategory_form.php [deleted file]
course/lib.php
course/manage.php [deleted file]
course/management.php
course/modduplicate.php
course/modlib.php
course/renderer.php
course/tests/behat/behat_course.php
course/tests/behat/category_change_visibility.feature
course/tests/behat/category_management.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_resort.feature
course/tests/behat/create_delete_course.feature
course/tests/courselib_test.php
enrol/database/lang/en/enrol_database.php
enrol/database/lib.php
enrol/database/settings.php
enrol/database/tests/sync_test.php
enrol/meta/tests/plugin_test.php
files/renderer.php
filter/activitynames/filter.php
filter/algebra/algebradebug.php
filter/algebra/pix.php
filter/emoticon/tests/filter_test.php
filter/tex/pix.php
filter/tex/texdebug.php
grade/edit/scale/edit_form.php
grade/edit/tree/category_form.php
grade/grading/form/guide/guideeditor.php
grade/grading/form/guide/js/guide.js
grade/grading/form/guide/js/guideeditor.js
grade/grading/form/guide/lib.php
grade/grading/form/guide/renderer.php
grade/grading/form/rubric/js/rubriceditor.js
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/rubriceditor.php
grade/report/grader/classes/event/report_viewed.php [new file with mode: 0644]
grade/report/grader/index.php
grade/report/grader/lang/en/gradereport_grader.php
grade/report/outcomes/classes/event/report_viewed.php [new file with mode: 0644]
grade/report/outcomes/index.php
grade/report/outcomes/lang/en/gradereport_outcomes.php
grade/report/overview/classes/event/report_viewed.php [new file with mode: 0644]
grade/report/overview/index.php
grade/report/overview/lang/en/gradereport_overview.php
grade/report/user/classes/event/report_viewed.php [new file with mode: 0644]
grade/report/user/index.php
grade/report/user/lang/en/gradereport_user.php
grade/tests/behat/grade_point_maximum.feature
group/autogroup.php
group/autogroup_form.php
group/lib.php
group/tests/behat/auto_creation.feature
index.php
install.php
install/lang/eo/langconfig.php [moved from lib/editor/tinymce/plugins/dragmath/version.php with 63% similarity]
install/lang/fr/install.php
install/lang/oc_lnc/admin.php [new file with mode: 0644]
install/lang/oc_lnc/error.php [new file with mode: 0644]
install/lang/oc_lnc/langconfig.php
install/lang/pt/error.php
install/lang/sr_cr/install.php
install/lang/sr_lt/install.php
lang/en/admin.php
lang/en/cache.php
lang/en/calendar.php
lang/en/grades.php
lang/en/group.php
lang/en/message.php
lang/en/moodle.php
lang/en/my.php
lang/en/plagiarism.php
lang/en/plugin.php
lang/en/repository.php
lib/accesslib.php
lib/authlib.php
lib/behat/form_field/behat_form_select.php
lib/bennu/iCalendar_rfc2445.php
lib/bennu/readme_moodle.txt
lib/classes/collator.php
lib/classes/component.php
lib/classes/event/blog_association_created.php
lib/classes/event/blog_entries_viewed.php
lib/classes/event/blog_entry_created.php
lib/classes/event/blog_entry_deleted.php
lib/classes/event/blog_entry_updated.php
lib/classes/event/cohort_member_added.php
lib/classes/event/cohort_member_removed.php
lib/classes/event/content_viewed.php
lib/classes/event/course_category_deleted.php
lib/classes/event/course_completed.php
lib/classes/event/course_content_deleted.php
lib/classes/event/course_created.php
lib/classes/event/course_deleted.php
lib/classes/event/course_module_created.php
lib/classes/event/course_restored.php
lib/classes/event/course_updated.php
lib/classes/event/course_user_report_viewed.php
lib/classes/event/course_viewed.php
lib/classes/event/email_failed.php
lib/classes/event/grade_report_viewed.php [new file with mode: 0644]
lib/classes/event/group_member_added.php
lib/classes/event/group_member_removed.php
lib/classes/event/mnet_access_control_created.php
lib/classes/event/mnet_access_control_updated.php
lib/classes/event/note_created.php
lib/classes/event/note_deleted.php
lib/classes/event/note_updated.php
lib/classes/event/notes_viewed.php
lib/classes/event/recent_activity_viewed.php
lib/classes/event/role_assigned.php
lib/classes/event/role_deleted.php
lib/classes/event/role_unassigned.php
lib/classes/event/user_created.php
lib/classes/event/user_graded.php
lib/classes/event/user_list_viewed.php
lib/classes/event/user_loggedinas.php
lib/classes/event/user_loggedout.php
lib/classes/event/user_login_failed.php
lib/classes/event/user_profile_viewed.php
lib/classes/event/user_updated.php
lib/classes/event/webservice_function_called.php
lib/classes/event/webservice_service_created.php
lib/classes/event/webservice_service_deleted.php
lib/classes/event/webservice_service_updated.php
lib/classes/event/webservice_service_user_added.php
lib/classes/event/webservice_service_user_removed.php
lib/classes/event/webservice_token_created.php
lib/classes/event/webservice_token_sent.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/theme.php
lib/classes/session/manager.php
lib/classes/task/adhoc_task.php
lib/classes/task/manager.php
lib/classes/text.php
lib/db/caches.php
lib/db/install.xml
lib/db/renamedclasses.php [new file with mode: 0644]
lib/db/upgrade.php
lib/ddl/database_manager.php
lib/ddl/mssql_sql_generator.php
lib/ddl/tests/ddl_test.php
lib/dragmath/COPYRIGHT.html [deleted file]
lib/dragmath/README.html [deleted file]
lib/dragmath/Version History.html [deleted file]
lib/dragmath/applet/Config.xml [deleted file]
lib/dragmath/applet/Display/MainApplet.class [deleted file]
lib/dragmath/applet/DragMath.jar [deleted file]
lib/dragmath/applet/formats/ASCIIMathML.xml [deleted file]
lib/dragmath/applet/formats/Latex.xml [deleted file]
lib/dragmath/applet/formats/Maple.xml [deleted file]
lib/dragmath/applet/formats/MathML.xml [deleted file]
lib/dragmath/applet/formats/Maxima.xml [deleted file]
lib/dragmath/applet/formats/MoodleTex.xml [deleted file]
lib/dragmath/applet/formats/Tex.xml [deleted file]
lib/dragmath/applet/lang/ca.xml [deleted file]
lib/dragmath/applet/lang/cz.xml [deleted file]
lib/dragmath/applet/lang/de.xml [deleted file]
lib/dragmath/applet/lang/en.xml [deleted file]
lib/dragmath/applet/lang/es.xml [deleted file]
lib/dragmath/applet/lang/fa.xml [deleted file]
lib/dragmath/applet/lang/fi.xml [deleted file]
lib/dragmath/applet/lang/fr.xml [deleted file]
lib/dragmath/applet/lang/it.xml [deleted file]
lib/dragmath/applet/lang/nl.xml [deleted file]
lib/dragmath/applet/lang/no.xml [deleted file]
lib/dragmath/applet/lang/pl.xml [deleted file]
lib/dragmath/applet/lang/pt-br.xml [deleted file]
lib/dragmath/applet/lang/ru.xml [deleted file]
lib/dragmath/applet/lang/sv.xml [deleted file]
lib/dragmath/applet/lib/AbsoluteLayout.jar [deleted file]
lib/dragmath/applet/lib/jdom.jar [deleted file]
lib/dragmath/applet/lib/jep.jar [deleted file]
lib/dragmath/applet/lib/swing-layout-1.0.jar [deleted file]
lib/dragmath/doc/about.html [deleted file]
lib/dragmath/doc/acknowledgments.html [deleted file]
lib/dragmath/doc/browser.html [deleted file]
lib/dragmath/doc/complex.html [deleted file]
lib/dragmath/doc/cvs.html [deleted file]
lib/dragmath/doc/editing.html [deleted file]
lib/dragmath/doc/img/plus.jpg [deleted file]
lib/dragmath/doc/img/sin on box.jpg [deleted file]
lib/dragmath/doc/img/sin on plus.jpg [deleted file]
lib/dragmath/doc/img/sin.jpg [deleted file]
lib/dragmath/doc/index.html [deleted file]
lib/dragmath/doc/integrations.html [deleted file]
lib/dragmath/doc/language.html [deleted file]
lib/dragmath/doc/navframe.html [deleted file]
lib/dragmath/doc/newformat.html [deleted file]
lib/dragmath/doc/newobject.html [deleted file]
lib/dragmath/doc/options.html [deleted file]
lib/dragmath/doc/parameters.html [deleted file]
lib/dragmath/doc/simple.html [deleted file]
lib/dragmath/readme_moodle.txt [deleted file]
lib/dtl/database_exporter.php
lib/dtl/database_importer.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js
lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js
lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-debug.js
lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-min.js
lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button.js
lib/editor/atto/plugins/accessibilityhelper/yui/src/button/js/button.js
lib/editor/atto/plugins/collapse/yui/build/moodle-atto_collapse-button/moodle-atto_collapse-button-debug.js
lib/editor/atto/plugins/collapse/yui/build/moodle-atto_collapse-button/moodle-atto_collapse-button-min.js
lib/editor/atto/plugins/collapse/yui/build/moodle-atto_collapse-button/moodle-atto_collapse-button.js
lib/editor/atto/plugins/collapse/yui/src/button/js/button.js
lib/editor/atto/plugins/equation/tests/behat/equation.feature
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js
lib/editor/atto/plugins/equation/yui/src/button/js/button.js
lib/editor/atto/plugins/image/tests/behat/image.feature
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/tinymce/db/upgrade.php
lib/editor/tinymce/plugins/dragmath/dragmath.php [deleted file]
lib/editor/tinymce/plugins/dragmath/lang/en/tinymce_dragmath.php [deleted file]
lib/editor/tinymce/plugins/dragmath/lib.php [deleted file]
lib/editor/tinymce/plugins/dragmath/pix/icon.png [deleted file]
lib/editor/tinymce/plugins/dragmath/settings.php [deleted file]
lib/editor/tinymce/plugins/dragmath/tinymce/editor_plugin.js [deleted file]
lib/editor/tinymce/plugins/dragmath/tinymce/img/dragmath.png [deleted file]
lib/editor/tinymce/plugins/dragmath/tinymce/js/dragmath.js [deleted file]
lib/editor/tinymce/upgrade.txt
lib/editor/tinymce/version.php
lib/enrollib.php
lib/form/editor.php
lib/form/filemanager.js
lib/form/filemanager.php
lib/grade/tests/grade_scale_test.php
lib/graphlib.php
lib/htmlpurifier/locallib.php
lib/modinfolib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputfactories.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/pagelib.php
lib/phpunit/classes/util.php
lib/phpunit/tests/advanced_test.php
lib/pluginlib.php
lib/questionlib.php
lib/sessionlib.php
lib/setup.php
lib/testing/classes/util.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/collator_test.php
lib/tests/events_test.php
lib/tests/fixtures/event_fixtures.php
lib/tests/fixtures/test_renderer_factory.php [new file with mode: 0644]
lib/tests/grades_externallib_test.php
lib/tests/htmlpurifier_test.php
lib/tests/moodlelib_test.php
lib/tests/outputfactories_test.php [new file with mode: 0644]
lib/tests/outputrequirementslib_test.php
lib/tests/session_manager_test.php
lib/tests/sessionlib_test.php [new file with mode: 0644]
lib/tests/text_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/weblib.php
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/notification/js/ajaxexception.js
lib/yui/src/notification/js/exception.js
lib/yui/src/notification/meta/notification.json
login/change_password.php
login/index.php
login/lib.php
message/lib.php
mod/assign/classes/event/reveal_identities_confirmation_page_viewed.php
mod/assign/classes/event/submission_confirmation_form_viewed.php
mod/assign/classes/event/submission_status_viewed.php
mod/assign/classes/event/workflow_state_updated.php
mod/assign/db/access.php
mod/assign/feedback/editpdf/ajax.php
mod/assign/feedback/editpdf/ajax_progress.php
mod/assign/feedback/editpdf/backup/moodle2/backup_assignfeedback_editpdf_subplugin.class.php
mod/assign/feedback/editpdf/backup/moodle2/restore_assignfeedback_editpdf_subplugin.class.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/page_editor.php
mod/assign/feedback/editpdf/locallib.php
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/editor.js
mod/assign/feedback/file/lib.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/renderer.php
mod/assign/styles.css
mod/assign/submission/comments/lib.php
mod/assign/submission/file/lib.php
mod/assign/submission/onlinetext/classes/event/assessable_uploaded.php
mod/assign/submission/onlinetext/classes/event/submission_updated.php
mod/assign/submission/onlinetext/lib.php
mod/assign/tests/events_test.php
mod/assign/tests/externallib_test.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/assign/version.php
mod/assignment/lib.php
mod/choice/classes/event/answer_submitted.php
mod/choice/classes/event/answer_updated.php
mod/choice/tests/events_test.php
mod/data/lib.php
mod/data/rsslib.php
mod/data/tests/lib_test.php
mod/data/tests/search_test.php
mod/feedback/classes/event/response_deleted.php
mod/feedback/classes/event/response_submitted.php
mod/feedback/item/textarea/lib.php
mod/feedback/item/textfield/lib.php
mod/feedback/tests/events_test.php
mod/feedback/upgrade.txt [new file with mode: 0644]
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/classes/event/discussion_subscription_created.php [new file with mode: 0644]
mod/forum/classes/event/discussion_subscription_deleted.php [new file with mode: 0644]
mod/forum/classes/event/subscription_created.php
mod/forum/classes/event/subscription_deleted.php
mod/forum/classes/event/user_report_viewed.php
mod/forum/classes/observer.php
mod/forum/classes/post_form.php
mod/forum/classes/subscriptions.php [new file with mode: 0644]
mod/forum/db/install.xml
mod/forum/db/upgrade.php
mod/forum/deprecatedlib.php
mod/forum/discuss.php
mod/forum/index.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/pix/t/subscribed.png [new file with mode: 0644]
mod/forum/pix/t/subscribed.svg [new file with mode: 0644]
mod/forum/pix/t/unsubscribed.png [new file with mode: 0644]
mod/forum/pix/t/unsubscribed.svg [new file with mode: 0644]
mod/forum/post.php
mod/forum/renderer.php
mod/forum/styles.css
mod/forum/subscribe.php
mod/forum/subscribe_ajax.php [new file with mode: 0644]
mod/forum/subscribers.php
mod/forum/tests/behat/discussion_subscriptions.feature [new file with mode: 0644]
mod/forum/tests/behat/forum_subscriptions.feature [new file with mode: 0644]
mod/forum/tests/behat/separate_group_discussions.feature [new file with mode: 0644]
mod/forum/tests/behat/separate_group_single_group_discussions.feature [new file with mode: 0644]
mod/forum/tests/events_test.php
mod/forum/tests/externallib_test.php
mod/forum/tests/generator_test.php
mod/forum/tests/lib_test.php
mod/forum/tests/mail_test.php [new file with mode: 0644]
mod/forum/tests/maildigest_test.php
mod/forum/tests/subscriptions_test.php [new file with mode: 0644]
mod/forum/unsubscribeall.php
mod/forum/upgrade.txt
mod/forum/version.php
mod/forum/view.php
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-debug.js [new file with mode: 0644]
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-min.js [new file with mode: 0644]
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle.js [new file with mode: 0644]
mod/forum/yui/src/subscriptiontoggle/build.json [new file with mode: 0644]
mod/forum/yui/src/subscriptiontoggle/js/toggle.js [new file with mode: 0644]
mod/forum/yui/src/subscriptiontoggle/meta/subscriptiontoggle.json [new file with mode: 0644]
mod/glossary/classes/event/entry_created.php
mod/glossary/classes/event/entry_deleted.php
mod/glossary/classes/event/entry_updated.php
mod/glossary/deleteentry.php
mod/glossary/rsslib.php
mod/imscp/locallib.php
mod/lesson/classes/event/essay_attempt_viewed.php
mod/lesson/tests/events_test.php
mod/lti/db/install.xml
mod/lti/db/upgrade.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/return.php
mod/lti/service.php
mod/lti/upgrade.txt
mod/lti/version.php
mod/quiz/attemptlib.php
mod/quiz/classes/event/attempt_abandoned.php
mod/quiz/classes/event/attempt_becameoverdue.php
mod/quiz/classes/event/attempt_submitted.php
mod/quiz/editlib.php
mod/quiz/lang/en/quiz.php
mod/quiz/renderer.php
mod/quiz/report/attemptsreport_form.php
mod/quiz/report/attemptsreport_options.php
mod/quiz/report/attemptsreport_table.php
mod/quiz/report/overview/lang/en/quiz_overview.php
mod/quiz/report/overview/overview_form.php
mod/quiz/report/overview/overview_options.php
mod/quiz/report/overview/report.php
mod/quiz/report/overview/tests/report_test.php [new file with mode: 0644]
mod/quiz/report/reportlib.php
mod/quiz/review.php
mod/quiz/styles.css
mod/quiz/tests/attempt_test.php [new file with mode: 0644]
mod/quiz/tests/editlib_test.php
mod/quiz/tests/reportlib_test.php
mod/scorm/classes/event/interactions_viewed.php
mod/scorm/classes/event/report_viewed.php
mod/scorm/classes/event/sco_launched.php
mod/scorm/classes/event/tracks_viewed.php
mod/scorm/classes/event/user_report_viewed.php
mod/survey/classes/event/report_downloaded.php
mod/survey/classes/event/report_viewed.php
mod/survey/lang/en/survey.php
mod/survey/report.php
mod/wiki/classes/event/page_locks_deleted.php
mod/wiki/classes/event/page_updated.php
mod/wiki/tests/events_test.php
mod/workshop/classes/event/assessable_uploaded.php
mod/workshop/classes/event/assessment_evaluated.php
mod/workshop/classes/event/assessment_evaluations_reset.php
mod/workshop/classes/event/assessment_reevaluated.php
mod/workshop/classes/event/assessments_reset.php
mod/workshop/classes/event/phase_switched.php
mod/workshop/classes/event/submission_assessed.php
mod/workshop/classes/event/submission_created.php
mod/workshop/classes/event/submission_reassessed.php
mod/workshop/classes/event/submission_updated.php
mod/workshop/classes/event/submission_viewed.php
mod/workshop/lang/en/workshop.php
mod/workshop/tests/events_test.php
notes/delete.php
notes/edit.php
notes/index.php
phpunit.xml.dist
question/category_class.php
question/classes/bank/action_column_base.php [new file with mode: 0644]
question/classes/bank/checkbox_column.php [new file with mode: 0644]
question/classes/bank/column_base.php [new file with mode: 0644]
question/classes/bank/copy_action_column.php [new file with mode: 0644]
question/classes/bank/creator_name_column.php [new file with mode: 0644]
question/classes/bank/delete_action_column.php [new file with mode: 0644]
question/classes/bank/edit_action_column.php [new file with mode: 0644]
question/classes/bank/modifier_name_column.php [new file with mode: 0644]
question/classes/bank/preview_action_column.php [new file with mode: 0644]
question/classes/bank/question_name_column.php [new file with mode: 0644]
question/classes/bank/question_text_row.php [new file with mode: 0644]
question/classes/bank/question_type_column.php [new file with mode: 0644]
question/classes/bank/row_base.php [new file with mode: 0644]
question/classes/bank/search/category_condition.php
question/classes/bank/view.php [new file with mode: 0644]
question/edit.php
question/editlib.php
question/engine/bank.php
question/engine/datalib.php
question/engine/lib.php
question/engine/questionattempt.php
question/engine/questionusage.php
question/engine/tests/helpers.php
question/export.php
question/export_form.php
question/format.php
question/format/blackboard_six/formatbase.php
question/format/gift/format.php
question/format/webct/format.php
question/format/xhtml/format.php
question/format/xml/format.php
question/import_form.php
question/previewlib.php
question/question.php
question/tests/behat/behat_question.php
question/tests/behat/question_categories.feature
question/type/calculated/datasetdefinitions_form.php
question/type/calculated/datasetitems_form.php
question/type/calculated/edit_calculated_form.php
question/type/calculated/question.php
question/type/calculated/questiontype.php
question/type/calculated/tests/formula_validation_test.php [new file with mode: 0644]
question/type/calculatedmulti/edit_calculatedmulti_form.php
question/type/calculatedmulti/questiontype.php
question/type/calculatedsimple/edit_calculatedsimple_form.php
question/type/calculatedsimple/questiontype.php
question/type/edit_question_form.php
question/type/essay/db/upgrade.php
question/type/match/question.php
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/questiontype.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/questiontype.php
question/type/questiontypebase.php
question/type/random/questiontype.php
question/type/randomsamatch/questiontype.php
question/type/shortanswer/edit_shortanswer_form.php
question/upgrade.txt
report/completion/classes/event/user_report_viewed.php
report/log/classes/renderer.php
report/log/classes/table_log.php
report/loglive/classes/renderer.php
report/loglive/classes/renderer_ajax.php
report/outline/classes/event/activity_report_viewed.php
report/outline/classes/event/report_viewed.php
report/questioninstances/classes/event/report_viewed.php
repository/boxnet/lib.php
repository/coursefiles/lib.php
repository/equella/callback.php
repository/equella/lib.php
repository/filepicker.js
repository/filepicker.php
repository/lib.php
repository/local/lib.php
repository/recent/lib.php
repository/repository_ajax.php
repository/upgrade.txt
repository/user/lib.php
theme/base/style/core.css
theme/base/style/dock.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/renderers/core_renderer.php
theme/bootstrapbase/style/moodle.css
theme/upgrade.txt
user/edit.php
user/editadvanced.php
user/externallib.php
user/lib.php
user/profile.php
user/tests/userlib_test.php
user/view.php
version.php

diff --git a/admin/cli/check_database_schema.php b/admin/cli/check_database_schema.php
new file mode 100644 (file)
index 0000000..3164a27
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+/**
+ * Validate that the current db structure matches the install.xml files.
+ *
+ * @package   core
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+$help = "Validate database structure
+
+Options:
+-h, --help            Print out this help.
+
+Example:
+\$ sudo -u www-data /usr/bin/php admin/cli/check_database_schema.php
+";
+
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'help' => false,
+    ),
+    array(
+        'h' => 'help',
+    )
+);
+
+if ($options['help']) {
+    echo $help;
+    exit(0);
+}
+
+if (empty($CFG->version)) {
+    echo "Database is not yet installed.\n";
+    exit(2);
+}
+
+$dbmanager = $DB->get_manager();
+$schema = $dbmanager->get_install_xml_schema();
+
+if (!$errors = $dbmanager->check_database_schema($schema)) {
+    echo "Database structure is ok.\n";
+    exit(0);
+}
+
+foreach ($errors as $table => $items) {
+    cli_separator();
+    echo "$table\n";
+    foreach ($items as $item) {
+        echo " * $item\n";
+    }
+}
+cli_separator();
+
+exit(1);
index df61269..9bb207a 100644 (file)
@@ -201,17 +201,9 @@ if (defined('COMPONENT_CLASSLOADER')) {
 require($CFG->dirroot.'/version.php');
 $CFG->target_release = $release;
 
-$_SESSION = array();
-$_SESSION['SESSION'] = new stdClass();
-$_SESSION['SESSION']->lang = $CFG->lang;
-$_SESSION['USER'] = new stdClass();
-$_SESSION['USER']->id = 0;
-$_SESSION['USER']->mnethostid = 1;
-
+\core\session\manager::init_empty_session();
 global $SESSION;
 global $USER;
-$SESSION = &$_SESSION['SESSION'];
-$USER    = &$_SESSION['USER'];
 
 global $COURSE;
 $COURSE = new stdClass();
index fb4e2c4..d6e544e 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
 
     // Allows the admin to configure mnet stuff
 
index 11cadee..b46c137 100644 (file)
@@ -74,8 +74,16 @@ abstract class core_role_capability_table_base {
     public function display() {
         if (count($this->capabilities) > self::NUM_CAPS_FOR_SEARCH) {
             global $PAGE;
-            $PAGE->requires->strings_for_js(array('filter', 'clear'), 'moodle');
-            $PAGE->requires->js_init_call('M.core_role.init_cap_table_filter', array($this->id, $this->context->id));
+            $jsmodule = array(
+                'name' => 'rolescapfilter',
+                'fullpath' => '/admin/roles/module.js',
+                'strings' => array(
+                    array('filter', 'moodle'),
+                    array('clear', 'moodle'),                ),
+                'requires' => array('node', 'cookie', 'escape')
+            );
+            $PAGE->requires->js_init_call('M.core_role.init_cap_table_filter', array($this->id, $this->context->id), false,
+                $jsmodule);
         }
         echo '<table class="' . implode(' ', $this->classes) . '" id="' . $this->id . '">' . "\n<thead>\n";
         echo '<tr><th class="name" align="left" scope="col">' . get_string('capability', 'core_role') . '</th>';
index 7326cf3..0cac01a 100644 (file)
@@ -51,7 +51,7 @@ M.core_role.init_cap_table_filter = function(Y, tableid, contextid) {
                 marginRight : 'auto'
             });
             // Create the capability search input.
-            this.input = Y.Node.create('<input type="text" id="'+this.table.get('id')+'capabilitysearch" value="'+filtervalue+'" />');
+            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>');
             // Create a clear button to clear the input.
index ab80e69..1583fab 100644 (file)
@@ -139,7 +139,6 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
         array(
             'context' => $context,
             'objectid' => $roleid,
-            'courseid' => $courseid
         )
     );
 
index 2a02183..91433c8 100644 (file)
@@ -80,7 +80,8 @@ class behat_admin extends behat_base {
             // Single element settings.
             try {
                 $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" .
-                    "[@id=//label[contains(normalize-space(.), $label)]/@for]";
+                    "[@id=//label[contains(normalize-space(.), $label)]/@for or " .
+                    "@id=//span[contains(normalize-space(.), $label)]/preceding-sibling::label[1]/@for]";
                 $fieldnode = $this->find('xpath', $fieldxpath, $exception);
 
                 $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" .
@@ -89,7 +90,7 @@ class behat_admin extends behat_base {
             } catch (ElementNotFoundException $e) {
 
                 // Multi element settings, interacting only the first one.
-                $fieldxpath = "//descendant::label[.= $label]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' form-item ')]" .
+                $fieldxpath = "//*[label[.= $label]|span[.= $label]]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' form-item ')]" .
                     "/descendant::div[@class='form-group']/descendant::*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]";
                 $fieldnode = $this->find('xpath', $fieldxpath);
 
diff --git a/admin/tests/behat/set_admin_settings_value.feature b/admin/tests/behat/set_admin_settings_value.feature
new file mode 100644 (file)
index 0000000..4edbb5c
--- /dev/null
@@ -0,0 +1,27 @@
+@core @core_admin
+Feature: Set admin settings value
+  In order to set admin settings value
+  As an admin
+  I need to set admin setting value and verify it is applied
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course fullname | C_shortname | 0 |
+    And I log in as "admin"
+    And I should see "Course fullname"
+    And I should not see "C_shortname Course fullname"
+
+  Scenario: set admin value with full name
+    Given I set the following administration settings values:
+      | Display extended course names | 1 |
+    When I press "Save changes"
+    And I am on homepage
+    Then I should see "C_shortname Course fullname"
+
+  Scenario: set admin value with short name
+    Given I set the following administration settings values:
+      | courselistshortnames | 1 |
+    When I press "Save changes"
+    And I am on homepage
+    Then I should see "C_shortname Course fullname"
\ No newline at end of file
index a55199a..f162e05 100644 (file)
@@ -28,20 +28,20 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['acknowledgement'] = 'Acknowledgement';
 $string['acknowledgementmust'] = 'You must acknowledge this';
-$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing add-ons. I accept and understand that add-ons (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
-$string['featuredisabled'] = 'The add-on installer is disabled on this site.';
-$string['installaddon'] = 'Install add-on!';
-$string['installaddons'] = 'Install add-ons';
-$string['installexception'] = 'Oops... An error occurred while trying to install the add-on. Turn debugging mode on to see details of the error.';
-$string['installfromrepo'] = 'Install add-ons from the Moodle plugins directory';
-$string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install an add-on. Note that your site full name, URL and Moodle version will be sent as well, to make the installation process easier for you.';
-$string['installfromzip'] = 'Install add-on from ZIP file';
-$string['installfromzip_help'] = 'An alternative to installing an add-on directly from the Moodle plugins directory is to upload a ZIP package of the add-on. The ZIP package should have the same structure as a package downloaded from the Moodle plugins directory.';
+$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing additional plugins. I accept and understand that plugins (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.';
+$string['featuredisabled'] = 'The plugin installer is disabled on this site.';
+$string['installaddon'] = 'Install plugin!';
+$string['installaddons'] = 'Install plugins';
+$string['installexception'] = 'Oops... An error occurred while trying to install the plugin. Turn debugging mode on to see details of the error.';
+$string['installfromrepo'] = 'Install plugins from the Moodle plugins directory';
+$string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install a plugin. Note that your site full name, URL and Moodle version will be sent as well, to make the installation process easier for you.';
+$string['installfromzip'] = 'Install plugin from ZIP file';
+$string['installfromzip_help'] = 'An alternative to installing a plugin directly from the Moodle plugins directory is to upload a ZIP package of the plugin. The ZIP package should have the same structure as a package downloaded from the Moodle plugins directory.';
 $string['installfromzipfile'] = 'ZIP package';
 $string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory, named to match the plugin. The ZIP will be extracted into an appropriate location for the plugin type. If the package has been downloaded from the Moodle plugins directory then it will have this structure.';
 $string['installfromziprootdir'] = 'Rename the root directory';
 $string['installfromziprootdir_help'] = 'Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. If so, the correct name may be entered here.';
-$string['installfromzipsubmit'] = 'Install add-on from the ZIP file';
+$string['installfromzipsubmit'] = 'Install plugin from the ZIP file';
 $string['installfromziptype'] = 'Plugin type';
 $string['installfromziptype_help'] = 'Choose the correct type of plugin you are about to install. Warning: The installation procedure can fail badly if an incorrect plugin type is specified.';
 $string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process.';
@@ -49,54 +49,54 @@ $string['permcheckerror'] = 'Error while checking for write permission';
 $string['permcheckprogress'] = 'Checking for write permission ...';
 $string['permcheckresultno'] = 'Plugin type location <em>{$a->path}</em> is not writable';
 $string['permcheckresultyes'] = 'Plugin type location <em>{$a->path}</em> is writable';
-$string['pluginname'] = 'Add-on installer';
-$string['remoterequestalreadyinstalled'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. However, this plugin is <strong>already installed</strong> on the site.';
-$string['remoterequestconfirm'] = 'There is a request to install add-on <strong>{$a->name}</strong> ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. If you continue, the add-on ZIP package will be downloaded for validation. Nothing will be installed yet.';
-$string['remoterequestinvalid'] = 'There is a request to install an add-on from the Moodle plugins directory on this site. Unfortunately the request is not valid and so the add-on cannot be installed.';
-$string['remoterequestpermcheck'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. However, the plugin type location <strong>{$a->typepath}</strong> is <strong>not writable</strong>. You need to give write access for the web server user to the plugin type location, then press the continue button to repeat the check.';
-$string['remoterequestpluginfoexception'] = 'Oops... An error occurred while trying to obtain information about the add-on {$a->name} ({$a->component}) version {$a->version}. The add-on cannot be installed. Turn debugging mode on to see details of the error.';
-$string['validation'] = 'Add-on package validation';
+$string['pluginname'] = 'Plugin installer';
+$string['remoterequestalreadyinstalled'] = 'There is a request to install plugin {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. However, this plugin is <strong>already installed</strong> on the site.';
+$string['remoterequestconfirm'] = 'There is a request to install plugin <strong>{$a->name}</strong> ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. If you continue, the plugin ZIP package will be downloaded for validation. Nothing will be installed yet.';
+$string['remoterequestinvalid'] = 'There is a request to install a plugin from the Moodle plugins directory on this site. Unfortunately the request is not valid and so the plugin cannot be installed.';
+$string['remoterequestpermcheck'] = 'There is a request to install plugin {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. However, the location <strong>{$a->typepath}</strong> is <strong>not writable</strong>. You need to give write access for the web server user to the location, then press the continue button to repeat the check.';
+$string['remoterequestpluginfoexception'] = 'Oops... An error occurred while trying to obtain information about the plugin {$a->name} ({$a->component}) version {$a->version}. The plugin cannot be installed. Turn debugging mode on to see details of the error.';
+$string['validation'] = 'Plugin package validation';
 $string['validationmsg_componentmatch'] = 'Full component name';
-$string['validationmsg_componentmismatchname'] = 'Add-on name mismatch';
-$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the declared add-on name.';
-$string['validationmsg_componentmismatchname_info'] = 'The add-on declares its name is \'{$a}\' but that does not match the name of the root directory.';
-$string['validationmsg_componentmismatchtype'] = 'Add-on type mismatch';
-$string['validationmsg_componentmismatchtype_info'] = 'You have selected the type \'{$a->expected}\' but the add-on declares its type is \'{$a->found}\'.';
+$string['validationmsg_componentmismatchname'] = 'Plugin name mismatch';
+$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the declared plugin name.';
+$string['validationmsg_componentmismatchname_info'] = 'The plugin declares its name is \'{$a}\' but that does not match the name of the root directory.';
+$string['validationmsg_componentmismatchtype'] = 'Plugin type mismatch';
+$string['validationmsg_componentmismatchtype_info'] = 'You have selected the type \'{$a->expected}\' but the plugin declares its type is \'{$a->found}\'.';
 $string['validationmsg_filenotexists'] = 'Extracted file not found';
 $string['validationmsg_filesnumber'] = 'Not enough files found in the package';
 $string['validationmsg_filestatus'] = 'Unable to extract all files';
 $string['validationmsg_filestatus_info'] = 'Attempting to extract file {$a->file} resulted in error \'{$a->status}\'.';
 $string['validationmsg_maturity'] = 'Declared maturity level';
-$string['validationmsg_maturity_help'] = 'The add-on can declare its maturity level. If the maintainer considers the add-on stable, the declared maturity level will read MATURITY_STABLE. All other maturity levels (such as alpha or beta) should be considered unstable and a warning is raised.';
+$string['validationmsg_maturity_help'] = 'The plugin can declare its maturity level. If the maintainer considers the plugin stable, the declared maturity level will read MATURITY_STABLE. All other maturity levels (such as alpha or beta) should be considered unstable and a warning is raised.';
 $string['validationmsg_missingexpectedlangenfile'] = 'English language file name mismatch';
-$string['validationmsg_missingexpectedlangenfile_info'] = 'The given add-on type is missing the expected English language file {$a}.';
+$string['validationmsg_missingexpectedlangenfile_info'] = 'The given plugin type is missing the expected English language file {$a}.';
 $string['validationmsg_missinglangenfile'] = 'No English language file found';
 $string['validationmsg_missinglangenfolder'] = 'Missing English language folder';
-$string['validationmsg_missingversion'] = 'Add-on does not declare its version';
+$string['validationmsg_missingversion'] = 'Plugin does not declare its version';
 $string['validationmsg_missingversionphp'] = 'File version.php not found';
 $string['validationmsg_multiplelangenfiles'] = 'Multiple English language files found';
 $string['validationmsg_onedir'] = 'Invalid structure of the ZIP package.';
-$string['validationmsg_onedir_help'] = 'The ZIP package must contain just one root directory that holds the add-on code. The name of that root directory must match the name of the plugin.';
+$string['validationmsg_onedir_help'] = 'The ZIP package must contain just one root directory that holds the plugin code. The name of that root directory must match the name of the plugin.';
 $string['validationmsg_pathwritable'] = 'Write access check';
-$string['validationmsg_pluginversion'] = 'Add-on version';
-$string['validationmsg_release'] = 'Add-on release';
+$string['validationmsg_pluginversion'] = 'Plugin version';
+$string['validationmsg_release'] = 'Plugin release';
 $string['validationmsg_requiresmoodle'] = 'Required Moodle version';
-$string['validationmsg_rootdir'] = 'Name of the add-on to be installed';
-$string['validationmsg_rootdir_help'] = 'The name of the root directory in the ZIP package forms the name of the add-on to be installed. If the name is not correct, you may wish to rename the root directory in the ZIP prior to installing the add-on.';
-$string['validationmsg_rootdirinvalid'] = 'Invalid add-on name';
-$string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory in the ZIP package violates formal syntax requirements. Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the add-on name.';
+$string['validationmsg_rootdir'] = 'Name of the plugin to be installed';
+$string['validationmsg_rootdir_help'] = 'The name of the root directory in the ZIP package forms the name of the plugin to be installed. If the name is not correct, you may wish to rename the root directory in the ZIP prior to installing the plugin.';
+$string['validationmsg_rootdirinvalid'] = 'Invalid plugin name';
+$string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory in the ZIP package violates formal syntax requirements. Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the plugin name.';
 $string['validationmsg_targetexists'] = 'Target location already exists';
-$string['validationmsg_targetexists_help'] = 'The directory that the add-on is to be installed to, must not exist yet.';
+$string['validationmsg_targetexists_help'] = 'The directory that the plugin is to be installed to must not yet exist.';
 $string['validationmsg_unknowntype'] = 'Unknown plugin type';
 $string['validationmsglevel_debug'] = 'Debug';
 $string['validationmsglevel_error'] = 'Error';
 $string['validationmsglevel_info'] = 'OK';
 $string['validationmsglevel_warning'] = 'Warning';
 $string['validationresult0'] = 'Validation failed!';
-$string['validationresult0_help'] = 'A serious problem was detected and so it is not safe to install the add-on. See the validation log messages for details.';
+$string['validationresult0_help'] = 'A serious problem was detected and so it is not safe to install the plugin. See the validation log messages for details.';
 $string['validationresult1'] = 'Validation passed!';
-$string['validationresult1_help'] = 'No serious problems were detected. You can continue with the add-on installation. See the validation log messages for more details and eventual warnings.';
-$string['validationresult1_help'] = 'The add-on package has been validated and no serious problems were detected.';
+$string['validationresult2_help'] = 'No serious problems were detected. You can continue with the plugin installation. See the validation log messages for more details and eventual warnings.';
+$string['validationresult1_help'] = 'The plugin package has been validated and no serious problems were detected.';
 $string['validationresultinfo'] = 'Info';
 $string['validationresultmsg'] = 'Message';
 $string['validationresultstatus'] = 'Status';
index be588be..96cfd0c 100644 (file)
@@ -72,7 +72,7 @@ trait buffered_writer {
         $entry['other'] = serialize($entry['other']);
         $entry['origin'] = $PAGE->requestorigin;
         $entry['ip'] = $PAGE->requestip;
-        $entry['realuserid'] = \core\session\manager::is_loggedinas() ? $_SESSION['USER']->realuser : null;
+        $entry['realuserid'] = \core\session\manager::is_loggedinas() ? $GLOBALS['USER']->realuser : null;
 
         $this->buffer[] = $entry;
         $this->count++;
index 48e1b39..6157cac 100644 (file)
@@ -130,8 +130,7 @@ class logstore_database_store_testcase extends advanced_testcase {
             array('context' => context_module::instance($module2->cmid), 'other' => array('sample' => 6, 'xx' => 9)));
         $event2->trigger();
 
-        $_SESSION['SESSION'] = new \stdClass();
-        $this->setUser(0);
+        \core\session\manager::init_empty_session();
         $this->assertFalse(\core\session\manager::is_loggedinas());
 
         $logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
index 5d25a2e..08d70d4 100644 (file)
@@ -95,8 +95,7 @@ class logstore_standard_store_testcase extends advanced_testcase {
         $event2->trigger();
         logstore_standard_restore::hack_executing(0);
 
-        $_SESSION['SESSION'] = new \stdClass();
-        $this->setUser(0);
+        \core\session\manager::init_empty_session();
         $this->assertFalse(\core\session\manager::is_loggedinas());
 
         $logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
index 768d4bc..4b6f344 100644 (file)
@@ -104,11 +104,11 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
 
 } else {
     echo $OUTPUT->header();
-    $error = optional_param('error', '', PARAM_RAW);
+    $error = optional_param('error', '', PARAM_NOTAGS);
     if ($error) {
         echo $OUTPUT->notification($error, 'notifyerror');
     }
-    $success = optional_param('success', '', PARAM_RAW);
+    $success = optional_param('success', '', PARAM_NOTAGS);
     if ($success) {
         echo $OUTPUT->notification($success, 'notifysuccess');
     }
index a45cb47..b50156f 100644 (file)
@@ -669,7 +669,7 @@ if ($formdata = $mform2->is_cancelled()) {
 
             if ($doupdate or $existinguser->password !== $oldpw) {
                 // We want only users that were really updated.
-                user_update_user($existinguser, false);
+                user_update_user($existinguser, false, false);
 
                 $upt->track('status', $struserupdated);
                 $usersupdated++;
@@ -687,6 +687,9 @@ if ($formdata = $mform2->is_cancelled()) {
                     }
                 }
 
+                // Trigger event.
+                \core\event\user_updated::create_from_userid($existinguser->id)->trigger();
+
             } else {
                 // no user information changed
                 $upt->track('status', $struseruptodate);
@@ -800,7 +803,7 @@ if ($formdata = $mform2->is_cancelled()) {
                 $upt->track('password', '-', 'normal', false);
             }
 
-            $user->id = user_create_user($user, false);
+            $user->id = user_create_user($user, false, false);
             $upt->track('username', html_writer::link(new moodle_url('/user/profile.php', array('id'=>$user->id)), s($user->username)), 'normal', false);
 
             // pre-process custom profile menu fields data from csv file
@@ -815,6 +818,9 @@ if ($formdata = $mform2->is_cancelled()) {
                 set_user_preference('create_password', 1, $user);
             }
 
+            // Trigger event.
+            \core\event\user_created::create_from_userid($user->id)->trigger();
+
             $upt->track('status', $struseradded);
             $upt->track('id', $user->id, 'normal', false);
             $usersnew++;
index cfbbe22..a7e1e64 100644 (file)
     $ufiltering->display_add();
     $ufiltering->display_active();
 
-    if (has_capability('moodle/user:create', $sitecontext)) {
-        echo $OUTPUT->heading('<a href="'.$securewwwroot.'/user/editadvanced.php?id=-1">'.get_string('addnewuser').'</a>');
-    }
     if (!empty($table)) {
         echo html_writer::start_tag('div', array('class'=>'no-overflow'));
         echo html_writer::table($table);
         echo html_writer::end_tag('div');
         echo $OUTPUT->paging_bar($usercount, $page, $perpage, $baseurl);
-        if (has_capability('moodle/user:create', $sitecontext)) {
-            echo $OUTPUT->heading('<a href="'.$securewwwroot.'/user/editadvanced.php?id=-1">'.get_string('addnewuser').'</a>');
-        }
+    }
+    if (has_capability('moodle/user:create', $sitecontext)) {
+        $url = new moodle_url($securewwwroot . '/user/editadvanced.php', array('id' => -1));
+        echo $OUTPUT->single_button($url, get_string('addnewuser'), 'get');
     }
 
     echo $OUTPUT->footer();
-
-
-
index ff06a3a..9d83dd1 100644 (file)
@@ -111,6 +111,26 @@ class auth_plugin_cas extends auth_plugin_ldap {
             return;
         }
 
+        // If the multi-authentication setting is used, check for the param before connecting to CAS.
+        if ($this->config->multiauth) {
+            $authCAS = optional_param('authCAS', '', PARAM_RAW);
+            if ($authCAS == 'NOCAS') {
+                return;
+            }
+            // Show authentication form for multi-authentication.
+            // Test pgtIou parameter for proxy mode (https connection in background from CAS server to the php server).
+            if ($authCAS != 'CAS' && !isset($_GET['pgtIou'])) {
+                $PAGE->set_url('/login/index.php');
+                $PAGE->navbar->add($CASform);
+                $PAGE->set_title("$site->fullname: $CASform");
+                $PAGE->set_heading($site->fullname);
+                echo $OUTPUT->header();
+                include($CFG->dirroot.'/auth/cas/cas_form.html');
+                echo $OUTPUT->footer();
+                exit();
+            }
+        }
+
         // Connection to CAS server
         $this->connectCAS();
 
@@ -134,27 +154,6 @@ class auth_plugin_cas extends auth_plugin_ldap {
             return;
         }
 
-        if ($this->config->multiauth) {
-            $authCAS = optional_param('authCAS', '', PARAM_RAW);
-            if ($authCAS == 'NOCAS') {
-                return;
-            }
-
-            // Show authentication form for multi-authentication
-            // test pgtIou parameter for proxy mode (https connection
-            // in background from CAS server to the php server)
-            if ($authCAS != 'CAS' && !isset($_GET['pgtIou'])) {
-                $PAGE->set_url('/login/index.php');
-                $PAGE->navbar->add($CASform);
-                $PAGE->set_title("$site->fullname: $CASform");
-                $PAGE->set_heading($site->fullname);
-                echo $OUTPUT->header();
-                include($CFG->dirroot.'/auth/cas/cas_form.html');
-                echo $OUTPUT->footer();
-                exit();
-            }
-        }
-
         // Force CAS authentication (if needed).
         if (!phpCAS::isAuthenticated()) {
             phpCAS::setLang($this->config->language);
@@ -162,19 +161,6 @@ class auth_plugin_cas extends auth_plugin_ldap {
         }
     }
 
-    /**
-     * Logout from the CAS
-     *
-     */
-    function prelogout_hook() {
-        global $CFG;
-
-        if (!empty($this->config->logoutcas)) {
-            $backurl = $CFG->wwwroot;
-            $this->connectCAS();
-            phpCAS::logoutWithURL($backurl);
-        }
-    }
 
     /**
      * Connect to the CAS (clientcas connection or proxycas connection)
@@ -510,4 +496,22 @@ class auth_plugin_cas extends auth_plugin_ldap {
             }
         }
     }
+
+    /**
+     * Post logout hook.
+     *
+     * Note: this method replace the prelogout_hook method to avoid redirect to CAS logout
+     * before the event userlogout being triggered.
+     *
+     * @param stdClass $user clone of USER object object before the user session was terminated
+     */
+    public function postlogout_hook($user) {
+        global $CFG;
+        // Only redirect to CAS logout if the user is logged as a CAS user.
+        if (!empty($this->config->logoutcas) && $user->auth == $this->authtype) {
+            $backurl = $CFG->wwwroot;
+            $this->connectCAS();
+            phpCAS::logoutWithRedirectService($backurl);
+        }
+    }
 }
index b071e32..2c36eab 100644 (file)
@@ -94,11 +94,14 @@ class auth_plugin_email extends auth_plugin_base {
             $user->calendartype = $CFG->calendartype;
         }
 
-        $user->id = user_create_user($user, false);
+        $user->id = user_create_user($user, false, false);
 
         // Save any custom profile field information.
         profile_save_data($user);
 
+        // Trigger event.
+        \core\event\user_created::create_from_userid($user->id)->trigger();
+
         if (! send_confirmation_email($user)) {
             print_error('auth_emailnoemail','auth_email');
         }
index 9004e26..cc1374e 100644 (file)
@@ -551,7 +551,7 @@ class auth_plugin_ldap extends auth_plugin_base {
             print_error('auth_ldap_create_error', 'auth_ldap');
         }
 
-        $user->id = user_create_user($user, false);
+        $user->id = user_create_user($user, false, false);
 
         // Save any custom profile field information
         profile_save_data($user);
@@ -564,6 +564,8 @@ class auth_plugin_ldap extends auth_plugin_base {
 
         $user = $DB->get_record('user', array('id'=>$user->id));
 
+        \core\event\user_created::create_from_userid($user->id)->trigger();
+
         if (! send_confirmation_email($user)) {
             print_error('noemail', 'auth_ldap');
         }
@@ -1027,7 +1029,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                         }
                     }
                 }
-                user_update_user($newuser, false);
+                user_update_user($newuser, false, false);
             }
         } else {
             return false;
index 13cdcc0..7f0855e 100644 (file)
@@ -279,6 +279,143 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($expectedlog, $event);
     }
 
+    /**
+     * Test logging in via LDAP calls a user_loggedin event.
+     */
+    public function test_ldap_user_signup() {
+        global $CFG, $DB;
+
+        // User to create.
+        $user = array(
+            'username' => 'usersignuptest1',
+            'password' => 'Moodle2014!',
+            'idnumber' => 'idsignuptest1',
+            'firstname' => 'First Name User Test 1',
+            'lastname' => 'Last Name User Test 1',
+            'middlename' => 'Middle Name User Test 1',
+            'lastnamephonetic' => '最後のお名前のテスト一号',
+            'firstnamephonetic' => 'お名前のテスト一号',
+            'alternatename' => 'Alternate Name User Test 1',
+            'email' => 'usersignuptest1@email.com',
+            'description' => 'This is a description for user 1',
+            'city' => 'Perth',
+            'country' => 'au',
+            'mnethostid' => $CFG->mnet_localhost_id,
+            'auth' => 'ldap'
+            );
+
+        if (!extension_loaded('ldap')) {
+            $this->markTestSkipped('LDAP extension is not loaded.');
+        }
+
+        $this->resetAfterTest();
+
+        require_once($CFG->dirroot.'/auth/ldap/auth.php');
+        require_once($CFG->libdir.'/ldaplib.php');
+
+        if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) {
+            $this->markTestSkipped('External LDAP test server not configured.');
+        }
+
+        // Make sure we can connect the server.
+        $debuginfo = '';
+        if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
+            $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo);
+        }
+
+        $this->enable_plugin();
+
+        // Create new empty test container.
+        $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN;
+
+        $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
+
+        $o = array();
+        $o['objectClass'] = array('dcObject', 'organizationalUnit');
+        $o['dc']         = 'moodletest';
+        $o['ou']         = 'MOODLETEST';
+        if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) {
+            $this->markTestSkipped('Can not create test LDAP container.');
+        }
+
+        // Create a few users.
+        $o = array();
+        $o['objectClass'] = array('organizationalUnit');
+        $o['ou']          = 'users';
+        ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
+
+        // Configure the plugin a bit.
+        set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth/ldap');
+        set_config('start_tls', 0, 'auth/ldap');
+        set_config('ldap_version', 3, 'auth/ldap');
+        set_config('ldapencoding', 'utf-8', 'auth/ldap');
+        set_config('pagesize', '2', 'auth/ldap');
+        set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth/ldap');
+        set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth/ldap');
+        set_config('user_type', 'rfc2307', 'auth/ldap');
+        set_config('contexts', 'ou=users,'.$topdn, 'auth/ldap');
+        set_config('search_sub', 0, 'auth/ldap');
+        set_config('opt_deref', LDAP_DEREF_NEVER, 'auth/ldap');
+        set_config('user_attribute', 'cn', 'auth/ldap');
+        set_config('memberattribute', 'memberuid', 'auth/ldap');
+        set_config('memberattribute_isdn', 0, 'auth/ldap');
+        set_config('creators', 'cn=creators,'.$topdn, 'auth/ldap');
+        set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth/ldap');
+
+        set_config('field_map_email', 'mail', 'auth/ldap');
+        set_config('field_updatelocal_email', 'oncreate', 'auth/ldap');
+        set_config('field_updateremote_email', '0', 'auth/ldap');
+        set_config('field_lock_email', 'unlocked', 'auth/ldap');
+
+        set_config('field_map_firstname', 'givenName', 'auth/ldap');
+        set_config('field_updatelocal_firstname', 'oncreate', 'auth/ldap');
+        set_config('field_updateremote_firstname', '0', 'auth/ldap');
+        set_config('field_lock_firstname', 'unlocked', 'auth/ldap');
+
+        set_config('field_map_lastname', 'sn', 'auth/ldap');
+        set_config('field_updatelocal_lastname', 'oncreate', 'auth/ldap');
+        set_config('field_updateremote_lastname', '0', 'auth/ldap');
+        set_config('field_lock_lastname', 'unlocked', 'auth/ldap');
+        set_config('passtype', 'md5', 'auth/ldap');
+        set_config('create_context', 'ou=users,'.$topdn, 'auth/ldap');
+
+        $this->assertEquals(2, $DB->count_records('user'));
+        $this->assertEquals(0, $DB->count_records('role_assignments'));
+
+        /** @var auth_plugin_ldap $auth */
+        $auth = get_auth_plugin('ldap');
+
+        $sink = $this->redirectEvents();
+        $auth->user_signup((object)$user, false);
+        $this->assertDebuggingCalled('Not sending email due to $CFG->noemailever config setting');
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Verify 2 events get generated.
+        $this->assertCount(2, $events);
+
+        // Get record from db.
+        $dbuser = $DB->get_record('user', array('username' => $user['username']));
+        $user['id'] = $dbuser->id;
+
+        // Last event is user_created.
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\user_created', $event);
+        $this->assertEquals($user['id'], $event->objectid);
+        $this->assertEquals('user_created', $event->get_legacy_eventname());
+        $this->assertEquals(context_user::instance($user['id']), $event->get_context());
+        $expectedlogdata = array(SITEID, 'user', 'add', '/view.php?id='.$event->objectid, fullname($dbuser));
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+
+        // First event is user_password_updated.
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\user_password_updated', $event);
+        $this->assertEventContextNotUsed($event);
+
+        // Delete user which we just created.
+        ldap_delete($connection, 'cn='.$user['username'].',ou=users,'.$topdn);
+    }
+
     protected function create_ldap_user($connection, $topdn, $i) {
         $o = array();
         $o['objectClass']   = array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount');
index a343c85..849dad2 100644 (file)
@@ -203,7 +203,7 @@ can directly edit the object $result.
 Example file:
 
 --
-<?PHP
+<?php
 
     // Set the zip code and the adress
     if ($_SERVER[$this->config->field_map_address] != '')
index f521442..ef38337 100644 (file)
     if (!empty($_SERVER[$pluginconfig->user_attribute])) {    // Shibboleth auto-login
         $frm = new stdClass();
         $frm->username = strtolower($_SERVER[$pluginconfig->user_attribute]);
-        $frm->password = substr(base64_encode($_SERVER[$pluginconfig->user_attribute]),0,8);
-        // The random password consists of the first 8 letters of the base 64 encoded user ID
-        // This password is never used unless the user account is converted to manual
+        // The password is never actually used, but needs to be passed to the functions 'user_login' and
+        // 'authenticate_user_login'. Shibboleth returns true for the function 'prevent_local_password', which is
+        // used when setting the password in 'update_internal_user_password'. When 'prevent_local_password'
+        // returns true, the password is set to 'not cached' (AUTH_PASSWORD_NOT_CACHED) in the Moodle DB. However,
+        // rather than setting the password to a hard-coded value, we will generate one each time, in case there are
+        // changes to the Shibboleth plugin and it is actually used.
+        $frm->password = generate_password(8);
 
     /// Check if the user has actually submitted login data to us
 
index a6cfb55..41ff9d3 100644 (file)
@@ -547,7 +547,8 @@ abstract class backup_controller_dbops extends backup_dbops {
                 self::apply_general_config_defaults($controller);
                 break;
             case backup::MODE_AUTOMATED:
-                // TODO: Move the loading from automatic stuff to here
+                // Load the automated defaults.
+                self::apply_auto_config_defaults($controller);
                 break;
             default:
                 // Nothing to do for other modes (IMPORT/HUB...). Some day we
@@ -555,6 +556,43 @@ abstract class backup_controller_dbops extends backup_dbops {
         }
     }
 
+    /**
+     * Sets the controller settings default values from the automated backup config.
+     *
+     * @param backup_controller $controller
+     */
+    private static function apply_auto_config_defaults(backup_controller $controller) {
+        $settings = array(
+            // Config name                   => Setting name.
+            'backup_auto_users'              => 'users',
+            'backup_auto_role_assignments'   => 'role_assignments',
+            'backup_auto_activities'         => 'activities',
+            'backup_auto_blocks'             => 'blocks',
+            'backup_auto_filters'            => 'filters',
+            'backup_auto_comments'           => 'comments',
+            'backup_auto_badges'             => 'badges',
+            'backup_auto_userscompletion'    => 'userscompletion',
+            'backup_auto_logs'               => 'logs',
+            'backup_auto_histories'          => 'grade_histories',
+            'backup_auto_questionbank'       => 'questionbank'
+        );
+        $plan = $controller->get_plan();
+        foreach ($settings as $config => $settingname) {
+            $value = get_config('backup', $config);
+            if ($value === false) {
+                // The setting is not set.
+                $controller->log('Could not find a value for the config ' . $config, BACKUP::LOG_DEBUG);
+                continue;
+            }
+            if ($plan->setting_exists($settingname)) {
+                $setting = $plan->get_setting($settingname);
+                $setting->set_value($value);
+            } else {
+                $controller->log('Unknown setting: ' . $settingname, BACKUP::LOG_DEBUG);
+            }
+        }
+    }
+
     /**
      * Sets the controller settings default values from the backup config.
      *
@@ -583,6 +621,7 @@ abstract class backup_controller_dbops extends backup_dbops {
                 // Ignore this because the config has not been set. get_config
                 // returns false if a setting doesn't exist, '0' is returned when
                 // the configuration is set to false.
+                $controller->log('Could not find a value for the config ' . $config, BACKUP::LOG_DEBUG);
                 continue;
             }
             $locked = (get_config('backup', $config.'_locked') == true);
@@ -594,6 +633,8 @@ abstract class backup_controller_dbops extends backup_dbops {
                         $setting->set_status(base_setting::LOCKED_BY_CONFIG);
                     }
                 }
+            } else {
+                $controller->log('Unknown setting: ' . $setting, BACKUP::LOG_DEBUG);
             }
         }
     }
index 0d826fb..bd00c32 100644 (file)
@@ -1019,7 +1019,7 @@ abstract class restore_dbops {
                         // Even if a file has been deleted since the backup was made, the file metadata will remain in the
                         // files table, and the file will not be moved to the trashdir.
                         // Files are not cleared from the files table by cron until several days after deletion.
-                        if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash))) {
+                        if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash), '', '*', 0, 1)) {
                             // Only grab one of the foundfiles - the file content should be the same for all entries.
                             $foundfile = reset($foundfiles);
                             $fs->create_file_from_storedfile($file_record, $foundfile->id);
index e57dc7e..219b530 100644 (file)
@@ -47,7 +47,7 @@ class backup_dbops_testcase extends advanced_testcase {
         $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3));
         $coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid));
 
-        $this->moduleid  = $coursemodule->id;
+        $this->moduleid  = $page->cmid;
         $this->sectionid = $DB->get_field("course_sections", 'id', array("section"=>$coursemodule->section, "course"=>$course->id));
         $this->courseid  = $coursemodule->course;
         $this->userid = 2; // admin
@@ -180,7 +180,7 @@ class backup_dbops_testcase extends advanced_testcase {
         $this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 0);
 
         // A MODE_SAMESITE controller - should not include files
-        $bc = new mock_backup_controller4dbops(backup::TYPE_1COURSE, $this->moduleid, backup::FORMAT_MOODLE,
+        $bc = new mock_backup_controller4dbops(backup::TYPE_1COURSE, $this->courseid, backup::FORMAT_MOODLE,
             backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $this->userid);
         $this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 0);
     }
index b964b67..434059b 100644 (file)
@@ -381,27 +381,6 @@ abstract class backup_cron_automated_helper {
 
         try {
 
-            $settings = array(
-                'users' => 'backup_auto_users',
-                'role_assignments' => 'backup_auto_role_assignments',
-                'activities' => 'backup_auto_activities',
-                'blocks' => 'backup_auto_blocks',
-                'filters' => 'backup_auto_filters',
-                'comments' => 'backup_auto_comments',
-                'badges' => 'backup_auto_badges',
-                'completion_information' => 'backup_auto_userscompletion',
-                'logs' => 'backup_auto_logs',
-                'histories' => 'backup_auto_histories',
-                'questionbank' => 'backup_auto_questionbank'
-            );
-            foreach ($settings as $setting => $configsetting) {
-                if ($bc->get_plan()->setting_exists($setting)) {
-                    if (isset($config->{$configsetting})) {
-                        $bc->get_plan()->get_setting($setting)->set_value($config->{$configsetting});
-                    }
-                }
-            }
-
             // Set the default filename.
             $format = $bc->get_format();
             $type = $bc->get_type();
index 6f5b5b5..acfb7b3 100644 (file)
@@ -167,18 +167,21 @@ class restore_plan extends base_plan implements loggable {
         parent::execute();
         $this->controller->set_status(backup::STATUS_FINISHED_OK);
 
-        // Trigger a course restored event.
-        $event = \core\event\course_restored::create(array(
-            'objectid' => $this->get_courseid(),
-            'userid' => $this->get_userid(),
-            'context' => context_course::instance($this->get_courseid()),
-            'other' => array('type' => $this->controller->get_type(),
-                             'target' => $this->controller->get_target(),
-                             'mode' => $this->controller->get_mode(),
-                             'operation' => $this->controller->get_operation(),
-                             'samesite' => $this->controller->is_samesite())
-        ));
-        $event->trigger();
+        // Check if we are restoring a course.
+        if ($this->controller->get_type() === backup::TYPE_1COURSE) {
+            // Trigger a course restored event.
+            $event = \core\event\course_restored::create(array(
+                'objectid' => $this->get_courseid(),
+                'userid' => $this->get_userid(),
+                'context' => context_course::instance($this->get_courseid()),
+                'other' => array('type' => $this->controller->get_type(),
+                                 'target' => $this->controller->get_target(),
+                                 'mode' => $this->controller->get_mode(),
+                                 'operation' => $this->controller->get_operation(),
+                                 'samesite' => $this->controller->is_samesite())
+            ));
+            $event->trigger();
+        }
     }
 
     /**
index c8870cc..93a51a7 100644 (file)
@@ -35,8 +35,22 @@ require_once($CFG->dirroot . '/backup/util/xml/output/memory_xml_output.class.ph
  */
 class backup_structure_testcase extends advanced_testcase {
 
-    protected $forumid;   // To store the inserted forum->id
-    protected $contextid; // Official contextid for these tests
+    /** @var int Store the inserted forum->id for use in test functions */
+    protected $forumid;
+    /** @var int Store the inserted discussion1->id for use in test functions */
+    protected $discussionid1;
+    /** @var int Store the inserted discussion2->id for use in test functions */
+    protected $discussionid2;
+    /** @var int Store the inserted post1->id for use in test functions */
+    protected $postid1;
+    /** @var int Store the inserted post2->id for use in test functions */
+    protected $postid2;
+    /** @var int Store the inserted post3->id for use in test functions */
+    protected $postid3;
+    /** @var int Store the inserted post4->id for use in test functions */
+    protected $postid4;
+    /** @var int Official contextid for these tests */
+    protected $contextid;
 
 
     protected function setUp() {
@@ -72,30 +86,30 @@ class backup_structure_testcase extends advanced_testcase {
 
         // Create two discussions
         $discussion1 = (object)array('course' => 1, 'forum' => $this->forumid, 'name' => 'd1', 'userid' => 100, 'groupid' => 200);
-        $d1id = $DB->insert_record('forum_discussions', $discussion1);
+        $this->discussionid1 = $DB->insert_record('forum_discussions', $discussion1);
         $discussion2 = (object)array('course' => 1, 'forum' => $this->forumid, 'name' => 'd2', 'userid' => 101, 'groupid' => 201);
-        $d2id = $DB->insert_record('forum_discussions', $discussion2);
+        $this->discussionid2 = $DB->insert_record('forum_discussions', $discussion2);
 
         // Create four posts
-        $post1 = (object)array('discussion' => $d1id, 'userid' => 100, 'subject' => 'p1', 'message' => 'm1');
-        $p1id = $DB->insert_record('forum_posts', $post1);
-        $post2 = (object)array('discussion' => $d1id, 'parent' => $p1id, 'userid' => 102, 'subject' => 'p2', 'message' => 'm2');
-        $p2id = $DB->insert_record('forum_posts', $post2);
-        $post3 = (object)array('discussion' => $d1id, 'parent' => $p2id, 'userid' => 103, 'subject' => 'p3', 'message' => 'm3');
-        $p3id = $DB->insert_record('forum_posts', $post3);
-        $post4 = (object)array('discussion' => $d2id, 'userid' => 101, 'subject' => 'p4', 'message' => 'm4');
-        $p4id = $DB->insert_record('forum_posts', $post4);
+        $post1 = (object)array('discussion' => $this->discussionid1, 'userid' => 100, 'subject' => 'p1', 'message' => 'm1');
+        $this->postid1 = $DB->insert_record('forum_posts', $post1);
+        $post2 = (object)array('discussion' => $this->discussionid1, 'parent' => $this->postid1, 'userid' => 102, 'subject' => 'p2', 'message' => 'm2');
+        $this->postid2 = $DB->insert_record('forum_posts', $post2);
+        $post3 = (object)array('discussion' => $this->discussionid1, 'parent' => $this->postid2, 'userid' => 103, 'subject' => 'p3', 'message' => 'm3');
+        $this->postid3 = $DB->insert_record('forum_posts', $post3);
+        $post4 = (object)array('discussion' => $this->discussionid2, 'userid' => 101, 'subject' => 'p4', 'message' => 'm4');
+        $this->postid4 = $DB->insert_record('forum_posts', $post4);
         // With two related file
         $f1_post1 = (object)array(
             'contenthash' => 'testp1', 'contextid' => $this->contextid, 'component'=>'mod_forum',
-            'filearea' => 'post', 'filename' => 'tp1', 'itemid' => $p1id,
+            'filearea' => 'post', 'filename' => 'tp1', 'itemid' => $this->postid1,
             'filesize' => 123, 'timecreated' => 0, 'timemodified' => 0,
             'pathnamehash' => 'testp1'
         );
         $DB->insert_record('files', $f1_post1);
         $f1_post2 = (object)array(
             'contenthash' => 'testp2', 'contextid' => $this->contextid, 'component'=>'mod_forum',
-            'filearea' => 'attachment', 'filename' => 'tp2', 'itemid' => $p2id,
+            'filearea' => 'attachment', 'filename' => 'tp2', 'itemid' => $this->postid2,
             'filesize' => 123, 'timecreated' => 0, 'timemodified' => 0,
             'pathnamehash' => 'testp2'
         );
@@ -103,16 +117,16 @@ class backup_structure_testcase extends advanced_testcase {
 
         // Create two ratings
         $rating1 = (object)array(
-            'contextid' => $this->contextid, 'userid' => 104, 'itemid' => $p1id, 'rating' => 2,
+            'contextid' => $this->contextid, 'userid' => 104, 'itemid' => $this->postid1, 'rating' => 2,
             'scaleid' => -1, 'timecreated' => time(), 'timemodified' => time());
         $r1id = $DB->insert_record('rating', $rating1);
         $rating2 = (object)array(
-            'contextid' => $this->contextid, 'userid' => 105, 'itemid' => $p1id, 'rating' => 3,
+            'contextid' => $this->contextid, 'userid' => 105, 'itemid' => $this->postid1, 'rating' => 3,
             'scaleid' => -1, 'timecreated' => time(), 'timemodified' => time());
         $r2id = $DB->insert_record('rating', $rating2);
 
         // Create 1 reads
-        $read1 = (object)array('userid' => 102, 'forumid' => $this->forumid, 'discussionid' => $d2id, 'postid' => $p4id);
+        $read1 = (object)array('userid' => 102, 'forumid' => $this->forumid, 'discussionid' => $this->discussionid2, 'postid' => $this->postid4);
         $DB->insert_record('forum_read', $read1);
     }
 
@@ -195,11 +209,11 @@ class backup_structure_testcase extends advanced_testcase {
 
         // Let's add 1 optigroup with 4 elements
         $alternative1 = new backup_optigroup_element('alternative1',
-            array('name', 'value'), '../../id', 1);
+            array('name', 'value'), '../../id', $this->postid1);
         $alternative2 = new backup_optigroup_element('alternative2',
-            array('name', 'value'), backup::VAR_PARENTID, 2);
+            array('name', 'value'), backup::VAR_PARENTID, $this->postid2);
         $alternative3 = new backup_optigroup_element('alternative3',
-            array('name', 'value'), '/forum/discussions/discussion/posts/post/id', 3);
+            array('name', 'value'), '/forum/discussions/discussion/posts/post/id', $this->postid3);
         $alternative4 = new backup_optigroup_element('alternative4',
             array('forumtype', 'forumname')); // Alternative without conditions
         // Create the optigroup, adding one element
@@ -239,7 +253,7 @@ class backup_structure_testcase extends advanced_testcase {
             array(backup::VAR_PARENTID)
         );
 
-        $read->set_source_table('forum_read', array('id' => '../../id'));
+        $read->set_source_table('forum_read', array('forumid' => '../../id'));
 
         $inventeds->set_source_array(array((object)array('reason' => 'I love Moodle', 'version' => '1.0'),
             (object)array('reason' => 'I love Moodle', 'version' => '2.0'))); // 2 object array
@@ -334,83 +348,83 @@ class backup_structure_testcase extends advanced_testcase {
                     $ratarr[$node->nodeName] = $node->nodeValue;
                 }
             }
-            $this->assertEquals($ratarr['userid'], $DB->get_field('rating', 'userid', array('id' => $ratarr['id'])));
-            $this->assertEquals($ratarr['itemid'], $DB->get_field('rating', 'itemid', array('id' => $ratarr['id'])));
-            $this->assertEquals($ratarr['post_rating'], $DB->get_field('rating', 'rating', array('id' => $ratarr['id'])));
+            $this->assertEquals($DB->get_field('rating', 'userid', array('id' => $ratarr['id'])), $ratarr['userid']);
+            $this->assertEquals($DB->get_field('rating', 'itemid', array('id' => $ratarr['id'])), $ratarr['itemid']);
+            $this->assertEquals($DB->get_field('rating', 'rating', array('id' => $ratarr['id'])), $ratarr['post_rating']);
         }
 
         // Check forum has "blockeperiod" with value 0 (was declared by object instead of name)
         $query = '/forum[blockperiod="0"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check forum is missing "completiondiscussions" (as we are using mock_skip_final_element)
         $query = '/forum/completiondiscussions';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
+        $this->assertEquals(0, $result->length);
 
         // Check forum has "completionreplies" with value "original was 0, now changed" (because of mock_modify_final_element)
         $query = '/forum[completionreplies="original was 0, now changed"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check forum has "completionposts" with value "intercepted!" (because of mock_final_element_interceptor)
         $query = '/forum[completionposts="intercepted!"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check there isn't any alternative2 tag, as far as it hasn't source defined
         $query = '//alternative2';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
+        $this->assertEquals(0, $result->length);
 
         // Check there are 4 "field1" elements
         $query = '/forum/discussions/discussion/posts/post//field1';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 4);
+        $this->assertEquals(4, $result->length);
 
         // Check first post has one name element with value "alternative1"
-        $query = '/forum/discussions/discussion/posts/post[@id="1"][name="alternative1"]';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid1.'"][name="alternative1"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check there are two "dupetest1" elements
         $query = '/forum/discussions/discussion/posts/post//dupetest1';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 2);
+        $this->assertEquals(2, $result->length);
 
         // Check second post has one name element with value "dupetest2"
-        $query = '/forum/discussions/discussion/posts/post[@id="2"]/dupetest2';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/dupetest2';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check element "dupetest2" of second post has one field1 element with value "2"
-        $query = '/forum/discussions/discussion/posts/post[@id="2"]/dupetest2[field1="2"]';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/dupetest2[field1="2"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check forth post has no name element
-        $query = '/forum/discussions/discussion/posts/post[@id="4"]/name';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid4.'"]/name';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
+        $this->assertEquals(0, $result->length);
 
         // Check 1st, 2nd and 3rd posts have no forumtype element
-        $query = '/forum/discussions/discussion/posts/post[@id="1"]/forumtype';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid1.'"]/forumtype';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
-        $query = '/forum/discussions/discussion/posts/post[@id="2"]/forumtype';
+        $this->assertEquals(0, $result->length);
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/forumtype';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
-        $query = '/forum/discussions/discussion/posts/post[@id="3"]/forumtype';
+        $this->assertEquals(0, $result->length);
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid3.'"]/forumtype';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
+        $this->assertEquals(0, $result->length);
 
         // Check 4th post has one forumtype element with value "general"
         // (because it doesn't matches alternatives 1, 2, 3, then alternative 4,
         // the one without conditions is being applied)
-        $query = '/forum/discussions/discussion/posts/post[@id="4"][forumtype="general"]';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid4.'"][forumtype="general"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check annotations information against DB
         // Count records in original tables
index a297c48..e705270 100644 (file)
@@ -244,9 +244,20 @@ class core_backup_renderer extends plugin_renderer_base {
         global $CFG, $PAGE;
         require_once($CFG->dirroot.'/course/lib.php');
 
+        // These variables are used to check if the form using this function was submitted.
+        $target = optional_param('target', false, PARAM_INT);
+        $targetid = optional_param('targetid', null, PARAM_INT);
+
+        // Check if they submitted the form but did not provide all the data we need.
+        $missingdata = false;
+        if ($target and is_null($targetid)) {
+            $missingdata = true;
+        }
+
         $nextstageurl->param('sesskey', sesskey());
 
-        $form = html_writer::start_tag('form', array('method'=>'post', 'action'=>$nextstageurl->out_omit_querystring()));
+        $form = html_writer::start_tag('form', array('method' => 'post', 'action' => $nextstageurl->out_omit_querystring(),
+            'class' => 'mform'));
         foreach ($nextstageurl->params() as $key=>$value) {
             $form .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>$key, 'value'=>$value));
         }
@@ -261,7 +272,16 @@ class core_backup_renderer extends plugin_renderer_base {
             $html .= html_writer::start_tag('div', array('class'=>'bcs-new-course backup-section'));
             $html .= $this->output->heading(get_string('restoretonewcourse', 'backup'), 2, array('class'=>'header'));
             $html .= $this->backup_detail_input(get_string('restoretonewcourse', 'backup'), 'radio', 'target', backup::TARGET_NEW_COURSE, array('checked'=>'checked'));
-            $html .= $this->backup_detail_pair(get_string('selectacategory', 'backup'), $this->render($categories));
+            $selectacategoryhtml = $this->backup_detail_pair(get_string('selectacategory', 'backup'), $this->render($categories));
+            // Display the category selection as required if the form was submitted but this data was not supplied.
+            if ($missingdata && $target == backup::TARGET_NEW_COURSE) {
+                $html .= html_writer::span(get_string('required'), 'error');
+                $html .= html_writer::start_tag('fieldset', array('class' => 'error'));
+                $html .= $selectacategoryhtml;
+                $html .= html_writer::end_tag('fieldset');
+            } else {
+                $html .= $selectacategoryhtml;
+            }
             $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('continue'))));
             $html .= html_writer::end_tag('div');
             $html .= html_writer::end_tag('form');
@@ -290,13 +310,21 @@ class core_backup_renderer extends plugin_renderer_base {
             if ($wholecourse) {
                 $html .= $this->backup_detail_input(get_string('restoretoexistingcourseadding', 'backup'), 'radio', 'target', backup::TARGET_EXISTING_ADDING, array('checked'=>'checked'));
                 $html .= $this->backup_detail_input(get_string('restoretoexistingcoursedeleting', 'backup'), 'radio', 'target', backup::TARGET_EXISTING_DELETING);
-                $html .= $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
             } else {
                 // We only allow restore adding to existing for now. Enforce it here.
                 $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'target', 'value'=>backup::TARGET_EXISTING_ADDING));
                 $courses->invalidate_results(); // Clean list of courses
                 $courses->set_include_currentcourse(); // Show current course in the list
-                $html .= $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
+            }
+            $selectacoursehtml = $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
+            // Display the course selection as required if the form was submitted but this data was not supplied.
+            if ($missingdata && $target == backup::TARGET_EXISTING_ADDING) {
+                $html .= html_writer::span(get_string('required'), 'error');
+                $html .= html_writer::start_tag('fieldset', array('class' => 'error'));
+                $html .= $selectacoursehtml;
+                $html .= html_writer::end_tag('fieldset');
+            } else {
+                $html .= $selectacoursehtml;
             }
             $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('continue'))));
             $html .= html_writer::end_tag('div');
index fdf1c13..94e02e3 100644 (file)
@@ -29,8 +29,7 @@ Feature: Backup Moodle courses
     When I backup "Course 1" course using this options:
       | Initial | Include calendar events | 0 |
       | Initial | Include course logs | 1 |
-      | Schema | setting_section_section_5_userinfo | 0 |
-      | Schema | setting_section_section_5_included | 0 |
+      | Schema | Topic 5 | 0 |
       | Confirmation | Filename | test_backup.mbz |
     Then I should see "Restore"
     And I click on "Restore" "link" in the "test_backup.mbz" "table_row"
@@ -50,10 +49,8 @@ Feature: Backup Moodle courses
   @javascript
   Scenario: Backup selecting just one section
     When I backup "Course 2" course using this options:
-      | Schema | setting_section_section_2_userinfo | 0 |
-      | Schema | setting_section_section_2_userinfo | 0 |
-      | Schema | setting_section_section_4_included | 0 |
-      | Schema | setting_section_section_4_included | 0 |
+      | Schema | Test data | 0 |
+      | Schema | Topic 2 | 0 |
       | Confirmation | Filename | test_backup.mbz |
     Then I should see "Course backup area"
     And I click on "Restore" "link" in the "test_backup.mbz" "table_row"
index e5433f1..e5476ca 100644 (file)
@@ -52,10 +52,8 @@ Feature: Restore Moodle 2 course backups
     When I backup "Course 3" course using this options:
       | Confirmation | Filename | test_backup.mbz |
     And I restore "test_backup.mbz" backup into "Course 2" course using this options:
-      | Schema | setting_section_section_3_included | 0 |
-      | Schema | setting_section_section_3_userinfo | 0 |
-      | Schema | setting_section_section_5_included | 0 |
-      | Schema | setting_section_section_5_userinfo | 0 |
+      | Schema | Test database name | 0 |
+      | Schema | Section 2 | 0 |
     Then I should see "Course 2"
     And I should see "Test assign name"
     And I should not see "Test database name"
@@ -70,8 +68,7 @@ Feature: Restore Moodle 2 course backups
       | Description | Test forum post backup description |
     And I follow "Restore"
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
-      | Schema | setting_section_section_8_userinfo | 0 |
-      | Schema | setting_section_section_8_included | 0 |
+      | Schema | Section 3 | 0 |
     Then I should see "Course 1"
     And I should not see "Section 3"
     And I should not see "Test forum post backup name"
index c2f7a63..29f8cb4 100644 (file)
@@ -52,7 +52,16 @@ $PAGE->set_title(get_string('issuedbadge', 'badges'));
 if (isloggedin()) {
     $PAGE->set_heading($badge->badgeclass['name']);
     $PAGE->navbar->add($badge->badgeclass['name']);
-    $url = new moodle_url('/badges/mybadges.php');
+    if ($badge->recipient->id == $USER->id) {
+        $url = new moodle_url('/badges/mybadges.php');
+    } else {
+        $url = new moodle_url($CFG->wwwroot);
+    }
+    navigation_node::override_active_url($url);
+} else {
+    $PAGE->set_heading($badge->badgeclass['name']);
+    $PAGE->navbar->add($badge->badgeclass['name']);
+    $url = new moodle_url($CFG->wwwroot);
     navigation_node::override_active_url($url);
 }
 
index 24e52d7..cebc483 100644 (file)
@@ -54,7 +54,7 @@ class edit_details_form extends moodleform {
         $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
 
         $mform->addElement('textarea', 'description', get_string('description', 'badges'), 'wrap="virtual" rows="8" cols="70"');
-        $mform->setType('description', PARAM_CLEANHTML);
+        $mform->setType('description', PARAM_NOTAGS);
         $mform->addRule('description', null, 'required');
 
         $str = $action == 'new' ? get_string('badgeimage', 'badges') : get_string('newimage', 'badges');
index f18883b..1a9cee8 100644 (file)
@@ -82,6 +82,14 @@ $badge = new external_badge($badge, $userid);
 
 $PAGE->set_pagelayout('base');
 $PAGE->set_title(get_string('issuedbadge', 'badges'));
+$PAGE->set_heading(s($badge->issued->assertion->badge->name));
+$PAGE->navbar->add(s($badge->issued->assertion->badge->name));
+if (isloggedin() && $USER->id == $userid) {
+    $url = new moodle_url('/badges/mybadges.php');
+} else {
+    $url = new moodle_url($CFG->wwwroot);
+}
+navigation_node::override_active_url($url);
 
 echo $OUTPUT->header();
 
index 17103de..52f7205 100644 (file)
@@ -41,7 +41,7 @@ class core_badges_renderer extends plugin_renderer_base {
                 $bname = $badge->name;
                 $imageurl = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', 'f1', false);
             } else {
-                $bname = $badge->assertion->badge->name;
+                $bname = s($badge->assertion->badge->name);
                 $imageurl = $badge->imageUrl;
             }
 
@@ -135,34 +135,24 @@ class core_badges_renderer extends plugin_renderer_base {
         $display = "";
 
         // Badge details.
-        $display .= html_writer::start_tag('fieldset', array('class' => 'generalbox'));
-        $display .= html_writer::tag('legend', get_string('badgedetails', 'badges'), array('class' => 'bold'));
-
-        $detailstable = new html_table();
-        $detailstable->attributes = array('class' => 'clearfix', 'id' => 'badgedetails');
-        $detailstable->data[] = array(get_string('name') . ":", $badge->name);
-        $detailstable->data[] = array(get_string('description', 'badges') . ":", $badge->description);
-        $detailstable->data[] = array(get_string('createdon', 'search') . ":", userdate($badge->timecreated));
-        $detailstable->data[] = array(get_string('badgeimage', 'badges') . ":",
-                print_badge_image($badge, $context, 'large'));
-        $display .= html_writer::table($detailstable);
-        $display .= html_writer::end_tag('fieldset');
 
-        // Issuer details.
-        $display .= html_writer::start_tag('fieldset', array('class' => 'generalbox'));
-        $display .= html_writer::tag('legend', get_string('issuerdetails', 'badges'), array('class' => 'bold'));
+        $display .= $this->heading(get_string('badgedetails', 'badges'), 3);
+        $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('badgeimage', 'badges')] = print_badge_image($badge, $context, 'large');
+        $display .= $this->definition_list($dl);
 
-        $issuertable = new html_table();
-        $issuertable->attributes = array('class' => 'clearfix', 'id' => 'badgeissuer');
-        $issuertable->data[] = array(get_string('issuername', 'badges') . ":", $badge->issuername);
-        $issuertable->data[] = array(get_string('contact', 'badges') . ":",
-                html_writer::tag('a', $badge->issuercontact, array('href' => 'mailto:' . $badge->issuercontact)));
-        $display .= html_writer::table($issuertable);
-        $display .= html_writer::end_tag('fieldset');
+        // Issuer details.
+        $display .= $this->heading(get_string('issuerdetails', 'badges'), 3);
+        $dl = array();
+        $dl[get_string('issuername', 'badges')] = $badge->issuername;
+        $dl[get_string('contact', 'badges')] = html_writer::tag('a', $badge->issuercontact, array('href' => 'mailto:' . $badge->issuercontact));
+        $display .= $this->definition_list($dl);
 
         // Issuance details if any.
-        $display .= html_writer::start_tag('fieldset', array('class' => 'generalbox'));
-        $display .= html_writer::tag('legend', get_string('issuancedetails', 'badges'), array('class' => 'bold'));
+        $display .= $this->heading(get_string('issuancedetails', 'badges'), 3);
         if ($badge->can_expire()) {
             if ($badge->expiredate) {
                 $display .= get_string('expiredate', 'badges', userdate($badge->expiredate));
@@ -180,11 +170,9 @@ class core_badges_renderer extends plugin_renderer_base {
         } else {
             $display .= get_string('noexpiry', 'badges');
         }
-        $display .= html_writer::end_tag('fieldset');
 
         // Criteria details if any.
-        $display .= html_writer::start_tag('fieldset', array('class' => 'generalbox'));
-        $display .= html_writer::tag('legend', get_string('bcriteria', 'badges'), array('class' => 'bold'));
+        $display .= $this->heading(get_string('bcriteria', 'badges'), 3);
         if ($badge->has_criteria()) {
             $display .= self::print_badge_criteria($badge);
         } else {
@@ -195,12 +183,10 @@ class core_badges_renderer extends plugin_renderer_base {
                     get_string('addcriteria', 'badges'), 'POST', array('class' => 'activatebadge'));
             }
         }
-        $display .= html_writer::end_tag('fieldset');
 
         // Awards details if any.
         if (has_capability('moodle/badges:viewawarded', $context)) {
-            $display .= html_writer::start_tag('fieldset', array('class' => 'generalbox'));
-            $display .= html_writer::tag('legend', get_string('awards', 'badges'), array('class' => 'bold'));
+            $display .= $this->heading(get_string('awards', 'badges'), 3);
             if ($badge->has_awards()) {
                 $url = new moodle_url('/badges/recipients.php', array('id' => $badge->id));
                 $a = new stdClass();
@@ -218,10 +204,9 @@ class core_badges_renderer extends plugin_renderer_base {
                         new moodle_url('/badges/award.php', array('id' => $badge->id)),
                         get_string('award', 'badges'), 'POST', array('class' => 'activatebadge'));
             }
-            $display .= html_writer::end_tag('fieldset');
         }
 
-        return $display;
+        return html_writer::div($display, null, array('id' => 'badge-overview'));
     }
 
     // Prints action icons for the badge.
@@ -285,19 +270,24 @@ class core_badges_renderer extends plugin_renderer_base {
         $badgeclass = $ibadge->badgeclass;
         $badge = new badge($ibadge->badgeid);
         $now = time();
+        $expiration = isset($issued['expires']) ? $issued['expires'] : $now + 86400;
 
-        $table = new html_table();
-        $table->id = 'issued-badge-table';
+        $output = '';
+        $output .= html_writer::start_tag('div', array('id' => 'badge'));
+        $output .= html_writer::start_tag('div', array('id' => 'badge-image'));
+        $output .= html_writer::empty_tag('img', array('src' => $badgeclass['image'], 'alt' => $badge->name));
+        if ($expiration < $now) {
+            $output .= $this->output->pix_icon('i/expired',
+            get_string('expireddate', 'badges', userdate($issued['expires'])),
+                'moodle',
+                array('class' => 'expireimage'));
+        }
 
-        $imagetable = new html_table();
-        $imagetable->attributes = array('class' => 'clearfix badgeissuedimage');
-        $imagetable->data[] = array(html_writer::empty_tag('img', array('src' => $badgeclass['image'])));
         if ($USER->id == $userinfo->id && !empty($CFG->enablebadges)) {
-            $imagetable->data[] = array($this->output->single_button(
+            $output .= $this->output->single_button(
                         new moodle_url('/badges/badge.php', array('hash' => $issued['uid'], 'bake' => true)),
                         get_string('download'),
-                        'POST'));
-            $expiration = isset($issued['expires']) ? $issued['expires'] : $now + 86400;
+                        'POST');
             if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $now) && badges_user_has_backpack($USER->id)) {
                 $assertion = new moodle_url('/badges/assertion.php', array('b' => $issued['uid']));
                 $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
@@ -307,57 +297,55 @@ class core_badges_renderer extends plugin_renderer_base {
                         'value' => get_string('addtobackpack', 'badges'));
                 $tobackpack = html_writer::tag('input', '', $attributes);
                 $this->output->add_action_handler($action, 'addbutton');
-                $imagetable->data[] = array($tobackpack);
+                $output .= $tobackpack;
             }
         }
-        $datatable = new html_table();
-        $datatable->attributes = array('class' => 'badgeissuedinfo');
-        $datatable->colclasses = array('bfield', 'bvalue');
+        $output .= html_writer::end_tag('div');
 
+        $output .= html_writer::start_tag('div', array('id' => 'badge-details'));
         // Recipient information.
-        $datatable->data[] = array($this->output->heading(get_string('recipientdetails', 'badges'), 3), '');
+        $output .= $this->output->heading(get_string('recipientdetails', 'badges'), 3);
+        $dl = array();
         if ($userinfo->deleted) {
             $strdata = new stdClass();
             $strdata->user = fullname($userinfo);
             $strdata->site = format_string($SITE->fullname, true, array('context' => context_system::instance()));
-            $datatable->data[] = array(get_string('name'), get_string('error:userdeleted', 'badges', $strdata));
+
+            $dl[get_string('name')] = get_string('error:userdeleted', 'badges', $strdata);
         } else {
-            $datatable->data[] = array(get_string('name'), fullname($userinfo));
+            $dl[get_string('name')] = fullname($userinfo);
         }
+        $output .= $this->definition_list($dl);
 
-        $datatable->data[] = array($this->output->heading(get_string('issuerdetails', 'badges'), 3), '');
-        $datatable->data[] = array(get_string('issuername', 'badges'), $badge->issuername);
+        $output .= $this->output->heading(get_string('issuerdetails', 'badges'), 3);
+        $dl = array();
+        $dl[get_string('issuername', 'badges')] = $badge->issuername;
         if (isset($badge->issuercontact) && !empty($badge->issuercontact)) {
-            $datatable->data[] = array(get_string('contact', 'badges'), obfuscate_mailto($badge->issuercontact));
+            $dl[get_string('contact', 'badges')] = obfuscate_mailto($badge->issuercontact);
         }
-        $datatable->data[] = array($this->output->heading(get_string('badgedetails', 'badges'), 3), '');
-        $datatable->data[] = array(get_string('name'), $badge->name);
-        $datatable->data[] = array(get_string('description', 'badges'), $badge->description);
+        $output .= $this->definition_list($dl);
+
+        $output .= $this->output->heading(get_string('badgedetails', 'badges'), 3);
+        $dl = array();
+        $dl[get_string('name')] = $badge->name;
+        $dl[get_string('description', 'badges')] = $badge->description;
 
         if ($badge->type == BADGE_TYPE_COURSE && isset($badge->courseid)) {
             $coursename = $DB->get_field('course', 'fullname', array('id' => $badge->courseid));
-            $datatable->data[] = array(get_string('course'), $coursename);
+            $dl[get_string('course')] = $coursename;
         }
+        $dl[get_string('bcriteria', 'badges')] = self::print_badge_criteria($badge);
+        $output .= $this->definition_list($dl);
 
-        $datatable->data[] = array(get_string('bcriteria', 'badges'), self::print_badge_criteria($badge));
-        $datatable->data[] = array($this->output->heading(get_string('issuancedetails', 'badges'), 3), '');
-        $datatable->data[] = array(get_string('dateawarded', 'badges'), userdate($issued['issuedOn']));
+        $output .= $this->output->heading(get_string('issuancedetails', 'badges'), 3);
+        $dl = array();
+        $dl[get_string('dateawarded', 'badges')] = userdate($issued['issuedOn']);
         if (isset($issued['expires'])) {
             if ($issued['expires'] < $now) {
-                $cell = new html_table_cell(userdate($issued['expires']) . get_string('warnexpired', 'badges'));
-                $cell->attributes = array('class' => 'notifyproblem warning');
-                $datatable->data[] = array(get_string('expirydate', 'badges'), $cell);
+                $dl[get_string('expirydate', 'badges')] = userdate($issued['expires']) . get_string('warnexpired', 'badges');
 
-                $image = html_writer::start_tag('div', array('class' => 'badge'));
-                $image .= html_writer::empty_tag('img', array('src' => $badgeclass['image']));
-                $image .= $this->output->pix_icon('i/expired',
-                                get_string('expireddate', 'badges', userdate($issued['expires'])),
-                                'moodle',
-                                array('class' => 'expireimage'));
-                $image .= html_writer::end_tag('div');
-                $imagetable->data[0] = array($image);
             } else {
-                $datatable->data[] = array(get_string('expirydate', 'badges'), userdate($issued['expires']));
+                $dl[get_string('expirydate', 'badges')] = userdate($issued['expires']);
             }
         }
 
@@ -379,14 +367,11 @@ class core_badges_renderer extends plugin_renderer_base {
             }
         }
 
-        $datatable->data[] = array(get_string('evidence', 'badges'),
-                get_string('completioninfo', 'badges') .
-                html_writer::alist($items, array(), 'ul'));
-        $table->attributes = array('class' => 'generalbox boxaligncenter issuedbadgebox');
-        $table->data[] = array(html_writer::table($imagetable), html_writer::table($datatable));
-        $htmlbadge = html_writer::table($table);
+        $dl[get_string('evidence', 'badges')] = get_string('completioninfo', 'badges') . html_writer::alist($items, array(), 'ul');
+        $output .= $this->definition_list($dl);
+        $output .= html_writer::end_tag('div');
 
-        return $htmlbadge;
+        return $output;
     }
 
     // Outputs external badge.
@@ -396,80 +381,82 @@ class core_badges_renderer extends plugin_renderer_base {
         $issuer = $assertion->badge->issuer;
         $userinfo = $ibadge->recipient;
         $table = new html_table();
+        $today = strtotime(date('Y-m-d'));
 
-        $imagetable = new html_table();
-        $imagetable->attributes = array('class' => 'clearfix badgeissuedimage');
-        $imagetable->data[] = array(html_writer::empty_tag('img', array('src' => $issued->imageUrl, 'width' => '100px')));
+        $output = '';
+        $output .= html_writer::start_tag('div', array('id' => 'badge'));
+        $output .= html_writer::start_tag('div', array('id' => 'badge-image'));
+        $output .= html_writer::empty_tag('img', array('src' => $issued->imageUrl));
+        if (isset($assertion->expires)) {
+            $expiration = !strtotime($assertion->expires) ? s($assertion->expires) : strtotime($assertion->expires);
+            if ($expiration < $today) {
+                $output .= $this->output->pix_icon('i/expired',
+                        get_string('expireddate', 'badges', userdate($expiration)),
+                        'moodle',
+                        array('class' => 'expireimage'));
+            }
+        }
+        $output .= html_writer::end_tag('div');
 
-        $datatable = new html_table();
-        $datatable->attributes = array('class' => 'badgeissuedinfo');
-        $datatable->colclasses = array('bfield', 'bvalue');
+        $output .= html_writer::start_tag('div', array('id' => 'badge-details'));
 
         // Recipient information.
-        $datatable->data[] = array($this->output->heading(get_string('recipientdetails', 'badges'), 3), '');
+        $output .= $this->output->heading(get_string('recipientdetails', 'badges'), 3);
+        $dl = array();
         // Technically, we should alway have a user at this point, but added an extra check just in case.
         if ($userinfo) {
-            $notify = '';
             if (!$ibadge->valid) {
                 $notify = $this->output->notification(get_string('recipientvalidationproblem', 'badges'), 'notifynotice');
+                $dl[get_string('name')] = fullname($userinfo) . $notify;
+            } else {
+                $dl[get_string('name')] = fullname($userinfo);
             }
-            $datatable->data[] = array(get_string('name'), fullname($userinfo). $notify);
         } else {
             $notify = $this->output->notification(get_string('recipientidentificationproblem', 'badges'), 'notifynotice');
-            $datatable->data[] = array(get_string('name'), $notify);
+            $dl[get_string('name')] = $notify;
         }
+        $output .= $this->definition_list($dl);
+
+        $output .= $this->output->heading(get_string('issuerdetails', 'badges'), 3);
+        $dl = array();
+        $dl[get_string('issuername', 'badges')] = s($issuer->name);
+        $dl[get_string('issuerurl', 'badges')] = html_writer::tag('a', $issuer->origin, array('href' => $issuer->origin));
 
-        $datatable->data[] = array($this->output->heading(get_string('issuerdetails', 'badges'), 3), '');
-        $datatable->data[] = array(get_string('issuername', 'badges'), $issuer->name);
-        $datatable->data[] = array(get_string('issuerurl', 'badges'),
-                html_writer::tag('a', $issuer->origin, array('href' => $issuer->origin)));
         if (isset($issuer->contact)) {
-            $datatable->data[] = array(get_string('contact', 'badges'), obfuscate_mailto($issuer->contact));
+            $dl[get_string('contact', 'badges')] = obfuscate_mailto($issuer->contact);
         }
-        $datatable->data[] = array($this->output->heading(get_string('badgedetails', 'badges'), 3), '');
-        $datatable->data[] = array(get_string('name'), $assertion->badge->name);
-        $datatable->data[] = array(get_string('description', 'badges'), $assertion->badge->description);
-        $datatable->data[] = array(get_string('bcriteria', 'badges'),
-                html_writer::tag('a', $assertion->badge->criteria, array('href' => $assertion->badge->criteria)));
-        $datatable->data[] = array($this->output->heading(get_string('issuancedetails', 'badges'), 3), '');
+        $output .= $this->definition_list($dl);
+
+        $output .= $this->output->heading(get_string('badgedetails', 'badges'), 3);
+        $dl = array();
+        $dl[get_string('name')] = s($assertion->badge->name);
+        $dl[get_string('description', 'badges')] = s($assertion->badge->description);
+        $dl[get_string('bcriteria', 'badges')] = html_writer::tag('a', s($assertion->badge->criteria), array('href' => $assertion->badge->criteria));
+        $output .= $this->definition_list($dl);
+
+        $output .= $this->output->heading(get_string('issuancedetails', 'badges'), 3);
+        $dl = array();
         if (isset($assertion->issued_on)) {
-            $datatable->data[] = array(get_string('dateawarded', 'badges'), $assertion->issued_on);
+            $issuedate = !strtotime($assertion->issued_on) ? s($assertion->issued_on) : strtotime($assertion->issued_on);
+            $dl[get_string('dateawarded', 'badges')] = userdate($issuedate);
         }
-        if (isset($assertion->badge->expire)) {
-            $today_date = date('Y-m-d');
-            $today = strtotime($today_date);
-            $expiration = strtotime($assertion->badge->expire);
+        if (isset($assertion->expires)) {
             if ($expiration < $today) {
-                $cell = new html_table_cell($assertion->badge->expire . get_string('warnexpired', 'badges'));
-                $cell->attributes = array('class' => 'notifyproblem warning');
-                $datatable->data[] = array(get_string('expirydate', 'badges'), $cell);
-
-                $image = html_writer::start_tag('div', array('class' => 'badge'));
-                $image .= html_writer::empty_tag('img', array('src' => $issued['badge']['image']));
-                $image .= html_writer::start_tag('span', array('class' => 'expired'))
-                            . $this->output->pix_icon('i/expired',
-                                get_string('expireddate', 'badges', $assertion->badge->expire),
-                                'moodle',
-                                array('class' => 'expireimage'))
-                            . html_writer::end_tag('span');
-                $image .= html_writer::end_tag('div');
-                $imagetable->data[0] = array($image);
+                $dl[get_string('expirydate', 'badges')] = userdate($expiration) . get_string('warnexpired', 'badges');
             } else {
-                $datatable->data[] = array(get_string('expirydate', 'badges'), $assertion->badge->expire);
+                $dl[get_string('expirydate', 'badges')] = userdate($expiration);
             }
         }
         if (isset($assertion->evidence)) {
-            $datatable->data[] = array(get_string('evidence', 'badges'),
-                html_writer::tag('a', $assertion->evidence, array('href' => $assertion->evidence)));
+            $dl[get_string('evidence', 'badges')] = html_writer::tag('a', s($assertion->evidence), array('href' => $assertion->evidence));
         }
-        $table->attributes = array('class' => 'generalbox boxaligncenter issuedbadgebox');
-        $table->data[] = array(html_writer::table($imagetable), html_writer::table($datatable));
-        $htmlbadge = html_writer::table($table);
+        $output .= $this->definition_list($dl);
+        $output .= html_writer::end_tag('div');
 
-        return $htmlbadge;
+        return $output;
     }
 
-    // Outputs table of user badges.
+    // Displays the user badges.
     protected function render_badge_user_collection(badge_user_collection $badges) {
         global $CFG, $USER, $SITE;
         $backpack = $badges->backpack;
@@ -496,10 +483,8 @@ class core_badges_renderer extends plugin_renderer_base {
         $heading = get_string('localbadges', 'badges', format_string($SITE->fullname, true, array('context' => context_system::instance())));
         $localhtml .= html_writer::tag('legend', $this->output->heading_with_help($heading, 'localbadgesh', 'badges'));
         if ($badges->badges) {
-            $table = new html_table();
-            $table->attributes['class'] = 'statustable';
-            $table->data[] = array($this->output->heading(get_string('badgesearned', 'badges', $badges->totalcount), 4, 'activatebadge'), $downloadall);
-            $downloadbutton = html_writer::table($table);
+            $downloadbutton = $this->output->heading(get_string('badgesearned', 'badges', $badges->totalcount), 4, 'activatebadge');
+            $downloadbutton .= $downloadall;
 
             $htmllist = $this->print_badges_list($badges->badges, $USER->id);
             $localhtml .= $backpackconnect . $downloadbutton . $searchform . $htmlpagingbar . $htmllist . $htmlpagingbar;
@@ -534,7 +519,7 @@ class core_badges_renderer extends plugin_renderer_base {
         return $localhtml . $externalhtml;
     }
 
-    // Outputs table of available badges.
+    // Displays the available badges.
     protected function render_badge_collection(badge_collection $badges) {
         $paging = new paging_bar($badges->totalcount, $badges->page, $badges->perpage, $this->page->url, 'page');
         $htmlpagingbar = $this->render($paging);
@@ -681,8 +666,6 @@ class core_badges_renderer extends plugin_renderer_base {
      */
     public function print_badge_status_box(badge $badge) {
         if (has_capability('moodle/badges:configurecriteria', $badge->get_context())) {
-            $table = new html_table();
-            $table->attributes['class'] = 'boxaligncenter statustable';
 
             if (!$badge->has_criteria()) {
                 $criteriaurl = new moodle_url('/badges/criteria.php', array('id' => $badge->id));
@@ -694,7 +677,8 @@ class core_badges_renderer extends plugin_renderer_base {
                 } else {
                     $action = '';
                 }
-                $row = array($status, $action);
+
+                $message = $status . $action;
             } else {
                 $status = get_string('statusmessage_' . $badge->status, 'badges');
                 if ($badge->is_active()) {
@@ -708,12 +692,13 @@ class core_badges_renderer extends plugin_renderer_base {
                                       'return' => $this->page->url->out_as_local_url(false))),
                             get_string('activate', 'badges'), 'POST', array('class' => 'activatebadge'));
                 }
-                $row = array($status . $this->output->help_icon('status', 'badges'), $action);
+
+                $message = $status . $this->output->help_icon('status', 'badges') . $action;
+
             }
-            $table->data[] = $row;
 
             $style = $badge->is_active() ? 'generalbox statusbox active' : 'generalbox statusbox inactive';
-            return $this->output->box(html_writer::table($table), $style);
+            return $this->output->box($message, $style);
         }
 
         return null;
@@ -898,6 +883,22 @@ class core_badges_renderer extends plugin_renderer_base {
 
         return $out;
     }
+
+    /**
+     * Renders a definition list
+     *
+     * @param array $items the list of items to define
+     * @param array
+     */
+    protected function definition_list(array $items, array $attributes = array()) {
+        $output = html_writer::start_tag('dl', $attributes);
+        foreach ($items as $label => $value) {
+            $output .= html_writer::tag('dt', $label);
+            $output .= html_writer::tag('dd', $value);
+        }
+        $output .= html_writer::end_tag('dl');
+        return $output;
+    }
 }
 
 /**
@@ -970,9 +971,10 @@ class external_badge implements renderable {
         global $DB;
         // At this point a user has connected a backpack. So, we are going to get
         // their backpack email rather than their account email.
-        $user = $DB->get_record_sql('SELECT u.lastname, u.firstname, b.email
+        $namefields = get_all_user_name_fields(true, 'u');
+        $user = $DB->get_record_sql("SELECT {$namefields}, b.email
                     FROM {user} u INNER JOIN {badge_backpack} b ON u.id = b.userid
-                    WHERE userid = :userid', array('userid' => $recipient), IGNORE_MISSING);
+                    WHERE userid = :userid", array('userid' => $recipient), IGNORE_MISSING);
 
         $this->issued = $badge;
         $this->recipient = $user;
index 37b534c..ac03807 100644 (file)
@@ -10,9 +10,7 @@ Feature: Add badges to the system
 
   @javascript
   Scenario: Setting badges settings
-    Given I expand "Site administration" node
-    And I expand "Badges" node
-    And I follow "Badges settings"
+    Given I navigate to "Badges settings" node in "Site administration > Badges"
     And I set the field "Default badge issuer name" to "Test Badge Site"
     And I set the field "Default badge issuer contact details" to "testuser@test-badge-site.com"
     And I press "Save changes"
@@ -22,15 +20,12 @@ Feature: Add badges to the system
 
   @javascript
   Scenario: Accessing the badges
-    Given I expand "Site pages" node
-    And I follow "Site badges"
+    Given I navigate to "Site badges" node in "Site pages"
     Then I should see "There are no badges available."
 
   @javascript
   Scenario: Add a badge
-    Given I expand "Site administration" node
-    And I expand "Badges" node
-    And I follow "Add a new badge"
+    Given I navigate to "Add a new badge" node in "Site administration > Badges"
     And I set the following fields to these values:
       | Name | Test Badge |
       | Description | Test badge description |
index ffe9b3d..d277721 100644 (file)
@@ -85,7 +85,7 @@ Feature: Award badges
       | student2 | C1 | student |
     And I log in as "teacher1"
     And I follow "Course 1"
-    And I click on "//span[text()='Badges']" "xpath_element" in the "Administration" "block"
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
@@ -142,7 +142,7 @@ Feature: Award badges
     And I log out
     And I log in as "teacher1"
     And I follow "Course 1"
-    And I click on "//span[text()='Badges']" "xpath_element" in the "Administration" "block"
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
@@ -203,7 +203,7 @@ Feature: Award badges
     And I log out
     And I log in as "teacher1"
     And I follow "Course 1"
-    And I click on "//span[text()='Badges']" "xpath_element" in the "Administration" "block"
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
     And I follow "Add a new badge"
     And I set the following fields to these values:
       | Name | Course Badge |
@@ -212,8 +212,7 @@ Feature: Award badges
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Course completion"
-    And I set the following fields to these values:
-      | grade_2 | 0 |
+    And I set the field with xpath "//fieldset/input[1]" to "0"
     And I press "Save"
     And I press "Enable access"
     When I press "Continue"
index cead2b2..ecfa3b9 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
index 79eeb0e..b1a0b7b 100644 (file)
@@ -31,7 +31,7 @@ Feature: Test expand my courses navigation setting
   Scenario: The My Courses branch is collapsed when expand my courses is off
     Given I log in as "admin"
     And I set the following administration settings values:
-      | Expand My Courses initially on My Moodle page | 0 |
+      | Show My courses expanded on My home | 0 |
     And I log out
     Given I log in as "student1"
     And I follow "My home"
@@ -43,7 +43,7 @@ Feature: Test expand my courses navigation setting
   Scenario: My Courses can be expanded on the My Moodle page when expand my courses is off
     Given I log in as "admin"
     And I set the following administration settings values:
-      | Expand My Courses initially on My Moodle page | 0 |
+      | Show My courses expanded on My home | 0 |
     And I log out
     Given I log in as "student1"
     And I follow "My home"
@@ -53,4 +53,4 @@ Feature: Test expand my courses navigation setting
     And I expand "My courses" node
     Then I should see "c1" in the "Navigation" "block"
     And I should see "c2" in the "Navigation" "block"
-    And I should not see "c3" in the "Navigation" "block"
\ No newline at end of file
+    And I should not see "c3" in the "Navigation" "block"
index fadc8ad..ebf4c96 100644 (file)
@@ -31,8 +31,12 @@ include_once('locallib.php');
 $action   = required_param('action', PARAM_ALPHA);
 $id       = optional_param('entryid', 0, PARAM_INT);
 $confirm  = optional_param('confirm', 0, PARAM_BOOL);
-$modid    = optional_param('modid', 0, PARAM_INT); // To associate the entry with a module instance
-$courseid = optional_param('courseid', 0, PARAM_INT); // To associate the entry with a course
+$modid = optional_param('modid', 0, PARAM_INT); // To associate the entry with a module instance.
+$courseid = optional_param('courseid', 0, PARAM_INT); // To associate the entry with a course.
+
+if ($action == 'edit') {
+    $id = required_param('entryid', PARAM_INT);
+}
 
 $PAGE->set_url('/blog/edit.php', array('action' => $action, 'entryid' => $id, 'confirm' => $confirm, 'modid' => $modid, 'courseid' => $courseid));
 
@@ -41,6 +45,20 @@ if (!empty($id) && $action == 'add') {
     $id = null;
 }
 
+// Blogs are always in system context.
+$sitecontext = context_system::instance();
+$PAGE->set_context($sitecontext);
+
+require_login($courseid);
+
+if (empty($CFG->enableblogs)) {
+    print_error('blogdisable', 'blog');
+}
+
+if (isguestuser()) {
+    print_error('noguestentry', 'blog');
+}
+
 $returnurl = new moodle_url('/blog/index.php');
 
 if (!empty($courseid) && empty($modid)) {
@@ -54,27 +72,8 @@ if (!empty($modid)) {
     $returnurl->param('courseid', $courseid);
 }
 
-// Blogs are always in system context.
-$sitecontext = context_system::instance();
-$PAGE->set_context($sitecontext);
-
-
 $blogheaders = blog_get_headers();
 
-require_login($courseid);
-
-if ($action == 'edit') {
-    $id = required_param('entryid', PARAM_INT);
-}
-
-if (empty($CFG->enableblogs)) {
-    print_error('blogdisable', 'blog');
-}
-
-if (isguestuser()) {
-    print_error('noguestentry', 'blog');
-}
-
 if (!has_capability('moodle/blog:create', $sitecontext) && !has_capability('moodle/blog:manageentries', $sitecontext)) {
     print_error('cannoteditentryorblog');
 }
index b28860a..458aaa3 100644 (file)
@@ -79,13 +79,16 @@ if ($externalblogform->is_cancelled()){
             $newexternal->description = (empty($data->description)) ? $rss->get_description() : $data->description;
             $newexternal->userid = $USER->id;
             $newexternal->url = $data->url;
-            $newexternal->filtertags = $data->filtertags;
+            $newexternal->filtertags = (!empty($data->filtertags)) ? $data->filtertags : null;
             $newexternal->timemodified = time();
 
             $newexternal->id = $DB->insert_record('blog_external', $newexternal);
             blog_sync_external_entries($newexternal);
-            tag_set('blog_external', $newexternal->id, explode(',', $data->autotags), 'core',
-                context_user::instance($newexternal->userid)->id);
+            if ($CFG->usetags) {
+                $autotags = (!empty($data->autotags)) ? $data->autotags : null;
+                tag_set('blog_external', $newexternal->id, explode(',', $autotags), 'core',
+                    context_user::instance($newexternal->userid)->id);
+            }
 
             break;
 
@@ -99,13 +102,15 @@ if ($externalblogform->is_cancelled()){
                 $external->description = (empty($data->description)) ? $rss->get_description() : $data->description;
                 $external->userid = $USER->id;
                 $external->url = $data->url;
-                $external->filtertags = $data->filtertags;
+                $external->filtertags = (!empty($data->filtertags)) ? $data->filtertags : null;
                 $external->timemodified = time();
 
                 $DB->update_record('blog_external', $external);
-                tag_set('blog_external', $external->id, explode(',', $data->autotags), 'core',
-                    context_user::instance($newexternal->userid)->id);
-
+                if ($CFG->usetags) {
+                    $autotags = (!empty($data->autotags)) ? $data->autotags : null;
+                    tag_set('blog_external', $external->id, explode(',', $autotags), 'core',
+                        context_user::instance($external->userid)->id);
+                }
             } else {
                 print_error('wrongexternalid', 'blog');
             }
index 696e79a..fb64d29 100644 (file)
@@ -118,7 +118,9 @@ class blog_edit_external_form extends moodleform {
         if ($id = $mform->getElementValue('id')) {
             $mform->setDefault('autotags', implode(',', tag_get_tags_array('blog_external', $id)));
             $mform->freeze('url');
-            $mform->freeze('filtertags');
+            if ($mform->elementExists('filtertags')) {
+                $mform->freeze('filtertags');
+            }
             // TODO change the filtertags element to a multiple select, using the tags of the external blog
             // Use $rss->get_channel_tags()
         }
index efa99ea..4bdfd35 100644 (file)
@@ -34,10 +34,6 @@ foreach ($url_params as $var => $val) {
 }
 $PAGE->set_url('/blog/index.php', $url_params);
 
-if (empty($CFG->enableblogs)) {
-    print_error('blogdisable', 'blog');
-}
-
 //correct tagid if a text tag is provided as a param
 if (!empty($tag)) {
     if ($tagrec = $DB->get_record('tag', array('name' => $tag))) {
@@ -47,15 +43,6 @@ if (!empty($tag)) {
     }
 }
 
-// add courseid if modid or groupid is specified: This is used for navigation and title
-if (!empty($modid) && empty($courseid)) {
-    $courseid = $DB->get_field('course_modules', 'course', array('id'=>$modid));
-}
-
-if (!empty($groupid) && empty($courseid)) {
-    $courseid = $DB->get_field('groups', 'courseid', array('id'=>$groupid));
-}
-
 $sitecontext = context_system::instance();
 // Blogs are always in system context.
 $PAGE->set_context($sitecontext);
@@ -84,6 +71,19 @@ if ($CFG->bloglevel == BLOG_GLOBAL_LEVEL) {
     print_error('blogdisable', 'blog');
 }
 
+if (empty($CFG->enableblogs)) {
+    print_error('blogdisable', 'blog');
+}
+
+// Add courseid if modid or groupid is specified: This is used for navigation and title.
+if (!empty($modid) && empty($courseid)) {
+    $courseid = $DB->get_field('course_modules', 'course', array('id' => $modid));
+}
+
+if (!empty($groupid) && empty($courseid)) {
+    $courseid = $DB->get_field('groups', 'courseid', array('id' => $groupid));
+}
+
 
 if (!$userid && has_capability('moodle/blog:view', $sitecontext) && $CFG->bloglevel > BLOG_USER_LEVEL) {
     if ($entryid) {
index cf1c1de..2db2265 100644 (file)
@@ -326,6 +326,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('associateid' => 2 , 'blogid' => 3, 'subject' => 'blog subject')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'associatetype\' value must be set in other and be a valid type.', $e->getMessage());
@@ -334,6 +335,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('associateid' => 2 , 'blogid' => 3, 'associatetype' => 'random', 'subject' => 'blog subject')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'associatetype\' value must be set in other and be a valid type.', $e->getMessage());
@@ -343,6 +345,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('blogid' => 3, 'associatetype' => 'course', 'subject' => 'blog subject')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'associateid\' value must be set in other.', $e->getMessage());
@@ -352,6 +355,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('associateid' => 3, 'associatetype' => 'course', 'subject' => 'blog subject')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'blogid\' value must be set in other.', $e->getMessage());
@@ -361,6 +365,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('blogid' => 3, 'associateid' => 3, 'associatetype' => 'course')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'subject\' value must be set in other.', $e->getMessage());
index 7745f0e..083f4e6 100644 (file)
@@ -266,3 +266,7 @@ There are a couple of considerations to using this method:
 * If you have configured your cache before setting $CFG->altcacheconfigpath you will need to copy it from moodledata/muc/config.php to the destination you specified.
 * This allows you to share a cache config between sites.
 * It also allows you to use unit tests to test your sites cache config.
+
+Please be aware that if you are using Memcache or Memcached it is recommended to use dedicated Memcached servers.
+When caches get purged the memcached servers you have configured get purged, any data stored within them whether it belongs to Moodle or not will be removed.
+If you are using Memcached for sessions as well as caching/testing and caches get purged your sessions will be removed prematurely and users will be need to start again.
\ No newline at end of file
index 22c20db..b5cfcc1 100644 (file)
@@ -356,13 +356,13 @@ abstract class cache_store implements cache_store_interface {
      * Initialises a test instance for unit tests.
      *
      * This differs from initialise_test_instance in that it doesn't rely on interacting with the config table.
-     * By default however it calls initialise_test_instance to support backwards compatability.
+     * By default however it calls initialise_test_instance to support backwards compatibility.
      *
      * @since 2.8
      * @param cache_definition $definition
      * @return cache_store|false
      */
     public static function initialise_unit_test_instance(cache_definition $definition) {
-        return self::initialise_test_instance($definition);
+        return static::initialise_test_instance($definition);
     }
 }
index eb7c999..5a90f39 100644 (file)
@@ -52,5 +52,54 @@ class cachestore_memcache_addinstance_form extends cachestore_addinstance_form {
         $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
         $form->setDefault('prefix', 'mdl_');
         $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_memcache'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
+
+        $form->addElement('header', 'clusteredheader', get_string('clustered', 'cachestore_memcache'));
+
+        $form->addElement('checkbox', 'clustered', get_string('clustered', 'cachestore_memcache'));
+        $form->setDefault('checkbox', false);
+        $form->addHelpButton('clustered', 'clustered', 'cachestore_memcache');
+
+        $form->addElement('textarea', 'setservers', get_string('setservers', 'cachestore_memcache'),
+                array('cols' => 75, 'rows' => 5));
+        $form->addHelpButton('setservers', 'setservers', 'cachestore_memcache');
+        $form->disabledIf('setservers', 'clustered');
+        $form->setType('setservers', PARAM_RAW);
+    }
+
+    /**
+     * Perform minimal validation on the settings form.
+     *
+     * @param array $data
+     * @param array $files
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        if (isset($data['clustered']) && ($data['clustered'] == 1)) {
+            // Set servers is required with in cluster mode.
+            if (!isset($data['setservers'])) {
+                $errors['setservers'] = get_string('required');
+            } else {
+                $trimmed = trim($data['setservers']);
+                if (empty($trimmed)) {
+                    $errors['setservers'] = get_string('required');
+                }
+            }
+
+            $validservers = false;
+            if (isset($data['servers'])) {
+                $servers = trim($data['servers']);
+                $servers = explode("\n", $servers);
+                if (count($servers) === 1) {
+                    $validservers = true;
+                }
+            }
+
+            if (!$validservers) {
+                $errors['servers'] = get_string('serversclusterinvalid', 'cachestore_memcache');
+            }
+        }
+
+        return $errors;
     }
 }
\ No newline at end of file
index fb87d28..78d3947 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['clustered'] = 'Enable clustered servers';
+$string['clustered_help'] = 'This is used to allow read-one, set-multi functionality.
+
+The intended use case is to create an improved store for load-balanced configurations. The store will fetch from one server (usually localhost), but set to many (all the servers in the load-balance pool). For caches with very high read to set ratios, this saves a significant amount of network overhead.
+
+When this setting is enabled, the server listed above will be used for fetching.';
+$string['clusteredheader'] = 'Split servers';
 $string['pluginname'] = 'Memcache';
 $string['prefix'] = 'Key prefix';
 $string['prefix_help'] = 'This prefix is used for all key names on the memcache server.
@@ -42,6 +49,20 @@ For example:
 server.url.com
 ipaddress:port
 servername:port:weight
+</pre>
+
+If *Enable clustered servers* is enabled below, there must be only one server listed here. This would usually be a name that always resolves to the local manchine, like 127.0.0.1 or localhost.';
+$string['serversclusterinvalid'] = 'Exactly one server is required when clustering is enabled.';
+$string['setservers'] = 'Set Servers';
+$string['setservers_help'] = 'This is the list of servers that will updated when data is modified in the cache. Generally the fully qualified name of each server in the pool.
+It **must** include the server listed in *Servers* above, even if by a different hostname.
+Servers should be defined one per line and consist of a server address and optionally a port.
+If no port is provided then the default port (11211) is used.
+
+For example:
+<pre>
+server.url.com
+ipaddress:port
 </pre>';
 $string['testservers'] = 'Test servers';
 $string['testservers_desc'] = 'The test servers get used for unit tests and for performance tests. It is entirely optional to set up test servers. Servers should be defined one per line and consist of a server address and optionally a port and weight.
index 42db75b..3a7da5f 100644 (file)
@@ -87,6 +87,24 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      */
     protected $definition;
 
+    /**
+     * Set to true when this store is clustered.
+     * @var bool
+     */
+    protected $clustered = false;
+
+    /**
+     * Array of servers to set when in clustered mode.
+     * @var array
+     */
+    protected $setservers = array();
+
+    /**
+     * The an array of memcache connections for the set servers, once established.
+     * @var array
+     */
+    protected $setconnections = array();
+
     /**
      * Default prefix for key names.
      * @var string
@@ -123,6 +141,30 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
             }
             $this->servers[] = $server;
         }
+
+        $this->clustered = array_key_exists('clustered', $configuration) ? (bool)$configuration['clustered'] : false;
+
+        if ($this->clustered) {
+            if (!array_key_exists('setservers', $configuration) || (count($configuration['setservers']) < 1)) {
+                // Can't setup clustering without set servers.
+                return;
+            }
+            if (count($this->servers) !== 1) {
+                // Can only setup cluster with exactly 1 get server.
+                return;
+            }
+            foreach ($configuration['setservers'] as $server) {
+                // We do not use weights (3rd part) on these servers.
+                if (!is_array($server)) {
+                    $server = explode(':', $server, 3);
+                }
+                if (!array_key_exists(1, $server)) {
+                    $server[1] = 11211;
+                }
+                $this->setservers[] = $server;
+            }
+        }
+
         if (empty($configuration['prefix'])) {
             $this->prefix = self::DEFAULT_PREFIX;
         } else {
@@ -133,6 +175,16 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         foreach ($this->servers as $server) {
             $this->connection->addServer($server[0], (int) $server[1], true, (int) $server[2]);
         }
+
+        if ($this->clustered) {
+            foreach ($this->setservers as $setserver) {
+                // Since we will have a number of them with the same name, append server and port.
+                $connection = new Memcache;
+                $connection->addServer($setserver[0], $setserver[1]);
+                $this->setconnections[] = $connection;
+            }
+        }
+
         // Test the connection to the pool of servers.
         $this->isready = @$this->connection->set($this->parse_key('ping'), 'ping', MEMCACHE_COMPRESSED, 1);
     }
@@ -280,6 +332,15 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return bool True if the operation was a success false otherwise.
      */
     public function set($key, $data) {
+        if ($this->clustered) {
+            $status = true;
+            foreach ($this->setconnections as $connection) {
+                $status = $connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl())
+                        && $status;
+            }
+            return $status;
+        }
+
         return $this->connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
     }
 
@@ -294,7 +355,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
     public function set_many(array $keyvaluearray) {
         $count = 0;
         foreach ($keyvaluearray as $pair) {
-            if ($this->connection->set($this->parse_key($pair['key']), $pair['value'], MEMCACHE_COMPRESSED, $this->definition->get_ttl())) {
+            if ($this->set($pair['key'], $pair['value'])) {
                 $count++;
             }
         }
@@ -308,6 +369,14 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return bool Returns true if the operation was a success, false otherwise.
      */
     public function delete($key) {
+        if ($this->clustered) {
+            $status = true;
+            foreach ($this->setconnections as $connection) {
+                $status = $connection->delete($this->parse_key($key)) && $status;
+            }
+            return $status;
+        }
+
         return $this->connection->delete($this->parse_key($key));
     }
 
@@ -334,7 +403,13 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      */
     public function purge() {
         if ($this->isready) {
-            $this->connection->flush();
+            if ($this->clustered) {
+                foreach ($this->setconnections as $connection) {
+                    $connection->flush();
+                }
+            } else {
+                $this->connection->flush();
+            }
         }
 
         return true;
@@ -358,9 +433,33 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
             }
             $servers[] = explode(':', $line, 3);
         }
+
+        $clustered = false;
+        if (isset($data->clustered)) {
+            $clustered = true;
+        }
+
+        $lines = explode("\n", $data->setservers);
+        $setservers = array();
+        foreach ($lines as $line) {
+            // Trim surrounding colons and default whitespace.
+            $line = trim(trim($line), ":");
+            if ($line === '') {
+                continue;
+            }
+            $setserver = explode(':', $line, 3);
+            // We don't use weights, so display a debug message.
+            if (count($setserver) > 2) {
+                debugging('Memcache Set Server '.$setserver[0].' has too many parameters.');
+            }
+            $setservers[] = $setserver;
+        }
+
         return array(
             'servers' => $servers,
             'prefix' => $data->prefix,
+            'clustered' => $clustered,
+            'setservers' => $setservers
         );
     }
 
@@ -384,6 +483,16 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         } else {
             $data['prefix'] = self::DEFAULT_PREFIX;
         }
+        if (isset($config['clustered'])) {
+            $data['clustered'] = (bool)$config['clustered'];
+        }
+        if (!empty($config['setservers'])) {
+            $servers = array();
+            foreach ($config['setservers'] as $server) {
+                $servers[] = join(":", $server);
+            }
+            $data['setservers'] = join("\n", $servers);
+        }
 
         $editform->set_data($data);
     }
@@ -423,6 +532,12 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
 
         $configuration = array();
         $configuration['servers'] = explode("\n", $config->testservers);
+        if (!empty($config->testclustered)) {
+            $configuration['clustered'] = $config->testclustered;
+        }
+        if (!empty($config->testsetservers)) {
+            $configuration['setservers'] = explode("\n", $config->testsetservers);
+        }
 
         $store = new cachestore_memcache('Test memcache', $configuration);
         $store->initialise($definition);
index f8229c5..26d8984 100644 (file)
@@ -30,4 +30,4 @@ $settings->add(new admin_setting_configtextarea(
         'cachestore_memcache/testservers',
         new lang_string('testservers', 'cachestore_memcache'),
         new lang_string('testservers_desc', 'cachestore_memcache'),
-        '', PARAM_RAW, 60, 3));
\ No newline at end of file
+        '', PARAM_RAW, 60, 3));
index 62b52f4..63d366c 100644 (file)
@@ -54,6 +54,8 @@ class cachestore_memcache_test extends cachestore_tests {
      * Tests the valid keys to ensure they work.
      */
     public function test_valid_keys() {
+        $this->resetAfterTest(true);
+
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
         $instance = cachestore_memcache::initialise_unit_test_instance($definition);
 
@@ -69,15 +71,197 @@ class cachestore_memcache_test extends cachestore_tests {
             // Underscores.
             'a_1', '1_a', '_a1', 'a1_'
         );
+
+        // Set some keys.
         foreach ($keys as $key) {
             $this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
         }
+
+        // Get some keys.
         foreach ($keys as $key) {
             $this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
         }
+
+        // Try get many.
         $values = $instance->get_many($keys);
         foreach ($values as $key => $value) {
             $this->assertEquals($key, $value);
         }
+
+        // Reset a key.
+        $this->assertTrue($instance->set($keys[0], 'New'), "Failed to reset key `$key`");
+        $this->assertEquals('New', $instance->get($keys[0]), "Failed to get reset key `$key`");
+
+        // Delete and check that we can't retrieve.
+        foreach ($keys as $key) {
+            $this->assertTrue($instance->delete($key), "Failed to delete key `$key`");
+            $this->assertFalse($instance->get($key), "Retrieved deleted key `$key`");
+        }
+
+        // Try set many, and check that count is correct.
+        $many = array();
+        foreach ($keys as $key) {
+            $many[] = array('key' => $key, 'value' => $key);
+        }
+        $returncount = $instance->set_many($many);
+        $this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
+
+        // Check keys retrieved with get_many.
+        $values = $instance->get_many($keys);
+        foreach ($keys as $key) {
+            $this->assertTrue(isset($values[$key]), "Failed to get_many key `$key`");
+            $this->assertEquals($key, $values[$key], "Failed to match get_many key `$key`");
+        }
+
+        // Delete many, make sure count matches.
+        $returncount = $instance->delete_many($keys);
+        $this->assertEquals(count($many), $returncount, 'Delete many count didn\'t match');
+
+        // Check that each key was deleted.
+        foreach ($keys as $key) {
+            $this->assertFalse($instance->get($key), "Retrieved many deleted key `$key`");
+        }
+
+        // Set the keys again.
+        $returncount = $instance->set_many($many);
+        $this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
+
+        // Purge.
+        $this->assertTrue($instance->purge(), 'Failure to purge');
+
+        // Delete and check that we can't retrieve.
+        foreach ($keys as $key) {
+            $this->assertFalse($instance->get($key), "Retrieved purged key `$key`");
+        }
+    }
+
+    /**
+     * Tests the clustering feature.
+     */
+    public function test_clustered() {
+        $this->resetAfterTest(true);
+
+        if (!defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS')) {
+            $this->markTestSkipped();
+        }
+
+        $testservers = explode("\n", trim(TEST_CACHESTORE_MEMCACHE_TESTSERVERS));
+
+        if (count($testservers) < 2) {
+            $this->markTestSkipped();
+        }
+
+        // User the first server as our primary.
+        set_config('testservers', $testservers[0], 'cachestore_memcache');
+        set_config('testsetservers', TEST_CACHESTORE_MEMCACHE_TESTSERVERS, 'cachestore_memcache');
+        set_config('testclustered', true, 'cachestore_memcache');
+
+        // First and instance that we can use to test the second server.
+        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
+        $instance = cachestore_memcache::initialise_test_instance($definition);
+
+        if (!$instance) {
+            $this->markTestSkipped();
+        }
+
+        // Now we are going to setup a connection to each independent server.
+        set_config('testclustered', false, 'cachestore_memcache');
+        set_config('testsetservers', '', 'cachestore_memcache');
+        $checkinstances = array();
+        foreach ($testservers as $testserver) {
+            set_config('testservers', $testserver, 'cachestore_memcache');
+            $checkinstance = cachestore_memcache::initialise_test_instance($definition);
+            if (!$checkinstance) {
+                $this->markTestSkipped();
+            }
+            $checkinstances[] = $checkinstance;
+        }
+
+        $keys = array(
+            // Alphanumeric.
+            'abc', 'ABC', '123', 'aB1', '1aB',
+            // Hyphens.
+            'a-1', '1-a', '-a1', 'a1-',
+            // Underscores.
+            'a_1', '1_a', '_a1', 'a1_'
+        );
+
+        // Set each key.
+        foreach ($keys as $key) {
+            $this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
+        }
+
+        // Check each key.
+        foreach ($keys as $key) {
+            $this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
+            foreach ($checkinstances as $id => $checkinstance) {
+                $this->assertEquals($key, $checkinstance->get($key), "Failed to get key `$key` from server $id");
+            }
+        }
+
+        // Reset a key.
+        $this->assertTrue($instance->set($keys[0], 'New'), "Failed to reset key `$key`");
+        $this->assertEquals('New', $instance->get($keys[0]), "Failed to get reset key `$key`");
+        foreach ($checkinstances as $id => $checkinstance) {
+            $this->assertEquals('New', $checkinstance->get($keys[0]), "Failed to get reset key `$key` from server $id");
+        }
+
+        // Delete and check that we can't retrieve.
+        foreach ($keys as $key) {
+            $this->assertTrue($instance->delete($key), "Failed to delete key `$key`");
+            $this->assertFalse($instance->get($key), "Retrieved deleted key `$key`");
+            foreach ($checkinstances as $id => $checkinstance) {
+                $this->assertFalse($checkinstance->get($key), "Retrieved deleted key `$key` from server $id");
+            }
+        }
+
+        // Try set many, and check that count is correct.
+        $many = array();
+        foreach ($keys as $key) {
+            $many[] = array('key' => $key, 'value' => $key);
+        }
+        $returncount = $instance->set_many($many);
+        $this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
+
+        // Check keys retrieved with get_many.
+        $values = $instance->get_many($keys);
+        foreach ($keys as $key) {
+            $this->assertTrue(isset($values[$key]), "Failed to get_many key `$key`");
+            $this->assertEquals($key, $values[$key], "Failed to match get_many key `$key`");
+        }
+        foreach ($checkinstances as $id => $checkinstance) {
+            $values = $checkinstance->get_many($keys);
+            foreach ($keys as $key) {
+                $this->assertTrue(isset($values[$key]), "Failed to get_many key `$key` from server $id");
+                $this->assertEquals($key, $values[$key], "Failed to get_many key `$key` from server $id");
+            }
+        }
+
+        // Delete many, make sure count matches.
+        $returncount = $instance->delete_many($keys);
+        $this->assertEquals(count($many), $returncount, 'Delete many count didn\'t match');
+
+        // Check that each key was deleted.
+        foreach ($keys as $key) {
+            $this->assertFalse($instance->get($key), "Retrieved many deleted key `$key`");
+            foreach ($checkinstances as $id => $checkinstance) {
+                $this->assertFalse($checkinstance->get($key), "Retrieved many deleted key `$key` from server $id");
+            }
+        }
+
+        // Set the keys again.
+        $returncount = $instance->set_many($many);
+        $this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
+
+        // Purge.
+        $this->assertTrue($instance->purge(), 'Failure to purge');
+
+        // Delete and check that we can't retrieve.
+        foreach ($keys as $key) {
+            $this->assertFalse($instance->get($key), "Retrieved purged key `$key`");
+            foreach ($checkinstances as $id => $checkinstance) {
+                $this->assertFalse($checkinstance->get($key), "Retrieved purged key `$key` from server 2");
+            }
+        }
     }
 }
index c833a18..12bfbbc 100644 (file)
@@ -74,5 +74,54 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form
         $form->addHelpButton('bufferwrites', 'bufferwrites', 'cachestore_memcached');
         $form->setDefault('bufferwrites', 0);
         $form->setType('bufferwrites', PARAM_BOOL);
+
+        $form->addElement('header', 'clusteredheader', get_string('clustered', 'cachestore_memcached'));
+
+        $form->addElement('checkbox', 'clustered', get_string('clustered', 'cachestore_memcached'));
+        $form->setDefault('checkbox', false);
+        $form->addHelpButton('clustered', 'clustered', 'cachestore_memcached');
+
+        $form->addElement('textarea', 'setservers', get_string('setservers', 'cachestore_memcached'),
+                array('cols' => 75, 'rows' => 5));
+        $form->addHelpButton('setservers', 'setservers', 'cachestore_memcached');
+        $form->disabledIf('setservers', 'clustered');
+        $form->setType('setservers', PARAM_RAW);
+    }
+
+    /**
+     * Perform minimal validation on the settings form.
+     *
+     * @param array $data
+     * @param array $files
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        if (isset($data['clustered']) && ($data['clustered'] == 1)) {
+            // Set servers is required with in cluster mode.
+            if (!isset($data['setservers'])) {
+                $errors['setservers'] = get_string('required');
+            } else {
+                $trimmed = trim($data['setservers']);
+                if (empty($trimmed)) {
+                    $errors['setservers'] = get_string('required');
+                }
+            }
+
+            $validservers = false;
+            if (isset($data['servers'])) {
+                $servers = trim($data['servers']);
+                $servers = explode("\n", $servers);
+                if (count($servers) === 1) {
+                    $validservers = true;
+                }
+            }
+
+            if (!$validservers) {
+                $errors['servers'] = get_string('serversclusterinvalid', 'cachestore_memcached');
+            }
+        }
+
+        return $errors;
     }
 }
index 87713f6..5d35c1c 100644 (file)
@@ -28,6 +28,13 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['bufferwrites'] = 'Buffer writes';
 $string['bufferwrites_help'] = 'Enables or disables buffered I/O. Enabling buffered I/O causes storage commands to "buffer" instead of being sent. Any action that retrieves data causes this buffer to be sent to the remote connection. Quitting the connection or closing down the connection will also cause the buffered data to be pushed to the remote connection.';
+$string['clustered'] = 'Enable clustered servers';
+$string['clustered_help'] = 'This is used to allow read-one, set-multi functionality.
+
+The intended use case is to create an improved store for load-balanced configurations. The store will fetch from one server (usually localhost), but set to many (all the servers in the load-balance pool). For caches with very high read to set ratios, this saves a significant amount of network overhead.
+
+When this setting is enabled, the server listed above will be used for fetching.';
+$string['clusteredheader'] = 'Split servers';
 $string['hash'] = 'Hash method';
 $string['hash_help'] = 'Specifies the hashing algorithm used for the item keys. Each hash algorithm has its advantages and its disadvantages. Go with the default if you don\'t know or don\'t care.';
 $string['hash_default'] = 'Default (one-at-a-time)';
@@ -56,6 +63,20 @@ For example:
 server.url.com
 ipaddress:port
 servername:port:weight
+</pre>
+
+If *Enable clustered servers* is enabled below, there must be only one server listed here. This would usually be a name that always resolves to the local manchine, like 127.0.0.1 or localhost.';
+$string['serversclusterinvalid'] = 'Exactly one server is required when clustering is enabled.';
+$string['setservers'] = 'Set Servers';
+$string['setservers_help'] = 'This is the list of servers that will updated when data is modified in the cache. Generally the fully qualified name of each server in the pool.
+It **must** include the server listed in *Servers* above, even if by a different hostname.
+Servers should be defined one per line and consist of a server address and optionally a port.
+If no port is provided then the default port (11211) is used.
+
+For example:
+<pre>
+server.url.com
+ipaddress:port
 </pre>';
 $string['testservers'] = 'Test servers';
 $string['testservers_desc'] = 'The test servers get used for unit tests and for performance tests. It is entirely optional to set up test servers. Servers should be defined one per line and consist of a server address and optionally a port and weight.