Merge branch 'MDL-51415-master' of git://github.com/jleyva/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 30 Sep 2015 06:15:29 +0000 (14:15 +0800)
committerDavid Monllao <davidm@moodle.com>
Wed, 30 Sep 2015 06:15:29 +0000 (14:15 +0800)
353 files changed:
.jshintrc
admin/cli/install.php
admin/index.php
admin/renderer.php
admin/settings/courses.php
admin/tool/task/cli/schedule_task.php
admin/tool/templatelibrary/amd/build/display.min.js
admin/tool/templatelibrary/amd/build/search.min.js
admin/tool/templatelibrary/amd/src/display.js
admin/tool/templatelibrary/amd/src/search.js
admin/user.php
backup/upgrade.txt
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/tests/cronhelper_test.php
badges/backpack_form.php
badges/backpackconnect.php
blocks/activity_results/styles.css
cohort/lib.php
config-dist.php
course/delete.php
course/editsection.php
course/externallib.php
course/format/renderer.php
course/format/topics/format.js
course/format/topics/lang/en/format_topics.php
course/format/topics/renderer.php
course/format/topics/styles.css
course/format/topics/tests/behat/edit_delete_sections.feature
course/format/upgrade.txt
course/format/weeks/format.js
course/format/weeks/lang/en/format_weeks.php
course/format/weeks/styles.css
course/format/weeks/tests/behat/edit_delete_sections.feature
course/lib.php
course/tests/behat/behat_course.php
course/tests/behat/create_delete_course.feature
course/tests/courselib_test.php
course/tests/externallib_test.php
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js
course/yui/src/dragdrop/js/section.js
course/yui/src/toolboxes/js/section.js
enrol/cohort/edit.php
enrol/flatfile/lib.php
enrol/guest/lib.php
enrol/manual/edit.php
enrol/meta/classes/observer.php
enrol/meta/db/events.php
enrol/meta/tests/plugin_test.php
enrol/meta/version.php
enrol/paypal/edit.php
enrol/self/classes/empty_form.php [new file with mode: 0644]
enrol/self/edit.php
enrol/self/lib.php
enrol/tests/enrollib_test.php
enrol/upgrade.txt
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js
filter/glossary/yui/src/autolinker/js/autolinker.js
grade/import/direct/index.php
grade/lib.php
install/stringnames.txt
lang/en/admin.php
lang/en/backup.php
lang/en/enrol.php
lang/en/message.php
lang/en/moodle.php
lang/en/my.php
lang/en/plugin.php
lib/amd/build/event.min.js [new file with mode: 0644]
lib/amd/build/first.min.js
lib/amd/build/templates.min.js
lib/amd/src/event.js [new file with mode: 0644]
lib/amd/src/first.js
lib/amd/src/templates.js
lib/badgeslib.php
lib/blocklib.php
lib/classes/event/enrol_instance_created.php [new file with mode: 0644]
lib/classes/event/enrol_instance_deleted.php [new file with mode: 0644]
lib/classes/event/enrol_instance_updated.php [new file with mode: 0644]
lib/classes/event/message_deleted.php [new file with mode: 0644]
lib/classes/message/inbound/handler.php
lib/classes/plugin_manager.php
lib/clilib.php
lib/cronlib.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
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/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/commands.js
lib/enrollib.php
lib/grade/grade_category.php
lib/grade/grade_item.php
lib/installlib.php
lib/javascript-static.js
lib/myprofilelib.php
lib/navigationlib.php
lib/outputrequirementslib.php
lib/tests/behat/behat_general.php
lib/tests/blocklib_test.php
lib/tests/fixtures/messageinbound/gmail.test [new file with mode: 0644]
lib/tests/fixtures/messageinbound/outlook.test
lib/tests/messageinbound_test.php
lib/tests/navigationlib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.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/src/dragdrop/js/dragdrop.js
lib/yui/src/notification/js/dialogue.js
message/lib.php
message/tests/events_test.php
mod/assign/lib.php
mod/assign/tests/lib_test.php
mod/book/classes/external.php
mod/book/db/services.php
mod/book/tests/externallib_test.php
mod/book/version.php
mod/choice/classes/external.php
mod/choice/db/services.php
mod/choice/tests/externallib_test.php
mod/choice/version.php
mod/data/backup/moodle2/backup_data_stepslib.php
mod/data/classes/external.php
mod/data/db/install.xml
mod/data/db/upgrade.php
mod/data/edit.php
mod/data/field/latlong/field.class.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/mod_form.php
mod/data/tests/behat/manageapproved.feature [new file with mode: 0644]
mod/data/tests/behat/required_entries.feature
mod/data/tests/externallib_test.php
mod/data/tests/lib_test.php
mod/data/version.php
mod/data/view.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/renderer.php
mod/forum/styles.css
mod/forum/tests/behat/posts_ordering_blog.feature [new file with mode: 0644]
mod/forum/tests/behat/posts_ordering_general.feature [new file with mode: 0644]
mod/forum/tests/behat/timed_discussions.feature [new file with mode: 0644]
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/upgrade.txt
mod/forum/view.php
mod/imscp/classes/external.php
mod/imscp/db/services.php
mod/imscp/tests/externallib_test.php
mod/imscp/version.php
mod/lti/launch.php
mod/lti/locallib.php
mod/lti/service/memberships/classes/local/resource/contextmemberships.php [new file with mode: 0644]
mod/lti/service/memberships/classes/local/resource/linkmemberships.php [new file with mode: 0644]
mod/lti/service/memberships/classes/local/service/memberships.php [new file with mode: 0644]
mod/lti/service/memberships/lang/en/ltiservice_memberships.php [new file with mode: 0644]
mod/lti/service/memberships/version.php [new file with mode: 0644]
mod/lti/tests/locallib_test.php
mod/lti/view.php
mod/quiz/locallib.php
mod/scorm/classes/external.php
mod/scorm/db/services.php
mod/scorm/tests/externallib_test.php
mod/scorm/version.php
mod/workshop/form/edit_form.php
mod/workshop/form/rubric/edit_form.php
mod/workshop/form/rubric/lang/en/workshopform_rubric.php
my/indexsys.php
my/lib.php
my/tests/behat/reset_all_pages.feature [new file with mode: 0644]
pix/i/delete.png [new file with mode: 0644]
pix/i/delete.svg [new file with mode: 0644]
question/behaviour/manualgraded/tests/walkthrough_test.php
question/classes/bank/view.php
question/engine/lib.php
question/engine/tests/questionengine_test.php
question/tests/behat/delete_questions.feature
question/type/ddimageortext/backup/moodle2/backup_qtype_ddimageortext_plugin.class.php [new file with mode: 0644]
question/type/ddimageortext/backup/moodle2/restore_qtype_ddimageortext_plugin.class.php [new file with mode: 0644]
question/type/ddimageortext/db/install.xml [new file with mode: 0644]
question/type/ddimageortext/edit_ddimageortext_form.php [new file with mode: 0644]
question/type/ddimageortext/edit_ddtoimage_form_base.php [new file with mode: 0644]
question/type/ddimageortext/lang/en/qtype_ddimageortext.php [new file with mode: 0644]
question/type/ddimageortext/lib.php [new file with mode: 0644]
question/type/ddimageortext/pix/icon.png [new file with mode: 0644]
question/type/ddimageortext/question.php [new file with mode: 0644]
question/type/ddimageortext/questionbase.php [new file with mode: 0644]
question/type/ddimageortext/questiontype.php [new file with mode: 0644]
question/type/ddimageortext/questiontypebase.php [new file with mode: 0644]
question/type/ddimageortext/renderer.php [new file with mode: 0644]
question/type/ddimageortext/rendererbase.php [new file with mode: 0644]
question/type/ddimageortext/styles.css [new file with mode: 0644]
question/type/ddimageortext/tests/behat/add.feature [new file with mode: 0644]
question/type/ddimageortext/tests/behat/backup_and_restore.feature [new file with mode: 0644]
question/type/ddimageortext/tests/behat/behat_qtype_ddimageortext.php [new file with mode: 0644]
question/type/ddimageortext/tests/behat/edit.feature [new file with mode: 0644]
question/type/ddimageortext/tests/behat/export.feature [new file with mode: 0644]
question/type/ddimageortext/tests/behat/import.feature [new file with mode: 0644]
question/type/ddimageortext/tests/behat/preview.feature [new file with mode: 0644]
question/type/ddimageortext/tests/fixtures/oceanflooranswer.jpg [new file with mode: 0644]
question/type/ddimageortext/tests/fixtures/oceanfloorbase.jpg [new file with mode: 0644]
question/type/ddimageortext/tests/fixtures/testquestion.moodle.xml [new file with mode: 0644]
question/type/ddimageortext/tests/helper.php [new file with mode: 0644]
question/type/ddimageortext/tests/question_test.php [new file with mode: 0644]
question/type/ddimageortext/tests/questiontype_test.php [new file with mode: 0644]
question/type/ddimageortext/tests/walkthrough_test.php [new file with mode: 0644]
question/type/ddimageortext/version.php [new file with mode: 0644]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd-debug.js [new file with mode: 0644]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd-min.js [new file with mode: 0644]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-dd/moodle-qtype_ddimageortext-dd.js [new file with mode: 0644]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-form/moodle-qtype_ddimageortext-form-debug.js [new file with mode: 0644]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-form/moodle-qtype_ddimageortext-form-min.js [new file with mode: 0644]
question/type/ddimageortext/yui/build/moodle-qtype_ddimageortext-form/moodle-qtype_ddimageortext-form.js [new file with mode: 0644]
question/type/ddimageortext/yui/src/ddimageortext/build.json [new file with mode: 0644]
question/type/ddimageortext/yui/src/ddimageortext/js/ddimageortext.js [new file with mode: 0644]
question/type/ddimageortext/yui/src/ddimageortext/meta/ddimageortext.json [new file with mode: 0644]
question/type/ddimageortext/yui/src/form/build.json [new file with mode: 0644]
question/type/ddimageortext/yui/src/form/js/form.js [new file with mode: 0644]
question/type/ddimageortext/yui/src/form/meta/form.json [new file with mode: 0644]
question/type/ddmarker/backup/moodle2/backup_qtype_ddmarker_plugin.class.php [new file with mode: 0644]
question/type/ddmarker/backup/moodle2/restore_qtype_ddmarker_plugin.class.php [new file with mode: 0644]
question/type/ddmarker/db/install.xml [new file with mode: 0644]
question/type/ddmarker/db/upgrade.php [new file with mode: 0644]
question/type/ddmarker/edit_ddmarker_form.php [new file with mode: 0644]
question/type/ddmarker/lang/en/qtype_ddmarker.php [new file with mode: 0644]
question/type/ddmarker/lib.php [new file with mode: 0644]
question/type/ddmarker/pix/crosshairs.png [new file with mode: 0644]
question/type/ddmarker/pix/crosshairs.xcf [new file with mode: 0644]
question/type/ddmarker/pix/grid.png [new file with mode: 0644]
question/type/ddmarker/pix/grid.xcf [new file with mode: 0644]
question/type/ddmarker/pix/icon.png [new file with mode: 0644]
question/type/ddmarker/question.php [new file with mode: 0644]
question/type/ddmarker/questiontype.php [new file with mode: 0644]
question/type/ddmarker/renderer.php [new file with mode: 0644]
question/type/ddmarker/shapes.php [new file with mode: 0644]
question/type/ddmarker/styles.css [new file with mode: 0644]
question/type/ddmarker/tests/behat/add.feature [new file with mode: 0644]
question/type/ddmarker/tests/behat/backup_and_restore.feature [new file with mode: 0644]
question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php [new file with mode: 0644]
question/type/ddmarker/tests/behat/edit.feature [new file with mode: 0644]
question/type/ddmarker/tests/behat/export.feature [new file with mode: 0644]
question/type/ddmarker/tests/behat/import.feature [new file with mode: 0644]
question/type/ddmarker/tests/behat/preview.feature [new file with mode: 0644]
question/type/ddmarker/tests/fixtures/mkmap.png [new file with mode: 0644]
question/type/ddmarker/tests/fixtures/testquestion.moodle.xml [new file with mode: 0644]
question/type/ddmarker/tests/helper.php [new file with mode: 0644]
question/type/ddmarker/tests/question_test.php [new file with mode: 0644]
question/type/ddmarker/tests/questiontype_test.php [new file with mode: 0644]
question/type/ddmarker/tests/shapes_test.php [new file with mode: 0644]
question/type/ddmarker/tests/walkthrough_test.php [new file with mode: 0644]
question/type/ddmarker/version.php [new file with mode: 0644]
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-dd/moodle-qtype_ddmarker-dd-debug.js [new file with mode: 0644]
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-dd/moodle-qtype_ddmarker-dd-min.js [new file with mode: 0644]
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-dd/moodle-qtype_ddmarker-dd.js [new file with mode: 0644]
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form-debug.js [new file with mode: 0644]
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form-min.js [new file with mode: 0644]
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form.js [new file with mode: 0644]
question/type/ddmarker/yui/src/ddmarker/build.json [new file with mode: 0644]
question/type/ddmarker/yui/src/ddmarker/js/ddmarker.js [new file with mode: 0644]
question/type/ddmarker/yui/src/ddmarker/meta/ddmarker.json [new file with mode: 0644]
question/type/ddmarker/yui/src/form/build.json [new file with mode: 0644]
question/type/ddmarker/yui/src/form/js/form.js [new file with mode: 0644]
question/type/ddmarker/yui/src/form/meta/form.json [new file with mode: 0644]
question/type/ddwtos/backup/moodle2/backup_qtype_ddwtos_plugin.class.php [new file with mode: 0644]
question/type/ddwtos/backup/moodle2/restore_qtype_ddwtos_plugin.class.php [new file with mode: 0644]
question/type/ddwtos/db/install.xml [new file with mode: 0644]
question/type/ddwtos/edit_ddwtos_form.php [new file with mode: 0644]
question/type/ddwtos/lang/en/qtype_ddwtos.php [new file with mode: 0644]
question/type/ddwtos/lib.php [new file with mode: 0644]
question/type/ddwtos/pix/icon.png [new file with mode: 0644]
question/type/ddwtos/question.php [new file with mode: 0644]
question/type/ddwtos/questiontype.php [new file with mode: 0644]
question/type/ddwtos/renderer.php [new file with mode: 0644]
question/type/ddwtos/styles.css [new file with mode: 0644]
question/type/ddwtos/tests/behat/add.feature [new file with mode: 0644]
question/type/ddwtos/tests/behat/backup_and_restore.feature [new file with mode: 0644]
question/type/ddwtos/tests/behat/behat_qtype_ddwtos.php [new file with mode: 0644]
question/type/ddwtos/tests/behat/edit.feature [new file with mode: 0644]
question/type/ddwtos/tests/behat/export.feature [new file with mode: 0644]
question/type/ddwtos/tests/behat/import.feature [new file with mode: 0644]
question/type/ddwtos/tests/behat/preview.feature [new file with mode: 0644]
question/type/ddwtos/tests/fixtures/testquestion.moodle.xml [new file with mode: 0644]
question/type/ddwtos/tests/helper.php [new file with mode: 0644]
question/type/ddwtos/tests/question_test.php [new file with mode: 0644]
question/type/ddwtos/tests/questiontype_test.php [new file with mode: 0644]
question/type/ddwtos/tests/walkthrough_test.php [new file with mode: 0644]
question/type/ddwtos/version.php [new file with mode: 0644]
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-debug.js [new file with mode: 0644]
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd-min.js [new file with mode: 0644]
question/type/ddwtos/yui/build/moodle-qtype_ddwtos-dd/moodle-qtype_ddwtos-dd.js [new file with mode: 0644]
question/type/ddwtos/yui/src/ddwtos/build.json [new file with mode: 0644]
question/type/ddwtos/yui/src/ddwtos/js/ddwtos.js [new file with mode: 0644]
question/type/ddwtos/yui/src/ddwtos/meta/ddwtos.json [new file with mode: 0644]
question/type/gapselect/backup/moodle2/backup_qtype_gapselect_plugin.class.php [new file with mode: 0644]
question/type/gapselect/backup/moodle2/restore_qtype_gapselect_plugin.class.php [new file with mode: 0644]
question/type/gapselect/db/install.xml [new file with mode: 0644]
question/type/gapselect/edit_form_base.php [new file with mode: 0644]
question/type/gapselect/edit_gapselect_form.php [new file with mode: 0644]
question/type/gapselect/lang/en/qtype_gapselect.php [new file with mode: 0644]
question/type/gapselect/lib.php [new file with mode: 0644]
question/type/gapselect/pix/icon.png [new file with mode: 0644]
question/type/gapselect/question.php [new file with mode: 0644]
question/type/gapselect/questionbase.php [new file with mode: 0644]
question/type/gapselect/questiontype.php [new file with mode: 0644]
question/type/gapselect/questiontypebase.php [new file with mode: 0644]
question/type/gapselect/renderer.php [new file with mode: 0644]
question/type/gapselect/rendererbase.php [new file with mode: 0644]
question/type/gapselect/styles.css [new file with mode: 0644]
question/type/gapselect/tests/behat/basic_test.feature [new file with mode: 0644]
question/type/gapselect/tests/behat/behat_qtype_gapselect.php [new file with mode: 0644]
question/type/gapselect/tests/behat/import_test.feature [new file with mode: 0644]
question/type/gapselect/tests/edit_form_test.php [new file with mode: 0644]
question/type/gapselect/tests/fixtures/testquestion.moodle.xml [new file with mode: 0644]
question/type/gapselect/tests/helper.php [new file with mode: 0644]
question/type/gapselect/tests/question_test.php [new file with mode: 0644]
question/type/gapselect/tests/questiontype_test.php [new file with mode: 0644]
question/type/gapselect/tests/walkthrough_test.php [new file with mode: 0644]
question/type/gapselect/version.php [new file with mode: 0644]
tag/tests/behat/edit_tag.feature
theme/base/style/core.css
theme/base/style/course.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css
user/externallib.php
user/lib.php
user/preferences.php
user/profilesys.php
user/tests/behat/view_preferences_page.feature [new file with mode: 0644]
version.php

index 8b8a806..ee94a05 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -35,7 +35,8 @@
     "plusplus":     false,
     "predef": [
         "M",
-        "define"
+        "define",
+        "require"
     ],
     "proto":        false,
     "regexdash":    false,
index 131056d..523f9e8 100644 (file)
@@ -74,6 +74,7 @@ Options:
 --adminpass=PASSWORD  Password for the moodle admin account,
                       required in non-interactive mode.
 --adminemail=STRING   Email address for the moodle admin account.
+--upgradekey=STRING   The upgrade key to be set in the config.php, leave empty to not set it.
 --non-interactive     No interactive questions, installation fails if any
                       problem encountered.
 --agree-license       Indicates agreement with software license,
@@ -258,6 +259,7 @@ list($options, $unrecognized) = cli_get_params(
         'adminuser'         => 'admin',
         'adminpass'         => '',
         'adminemail'        => '',
+        'upgradekey'        => '',
         'non-interactive'   => false,
         'agree-license'     => false,
         'allow-unstable'    => false,
@@ -273,7 +275,8 @@ $interactive = empty($options['non-interactive']);
 
 // set up language
 $lang = clean_param($options['lang'], PARAM_SAFEDIR);
-if (file_exists($CFG->dirroot.'/install/lang/'.$lang)) {
+$languages = get_string_manager()->get_list_of_translations();
+if (array_key_exists($lang, $languages)) {
     $CFG->lang = $lang;
 }
 
@@ -295,23 +298,34 @@ echo get_string('cliinstallheader', 'install', $CFG->target_release)."\n";
 //Fist select language
 if ($interactive) {
     cli_separator();
-    $languages = get_string_manager()->get_list_of_translations();
     // Do not put the langs into columns because it is not compatible with RTL.
-    $langlist = implode("\n", $languages);
     $default = $CFG->lang;
-    cli_heading(get_string('availablelangs', 'install'));
-    echo $langlist."\n";
+    cli_heading(get_string('chooselanguagehead', 'install'));
+    if (array_key_exists($default, $languages)) {
+        echo $default.' - '.$languages[$default]."\n";
+    }
+    if ($default !== 'en') {
+        echo 'en - English (en)'."\n";
+    }
+    echo '? - '.get_string('availablelangs', 'install')."\n";
     $prompt = get_string('clitypevaluedefault', 'admin', $CFG->lang);
     $error = '';
     do {
         echo $error;
         $input = cli_input($prompt, $default);
-        $input = clean_param($input, PARAM_SAFEDIR);
 
-        if (!file_exists($CFG->dirroot.'/install/lang/'.$input)) {
-            $error = get_string('cliincorrectvalueretry', 'admin')."\n";
+        if ($input === '?') {
+            echo implode("\n", $languages)."\n";
+            $error = "\n";
+
         } else {
-            $error = '';
+            $input = clean_param($input, PARAM_SAFEDIR);
+
+            if (!array_key_exists($input, $languages)) {
+                $error = get_string('cliincorrectvalueretry', 'admin')."\n";
+            } else {
+                $error = '';
+            }
         }
     } while ($error !== '');
     $CFG->lang = $input;
@@ -710,6 +724,24 @@ if (!empty($options['adminemail']) && !validate_email($options['adminemail'])) {
     cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
 }
 
+// Ask for the upgrade key.
+if ($interactive) {
+    cli_separator();
+    cli_heading(get_string('upgradekeyset', 'admin'));
+    if ($options['upgradekey'] !== '') {
+        $prompt = get_string('clitypevaluedefault', 'admin', $options['upgradekey']);
+        $options['upgradekey'] = cli_input($prompt, $options['upgradekey']);
+    } else {
+        $prompt = get_string('clitypevalue', 'admin');
+        $options['upgradekey'] = cli_input($prompt);
+    }
+}
+
+// Set the upgrade key if it was provided.
+if ($options['upgradekey'] !== '') {
+    $CFG->upgradekey = $options['upgradekey'];
+}
+
 if ($interactive) {
     if (!$options['agree-license']) {
         cli_separator();
index 1cea8f8..3e73aa4 100644 (file)
@@ -54,6 +54,16 @@ if (!function_exists('json_encode') || !function_exists('json_decode')) {
 
 define('NO_OUTPUT_BUFFERING', true);
 
+if (isset($_POST['upgradekey'])) {
+    // Before you start reporting issues about the collision attacks against
+    // SHA-1, you should understand that we are not actually attempting to do
+    // any cryptography here. This is hashed purely so that the key is not
+    // that apparent in the address bar itself. Anyone who catches the HTTP
+    // traffic can immediately use it as a valid admin key.
+    header('Location: index.php?cache=0&upgradekeyhash='.sha1($_POST['upgradekey']));
+    die();
+}
+
 if ((isset($_GET['cache']) and $_GET['cache'] === '0')
         or (isset($_POST['cache']) and $_POST['cache'] === '0')
         or (!isset($_POST['cache']) and !isset($_GET['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey']))) {
@@ -95,10 +105,14 @@ $showallplugins = optional_param('showallplugins', 0, PARAM_BOOL);
 $agreelicense   = optional_param('agreelicense', 0, PARAM_BOOL);
 $fetchupdates   = optional_param('fetchupdates', 0, PARAM_BOOL);
 $newaddonreq    = optional_param('installaddonrequest', null, PARAM_RAW);
+$upgradekeyhash = optional_param('upgradekeyhash', null, PARAM_ALPHANUM);
 
 // Set up PAGE.
 $url = new moodle_url('/admin/index.php');
 $url->param('cache', $cache);
+if (isset($upgradekeyhash)) {
+    $url->param('upgradekeyhash', $upgradekeyhash);
+}
 $PAGE->set_url($url);
 unset($url);
 
@@ -203,7 +217,7 @@ if (!core_tables_exist()) {
         $PAGE->set_heading($strinstallation . ' - Moodle ' . $CFG->target_release);
 
         $output = $PAGE->get_renderer('core', 'admin');
-        $url = new moodle_url('/admin/index.php', array('agreelicense' => 1, 'confirmrelease' => 1, 'lang' => $CFG->lang));
+        $url = new moodle_url($PAGE->url, array('agreelicense' => 1, 'confirmrelease' => 1, 'lang' => $CFG->lang));
         echo $output->unsatisfied_dependencies_page($version, $failed, $url);
         die();
     }
@@ -253,11 +267,13 @@ if (empty($CFG->version)) {
 // Detect config cache inconsistency, this happens when you switch branches on dev servers.
 if ($CFG->version != $DB->get_field('config', 'value', array('name'=>'version'))) {
     purge_all_caches();
-    redirect(new moodle_url('/admin/index.php'), 'Config cache inconsistency detected, resetting caches...');
+    redirect(new moodle_url($PAGE->url), 'Config cache inconsistency detected, resetting caches...');
 }
 
 if (!$cache and $version > $CFG->version) {  // upgrade
 
+    check_upgrade_key($upgradekeyhash);
+
     // Warning about upgrading a test site.
     $testsite = false;
     if (defined('BEHAT_SITE_RUNNING')) {
@@ -318,7 +334,7 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         $PAGE->set_heading($strplugincheck);
         $PAGE->set_cacheable(false);
 
-        $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
+        $reloadurl = new moodle_url($PAGE->url, array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
 
         if ($fetchupdates) {
             // No sesskey support guaranteed here, because sessions might not work yet.
@@ -342,15 +358,15 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         }
 
         echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
-                $version, $showallplugins, $reloadurl,
-                new moodle_url('/admin/index.php', array('confirmupgrade'=>1, 'confirmrelease'=>1, 'confirmplugincheck'=>1, 'cache'=>0)));
+                $version, $showallplugins, $reloadurl, new moodle_url($PAGE->url, array(
+                'confirmupgrade' => 1, 'confirmrelease' => 1, 'confirmplugincheck' => 1, 'cache' => 0)));
         die();
 
     } else {
         // Always verify plugin dependencies!
         $failed = array();
         if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
-            $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
+            $reloadurl = new moodle_url($PAGE->url, array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
             echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
             die();
         }
@@ -374,6 +390,9 @@ if (!$cache and $branch <> $CFG->branch) {  // Update the branch
 }
 
 if (!$cache and moodle_needs_upgrading()) {
+
+    check_upgrade_key($upgradekeyhash);
+
     if (!$PAGE->headerprinted) {
         // means core upgrade or installation was not already done
 
@@ -413,7 +432,7 @@ if (!$cache and moodle_needs_upgrading()) {
             echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
                     $version, $showallplugins,
                     new moodle_url($PAGE->url),
-                    new moodle_url('/admin/index.php', array('confirmplugincheck'=>1, 'cache'=>0)));
+                    new moodle_url($PAGE->url, array('confirmplugincheck' => 1, 'cache' => 0)));
             die();
         }
 
@@ -422,7 +441,7 @@ if (!$cache and moodle_needs_upgrading()) {
         if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
             /** @var core_admin_renderer $output */
             $output = $PAGE->get_renderer('core', 'admin');
-            $reloadurl = new moodle_url('/admin/index.php', array('cache' => 0));
+            $reloadurl = new moodle_url($PAGE->url, array('cache' => 0));
             echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
             die();
         }
index 5d75e70..40036da 100644 (file)
@@ -43,7 +43,8 @@ class core_admin_renderer extends plugin_renderer_base {
         $copyrightnotice = text_to_html(get_string('gpl3'));
         $copyrightnotice = str_replace('target="_blank"', 'onclick="this.target=\'_blank\'"', $copyrightnotice); // extremely ugly validation hack
 
-        $continue = new single_button(new moodle_url('/admin/index.php', array('lang'=>$CFG->lang, 'agreelicense'=>1)), get_string('continue'), 'get');
+        $continue = new single_button(new moodle_url($this->page->url, array(
+            'lang' => $CFG->lang, 'agreelicense' => 1)), get_string('continue'), 'get');
 
         $output .= $this->header();
         $output .= $this->heading('<a href="http://moodle.org">Moodle</a> - Modular Object-Oriented Dynamic Learning Environment');
@@ -96,10 +97,11 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->environment_check_table($envstatus, $environment_results);
 
         if (!$envstatus) {
-            $output .= $this->upgrade_reload(new moodle_url('/admin/index.php', array('agreelicense' => 1, 'lang' => $CFG->lang)));
+            $output .= $this->upgrade_reload(new moodle_url($this->page->url, array('agreelicense' => 1, 'lang' => $CFG->lang)));
         } else {
             $output .= $this->notification(get_string('environmentok', 'admin'), 'notifysuccess');
-            $output .= $this->continue_button(new moodle_url('/admin/index.php', array('agreelicense'=>1, 'confirmrelease'=>1, 'lang'=>$CFG->lang)));
+            $output .= $this->continue_button(new moodle_url($this->page->url, array(
+                'agreelicense' => 1, 'confirmrelease' => 1, 'lang' => $CFG->lang)));
         }
 
         $output .= $this->footer();
@@ -140,7 +142,7 @@ class core_admin_renderer extends plugin_renderer_base {
     public function upgrade_confirm_page($strnewversion, $maturity, $testsite) {
         $output = '';
 
-        $continueurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'cache' => 0));
+        $continueurl = new moodle_url($this->page->url, array('confirmupgrade' => 1, 'cache' => 0));
         $continue = new single_button($continueurl, get_string('continue'), 'get');
         $cancelurl = new moodle_url('/admin/index.php');
 
@@ -170,7 +172,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->environment_check_table($envstatus, $environment_results);
 
         if (!$envstatus) {
-            $output .= $this->upgrade_reload(new moodle_url('/admin/index.php'), array('confirmupgrade' => 1, 'cache' => 0));
+            $output .= $this->upgrade_reload(new moodle_url($this->page->url, array('confirmupgrade' => 1, 'cache' => 0)));
 
         } else {
             $output .= $this->notification(get_string('environmentok', 'admin'), 'notifysuccess');
@@ -179,7 +181,8 @@ class core_admin_renderer extends plugin_renderer_base {
                 $output .= $this->box(get_string('langpackwillbeupdated', 'admin'), 'generalbox', 'notice');
             }
 
-            $output .= $this->continue_button(new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0)));
+            $output .= $this->continue_button(new moodle_url($this->page->url, array(
+                'confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0)));
         }
 
         $output .= $this->footer();
@@ -991,7 +994,7 @@ class core_admin_renderer extends plugin_renderer_base {
             $out  = $this->output->container_start('nonehighlighted', 'plugins-check-info');
             $out .= $this->output->heading(get_string('nonehighlighted', 'core_plugin'));
             if (empty($options['full'])) {
-                $out .= html_writer::link(new moodle_url('/admin/index.php',
+                $out .= html_writer::link(new moodle_url($this->page->url,
                     array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
                     get_string('nonehighlightedinfo', 'core_plugin'));
             }
@@ -999,13 +1002,14 @@ class core_admin_renderer extends plugin_renderer_base {
 
         } else {
             $out  = $this->output->container_start('somehighlighted', 'plugins-check-info');
-            $out .= $this->output->heading(get_string('somehighlighted', 'core_plugin', $sumofhighlighted));
             if (empty($options['full'])) {
-                $out .= html_writer::link(new moodle_url('/admin/index.php',
+                $out .= $this->output->heading(get_string('somehighlighted', 'core_plugin', $sumofhighlighted));
+                $out .= html_writer::link(new moodle_url($this->page->url,
                     array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
                     get_string('somehighlightedinfo', 'core_plugin'));
             } else {
-                $out .= html_writer::link(new moodle_url('/admin/index.php',
+                $out .= $this->output->heading(get_string('somehighlightedall', 'core_plugin', $sumofhighlighted));
+                $out .= html_writer::link(new moodle_url($this->page->url,
                     array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 0, 'cache' => 0)),
                     get_string('somehighlightedonly', 'core_plugin'));
             }
@@ -1571,4 +1575,26 @@ class core_admin_renderer extends plugin_renderer_base {
 
         return $output;
     }
+
+    /**
+     * Render a simple page for providing the upgrade key.
+     *
+     * @param moodle_url|string $url
+     * @return string
+     */
+    public function upgradekey_form_page($url) {
+
+        $output = '';
+        $output .= $this->header();
+        $output .= $this->container_start('upgradekeyreq');
+        $output .= $this->heading(get_string('upgradekeyreq', 'core_admin'));
+        $output .= html_writer::start_tag('form', array('method' => 'POST', 'action' => $url));
+        $output .= html_writer::empty_tag('input', array('name' => 'upgradekey', 'type' => 'password'));
+        $output .= html_writer::empty_tag('input', array('value' => get_string('submit'), 'type' => 'submit'));
+        $output .= html_writer::end_tag('form');
+        $output .= $this->container_end();
+        $output .= $this->footer();
+
+        return $output;
+    }
 }
index 1442ab8..bd88630 100644 (file)
@@ -226,7 +226,8 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     );
     $temp->add(new admin_setting_configselect('backup/backup_auto_storage', new lang_string('automatedstorage', 'backup'), new lang_string('automatedstoragehelp', 'backup'), 0, $storageoptions));
     $temp->add(new admin_setting_special_backup_auto_destination());
-    $keepoptoins = array(
+
+    $maxkeptoptions = array(
         0 => new lang_string('all'), 1 => '1',
         2 => '2',
         5 => '5',
@@ -240,7 +241,44 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
         300 => '300',
         400 => '400',
         500 => '500');
-    $temp->add(new admin_setting_configselect('backup/backup_auto_keep', new lang_string('keep'), new lang_string('backupkeephelp'), 1, $keepoptoins));
+    $temp->add(new admin_setting_configselect('backup/backup_auto_max_kept', new lang_string('automatedmaxkept', 'backup'),
+            new lang_string('automatedmaxkepthelp', 'backup'), 1, $maxkeptoptions));
+
+    $automateddeletedaysoptions = array(
+        0 => new lang_string('never'),
+        1000 => new lang_string('numdays', '', 1000),
+        365  => new lang_string('numdays', '', 365),
+        180  => new lang_string('numdays', '', 180),
+        150  => new lang_string('numdays', '', 150),
+        120  => new lang_string('numdays', '', 120),
+        90   => new lang_string('numdays', '', 90),
+        60   => new lang_string('numdays', '', 60),
+        35   => new lang_string('numdays', '', 35),
+        10   => new lang_string('numdays', '', 10),
+        5    => new lang_string('numdays', '', 5),
+        2    => new lang_string('numdays', '', 2)
+    );
+    $temp->add(new admin_setting_configselect('backup/backup_auto_delete_days', new lang_string('automateddeletedays', 'backup'),
+            '', 0, $automateddeletedaysoptions));
+
+    $minkeptoptions = array(
+        0 => new lang_string('none'),
+        1 => '1',
+        2 => '2',
+        5 => '5',
+        10 => '10',
+        20 => '20',
+        30 => '30',
+        40 => '40',
+        50 => '50',
+        100 => '100',
+        200 => '200',
+        300 => '300',
+        400 => '400'
+    );
+    $temp->add(new admin_setting_configselect('backup/backup_auto_min_kept', new lang_string('automatedminkept', 'backup'),
+            new lang_string('automatedminkepthelp', 'backup'), 0, $minkeptoptions));
+
     $temp->add(new admin_setting_configcheckbox('backup/backup_shortname', new lang_string('backup_shortname', 'admin'), new lang_string('backup_shortnamehelp', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_skip_hidden', new lang_string('skiphidden', 'backup'), new lang_string('skiphiddenhelp', 'backup'), 1));
     $temp->add(new admin_setting_configselect('backup/backup_auto_skip_modif_days', new lang_string('skipmodifdays', 'backup'), new lang_string('skipmodifdayshelp', 'backup'), 30, array(
index 659ee2e..545e190 100644 (file)
@@ -110,7 +110,8 @@ if ($execute = $options['execute']) {
     $predbqueries = $DB->perf_get_queries();
     $pretime = microtime(true);
 
-    mtrace("Scheduled task: " . $task->get_name());
+    $fullname = $task->get_name() . ' (' . get_class($task) . ')';
+    mtrace('Execute scheduled task: ' . $fullname);
     // NOTE: it would be tricky to move this code to \core\task\manager class,
     //       because we want to do detailed error reporting.
     $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
@@ -138,7 +139,7 @@ if ($execute = $options['execute']) {
             mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
             mtrace("... used " . (microtime(1) - $pretime) . " seconds");
         }
-        mtrace("Task completed.");
+        mtrace('Scheduled task complete: ' . $fullname);
         \core\task\manager::scheduled_task_complete($task);
         get_mailer('close');
         exit(0);
@@ -148,7 +149,7 @@ if ($execute = $options['execute']) {
         }
         mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
         mtrace("... used " . (microtime(true) - $pretime) . " seconds");
-        mtrace("Task failed: " . $e->getMessage());
+        mtrace('Scheduled task failed: ' . $fullname . ',' . $e->getMessage());
         if ($CFG->debugdeveloper) {
             if (!empty($e->debuginfo)) {
                 mtrace("Debug info:");
index 49c0afe..55ae25d 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/display.min.js and b/admin/tool/templatelibrary/amd/build/display.min.js differ
index 869d27b..b8458ae 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js and b/admin/tool/templatelibrary/amd/build/search.min.js differ
index bb2f77a..05507ee 100644 (file)
@@ -99,9 +99,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
         }
         if (context) {
             templates.render(templateName, context).done(function(html, js) {
-                $('[data-region="displaytemplateexample"]').empty();
-                $('[data-region="displaytemplateexample"]').append(html);
-                templates.runTemplateJS(js);
+                templates.replaceNodeContents($('[data-region="displaytemplateexample"]'), html, js);
             }).fail(notification.exception);
         } else {
             str.get_string('templatehasnoexample', 'tool_templatelibrary').done(function(s) {
index efefb26..2c9d828 100644 (file)
@@ -32,8 +32,8 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
      */
     var reloadListTemplate = function(templateList) {
         templates.render('tool_templatelibrary/search_results', { templates: templateList })
-            .done(function (result) {
-                $('[data-region="searchresults"]').replaceWith(result);
+            .done(function (result, js) {
+                templates.replaceNode($('[data-region="searchresults"]'), result, js);
             }).fail(notification.exception);
     };
 
index b05838c..2dffa22 100644 (file)
             echo $OUTPUT->header();
             $fullname = fullname($user, true);
             echo $OUTPUT->heading(get_string('deleteuser', 'admin'));
+
             $optionsyes = array('delete'=>$delete, 'confirm'=>md5($delete), 'sesskey'=>sesskey());
-            echo $OUTPUT->confirm(get_string('deletecheckfull', '', "'$fullname'"), new moodle_url($returnurl, $optionsyes), $returnurl);
+            $deleteurl = new moodle_url($returnurl, $optionsyes);
+            $deletebutton = new single_button($deleteurl, get_string('delete'), 'post');
+
+            echo $OUTPUT->confirm(get_string('deletecheckfull', '', "'$fullname'"), $deletebutton, $returnurl);
             echo $OUTPUT->footer();
             die;
         } else if (data_submitted() and !$user->deleted) {
index 26e2e00..31e7f03 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /backup/*,
 information provided here is intended especially for developers.
 
+=== 3.0 ===
+
+* The backup_auto_keep setting, in automated backups configuration, is now
+  renamed to backup_auto_max_kept as part of a rationalise of naming (see MDL-50602)
+
 === 2.6 ===
 
 * The backup_controller_dbops::create_temptable_from_real_table()
index 1188bf7..8b6eeb8 100644 (file)
@@ -61,6 +61,13 @@ abstract class backup_cron_automated_helper {
     const AUTO_BACKUP_ENABLED = 1;
     const AUTO_BACKUP_MANUAL = 2;
 
+    /** Automated backup storage in course backup filearea */
+    const STORAGE_COURSE = 0;
+    /** Automated backup storage in specified directory */
+    const STORAGE_DIRECTORY = 1;
+    /** Automated backup storage in course backup filearea and specified directory */
+    const STORAGE_COURSE_AND_DIRECTORY = 2;
+
     /**
      * Runs the automated backups if required
      *
@@ -174,42 +181,42 @@ abstract class backup_cron_automated_helper {
                     $backupcourse->nextstarttime = $nextstarttime;
                     $DB->update_record('backup_courses', $backupcourse);
                     mtrace('Skipping ' . $course->fullname . ' (Not scheduled for backup until ' . $showtime . ')');
-                } else if ($skipped) { // Must have been skipped for a reason.
-                    $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED;
-                    $backupcourse->nextstarttime = $nextstarttime;
-                    $DB->update_record('backup_courses', $backupcourse);
-                    mtrace('Skipping ' . $course->fullname . ' (' . $skippedmessage . ')');
-                    mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime);
                 } else {
-                    // Backup every non-skipped courses.
-                    mtrace('Backing up '.$course->fullname.'...');
+                    if ($skipped) { // Must have been skipped for a reason.
+                        $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED;
+                        $backupcourse->nextstarttime = $nextstarttime;
+                        $DB->update_record('backup_courses', $backupcourse);
+                        mtrace('Skipping ' . $course->fullname . ' (' . $skippedmessage . ')');
+                        mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime);
+                    } else {
+                        // Backup every non-skipped courses.
+                        mtrace('Backing up '.$course->fullname.'...');
 
-                    // We have to send an email because we have included at least one backup.
-                    $emailpending = true;
+                        // We have to send an email because we have included at least one backup.
+                        $emailpending = true;
 
-                    // Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error).
-                    if ($backupcourse->laststatus != self::BACKUP_STATUS_UNFINISHED) {
-                        // Set laststarttime.
-                        $starttime = time();
+                        // Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error).
+                        if ($backupcourse->laststatus != self::BACKUP_STATUS_UNFINISHED) {
+                            // Set laststarttime.
+                            $starttime = time();
 
-                        $backupcourse->laststarttime = time();
-                        $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED;
-                        $DB->update_record('backup_courses', $backupcourse);
+                            $backupcourse->laststarttime = time();
+                            $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED;
+                            $DB->update_record('backup_courses', $backupcourse);
 
-                        $backupcourse->laststatus = backup_cron_automated_helper::launch_automated_backup($course, $backupcourse->laststarttime, $admin->id);
-                        $backupcourse->lastendtime = time();
-                        $backupcourse->nextstarttime = $nextstarttime;
+                            $backupcourse->laststatus = self::launch_automated_backup($course, $backupcourse->laststarttime,
+                                    $admin->id);
+                            $backupcourse->lastendtime = time();
+                            $backupcourse->nextstarttime = $nextstarttime;
 
-                        $DB->update_record('backup_courses', $backupcourse);
+                            $DB->update_record('backup_courses', $backupcourse);
 
-                        if ($backupcourse->laststatus === self::BACKUP_STATUS_OK) {
-                            // Clean up any excess course backups now that we have
-                            // taken a successful backup.
-                            $removedcount = backup_cron_automated_helper::remove_excess_backups($course);
+                            mtrace("complete - next execution: $showtime");
                         }
                     }
 
-                    mtrace("complete - next execution: $showtime");
+                    // Remove excess backups.
+                    $removedcount = self::remove_excess_backups($course, $now);
                 }
             }
             $rs->close();
@@ -537,98 +544,177 @@ abstract class backup_cron_automated_helper {
     }
 
     /**
-     * Removes excess backups from the external system and the local file system.
+     * Removes excess backups from a specified course.
      *
-     * The number of backups keep comes from $config->backup_auto_keep.
-     *
-     * @param stdClass $course object
-     * @return bool
+     * @param stdClass $course Course object
+     * @param int $now Starting time of the process
+     * @return bool Whether or not backups is being removed
      */
-    public static function remove_excess_backups($course) {
+    public static function remove_excess_backups($course, $now = null) {
         $config = get_config('backup');
-        $keep =     (int)$config->backup_auto_keep;
-        $storage =  $config->backup_auto_storage;
-        $dir =      $config->backup_auto_destination;
+        $maxkept = (int)$config->backup_auto_max_kept;
+        $storage = $config->backup_auto_storage;
+        $deletedays = (int)$config->backup_auto_delete_days;
 
-        if ($keep == 0) {
-            // Means keep all backup files.
+        if ($maxkept == 0 && $deletedays == 0) {
+            // Means keep all backup files and never delete backup after x days.
             return true;
         }
 
-        if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
-            $dir = null;
+        if (!isset($now)) {
+            $now = time();
         }
 
         // Clean up excess backups in the course backup filearea.
-        if ($storage == 0 || $storage == 2) {
-            $fs = get_file_storage();
-            $context = context_course::instance($course->id);
-            $component = 'backup';
-            $filearea = 'automated';
-            $itemid = 0;
-            $files = array();
-            // Store all the matching files into timemodified => stored_file array.
-            foreach ($fs->get_area_files($context->id, $component, $filearea, $itemid) as $file) {
-                $files[$file->get_timemodified()] = $file;
+        $deletedcoursebackups = false;
+        if ($storage == self::STORAGE_COURSE || $storage == self::STORAGE_COURSE_AND_DIRECTORY) {
+            $deletedcoursebackups = self::remove_excess_backups_from_course($course, $now);
+        }
+
+        // Clean up excess backups in the specified external directory.
+        $deleteddirectorybackups = false;
+        if ($storage == self::STORAGE_DIRECTORY || $storage == self::STORAGE_COURSE_AND_DIRECTORY) {
+            $deleteddirectorybackups = self::remove_excess_backups_from_directory($course, $now);
+        }
+
+        if ($deletedcoursebackups || $deleteddirectorybackups) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Removes excess backups in the course backup filearea from a specified course.
+     *
+     * @param stdClass $course Course object
+     * @param int $now Starting time of the process
+     * @return bool Whether or not backups are being removed
+     */
+    protected static function remove_excess_backups_from_course($course, $now) {
+        $fs = get_file_storage();
+        $context = context_course::instance($course->id);
+        $component = 'backup';
+        $filearea = 'automated';
+        $itemid = 0;
+        $backupfiles = array();
+        $backupfilesarea = $fs->get_area_files($context->id, $component, $filearea, $itemid, 'timemodified DESC', false);
+        // Store all the matching files into timemodified => stored_file array.
+        foreach ($backupfilesarea as $backupfile) {
+            $backupfiles[$backupfile->get_timemodified()] = $backupfile;
+        }
+
+        $backupstodelete = self::get_backups_to_delete($backupfiles, $now);
+        if ($backupstodelete) {
+            foreach ($backupstodelete as $backuptodelete) {
+                $backuptodelete->delete();
             }
-            if (count($files) <= $keep) {
-                // There are less matching files than the desired number to keep there is nothing to clean up.
-                return 0;
+            mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from the automated filearea');
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Removes excess backups in the specified external directory from a specified course.
+     *
+     * @param stdClass $course Course object
+     * @param int $now Starting time of the process
+     * @return bool Whether or not backups are being removed
+     */
+    protected static function remove_excess_backups_from_directory($course, $now) {
+        $config = get_config('backup');
+        $dir = $config->backup_auto_destination;
+
+        $isnotvaliddir = !file_exists($dir) || !is_dir($dir) || !is_writable($dir);
+        if ($isnotvaliddir) {
+            mtrace('Error: ' . $dir . ' does not appear to be a valid directory');
+            return false;
+        }
+
+        // Calculate backup filename regex, ignoring the date/time/info parts that can be
+        // variable, depending of languages, formats and automated backup settings.
+        $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-';
+        $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#';
+
+        // Store all the matching files into filename => timemodified array.
+        $backupfiles = array();
+        foreach (scandir($dir) as $backupfile) {
+            // Skip files not matching the naming convention.
+            if (!preg_match($regex, $backupfile)) {
+                continue;
             }
-            // Sort by keys descending (newer to older filemodified).
-            krsort($files);
-            $remove = array_splice($files, $keep);
-            foreach ($remove as $file) {
-                $file->delete();
+
+            // Read the information contained in the backup itself.
+            try {
+                $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $backupfile);
+            } catch (backup_helper_exception $e) {
+                mtrace('Error: ' . $backupfile . ' does not appear to be a valid backup (' . $e->errorcode . ')');
+                continue;
             }
-            //mtrace('Removed '.count($remove).' old backup file(s) from the automated filearea');
-        }
 
-        // Clean up excess backups in the specified external directory.
-        if (!empty($dir) && ($storage == 1 || $storage == 2)) {
-            // Calculate backup filename regex, ignoring the date/time/info parts that can be
-            // variable, depending of languages, formats and automated backup settings.
-            $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-';
-            $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#';
-
-            // Store all the matching files into filename => timemodified array.
-            $files = array();
-            foreach (scandir($dir) as $file) {
-                // Skip files not matching the naming convention.
-                if (!preg_match($regex, $file, $matches)) {
-                    continue;
-                }
+            // Make sure this backup concerns the course and site we are looking for.
+            if ($bcinfo->format === backup::FORMAT_MOODLE &&
+                    $bcinfo->type === backup::TYPE_1COURSE &&
+                    $bcinfo->original_course_id == $course->id &&
+                    backup_general_helper::backup_is_samesite($bcinfo)) {
+                $backupfiles[$bcinfo->backup_date] = $backupfile;
+            }
+        }
 
-                // Read the information contained in the backup itself.
-                try {
-                    $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $file);
-                } catch (backup_helper_exception $e) {
-                    mtrace('Error: ' . $file . ' does not appear to be a valid backup (' . $e->errorcode . ')');
-                    continue;
-                }
+        $backupstodelete = self::get_backups_to_delete($backupfiles, $now);
+        if ($backupstodelete) {
+            foreach ($backupstodelete as $backuptodelete) {
+                unlink($dir . '/' . $backuptodelete);
+            }
+            mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from external directory');
+            return true;
+        } else {
+            return false;
+        }
+    }
 
-                // Make sure this backup concerns the course and site we are looking for.
-                if ($bcinfo->format === backup::FORMAT_MOODLE &&
-                        $bcinfo->type === backup::TYPE_1COURSE &&
-                        $bcinfo->original_course_id == $course->id &&
-                        backup_general_helper::backup_is_samesite($bcinfo)) {
-                    $files[$file] = $bcinfo->backup_date;
+    /**
+     * Get the list of backup files to delete depending on the automated backup settings.
+     *
+     * @param array $backupfiles Existing backup files
+     * @param int $now Starting time of the process
+     * @return array Backup files to delete
+     */
+    protected static function get_backups_to_delete($backupfiles, $now) {
+        $config = get_config('backup');
+        $maxkept = (int)$config->backup_auto_max_kept;
+        $deletedays = (int)$config->backup_auto_delete_days;
+        $minkept = (int)$config->backup_auto_min_kept;
+
+        // Sort by keys descending (newer to older filemodified).
+        krsort($backupfiles);
+        $tokeep = $maxkept;
+        if ($deletedays > 0) {
+            $deletedayssecs = $deletedays * DAYSECS;
+            $tokeep = 0;
+            $backupfileskeys = array_keys($backupfiles);
+            foreach ($backupfileskeys as $timemodified) {
+                $mustdeletebackup = $timemodified < ($now - $deletedayssecs);
+                if ($mustdeletebackup || $tokeep >= $maxkept) {
+                    break;
                 }
+                $tokeep++;
             }
-            if (count($files) <= $keep) {
-                // There are less matching files than the desired number to keep there is nothing to clean up.
-                return 0;
-            }
-            // Sort by values descending (newer to older filemodified).
-            arsort($files);
-            $remove = array_splice($files, $keep);
-            foreach (array_keys($remove) as $file) {
-                unlink($dir . '/' . $file);
+
+            if ($tokeep < $minkept) {
+                $tokeep = $minkept;
             }
-            //mtrace('Removed '.count($remove).' old backup file(s) from external directory');
         }
 
-        return true;
+        if (count($backupfiles) <= $tokeep) {
+            // There are less or equal matching files than the desired number to keep, there is nothing to clean up.
+            return false;
+        } else {
+            $backupstodelete = array_splice($backupfiles, $tokeep);
+            return $backupstodelete;
+        }
     }
 
     /**
index cb88d53..320daac 100644 (file)
@@ -244,4 +244,100 @@ class backup_cron_helper_testcase extends advanced_testcase {
         $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
         $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
     }
+
+    /**
+     * Test {@link backup_cron_automated_helper::get_backups_to_delete}.
+     */
+    public function test_get_backups_to_delete() {
+        $this->resetAfterTest();
+        // Active only backup_auto_max_kept config to 2 days.
+        set_config('backup_auto_max_kept', '2', 'backup');
+        set_config('backup_auto_delete_days', '0', 'backup');
+        set_config('backup_auto_min_kept', '0', 'backup');
+
+        // No backups to delete.
+        $backupfiles = array(
+            '1000000000' => 'file1.mbz',
+            '1000432000' => 'file3.mbz'
+        );
+        $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000432000);
+        $this->assertFalse($deletedbackups);
+
+        // Older backup to delete.
+        $backupfiles['1000172800'] = 'file2.mbz';
+        $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000432000);
+        $this->assertEquals(1, count($deletedbackups));
+        $this->assertArrayHasKey('1000000000', $backupfiles);
+        $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+
+        // Activate backup_auto_max_kept to 5 days and backup_auto_delete_days to 10 days.
+        set_config('backup_auto_max_kept', '5', 'backup');
+        set_config('backup_auto_delete_days', '10', 'backup');
+        set_config('backup_auto_min_kept', '0', 'backup');
+
+        // No backups to delete. Timestamp is 1000000000 + 10 days.
+        $backupfiles['1000432001'] = 'file4.mbz';
+        $backupfiles['1000864000'] = 'file5.mbz';
+        $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000864000);
+        $this->assertFalse($deletedbackups);
+
+        // One old backup to delete. Timestamp is 1000000000 + 10 days + 1 second.
+        $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000864001);
+        $this->assertEquals(1, count($deletedbackups));
+        $this->assertArrayHasKey('1000000000', $backupfiles);
+        $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+
+        // Two old backups to delete. Timestamp is 1000000000 + 12 days + 1 second.
+        $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1001036801);
+        $this->assertEquals(2, count($deletedbackups));
+        $this->assertArrayHasKey('1000000000', $backupfiles);
+        $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+        $this->assertArrayHasKey('1000172800', $backupfiles);
+        $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+
+        // Activate backup_auto_max_kept to 5 days, backup_auto_delete_days to 10 days and backup_auto_min_kept to 2.
+        set_config('backup_auto_max_kept', '5', 'backup');
+        set_config('backup_auto_delete_days', '10', 'backup');
+        set_config('backup_auto_min_kept', '2', 'backup');
+
+        // Three instead of four old backups are deleted. Timestamp is 1000000000 + 16 days.
+        $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1001382400);
+        $this->assertEquals(3, count($deletedbackups));
+        $this->assertArrayHasKey('1000000000', $backupfiles);
+        $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+        $this->assertArrayHasKey('1000172800', $backupfiles);
+        $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+        $this->assertArrayHasKey('1000432000', $backupfiles);
+        $this->assertEquals('file3.mbz', $backupfiles['1000432000']);
+
+        // Three instead of all five backups are deleted. Timestamp is 1000000000 + 60 days.
+        $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1005184000);
+        $this->assertEquals(3, count($deletedbackups));
+        $this->assertArrayHasKey('1000000000', $backupfiles);
+        $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+        $this->assertArrayHasKey('1000172800', $backupfiles);
+        $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+        $this->assertArrayHasKey('1000432000', $backupfiles);
+        $this->assertEquals('file3.mbz', $backupfiles['1000432000']);
+    }
+}
+
+/**
+ * Provides access to protected methods we want to explicitly test
+ *
+ * @copyright 2015 Jean-Philippe Gaudreau <jp.gaudreau@umontreal.ca>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_backup_cron_automated_helper extends backup_cron_automated_helper {
+
+    /**
+     * Provides access to protected method get_backups_to_remove.
+     *
+     * @param array $backupfiles Existing backup files
+     * @param int $now Starting time of the process
+     * @return array Backup files to remove
+     */
+    public static function testable_get_backups_to_delete($backupfiles, $now) {
+        return parent::get_backups_to_delete($backupfiles, $now);
+    }
 }
index 1941738..ad9d427 100644 (file)
@@ -45,7 +45,7 @@ class edit_backpack_form extends moodleform {
         $mform->addElement('html', html_writer::tag('span', '', array('class' => 'notconnected', 'id' => 'connection-error')));
         $mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges'));
         $mform->addHelpButton('backpackheader', 'backpackconnection', 'badges');
-        $mform->addElement('static', 'url', get_string('url'), 'http://' . BADGE_BACKPACKURL);
+        $mform->addElement('static', 'url', get_string('url'), BADGE_BACKPACKURL);
         $status = html_writer::tag('span', get_string('notconnected', 'badges'),
             array('class' => 'notconnected', 'id' => 'connection-status'));
         $mform->addElement('static', 'status', get_string('status'), $status);
@@ -67,7 +67,7 @@ class edit_backpack_form extends moodleform {
         $mform->addElement('hidden', 'userid', $USER->id);
         $mform->setType('userid', PARAM_INT);
 
-        $mform->addElement('hidden', 'backpackurl', 'http://' . BADGE_BACKPACKURL);
+        $mform->addElement('hidden', 'backpackurl', BADGE_BACKPACKURL);
         $mform->setType('backpackurl', PARAM_URL);
 
     }
@@ -118,7 +118,7 @@ class edit_collections_form extends moodleform {
 
         $mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges'));
         $mform->addHelpButton('backpackheader', 'backpackconnection', 'badges');
-        $mform->addElement('static', 'url', get_string('url'), 'http://' . BADGE_BACKPACKURL);
+        $mform->addElement('static', 'url', get_string('url'), BADGE_BACKPACKURL);
 
         $status = html_writer::tag('span', get_string('connected', 'badges'), array('class' => 'connected'));
         $mform->addElement('static', 'status', get_string('status'), $status);
index 382749a..9365afe 100644 (file)
@@ -87,7 +87,7 @@ if (!isset($data->status) || $data->status != 'okay') {
 
 // Make sure email matches a backpack.
 $check = new stdClass();
-$check->backpackurl = 'http://' . BADGE_BACKPACKURL;
+$check->backpackurl = BADGE_BACKPACKURL;
 $check->email = $data->email;
 
 $bp = new OpenBadgesBackpackHandler($check);
@@ -106,7 +106,7 @@ if (isset($request->status) && $request->status == 'missing') {
 $obj = new stdClass();
 $obj->userid = $USER->id;
 $obj->email = $data->email;
-$obj->backpackurl = 'http://' . BADGE_BACKPACKURL;
+$obj->backpackurl = BADGE_BACKPACKURL;
 $obj->backpackuid = $backpackuid;
 $obj->autosync = 0;
 $obj->password = '';
index 39ea1cc..ae319eb 100644 (file)
@@ -1,6 +1,11 @@
 .block_activity_results {text-align: center;}
 .block_activity_results h1 {margin: 4px;font-size: 1.1em;}
-.block_activity_results table.grades {text-align: left;width: 100%;}
-.block_activity_results table.grades .number{text-align: right;width:10%;}
+.block_activity_results table.grades {text-align: left; width: 100%;}
+.block_activity_results table.grades .number{text-align: left; width:10%;}
+.block_activity_results table.grades .name{text-align: left; width:77%;}
 .block_activity_results table.grades .grade {text-align: right;}
-.block_activity_results table.grades caption {margin: 1em 0px 0px 0px;border-bottom-width: 1px;border-bottom-style: solid;font-weight: bold;}
+.block_activity_results table.grades caption {font-weight: bold; font-size: 18px;}
+
+.dir-rtl .block_activity_results table.grades {text-align: right;}
+.dir-rtl .block_activity_results table.grades .number{text-align: right;}
+.dir-rtl .block_activity_results table.grades .name{text-align: right;}
index e859cac..5c51578 100644 (file)
@@ -256,15 +256,17 @@ function cohort_get_available_cohorts($currentcontext, $withmembers = 0, $offset
     $groupbysql = '';
     $havingsql = '';
     if ($withmembers) {
-        $groupbysql = " GROUP BY $fieldssql";
+        $fieldssql .= ', s.memberscnt';
+        $subfields = "c.id, COUNT(DISTINCT cm.userid) AS memberscnt";
+        $groupbysql = " GROUP BY c.id";
         $fromsql = " LEFT JOIN {cohort_members} cm ON cm.cohortid = c.id ";
-        $fieldssql .= ', COUNT(DISTINCT cm.userid) AS memberscnt';
         if (in_array($withmembers,
                 array(COHORT_COUNT_ENROLLED_MEMBERS, COHORT_WITH_ENROLLED_MEMBERS_ONLY, COHORT_WITH_NOTENROLLED_MEMBERS_ONLY))) {
             list($esql, $params2) = get_enrolled_sql($currentcontext);
             $fromsql .= " LEFT JOIN ($esql) u ON u.id = cm.userid ";
             $params = array_merge($params2, $params);
-            $fieldssql .= ', COUNT(DISTINCT u.id) AS enrolledcnt';
+            $fieldssql .= ', s.enrolledcnt';
+            $subfields .= ', COUNT(DISTINCT u.id) AS enrolledcnt';
         }
         if ($withmembers == COHORT_WITH_MEMBERS_ONLY) {
             $havingsql = " HAVING COUNT(DISTINCT cm.userid) > 0";
@@ -280,13 +282,20 @@ function cohort_get_available_cohorts($currentcontext, $withmembers = 0, $offset
         $params = array_merge($params, $searchparams);
     }
 
-    $sql = "SELECT $fieldssql
-              FROM {cohort} c
-              $fromsql
-             WHERE $wheresql
-             $groupbysql
-             $havingsql
-          ORDER BY c.name, c.idnumber";
+    if ($withmembers) {
+        $sql = "SELECT " . str_replace('c.', 'cohort.', $fieldssql) . "
+                  FROM {cohort} cohort
+                  JOIN (SELECT $subfields
+                          FROM {cohort} c $fromsql
+                         WHERE $wheresql $groupbysql $havingsql
+                        ) s ON cohort.id = s.id
+              ORDER BY cohort.name, cohort.idnumber";
+    } else {
+        $sql = "SELECT $fieldssql
+                  FROM {cohort} c $fromsql
+                 WHERE $wheresql
+              ORDER BY c.name, c.idnumber";
+    }
 
     return $DB->get_records_sql($sql, $params, $offset, $limit);
 }
index df033d9..a9b59fc 100644 (file)
@@ -530,6 +530,19 @@ $CFG->admin = 'admin';
 // any icon inside the pix/f folder. You can also set the customdescription field
 // (shown above) and (for advanced use) the groups, string, and defaulticon fields.
 //
+// Upgrade key
+//
+// If the upgrade key is defined here, then the value must be provided every time
+// the site is being upgraded though the web interface, regardless of whether the
+// administrator is logged in or not. This prevents anonymous access to the upgrade
+// screens where the real authentication and authorization mechanisms can not be
+// relied on.
+//
+// It is strongly recommended to use a value different from your real account
+// password.
+//
+//      $CFG->upgradekey = 'put_some_password-like_value_here';
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index 31d4817..1ddc12d 100644 (file)
@@ -76,11 +76,12 @@ $strdeletecoursecheck = get_string("deletecoursecheck");
 $message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
 
 $continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
+$continuebutton = new single_button($continueurl, get_string('delete'), 'post');
 
 $PAGE->navbar->add($strdeletecheck);
 $PAGE->set_title("$SITE->shortname: $strdeletecheck");
 $PAGE->set_heading($SITE->fullname);
 echo $OUTPUT->header();
-echo $OUTPUT->confirm($message, $continueurl, $categoryurl);
+echo $OUTPUT->confirm($message, $continuebutton, $categoryurl);
 echo $OUTPUT->footer();
-exit;
\ No newline at end of file
+exit;
index 76b49eb..ea4724a 100644 (file)
@@ -66,7 +66,7 @@ if ($deletesection) {
             echo $OUTPUT->box_start('noticebox');
             $optionsyes = array('id' => $id, 'confirm' => 1, 'delete' => 1, 'sesskey' => sesskey());
             $deleteurl = new moodle_url('/course/editsection.php', $optionsyes);
-            $formcontinue = new single_button($deleteurl, get_string('continue'));
+            $formcontinue = new single_button($deleteurl, get_string('delete'));
             $formcancel = new single_button($cancelurl, get_string('cancel'), 'get');
             echo $OUTPUT->confirm(get_string('confirmdeletesection', '',
                 get_section_name($course, $sectioninfo)), $formcontinue, $formcancel);
index 29d066a..e36b268 100644 (file)
@@ -2316,6 +2316,108 @@ class core_course_external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function get_course_module_parameters() {
+        return new external_function_parameters(
+            array(
+                'cmid' => new external_value(PARAM_INT, 'The course module id')
+            )
+        );
+    }
+
+    /**
+     * Return information about a course module.
+     *
+     * @param int $cmid the course module id
+     * @return array of warnings and the course module
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function get_course_module($cmid) {
+
+        $params = self::validate_parameters(self::get_course_module_parameters(),
+                                            array(
+                                                'cmid' => $cmid,
+                                            ));
+
+        $warnings = array();
+
+        $cm = get_coursemodule_from_id(null, $params['cmid'], 0, true, MUST_EXIST);
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        // If the user has permissions to manage the activity, return all the information.
+        if (has_capability('moodle/course:manageactivities', $context)) {
+            $info = $cm;
+        } else {
+            // Return information is safe to show to any user.
+            $info = new stdClass();
+            $info->id = $cm->id;
+            $info->course = $cm->course;
+            $info->module = $cm->module;
+            $info->modname = $cm->modname;
+            $info->instance = $cm->instance;
+            $info->section = $cm->section;
+            $info->sectionnum = $cm->sectionnum;
+            $info->groupmode = $cm->groupmode;
+            $info->groupingid = $cm->groupingid;
+            $info->completion = $cm->completion;
+        }
+        // Format name.
+        $info->name = format_string($cm->name, true, array('context' => $context));
+
+        $result = array();
+        $result['cm'] = $info;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function get_course_module_returns() {
+        return new external_single_structure(
+            array(
+                'cm' => new external_single_structure(
+                    array(
+                        'id' => new external_value(PARAM_INT, 'The course module id'),
+                        'course' => new external_value(PARAM_INT, 'The course id'),
+                        'module' => new external_value(PARAM_INT, 'The module type id'),
+                        'name' => new external_value(PARAM_TEXT, 'The activity name'),
+                        'modname' => new external_value(PARAM_COMPONENT, 'The module component name (forum, assign, etc..)'),
+                        'instance' => new external_value(PARAM_INT, 'The activity instance id'),
+                        'section' => new external_value(PARAM_INT, 'The module section id'),
+                        'sectionnum' => new external_value(PARAM_INT, 'The module section number'),
+                        'groupmode' => new external_value(PARAM_INT, 'Group mode'),
+                        'groupingid' => new external_value(PARAM_INT, 'Grouping id'),
+                        'completion' => new external_value(PARAM_INT, 'If completion is enabled'),
+                        'idnumber' => new external_value(PARAM_RAW, 'Module id number', VALUE_OPTIONAL),
+                        'added' => new external_value(PARAM_INT, 'Time added', VALUE_OPTIONAL),
+                        'score' => new external_value(PARAM_INT, 'Score', VALUE_OPTIONAL),
+                        'indent' => new external_value(PARAM_INT, 'Indentation', VALUE_OPTIONAL),
+                        'visible' => new external_value(PARAM_INT, 'If visible', VALUE_OPTIONAL),
+                        'visibleold' => new external_value(PARAM_INT, 'Visible old', VALUE_OPTIONAL),
+                        'completiongradeitemnumber' => new external_value(PARAM_INT, 'Completion grade item', VALUE_OPTIONAL),
+                        'completionview' => new external_value(PARAM_INT, 'Completion view setting', VALUE_OPTIONAL),
+                        'completionexpected' => new external_value(PARAM_INT, 'Completion time expected', VALUE_OPTIONAL),
+                        'showdescription' => new external_value(PARAM_INT, 'If the description is showed', VALUE_OPTIONAL),
+                        'availability' => new external_value(PARAM_RAW, 'Availability settings', VALUE_OPTIONAL),
+                    )
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
 }
 
 /**
index af8533c..cb4c081 100644 (file)
@@ -86,6 +86,46 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         return $title;
     }
 
+    /**
+     * Generate the edit control action menu
+     *
+     * @param array $controls The edit control items from section_edit_control_items
+     * @param stdClass $course The course entry from DB
+     * @param stdClass $section The course_section entry from DB
+     * @return string HTML to output.
+     */
+    protected function section_edit_control_menu($controls, $course, $section) {
+        $o = "";
+        if (!empty($controls)) {
+            $menu = new action_menu();
+            if ($section->section && get_string_manager()->string_exists('sectionmenu', 'format_'.$course->format)) {
+                $menu->set_menu_trigger(get_string('sectionmenu', 'format_'.$course->format));
+            } else {
+                $menu->set_menu_trigger(get_string('sectionmenu'));
+            }
+            $menu->attributes['class'] .= ' section-actions';
+            foreach ($controls as $value) {
+                $url = empty($value['url']) ? '' : $value['url'];
+                $icon = empty($value['icon']) ? '' : $value['icon'];
+                $name = empty($value['name']) ? '' : $value['name'];
+                $attr = empty($value['attr']) ? '' : $value['attr'];
+                $class = empty($item['pixattr']['class']) ? '' : $item['pixattr']['class'];
+                $alt = empty($item['pixattr']['alt']) ? '' : $item['pixattr']['alt'];
+                $al = new action_menu_link_secondary(
+                    new moodle_url($url),
+                    new pix_icon($icon, $name, null, array('class' => "smallicon " . $class, 'alt' => $alt)),
+                    $name,
+                    $attr
+                );
+                $menu->add($al);
+            }
+
+            $o .= html_writer::div($this->render($menu), 'section_action_menu');
+        }
+
+        return $o;
+    }
+
     /**
      * Generate the content to displayed on the right part of a section
      * before course modules are included
@@ -98,12 +138,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
     protected function section_right_content($section, $course, $onsectionpage) {
         $o = $this->output->spacer();
 
-        if ($section->section != 0) {
-            $controls = $this->section_edit_controls($course, $section, $onsectionpage);
-            if (!empty($controls)) {
-                $o = implode('<br />', $controls);
-            }
-        }
+        $controls = $this->section_edit_control_items($course, $section, $onsectionpage);
+        $o .= $this->section_edit_control_menu($controls, $course, $section);
 
         return $o;
     }
@@ -160,6 +196,9 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             'class' => 'section main clearfix'.$sectionstyle, 'role'=>'region',
             'aria-label'=> get_section_name($course, $section)));
 
+        // Create a span that contains the section title to be used to create the keyboard section move menu.
+        $o .= html_writer::tag('span', $this->section_title($section, $course), array('class' => 'hidden sectionname'));
+
         $leftcontent = $this->section_left_content($section, $course, $onsectionpage);
         $o.= html_writer::tag('div', $leftcontent, array('class' => 'left side'));
 
@@ -181,17 +220,9 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
 
         $o.= html_writer::start_tag('div', array('class' => 'summary'));
         $o.= $this->format_summary_text($section);
-
-        $context = context_course::instance($course->id);
-        if ($PAGE->user_is_editing() && has_capability('moodle/course:update', $context)) {
-            $url = new moodle_url('/course/editsection.php', array('id'=>$section->id, 'sr'=>$sectionreturn));
-            $o.= html_writer::link($url,
-                html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/settings'),
-                    'class' => 'iconsmall edit', 'alt' => get_string('edit'))),
-                array('title' => get_string('editsummary')));
-        }
         $o.= html_writer::end_tag('div');
 
+        $context = context_course::instance($course->id);
         $o .= $this->section_availability_message($section,
                 has_capability('moodle/course:viewhiddensections', $context));
 
@@ -217,6 +248,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      * @param stdClass $section The course_section entry from DB
      * @param bool $onsectionpage true if being printed on a section page
      * @return array of links with edit controls
+     * @deprecated since Moodle 3.0 MDL-48947 - please do not use this function any more.
+     * @see format_section_renderer_base::section_edit_control_items()
      */
     protected function section_edit_controls($course, $section, $onsectionpage = false) {
         global $PAGE;
@@ -225,6 +258,45 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             return array();
         }
 
+        $controls = array();
+        $items = $this->section_edit_control_items($course, $section, $onsectionpage);
+
+        foreach ($items as $key => $item) {
+                $url = empty($item['url']) ? '' : $item['url'];
+                $icon = empty($item['icon']) ? '' : $item['icon'];
+                $name = empty($item['name']) ? '' : $item['name'];
+                $attr = empty($item['attr']) ? '' : $item['attr'];
+                $class = empty($item['pixattr']['class']) ? '' : $item['pixattr']['class'];
+                $alt = empty($item['pixattr']['alt']) ? '' : $item['pixattr']['alt'];
+                $controls[$key] = html_writer::link(
+                    new moodle_url($url),
+                    html_writer::empty_tag('img', array(
+                        'src' => $this->output->pix_url($icon),
+                        'class' => "icon " . $class,
+                        'alt' => $alt
+                    )),
+                    $attr);
+        }
+
+        debugging('section_edit_controls() is deprecated, please use section_edit_control_items() instead.', DEBUG_DEVELOPER);
+        return $controls;
+    }
+
+    /**
+     * Generate the edit control items of a section
+     *
+     * @param stdClass $course The course entry from DB
+     * @param stdClass $section The course_section entry from DB
+     * @param bool $onsectionpage true if being printed on a section page
+     * @return array of edit control items
+     */
+    protected function section_edit_control_items($course, $section, $onsectionpage = false) {
+        global $PAGE;
+
+        if (!$PAGE->user_is_editing()) {
+            return array();
+        }
+
         $coursecontext = context_course::instance($course->id);
         $isstealth = isset($course->numsections) && ($section->section > $course->numsections);
 
@@ -237,62 +309,94 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
 
         $controls = array();
 
-        $url = clone($baseurl);
-        if (!$isstealth && has_capability('moodle/course:sectionvisibility', $coursecontext)) {
-            if ($section->visible) { // Show the hide/show eye.
-                $strhidefromothers = get_string('hidefromothers', 'format_'.$course->format);
-                $url->param('hide', $section->section);
-                $controls[] = html_writer::link($url,
-                    html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/hide'),
-                    'class' => 'icon hide', 'alt' => $strhidefromothers)),
-                    array('title' => $strhidefromothers, 'class' => 'editing_showhide'));
+        if (!$isstealth && has_capability('moodle/course:update', $coursecontext)) {
+            if ($section->section > 0
+                && get_string_manager()->string_exists('editsection', 'format_'.$course->format)) {
+                $streditsection = get_string('editsection', 'format_'.$course->format);
             } else {
-                $strshowfromothers = get_string('showfromothers', 'format_'.$course->format);
-                $url->param('show',  $section->section);
-                $controls[] = html_writer::link($url,
-                    html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/show'),
-                    'class' => 'icon hide', 'alt' => $strshowfromothers)),
-                    array('title' => $strshowfromothers, 'class' => 'editing_showhide'));
+                $streditsection = get_string('editsection');
             }
-        }
 
-        if (course_can_delete_section($course, $section)) {
-            if (get_string_manager()->string_exists('deletesection', 'format_'.$course->format)) {
-                $strdelete = get_string('deletesection', 'format_'.$course->format);
-            } else {
-                $strdelete = get_string('deletesection');
-            }
-            $url = new moodle_url('/course/editsection.php', array('id' => $section->id,
-                'sr' => $onsectionpage ? $section->section : 0, 'delete' => 1));
-            $controls[] = html_writer::link($url,
-                html_writer::empty_tag('img', array('src' => $this->output->pix_url('t/delete'),
-                    'class' => 'icon delete', 'alt' => $strdelete)),
-                array('title' => $strdelete));
+            $controls['edit'] = array(
+                'url'   => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $onsectionpage)),
+                'icon' => 'i/settings',
+                'name' => $streditsection,
+                'pixattr' => array('class' => '', 'alt' => $streditsection),
+                'attr' => array('class' => 'icon edit', 'title' => $streditsection));
         }
 
-        if (!$isstealth && !$onsectionpage && has_capability('moodle/course:movesections', $coursecontext)) {
+        if ($section->section) {
             $url = clone($baseurl);
-            if ($section->section > 1) { // Add a arrow to move section up.
-                $url->param('section', $section->section);
-                $url->param('move', -1);
-                $strmoveup = get_string('moveup');
-
-                $controls[] = html_writer::link($url,
-                    html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/up'),
-                    'class' => 'icon up', 'alt' => $strmoveup)),
-                    array('title' => $strmoveup, 'class' => 'moveup'));
+            if (!$isstealth) {
+                if (has_capability('moodle/course:sectionvisibility', $coursecontext)) {
+                    if ($section->visible) { // Show the hide/show eye.
+                        $strhidefromothers = get_string('hidefromothers', 'format_'.$course->format);
+                        $url->param('hide', $section->section);
+                        $controls['visiblity'] = array(
+                            'url' => $url,
+                            'icon' => 'i/hide',
+                            'name' => $strhidefromothers,
+                            'pixattr' => array('class' => '', 'alt' => $strhidefromothers),
+                            'attr' => array('class' => 'icon editing_showhide', 'title' => $strhidefromothers));
+                    } else {
+                        $strshowfromothers = get_string('showfromothers', 'format_'.$course->format);
+                        $url->param('show',  $section->section);
+                        $controls['visiblity'] = array(
+                            'url' => $url,
+                            'icon' => 'i/show',
+                            'name' => $strshowfromothers,
+                            'pixattr' => array('class' => '', 'alt' => $strshowfromothers),
+                            'attr' => array('class' => 'icon editing_showhide', 'title' => $strshowfromothers));
+                    }
+                }
+
+                if (!$onsectionpage) {
+                    if (has_capability('moodle/course:movesections', $coursecontext)) {
+                        $url = clone($baseurl);
+                        if ($section->section > 1) { // Add a arrow to move section up.
+                            $url->param('section', $section->section);
+                            $url->param('move', -1);
+                            $strmoveup = get_string('moveup');
+                            $controls['moveup'] = array(
+                                'url' => $url,
+                                'icon' => 'i/up',
+                                'name' => $strmoveup,
+                                'pixattr' => array('class' => '', 'alt' => $strmoveup),
+                                'attr' => array('class' => 'icon moveup', 'title' => $strmoveup));
+                        }
+
+                        $url = clone($baseurl);
+                        if ($section->section < $course->numsections) { // Add a arrow to move section down.
+                            $url->param('section', $section->section);
+                            $url->param('move', 1);
+                            $strmovedown = get_string('movedown');
+                            $controls['movedown'] = array(
+                                'url' => $url,
+                                'icon' => 'i/down',
+                                'name' => $strmovedown,
+                                'pixattr' => array('class' => '', 'alt' => $strmovedown),
+                                'attr' => array('class' => 'icon movedown', 'title' => $strmovedown));
+                        }
+                    }
+                }
             }
 
-            $url = clone($baseurl);
-            if ($section->section < $course->numsections) { // Add a arrow to move section down.
-                $url->param('section', $section->section);
-                $url->param('move', 1);
-                $strmovedown =  get_string('movedown');
-
-                $controls[] = html_writer::link($url,
-                    html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/down'),
-                    'class' => 'icon down', 'alt' => $strmovedown)),
-                    array('title' => $strmovedown, 'class' => 'movedown'));
+            if (course_can_delete_section($course, $section)) {
+                if (get_string_manager()->string_exists('deletesection', 'format_'.$course->format)) {
+                    $strdelete = get_string('deletesection', 'format_'.$course->format);
+                } else {
+                    $strdelete = get_string('deletesection');
+                }
+                $url = new moodle_url('/course/editsection.php', array(
+                    'id' => $section->id,
+                    'sr' => $onsectionpage ? $section->section : 0,
+                    'delete' => 1));
+                $controls['delete'] = array(
+                    'url' => $url,
+                    'icon' => 'i/delete',
+                    'name' => $strdelete,
+                    'pixattr' => array('class' => '', 'alt' => $strdelete),
+                    'attr' => array('class' => 'icon delete', 'title' => $strdelete));
             }
         }
 
index 94bc371..4e98edb 100644 (file)
@@ -74,7 +74,7 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
 
         for (var i = sectionfrom; i <= sectionto; i++) {
             // Update section title.
-            sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+            sectionlist.item(i).all('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
             // Update move icon.
             ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
             str = ele.getAttribute('alt');
index 37e2f11..c24e68a 100644 (file)
  */
 
 $string['currentsection'] = 'This topic';
+$string['editsection'] = 'Edit topic';
 $string['deletesection'] = 'Delete topic';
 $string['sectionname'] = 'Topic';
 $string['pluginname'] = 'Topics format';
+$string['sectionmenu'] = 'Topic menu';
 $string['section0name'] = 'General';
 $string['page-course-view-topics'] = 'Any course main page in topics format';
 $string['page-course-view-topics-x'] = 'Any course page in topics format';
index 6e2b43b..45d0684 100644 (file)
@@ -74,14 +74,14 @@ class format_topics_renderer extends format_section_renderer_base {
     }
 
     /**
-     * Generate the edit controls of a section
+     * Generate the edit control items of a section
      *
      * @param stdClass $course The course entry from DB
      * @param stdClass $section The course_section entry from DB
      * @param bool $onsectionpage true if being printed on a section page
-     * @return array of links with edit controls
+     * @return array of edit control items
      */
-    protected function section_edit_controls($course, $section, $onsectionpage = false) {
+    protected function section_edit_control_items($course, $section, $onsectionpage = false) {
         global $PAGE;
 
         if (!$PAGE->user_is_editing()) {
@@ -99,22 +99,26 @@ class format_topics_renderer extends format_section_renderer_base {
 
         $isstealth = $section->section > $course->numsections;
         $controls = array();
-        if (!$isstealth && has_capability('moodle/course:setcurrentsection', $coursecontext)) {
+        if (!$isstealth && $section->section && has_capability('moodle/course:setcurrentsection', $coursecontext)) {
             if ($course->marker == $section->section) {  // Show the "light globe" on/off.
                 $url->param('marker', 0);
-                $controls[] = html_writer::link($url,
-                                    html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/marked'),
-                                        'class' => 'icon ', 'alt' => get_string('markedthistopic'))),
-                                    array('title' => get_string('markedthistopic'), 'class' => 'editing_highlight'));
+                $markedthistopic = get_string('markedthistopic');
+                $highlightoff = get_string('highlightoff');
+                $controls[] = array("url" => $url, "icon" => 'i/marked',
+                                    "name" => $highlightoff,
+                                    'pixattr' => array('class' => '', 'alt' => $markedthistopic),
+                                    "attr" => array('class' => 'editing_highlight', 'title' => $markedthistopic));
             } else {
                 $url->param('marker', $section->section);
-                $controls[] = html_writer::link($url,
-                                html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/marker'),
-                                    'class' => 'icon', 'alt' => get_string('markthistopic'))),
-                                array('title' => get_string('markthistopic'), 'class' => 'editing_highlight'));
+                $markthistopic = get_string('markthistopic');
+                $highlight = get_string('highlight');
+                $controls[] = array("url" => $url, "icon" => 'i/marker',
+                                    "name" => $highlight,
+                                    'pixattr' => array('class' => '', 'alt' => $markthistopic),
+                                    "attr" => array('class' => 'editing_highlight', 'title' => $markthistopic));
             }
         }
 
-        return array_merge($controls, parent::section_edit_controls($course, $section, $onsectionpage));
+        return array_merge($controls, parent::section_edit_control_items($course, $section, $onsectionpage));
     }
 }
index 55f52a5..068dac4 100644 (file)
@@ -1,9 +1,12 @@
 .course-content ul.topics {margin:0;}
 .course-content ul.topics li.section {list-style: none;margin:0 0 5px 0;padding:0;}
 .course-content ul.topics li.section .content {margin:0 40px;}
-.course-content ul.topics li.section .left {float:left;}
-.course-content ul.topics li.section .right {float:right;}
 .course-content ul.topics li.section .left,
-.course-content ul.topics li.section .right {width:40px;text-align:center;padding: 6px 0;}
+.course-content ul.topics li.section .right {width:40px;padding: 0 6px;}
 .course-content ul.topics li.section .right img.icon { padding: 0 0 4px 0;}
+.course-content ul.topics li.section .left {padding-top:22px;text-align: right;}
+.jsenabled .course-content ul.topics li.section .left,
+.jsenabled .course-content ul.topics li.section .right {width:auto;}
 .course-content ul.topics li.section .left .section-handle img.icon { padding:0; vertical-align: baseline; }
+.course-content ul.topics li.section .section_action_menu .textmenu,
+.course-content ul.topics li.section .section_action_menu .menu-action-text { white-space: nowrap; }
\ No newline at end of file
index 6b6cf0b..6973dea 100644 (file)
@@ -25,14 +25,14 @@ Feature: Sections can be edited and deleted in topics format
     And I turn editing mode on
 
   Scenario: Edit section summary in topics format
-    When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+    When I edit the section "2"
     And I set the following fields to these values:
       | Summary | Welcome to section 2 |
     And I press "Save changes"
     Then I should see "Welcome to section 2" in the "li#section-2" "css_element"
 
   Scenario: Edit section default name in topics format
-    When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+    When I edit the section "2"
     And I set the following fields to these values:
       | Use default section name | 0                        |
       | name                     | This is the second topic |
@@ -41,17 +41,17 @@ Feature: Sections can be edited and deleted in topics format
     And I should not see "Topic 2" in the "li#section-2" "css_element"
 
   Scenario: Deleting the last section in topics format
-    When I click on "Delete topic" "link" in the "li#section-5" "css_element"
+    When I delete section "5"
     Then I should see "Are you absolutely sure you want to completely delete \"Topic 5\" and all the activities it contains?"
-    And I press "Continue"
+    And I press "Delete"
     And I should not see "Topic 5"
     And I navigate to "Edit settings" node in "Course administration"
     And I expand all fieldsets
     And the field "Number of sections" matches value "4"
 
   Scenario: Deleting the middle section in topics format
-    When I click on "Delete topic" "link" in the "li#section-4" "css_element"
-    And I press "Continue"
+    When I delete section "4"
+    And I press "Delete"
     Then I should not see "Topic 5"
     And I should not see "Test chat name"
     And I should see "Test choice name" in the "li#section-4" "css_element"
@@ -62,8 +62,8 @@ Feature: Sections can be edited and deleted in topics format
   Scenario: Deleting the orphaned section in topics format
     When I follow "Reduce the number of sections"
     Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
-    And I click on "Delete topic" "link" in the "li#section-5" "css_element"
-    And I press "Continue"
+    And I delete section "5"
+    And I press "Delete"
     And I should not see "Topic 5"
     And I should not see "Orphaned activities"
     And "li#section-5" "css_element" should not exist
@@ -76,8 +76,8 @@ Feature: Sections can be edited and deleted in topics format
     Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
     And "li#section-5.orphaned" "css_element" should exist
     And "li#section-4.orphaned" "css_element" should not exist
-    And I click on "Delete topic" "link" in the "li#section-1" "css_element"
-    And I press "Continue"
+    And I delete section "1"
+    And I press "Delete"
     And I should not see "Test book name"
     And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
     And "li#section-5" "css_element" should not exist
index 6919320..4878b9b 100644 (file)
@@ -2,6 +2,10 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 3.0 ===
+* Course formats should now use section_edit_control_items and use the returned array of controls items and their attributes to create a
+  renderable menu or array of links.  Plugin calls to section_edit_controls will now include the section edit control in the returned array.
+
 === 2.9 ===
 * Course formats may support deleting sections, see MDL-10405 for more details.
   format_section_renderer_base::section_edit_controls() is now also called for
index eb5e106..546a24f 100644 (file)
@@ -74,7 +74,7 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
 
         for (var i = sectionfrom; i <= sectionto; i++) {
             // Update section title.
-            sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+            sectionlist.item(i).all('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
 
             // Update move icon.
             ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
index 86e24b7..2294574 100644 (file)
  */
 
 $string['currentsection'] = 'This week';
+$string['editsection'] = 'Edit week';
 $string['deletesection'] = 'Delete week';
 $string['sectionname'] = 'Week';
 $string['pluginname'] = 'Weekly format';
+$string['sectionmenu'] = 'Week menu';
 $string['section0name'] = 'General';
 $string['page-course-view-weeks'] = 'Any course main page in weeks format';
 $string['page-course-view-weeks-x'] = 'Any course page in weeks format';
index 11fd482..8f1a5b3 100644 (file)
@@ -1,9 +1,12 @@
 .course-content ul.weeks {margin:0;}
 .course-content ul.weeks li.section {list-style: none;margin:0 0 5px 0;padding:0;}
 .course-content ul.weeks li.section .content {margin:0 40px;}
-.course-content ul.weeks li.section .left {float:left;}
-.course-content ul.weeks li.section .right {float:right;}
 .course-content ul.weeks li.section .left,
-.course-content ul.weeks li.section .right {width:40px;text-align:center;padding: 6px 0;}
+.course-content ul.weeks li.section .right {width:40px;padding: 0 6px;}
 .course-content ul.weeks li.section .right img.icon { padding: 0 0 4px 0;}
+.course-content ul.weeks li.section .left {padding-top:22px;text-align: right;}
+.jsenabled .course-content ul.weeks li.section .left,
+.jsenabled .course-content ul.weeks li.section .right {width:auto;}
 .course-content ul.weeks li.section .left .section-handle img.icon { padding:0; vertical-align: baseline; }
+.course-content ul.weeks li.section .section_action_menu .textmenu,
+.course-content ul.weeks li.section .section_action_menu .menu-action-text { white-space: nowrap; }
\ No newline at end of file
index 76d4004..255fa83 100644 (file)
@@ -25,7 +25,7 @@ Feature: Sections can be edited and deleted in weeks format
     And I turn editing mode on
 
   Scenario: Edit section summary in weeks format
-    When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+    When I click on "Edit week" "link" in the "li#section-2" "css_element"
     And I set the following fields to these values:
       | Summary | Welcome to section 2 |
     And I press "Save changes"
@@ -33,7 +33,7 @@ Feature: Sections can be edited and deleted in weeks format
 
   Scenario: Edit section default name in weeks format
     Given I should see "8 May - 14 May" in the "li#section-2" "css_element"
-    When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+    When I click on "Edit week" "link" in the "li#section-2" "css_element"
     And I set the following fields to these values:
       | Use default section name | 0                       |
       | name                     | This is the second week |
@@ -43,9 +43,9 @@ Feature: Sections can be edited and deleted in weeks format
 
   Scenario: Deleting the last section in weeks format
     Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
-    When I click on "Delete week" "link" in the "li#section-5" "css_element"
+    When I delete section "5"
     Then I should see "Are you absolutely sure you want to completely delete \"29 May - 4 June\" and all the activities it contains?"
-    And I press "Continue"
+    And I press "Delete"
     And I should not see "29 May - 4 June"
     And I navigate to "Edit settings" node in "Course administration"
     And I expand all fieldsets
@@ -53,8 +53,8 @@ Feature: Sections can be edited and deleted in weeks format
 
   Scenario: Deleting the middle section in weeks format
     Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
-    When I click on "Delete week" "link" in the "li#section-4" "css_element"
-    And I press "Continue"
+    When I delete section "4"
+    And I press "Delete"
     Then I should not see "29 May - 4 June"
     And I should not see "Test chat name"
     And I should see "Test choice name" in the "li#section-4" "css_element"
@@ -65,8 +65,8 @@ Feature: Sections can be edited and deleted in weeks format
   Scenario: Deleting the orphaned section in weeks format
     When I follow "Reduce the number of sections"
     Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
-    And I click on "Delete week" "link" in the "li#section-5" "css_element"
-    And I press "Continue"
+    And I delete section "5"
+    And I press "Delete"
     And I should not see "29 May - 4 June"
     And I should not see "Orphaned activities"
     And "li#section-5" "css_element" should not exist
@@ -79,8 +79,8 @@ Feature: Sections can be edited and deleted in weeks format
     Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
     And "li#section-5.orphaned" "css_element" should exist
     And "li#section-4.orphaned" "css_element" should not exist
-    And I click on "Delete week" "link" in the "li#section-1" "css_element"
-    And I press "Continue"
+    And I delete section "1"
+    And I press "Delete"
     And I should not see "Test book name"
     And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
     And "li#section-5" "css_element" should not exist
index 5cbc191..38e19b6 100644 (file)
@@ -3322,6 +3322,8 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
             'edittitleinstructions',
             'show',
             'hide',
+            'highlight',
+            'highlightoff',
             'groupsnone',
             'groupsvisible',
             'groupsseparate',
index 9996533..ffe7470 100644 (file)
@@ -202,6 +202,56 @@ class behat_course extends behat_base {
 
     }
 
+
+    /**
+     * Opens a section edit menu if it is not already opened.
+     *
+     * @Given /^I open section "(?P<section_number>\d+)" edit menu$/
+     * @throws DriverException The step is not available when Javascript is disabled
+     * @param string $sectionnumber
+     */
+    public function i_open_section_edit_menu($sectionnumber) {
+        if (!$this->running_javascript()) {
+            throw new DriverException('Section edit menu not available when Javascript is disabled');
+        }
+
+        // If it is already opened we do nothing.
+        $xpath = $this->section_exists($sectionnumber);
+        $xpath .= "/descendant::div[contains(@class, 'section-actions')]/descendant::a[contains(@class, 'textmenu')]";
+
+        $exception = new ExpectationException('Section "' . $sectionnumber . '" was not found', $this->getSession());
+        $menu = $this->find('xpath', $xpath, $exception);
+        $menu->click();
+        $this->i_wait_until_section_is_available($sectionnumber);
+    }
+
+    /**
+     * Deletes course section.
+     *
+     * @Given /^I delete section "(?P<section_number>\d+)"$/
+     * @param int $sectionnumber The section number
+     * @return Given[]
+     */
+    public function i_delete_section($sectionnumber) {
+        // Ensures the section exists.
+        $xpath = $this->section_exists($sectionnumber);
+
+        // We need to know the course format as the text strings depends on them.
+        $courseformat = $this->get_course_format();
+        if (get_string_manager()->string_exists('deletesection', $courseformat)) {
+            $strdelete = get_string('deletesection', $courseformat);
+        } else {
+            $strdelete = get_string('deletesection');
+        }
+
+        // If javascript is on, link is inside a menu.
+        if ($this->running_javascript()) {
+            $this->i_open_section_edit_menu($sectionnumber);
+        }
+
+        return new Given('I click on "' . $strdelete . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
+    }
+
     /**
      * Turns course section highlighting on.
      *
@@ -214,6 +264,11 @@ class behat_course extends behat_base {
         // Ensures the section exists.
         $xpath = $this->section_exists($sectionnumber);
 
+        // If javascript is on, link is inside a menu.
+        if ($this->running_javascript()) {
+            $this->i_open_section_edit_menu($sectionnumber);
+        }
+
         return new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
     }
 
@@ -229,6 +284,11 @@ class behat_course extends behat_base {
         // Ensures the section exists.
         $xpath = $this->section_exists($sectionnumber);
 
+        // If javascript is on, link is inside a menu.
+        if ($this->running_javascript()) {
+            $this->i_open_section_edit_menu($sectionnumber);
+        }
+
         return new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
     }
 
@@ -271,7 +331,20 @@ class behat_course extends behat_base {
      * @param int $sectionnumber
      */
     public function i_edit_the_section($sectionnumber) {
-        return new Given('I click on "' . get_string('editsummary') . '" "link" in the "#section-' . $sectionnumber . '" "css_element"');
+        // If javascript is on, link is inside a menu.
+        if ($this->running_javascript()) {
+            $this->i_open_section_edit_menu($sectionnumber);
+        }
+
+        // We need to know the course format as the text strings depends on them.
+        $courseformat = $this->get_course_format();
+        if (get_string_manager()->string_exists('editsection', $courseformat)) {
+            $stredit = get_string('editsection', $courseformat);
+        } else {
+            $stredit = get_string('editsection');
+        }
+
+        return new Given('I click on "' . $stredit . '" "link" in the "#section-' . $sectionnumber . '" "css_element"');
     }
 
     /**
@@ -304,7 +377,7 @@ class behat_course extends behat_base {
         $xpath = $this->section_exists($sectionnumber);
 
         // The important checking, we can not check the img.
-        $xpath = $xpath . "/descendant::img[@alt='" . get_string('markedthistopic') . "'][contains(@src, 'marked')]";
+        $xpath = $xpath . "/descendant::img[contains(@src, 'marked')]";
         $exception = new ExpectationException('The "' . $sectionnumber . '" section is not highlighted', $this->getSession());
         $this->find('xpath', $xpath, $exception);
     }
@@ -409,9 +482,14 @@ class behat_course extends behat_base {
             throw new ExpectationException('The section is hidden', $this->getSession());
         }
 
-        // Hide section button should be visible.
+        // Edit menu should be visible.
         if ($this->is_course_editor()) {
-            $this->hide_section_icon_exists($sectionnumber);
+            $xpath = $sectionxpath .
+                     "/descendant::div[contains(@class, 'section-actions')]" .
+                     "/descendant::a[contains(@class, 'textmenu')]";
+            if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
+                throw new ExpectationException('The section edit menu is not available', $this->getSession());
+            }
         }
     }
 
@@ -431,6 +509,11 @@ class behat_course extends behat_base {
         // Ensures the section exists.
         $sectionxpath = $this->section_exists($sectionnumber);
 
+        // If javascript is on, link is inside a menu.
+        if ($this->running_javascript()) {
+            $this->i_open_section_edit_menu($sectionnumber);
+        }
+
         // Follows the link
         $moveuplink = $this->get_node_in_container('link', get_string('moveup'), 'xpath_element', $sectionxpath);
         $moveuplink->click();
@@ -452,6 +535,11 @@ class behat_course extends behat_base {
         // Ensures the section exists.
         $sectionxpath = $this->section_exists($sectionnumber);
 
+        // If javascript is on, link is inside a menu.
+        if ($this->running_javascript()) {
+            $this->i_open_section_edit_menu($sectionnumber);
+        }
+
         // Follows the link
         $movedownlink = $this->get_node_in_container('link', get_string('movedown'), 'xpath_element', $sectionxpath);
         $movedownlink->click();
@@ -876,10 +964,15 @@ class behat_course extends behat_base {
         // We need to know the course format as the text strings depends on them.
         $courseformat = $this->get_course_format();
 
+        // If javascript is on, link is inside a menu.
+        if ($this->running_javascript()) {
+            $this->i_open_section_edit_menu($sectionnumber);
+        }
+
         // Checking the show button alt text and show icon.
         $showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat));
         $linkxpath = $xpath . "/descendant::a[@title=$showtext]";
-        $imgxpath = $linkxpath . "/descendant::img[@alt=$showtext][contains(@src, 'show')]";
+        $imgxpath = $linkxpath . "/descendant::img[contains(@src, 'show')]";
 
         $exception = new ElementNotFoundException($this->getSession(), 'Show section icon ');
         $this->find('xpath', $imgxpath, $exception);
@@ -903,10 +996,15 @@ class behat_course extends behat_base {
         // We need to know the course format as the text strings depends on them.
         $courseformat = $this->get_course_format();
 
+        // If javascript is on, link is inside a menu.
+        if ($this->running_javascript()) {
+            $this->i_open_section_edit_menu($sectionnumber);
+        }
+
         // Checking the hide button alt text and hide icon.
         $hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat));
         $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
-        $imgxpath = $linkxpath . "/descendant::img[@alt=$hidetext][contains(@src, 'hide')]";
+        $imgxpath = $linkxpath . "/descendant::img[contains(@src, 'hide')]";
 
         $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
         $this->find('xpath', $imgxpath, $exception);
index 1c74098..fba44dd 100644 (file)
@@ -54,7 +54,7 @@ Feature: Test we can both create and delete a course.
     # Redirect
     And I should see "Delete TCCAC"
     And I should see "Test course: create a course (TCCAC)"
-    And I press "Continue"
+    And I press "Delete"
     # Redirect
     And I should see "Deleting TCCAC"
     And I should see "TCCAC has been completely deleted"
@@ -93,7 +93,7 @@ Feature: Test we can both create and delete a course.
     # Redirect
     And I should see "Delete TCCAC"
     And I should see "Test course: create a course (TCCAC)"
-    And I press "Continue"
+    And I press "Delete"
     # Redirect
     And I should see "Deleting TCCAC"
     And I should see "TCCAC has been completely deleted"
index 27b2603..e34a793 100644 (file)
@@ -1603,7 +1603,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $sink->close();
 
         // Validate the event.
-        $event = $events[0];
+        $event = array_pop($events);
         $this->assertInstanceOf('\core\event\course_created', $event);
         $this->assertEquals('course', $event->objecttable);
         $this->assertEquals($course->id, $event->objectid);
@@ -1633,7 +1633,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $imstestcase->imsplugin->cron();
         $events = $sink->get_events();
         $sink->close();
-        $event = $events[0];
+        $event = array_pop($events);
 
         // Validate the event triggered is \core\event\course_created. There is no need to validate the other values
         // as they have already been validated in the previous steps. Here we only want to make sure that when the
@@ -1750,7 +1750,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $sink->close();
 
         // Validate the event.
-        $event = $events[1];
+        $event = array_pop($events);
         $this->assertInstanceOf('\core\event\course_deleted', $event);
         $this->assertEquals('course', $event->objecttable);
         $this->assertEquals($course->id, $event->objectid);
@@ -1802,7 +1802,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $sink->close();
 
         // Validate the event.
-        $event = $events[0];
+        $event = array_pop($events);
         $this->assertInstanceOf('\core\event\course_content_deleted', $event);
         $this->assertEquals('course', $event->objecttable);
         $this->assertEquals($course->id, $event->objectid);
@@ -1927,7 +1927,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $sink->close();
 
         // Validate the event.
-        $event = $events[0];
+        $event = array_pop($events);
         $this->assertInstanceOf('\core\event\course_restored', $event);
         $this->assertEquals('course', $event->objecttable);
         $this->assertEquals($rc->get_courseid(), $event->objectid);
index 668c252..875d882 100644 (file)
@@ -1522,4 +1522,66 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEmpty($event->other);
 
     }
+
+    /**
+     * Test get_course_module
+     */
+    public function test_get_course_module() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $this->setAdminUser();
+        $course = self::getDataGenerator()->create_course();
+        $record = array(
+            'course' => $course->id,
+            'name' => 'First Chat'
+        );
+        $options = array(
+            'idnumber' => 'ABC',
+            'visible' => 0
+        );
+        // Hidden activity.
+        $chat = self::getDataGenerator()->create_module('chat', $record, $options);
+
+        // Test admin user can see the complete hidden activity.
+        $result = core_course_external::get_course_module($chat->cmid);
+        $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        // Test we retrieve all the fields.
+        $this->assertCount(22, $result['cm']);
+        $this->assertEquals($record['name'], $result['cm']['name']);
+        $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
+
+        $student = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+        self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
+        $this->setUser($student);
+
+        // The user shouldn't be able to see the activity.
+        try {
+            core_course_external::get_course_module($chat->cmid);
+            $this->fail('Exception expected due to invalid permissions.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+
+        // Make module visible.
+        set_coursemodule_visible($chat->cmid, 1);
+
+        // Test student user.
+        $result = core_course_external::get_course_module($chat->cmid);
+        $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        // Test we retrieve only the few files we can see.
+        $this->assertCount(11, $result['cm']);
+        $this->assertEquals($chat->cmid, $result['cm']['id']);
+        $this->assertEquals($course->id, $result['cm']['course']);
+        $this->assertEquals('chat', $result['cm']['modname']);
+        $this->assertEquals($chat->id, $result['cm']['instance']);
+
+    }
 }
index 08ce8ea..fe94ff0 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js differ
index 0af8c1b..ba2426d 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js differ
index 9da1d7d..7befde3 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js differ
index 6579845..be0b6a0 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js differ
index 3da12bd..f0323e8 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js differ
index 6579845..be0b6a0 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js differ
index 3799bb5..b585004 100644 (file)
@@ -75,10 +75,30 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
                     cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
 
                     if (moveup) {
-                        moveup.remove();
+                        if (moveup.previous('br')) {
+                            moveup.previous('br').remove();
+                        } else if (moveup.next('br')) {
+                            moveup.next('br').remove();
+                        }
+
+                        if (moveup.ancestor('.section_action_menu')) {
+                            moveup.ancestor('li').remove();
+                        } else {
+                            moveup.remove();
+                        }
                     }
                     if (movedown) {
-                        movedown.remove();
+                        if (movedown.previous('br')) {
+                            movedown.previous('br').remove();
+                        } else if (movedown.next('br')) {
+                            movedown.next('br').remove();
+                        }
+
+                        if (movedown.ancestor('.section_action_menu')) {
+                            movedown.ancestor('li').remove();
+                        } else {
+                            movedown.remove();
+                        }
                     }
 
                     // This section can be moved - add the class to indicate this to Y.DD.
index 1298a24..fe9b84e 100644 (file)
@@ -49,6 +49,7 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, {
         var section = e.target.ancestor(M.course.format.get_section_selector(Y)),
             button = e.target.ancestor('a', true),
             hideicon = button.one('img'),
+            buttontext = button.one('span'),
 
         // The value to submit
             value,
@@ -75,8 +76,11 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, {
             'src'   : M.util.image_url('i/' + nextaction)
         });
         button.set('title', newstring);
+        if (buttontext) {
+            buttontext.set('text', newstring);
+        }
 
-        // Change the highlight status
+        // Change the show/hide status
         var data = {
             'class' : 'section',
             'field' : 'visible',
@@ -121,6 +125,7 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, {
         var section = e.target.ancestor(M.course.format.get_section_selector(Y));
         var button = e.target.ancestor('a', true);
         var buttonicon = button.one('img');
+        var buttontext = button.one('span');
 
         // Determine whether the marker is currently set.
         var togglestatus = section.hasClass('current');
@@ -128,16 +133,21 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, {
 
         // Set the current highlighted item text.
         var old_string = M.util.get_string('markthistopic', 'moodle');
-        Y.one(SELECTOR.PAGECONTENT)
+
+        var selectedpage = Y.one(SELECTOR.PAGECONTENT);
+        selectedpage
             .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
             .set('title', old_string);
-        Y.one(SELECTOR.PAGECONTENT)
+        selectedpage
+            .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' span')
+            .set('text', M.util.get_string('highlight', 'moodle'));
+        selectedpage
             .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img')
             .set('alt', old_string)
             .set('src', M.util.image_url('i/marker'));
 
         // Remove the highlighting from all sections.
-        Y.one(SELECTOR.PAGECONTENT).all(M.course.format.get_section_selector(Y))
+        selectedpage.all(M.course.format.get_section_selector(Y))
             .removeClass('current');
 
         // Then add it if required to the selected section.
@@ -150,6 +160,10 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, {
             buttonicon
                 .set('alt', new_string)
                 .set('src', M.util.image_url('i/marked'));
+            if (buttontext) {
+                buttontext
+                    .set('text', M.util.get_string('highlightoff', 'moodle'));
+            }
         }
 
         // Change the highlight status.
index b921f15..3affaa9 100644 (file)
@@ -96,6 +96,7 @@ if ($mform->is_cancelled()) {
             $instance->customint2 = $groupid;
         }
         $DB->update_record('enrol', $instance);
+        \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
     }  else {
         // Create a new group for the cohort if requested.
         if ($data->customint2 == COHORT_CREATE_GROUP) {
index 24b3cee..57438fb 100644 (file)
@@ -441,7 +441,7 @@ class enrol_flatfile_plugin extends enrol_plugin {
             $notify = false;
             if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$user->id))) {
                 // Update only.
-                $this->update_user_enrol($instance, $user->id, ENROL_USER_ACTIVE, $roleid, $timestart, $timeend);
+                $this->update_user_enrol($instance, $user->id, ENROL_USER_ACTIVE, $timestart, $timeend);
                 if (!$DB->record_exists('role_assignments', array('contextid'=>$context->id, 'roleid'=>$roleid, 'userid'=>$user->id, 'component'=>'enrol_flatfile', 'itemid'=>$instance->id))) {
                     role_assign($roleid, $user->id, $context->id, 'enrol_flatfile', $instance->id);
                 }
index 9336e1c..f224f0a 100644 (file)
@@ -332,6 +332,7 @@ class enrol_guest_plugin extends enrol_plugin {
                         $instance->password = $data->{'enrol_guest_password_'.$i};
                     }
                     $DB->update_record('enrol', $instance);
+                    \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
 
                     if ($reset) {
                         $context = context_course::instance($course->id);
index d6d01e2..27e1de7 100644 (file)
@@ -92,13 +92,13 @@ if ($mform->is_cancelled()) {
         $instance->notifyall       = $data->notifyall;
         $instance->expirythreshold = $data->expirythreshold;
         $instance->timemodified    = time();
+        $markdirty = ($instance->status != $data->status);
+        $instance->status = $data->status;
 
         $DB->update_record('enrol', $instance);
+        \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
 
-        // Use standard API to update instance status.
-        if ($instance->status != $data->status) {
-            $instance = $DB->get_record('enrol', array('id'=>$instance->id));
-            $plugin->update_status($instance, $data->status);
+        if ($markdirty) {
             $context->mark_dirty();
         }
 
index dd62786..446fca5 100644 (file)
@@ -204,4 +204,30 @@ class enrol_meta_observer extends enrol_meta_handler {
 
         return true;
     }
+
+    /**
+     * Triggered via enrol_instance_updated event.
+     *
+     * @param \core\event\enrol_instance_updated $event
+     * @return boolean
+     */
+    public static function enrol_instance_updated(\core\event\enrol_instance_updated $event) {
+        global $DB;
+
+        if (!enrol_is_enabled('meta')) {
+            // This is slow, let enrol_meta_sync() deal with disabled plugin.
+            return true;
+        }
+
+        // Does anything want to sync with this parent?
+        $affectedcourses = $DB->get_fieldset_sql('SELECT DISTINCT courseid FROM {enrol} '.
+                'WHERE customint1 = ? AND enrol = ?',
+                array($event->courseid, 'meta'));
+
+        foreach ($affectedcourses as $courseid) {
+            enrol_meta_sync($courseid);
+        }
+
+        return true;
+    }
 }
index 8d2a590..560aefc 100644 (file)
@@ -52,4 +52,8 @@ $observers = array(
         'eventname'   => '\core\event\course_deleted',
         'callback'    => 'enrol_meta_observer::course_deleted',
     ),
+    array(
+        'eventname'   => '\core\event\enrol_instance_updated',
+        'callback'    => 'enrol_meta_observer::enrol_instance_updated',
+    ),
 );
index 4d3b549..9f0fb8a 100644 (file)
@@ -847,6 +847,16 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         // Disable manual enrolment in course1 and make sure all user enrolments in course2 are suspended.
         $manplugin->update_status($manual1, ENROL_INSTANCE_DISABLED);
         $allsuspendedenrolemnts = array_combine(array_keys($expectedenrolments), array_fill(0, 5, ENROL_USER_SUSPENDED));
+        $enrolmentstatuses = $DB->get_records_menu('user_enrolments', array('enrolid' => $meta2id), '', 'userid, status');
+        $this->assertEquals($allsuspendedenrolemnts, $enrolmentstatuses);
+
+        $manplugin->update_status($manual1, ENROL_INSTANCE_ENABLED);
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta2id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Disable events and repeat the same for course3 (testing sync):
+        $sink = $this->redirectEvents();
+        $manplugin->update_status($manual1, ENROL_INSTANCE_DISABLED);
         enrol_meta_sync($course3->id);
         $enrolmentstatuses = $DB->get_records_menu('user_enrolments', array('enrolid' => $meta3id), '', 'userid, status');
         $this->assertEquals($allsuspendedenrolemnts, $enrolmentstatuses);
@@ -855,5 +865,6 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         enrol_meta_sync($course3->id);
         $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
         $this->assertEquals($expectedenrolments, $enrolments);
+        $sink->close();
     }
 }
index 3df94e1..83de31c 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015082400;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'enrol_meta';      // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60*60;             // run cron every hour by default, it is not out-of-sync often
index 6175b3b..9c0541a 100644 (file)
@@ -76,6 +76,7 @@ if ($mform->is_cancelled()) {
         $instance->enrolenddate   = $data->enrolenddate;
         $instance->timemodified   = time();
         $DB->update_record('enrol', $instance);
+        \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
 
         if ($reset) {
             $context->mark_dirty();
diff --git a/enrol/self/classes/empty_form.php b/enrol/self/classes/empty_form.php
new file mode 100644 (file)
index 0000000..a54d70d
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+
+/**
+ * Empty enrol_self form.
+ *
+ * Useful to mimic valid enrol instances UI when the enrolment instance is not available.
+ *
+ * @package enrol_self
+ * @copyright 2015 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+class enrol_self_empty_form extends moodleform {
+
+    /**
+     * Form definition.
+     * @return void
+     */
+    public function definition() {
+        $this->_form->addElement('header', 'selfheader', $this->_customdata->header);
+        $this->_form->addElement('static', 'info', '', $this->_customdata->info);
+    }
+}
index a339b8e..f429c39 100644 (file)
@@ -109,6 +109,7 @@ if ($mform->is_cancelled()) {
         $instance->enrolenddate   = $data->enrolenddate;
         $instance->timemodified   = time();
         $DB->update_record('enrol', $instance);
+        \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
 
         if ($reset) {
             $context->mark_dirty();
index 5c668ef..6c1364b 100644 (file)
@@ -234,8 +234,8 @@ class enrol_self_plugin extends enrol_plugin {
 
         $enrolstatus = $this->can_self_enrol($instance);
 
-        // Don't show enrolment instance form, if user can't enrol using it.
         if (true === $enrolstatus) {
+            // This user can self enrol using this instance.
             $form = new enrol_self_enrol_form(NULL, $instance);
             $instanceid = optional_param('instance', 0, PARAM_INT);
             if ($instance->id == $instanceid) {
@@ -243,14 +243,23 @@ class enrol_self_plugin extends enrol_plugin {
                     $this->enrol_self($instance, $data);
                 }
             }
-
-            ob_start();
-            $form->display();
-            $output = ob_get_clean();
-            return $OUTPUT->box($output);
         } else {
-            return $OUTPUT->box($enrolstatus);
-        }
+            // This user can not self enrol using this instance. Using an empty form to keep
+            // the UI consistent with other enrolment plugins that returns a form.
+            $data = new stdClass();
+            $data->header = $this->get_instance_name($instance);
+            $data->info = $enrolstatus;
+
+            // The can_self_enrol call returns a button to the login page if the user is a
+            // guest, setting the login url to the form if that is the case.
+            $url = isguestuser() ? get_login_url() : null;
+            $form = new enrol_self_empty_form($url, $data);
+        }
+
+        ob_start();
+        $form->display();
+        $output = ob_get_clean();
+        return $OUTPUT->box($output);
     }
 
     /**
index 61a4392..1a88c50 100644 (file)
@@ -362,4 +362,64 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($expected, $event);
         $this->assertEventContextNotUsed($event);
     }
+
+    /**
+     * Test enrol_instance_created, enrol_instance_updated and enrol_instance_deleted events.
+     */
+    public function test_instance_events() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $selfplugin = enrol_get_plugin('self');
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+        $course = $this->getDataGenerator()->create_course();
+
+        // Creating enrol instance.
+        $sink = $this->redirectEvents();
+        $instanceid = $selfplugin->add_instance($course, array('status' => ENROL_INSTANCE_ENABLED,
+                                                                'name' => 'Test instance 1',
+                                                                'customint6' => 1,
+                                                                'roleid' => $studentrole->id));
+        $events = $sink->get_events();
+        $sink->close();
+
+        $this->assertCount(1, $events);
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\enrol_instance_created', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('self', $event->other['enrol']);
+        $this->assertEventContextNotUsed($event);
+
+        // Updating enrol instance.
+        $instance = $DB->get_record('enrol', array('id' => $instanceid));
+        $sink = $this->redirectEvents();
+        $selfplugin->update_status($instance, ENROL_INSTANCE_DISABLED);
+
+        $events = $sink->get_events();
+        $sink->close();
+
+        $this->assertCount(1, $events);
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\enrol_instance_updated', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('self', $event->other['enrol']);
+        $this->assertEventContextNotUsed($event);
+
+        // Deleting enrol instance.
+        $instance = $DB->get_record('enrol', array('id' => $instanceid));
+        $sink = $this->redirectEvents();
+        $selfplugin->delete_instance($instance);
+
+        $events = $sink->get_events();
+        $sink->close();
+
+        $this->assertCount(1, $events);
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\enrol_instance_deleted', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('self', $event->other['enrol']);
+        $this->assertEventContextNotUsed($event);
+    }
 }
index 40f9682..d409025 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in /enrol/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.0 ===
+
+* Added new events enrol_instance_created, enrol_instance_updated and
+  enrol_instance_deleted . Always trigger them when changing records in the
+  DB table 'enrol'.
+
 === 2.9 ===
 
 * External function core_enrol_external::get_users_courses now returns additional optional fields:
index b932ef7..ecffc3c 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js differ
index 34d316d..46b748d 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js differ
index b932ef7..ecffc3c 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js differ
index 564f348..3689b4a 100644 (file)
@@ -20,46 +20,53 @@ Y.extend(AUTOLINKER, Y.Base, {
     alertpanels: {},
     initializer : function() {
         var self = this;
-        Y.delegate('click', function(e){
-            e.preventDefault();
+        require(['core/event'], function(event) {
+            Y.delegate('click', function(e){
+                e.preventDefault();
 
-            //display a progress indicator
-            var title = '',
-                content = Y.Node.create('<div id="glossaryfilteroverlayprogress">' +
-                                        '<img src="' + M.cfg.loadingicon + '" class="spinner" />' +
-                                        '</div>'),
-                o = new Y.Overlay({
-                    headerContent :  title,
-                    bodyContent : content
-                }),
-                fullurl,
-                cfg;
-            self.overlay = o;
-            o.render(Y.one(document.body));
+                //display a progress indicator
+                var title = '',
+                    content = Y.Node.create('<div id="glossaryfilteroverlayprogress">' +
+                                            '<img src="' + M.cfg.loadingicon + '" class="spinner" />' +
+                                            '</div>'),
+                    o = new Y.Overlay({
+                        headerContent :  title,
+                        bodyContent : content
+                    }),
+                    fullurl,
+                    cfg;
+                self.overlay = o;
+                o.render(Y.one(document.body));
 
-            //Switch over to the ajax url and fetch the glossary item
-            fullurl = this.getAttribute('href').replace('showentry.php','showentry_ajax.php');
-            cfg = {
-                method: 'get',
-                context : self,
-                on: {
-                    success: function(id, o) {
-                        this.display_callback(o.responseText);
-                    },
-                    failure: function(id, o) {
-                        var debuginfo = o.statusText;
-                        if (M.cfg.developerdebug) {
-                            o.statusText += ' (' + fullurl + ')';
+                //Switch over to the ajax url and fetch the glossary item
+                fullurl = this.getAttribute('href').replace('showentry.php','showentry_ajax.php');
+                cfg = {
+                    method: 'get',
+                    context : self,
+                    on: {
+                        success: function(id, o) {
+                            this.display_callback(o.responseText, event);
+                        },
+                        failure: function(id, o) {
+                            var debuginfo = o.statusText;
+                            if (M.cfg.developerdebug) {
+                                o.statusText += ' (' + fullurl + ')';
+                            }
+                            new M.core.exception({ message: debuginfo });
                         }
-                        this.display_callback('bodyContent',debuginfo);
                     }
-                }
-            };
-            Y.io(fullurl, cfg);
+                };
+                Y.io(fullurl, cfg);
 
-        }, Y.one(document.body), 'a.glossary.autolink.concept');
+            }, Y.one(document.body), 'a.glossary.autolink.concept');
+        });
     },
-    display_callback : function(content) {
+    /**
+     * @method display_callback
+     * @param {String} content - Content to display
+     * @param {Object} event The amd event module used to fire events for jquery and yui.
+     */
+    display_callback : function(content, event) {
         var data,
             key,
             alertpanel,
@@ -76,7 +83,8 @@ Y.extend(AUTOLINKER, Y.Base, {
                     definition = data.entries[key].definition + data.entries[key].attachments;
                     alertpanel = new M.core.alert({title:data.entries[key].concept, draggable: true,
                         message:definition, modal:false, yesLabel: M.util.get_string('ok', 'moodle')});
-                    Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(alertpanel.get('boundingBox')))});
+                    // Notify the filters about the modified nodes.
+                    event.notifyFilterContentUpdated(alertpanel.get('boundingBox').getDOMNode());
                     Y.Node.one('#id_yuialertconfirm-' + alertpanel.get('COUNT')).focus();
 
                     // Register alertpanel for stacking.
index b014e43..44aabad 100644 (file)
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-require_once(__DIR__ . "../../../../config.php");
+require_once(__DIR__ . "/../../../config.php");
 require_once($CFG->libdir.'/gradelib.php');
 require_once($CFG->dirroot.'/grade/lib.php');
 require_once($CFG->dirroot.'/grade/import/lib.php');
index 2216d57..1c88196 100644 (file)
@@ -308,6 +308,7 @@ class graded_users_iterator {
                     $grades[$grade_item->id] =
                         new grade_grade(array('userid'=>$user->id, 'itemid'=>$grade_item->id), false);
                 }
+                $grades[$grade_item->id]->grade_item = $grade_item;
             }
         }
 
index 0e94de5..1e89ba4 100644 (file)
@@ -64,6 +64,7 @@ reload,moodle
 remotedownloaderror,error
 thisdirection,langconfig
 thislanguage,langconfig
+upgradekeyset,admin
 welcomep10,install
 welcomep20,install
 welcomep30,install
index 99715f5..4ac1e2b 100644 (file)
@@ -1105,6 +1105,8 @@ $string['upgradepluginsinfo_link'] = 'admin/upgradepluginsinfo';
 $string['upgradeerror'] = 'Unknown error upgrading {$a->plugin} to version {$a->version}, can not continue.';
 $string['upgradeforumread'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.<br />To use this functionality you need to <a href="{$a}">update your tables</a>.';
 $string['upgradeforumreadinfo'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.  To use this functionality you need to update your tables with all the tracking information for existing posts.  Depending on the size of your site this can take a long time (hours) and can be quite taxing on the database, so it\'s best to do it during a quiet period.  However, your site will continue functioning during this upgrade and users won\'t be affected.  Once you start this process you should let it finish (keep your browser window open).  However, if you stop the process by closing the window: don\'t worry, you can start over.<br /><br />Do you want to start the upgrading process now?';
+$string['upgradekeyreq'] = 'Upgrade key required';
+$string['upgradekeyset'] = 'Upgrade key (leave empty to not set it)';
 $string['upgradelogs'] = 'For full functionality, your old logs need to be upgraded.  <a href="{$a}">More information</a>';
 $string['upgradelogsinfo'] = 'Some changes have recently been made in the way logs are stored.  To be able to view all of your old logs on a per-activity basis, your old logs need to be upgraded.  Depending on your site this can take a long time (eg several hours) and can be quite taxing on the database for large sites.  Once you start this process you should let it finish (by keeping the browser window open).  Don\'t worry - your site will work fine for other people while the logs are being upgraded.<br /><br />Do you want to upgrade your logs now?';
 $string['upgradesettings'] = 'New settings';
index d8fd647..c0c0078 100644 (file)
@@ -30,6 +30,11 @@ $string['automatedbackupschedule'] = 'Schedule';
 $string['automatedbackupschedulehelp'] = 'Choose which days of the week to perform automated backups.';
 $string['automatedbackupsinactive'] = 'Automated backups haven\'t been enabled by the site admin';
 $string['automatedbackupstatus'] = 'Automated backup status';
+$string['automateddeletedays'] = 'Delete backups older than';
+$string['automatedmaxkept'] = 'Maximum number of backups kept';
+$string['automatedmaxkepthelp'] = 'This specifies the maximum number of recent automated backups to be kept for each course. Older backups will be deleted automatically.';
+$string['automatedminkept'] = 'Minimum number of backups kept';
+$string['automatedminkepthelp'] = 'If backups older than a specified number of days are deleted, it can happen that an inactive course ends up with no backup. To prevent this, a minimum number of backups kept should be specified.';
 $string['automatedsetup'] = 'Automated backup setup';
 $string['automatedsettings'] = 'Automated backup settings';
 $string['automatedstorage'] = 'Automated backup storage';
index 1ca0a6e..ec115e2 100644 (file)
@@ -48,6 +48,9 @@ $string['enrolcandidates'] = 'Not enrolled users';
 $string['enrolcandidatesmatching'] = 'Matching not enrolled users';
 $string['enrolcohort'] = 'Enrol cohort';
 $string['enrolcohortusers'] = 'Enrol users';
+$string['eventenrolinstancecreated'] = 'Enrolment instance created';
+$string['eventenrolinstancedeleted'] = 'Enrolment instance deleted';
+$string['eventenrolinstanceupdated'] = 'Enrolment instance updated';
 $string['enrollednewusers'] = 'Successfully enrolled {$a} new users';
 $string['enrolledusers'] = 'Enrolled users';
 $string['enrolledusersmatching'] = 'Matching enrolled users';
index 2acbcfa..a618353 100644 (file)
@@ -60,6 +60,7 @@ $string['eventmessagecontactadded'] = 'Message contact added';
 $string['eventmessagecontactblocked'] = 'Message contact blocked';
 $string['eventmessagecontactremoved'] = 'Message contact removed';
 $string['eventmessagecontactunblocked'] = 'Message contact unblocked';
+$string['eventmessagedeleted'] = 'Message deleted';
 $string['eventmessageviewed'] = 'Message viewed';
 $string['eventmessagesent'] = 'Message sent';
 $string['forced'] = 'Forced';
index 4a6e3fc..47cd797 100644 (file)
@@ -186,7 +186,6 @@ $string['backupfromthissite'] = 'Backup was made on this site?';
 $string['backupgradebookhistoryhelp'] = 'If enabled then gradebook history will be included in automated backups. Note that grade history must not be disabled in server settings (disablegradehistory) in order for this to work';
 $string['backupincludemoduleshelp'] = 'Choose whether you want to include course modules, with or without user data, in automated backups';
 $string['backupincludemoduleuserdatahelp'] = 'Choose whether you want to include module user data in automated backups.';
-$string['backupkeephelp'] = 'How many recent backups for each course do you want to keep? (older ones will be deleted automatically)';
 $string['backuplogdetailed'] = 'Detailed execution log';
 $string['backuploglaststatus'] = 'Last execution log';
 $string['backupmissinguserinfoperms'] = 'Note: This backup contains no user data. Exercise and Workshop activities will not be included in the backup, since these modules are not compatible with this type of backup.';
@@ -557,7 +556,7 @@ $string['editorpreferences'] = 'Editor preferences';
 $string['editorresettodefaults'] = 'Reset to default values';
 $string['editorsettings'] = 'Editor settings';
 $string['editorshortcutkeys'] = 'Editor shortcut keys';
-$string['editsettings'] = 'Edit settings';
+$string['editsection'] = 'Edit section';
 $string['editsummary'] = 'Edit summary';
 $string['edittitle'] = 'Edit title';
 $string['edittitleinstructions'] = 'Escape to cancel, Enter when finished';
@@ -924,6 +923,8 @@ $string['hidepicture'] = 'Hide picture';
 $string['hidesection'] = 'Hide section {$a}';
 $string['hidesettings'] = 'Hide settings';
 $string['hideshowblocks'] = 'Hide or show blocks';
+$string['highlight'] = 'Highlight';
+$string['highlightoff'] = 'Remove highlight';
 $string['hits'] = 'Hits';
 $string['hitsoncourse'] = 'Hits on {$a->coursename} by {$a->username}';
 $string['hitsoncoursetoday'] = 'Today\'s hits on {$a->coursename} by {$a->username}';
@@ -1632,6 +1633,7 @@ $string['secondstotime86400'] = '1 day';
 $string['secretalreadyused'] = 'Change password confirmation link was already used, password was not changed.';
 $string['secs'] = 'secs';
 $string['section'] = 'Section';
+$string['sectionmenu'] = 'Section menu';
 $string['sectionname'] = 'Section name';
 $string['sections'] = 'Sections';
 $string['sectionusedefaultname'] = 'Use default section name';
index b1c8f86..ea1e734 100644 (file)
@@ -30,7 +30,11 @@ $string['pinblocksexplan'] = 'Any block settings you configure here will be visi
 $string['defaultpage'] = 'Default My Moodle page';
 $string['defaultprofilepage'] = 'Default profile page';
 $string['addpage'] = 'Add page';
+$string['alldashboardswerereset'] = 'All Dashboard pages have been reset to default.';
+$string['allprofileswerereset'] = 'All profile pages have been reset to default.';
 $string['delpage'] = 'Delete page';
 $string['managepages'] = 'Manage pages';
+$string['reseteveryonesdashboard'] = 'Reset Dashboard for all users';
+$string['reseteveryonesprofile'] = 'Reset profile for all users';
 $string['resetpage'] = 'Reset page to default';
 $string['reseterror'] = 'There was an error resetting your page';
index 6a65b98..95db37f 100644 (file)
@@ -72,6 +72,7 @@ $string['requires'] = 'Requires';
 $string['rootdir'] = 'Directory';
 $string['settings'] = 'Settings';
 $string['somehighlighted'] = 'Number of plugins requiring your attention: {$a}';
+$string['somehighlightedall'] = 'Number of installed plugins: {$a}';
 $string['somehighlightedinfo'] = 'Display the full list of installed plugins';
 $string['somehighlightedonly'] = 'Display only plugins requiring your attention';
 $string['source'] = 'Source';
diff --git a/lib/amd/build/event.min.js b/lib/amd/build/event.min.js
new file mode 100644 (file)
index 0000000..86a6407
Binary files /dev/null and b/lib/amd/build/event.min.js differ
index 9dc3a61..7d7c42d 100644 (file)
Binary files a/lib/amd/build/first.min.js and b/lib/amd/build/first.min.js differ
index c75432b..71f74a7 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
diff --git a/lib/amd/src/event.js b/lib/amd/src/event.js
new file mode 100644 (file)
index 0000000..ce20865
--- /dev/null
@@ -0,0 +1,52 @@
+// 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/>.
+
+/**
+ * Global registry of core events that can be triggered/listened for.
+ *
+ * @module     core/event
+ * @package    core
+ * @class      event
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      3.0
+ */
+define([ 'jquery', 'core/yui' ],
+       function($, Y) {
+
+    return /** @alias module:core/event */ {
+        // Public variables and functions.
+        /**
+         * Trigger an event using both JQuery and YUI
+         *
+         * @method notifyFilterContentUpdated
+         * @param {string}|{JQuery} nodes - Selector or list of elements that were inserted.
+         */
+        notifyFilterContentUpdated: function(nodes) {
+            nodes = $(nodes);
+            Y.use('event', 'moodle-core-event', function(Y) {
+                // Trigger it the JQuery way.
+                $('document').trigger(M.core.event.FILTER_CONTENT_UPDATED, nodes);
+
+                // Create a YUI NodeList from our JQuery Object.
+                var yuiNodes = new Y.NodeList(nodes.get());
+
+                // And again for YUI.
+                Y.fire(M.core.event.FILTER_CONTENT_UPDATED, { nodes: yuiNodes });
+            });
+        },
+
+    };
+});
index 704c0c5..72646f0 100644 (file)
  * Because every module is returned from a request for any other module, this
  * forces the loading of all modules with a single request.
  *
+ * This function also sets up the listeners for ajax requests so we can tell
+ * if any requests are still in progress.
+ *
  * @module     core/first
  * @package    core
  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since      2.9
  */
-define(function() { });
+define(['jquery'], function($) {
+    $(document).bind("ajaxStart", function(){
+        M.util.js_pending('jq');
+    }).bind("ajaxStop", function(){
+        M.util.js_complete('jq');
+    });
+});
index a152e03..6e4bc0c 100644 (file)
@@ -30,9 +30,10 @@ define([ 'core/mustache',
          'core/notification',
          'core/url',
          'core/config',
-         'core/localstorage'
+         'core/localstorage',
+         'core/event'
        ],
-       function(mustache, $, ajax, str, notification, coreurl, config, storage) {
+       function(mustache, $, ajax, str, notification, coreurl, config, storage, event) {
 
     // Private variables and functions.
 
@@ -326,6 +327,50 @@ define([ 'core/mustache',
         return deferred.promise();
     };
 
+    /**
+     * Execute a block of JS returned from a template.
+     * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
+     *
+     * @method runTemplateJS
+     * @param {string} source - A block of javascript.
+     */
+    var runTemplateJS = function(source) {
+        if (source.trim() !== '') {
+            var newscript = $('<script>').attr('type','text/javascript').html(source);
+            $('head').append(newscript);
+        }
+    };
+
+    /**
+     * Do some DOM replacement and trigger correct events and fire javascript.
+     *
+     * @method domReplace
+     * @private
+     * @param {JQuery} element - Element or selector to replace.
+     * @param {String} newHTML - HTML to insert / replace.
+     * @param {String} newJS - Javascript to run after the insertion.
+     * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
+     */
+    var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
+        var replaceNode = $(element);
+        if (replaceNode.length) {
+            // First create the dom nodes so we have a reference to them.
+            var newNodes = $(newHTML);
+            // Do the replacement in the page.
+            if (replaceChildNodes) {
+                replaceNode.empty();
+                replaceNode.append(newNodes);
+            } else {
+                replaceNode.replaceWith(newNodes);
+            }
+            // Run any javascript associated with the new HTML.
+            runTemplateJS(newJS);
+            // Notify all filters about the new content.
+            event.notifyFilterContentUpdated(newNodes);
+        }
+    };
+
+
     return /** @alias module:core/templates */ {
         // Public variables and functions.
         /**
@@ -379,12 +424,28 @@ define([ 'core/mustache',
          * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
          *
          * @method runTemplateJS
-         * @private
          * @param {string} source - A block of javascript.
          */
-        runTemplateJS: function(source) {
-            var newscript = $('<script>').attr('type','text/javascript').html(source);
-            $('head').append(newscript);
+        runTemplateJS: runTemplateJS,
+
+        /**
+         * Replace a node in the page with some new HTML and run the JS.
+         *
+         * @method replaceNodeContents
+         * @param {string} source - A block of javascript.
+         */
+        replaceNodeContents: function(element, newHTML, newJS) {
+            return domReplace(element, newHTML, newJS, true);
+        },
+
+        /**
+         * Insert a node in the page with some new HTML and run the JS.
+         *
+         * @method replaceNode
+         * @param {string} source - A block of javascript.
+         */
+        replaceNode: function(element, newHTML, newJS) {
+            return domReplace(element, newHTML, newJS, false);
         }
     };
 });
index 8e3d7e5..d88de86 100644 (file)
@@ -96,7 +96,7 @@ define('BADGE_MESSAGE_MONTHLY', 4);
 /*
  * URL of backpack. Currently only the Open Badges backpack is supported.
  */
-define('BADGE_BACKPACKURL', 'backpack.openbadges.org');
+define('BADGE_BACKPACKURL', 'https://backpack.openbadges.org');
 
 /**
  * Class that represents badge.
@@ -1160,7 +1160,7 @@ function badges_check_backpack_accessibility() {
         'HEADER' => 0,
         'CONNECTTIMEOUT' => 2,
     );
-    $location = 'http://' . BADGE_BACKPACKURL . '/baker';
+    $location = BADGE_BACKPACKURL . '/baker';
     $out = $curl->get($location, array('assertion' => $fakeassertion->out(false)), $options);
 
     $data = json_decode($out);
@@ -1228,8 +1228,7 @@ function badges_setup_backpack_js() {
     global $CFG, $PAGE;
     if (!empty($CFG->badges_allowexternalbackpack)) {
         $PAGE->requires->string_for_js('error:backpackproblem', 'badges');
-        $protocol = (is_https()) ? 'https://' : 'http://';
-        $PAGE->requires->js(new moodle_url($protocol . BADGE_BACKPACKURL . '/issuer.js'), true);
+        $PAGE->requires->js(new moodle_url(BADGE_BACKPACKURL . '/issuer.js'), true);
         $PAGE->requires->js('/badges/backpack.js', true);
     }
 }
index 4866dc2..56208b6 100644 (file)
@@ -2055,6 +2055,31 @@ function blocks_delete_instance($instance, $nolongerused = false, $skipblockstab
     }
 }
 
+/**
+ * Delete multiple blocks at once.
+ *
+ * @param array $instanceids A list of block instance ID.
+ */
+function blocks_delete_instances($instanceids) {
+    global $DB;
+
+    $instances = $DB->get_recordset_list('block_instances', 'id', $instanceids);
+    foreach ($instances as $instance) {
+        blocks_delete_instance($instance, false, true);
+    }
+    $instances->close();
+
+    $DB->delete_records_list('block_positions', 'blockinstanceid', $instanceids);
+    $DB->delete_records_list('block_instances', 'id', $instanceids);
+
+    $preferences = array();
+    foreach ($instanceids as $instanceid) {
+        $preferences[] = 'block' . $instanceid . 'hidden';
+        $preferences[] = 'docked_block_instance_' . $instanceid;
+    }
+    $DB->delete_records_list('user_preferences', 'name', $preferences);
+}
+
 /**
  * Delete all the blocks that belong to a particular context.
  *
diff --git a/lib/classes/event/enrol_instance_created.php b/lib/classes/event/enrol_instance_created.php
new file mode 100644 (file)
index 0000000..ba03ea4
--- /dev/null
@@ -0,0 +1,109 @@
+<?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/>.
+
+/**
+ * Enrol instance created event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Enrol instance created event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string enrol: name of enrol method
+ * }
+ *
+ * @package    core
+ * @since      Moodle 2.9
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_instance_created extends base {
+
+    /**
+     * Api to Create new event from enrol object.
+     *
+     * @param \stdClass $enrol record from DB table 'enrol'
+     * @return \core\event\base returns instance of new event
+     */
+    public static final function create_from_record($enrol) {
+        $event = static::create(array(
+            'context'  => \context_course::instance($enrol->courseid),
+            'objectid' => $enrol->id,
+            'other'    => array('enrol' => $enrol->enrol)
+        ));
+        return $event;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' created the instance of enrolment method '" .
+                $this->other['enrol'] . "' with id '$this->objectid'.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventenrolinstancecreated', 'enrol');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/enrol/instances.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'enrol';
+    }
+
+    /**
+     * custom validations
+     *
+     * Throw \coding_exception notice in case of any problems.
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['enrol'])) {
+            throw new \coding_exception('The \'enrol\' value must be set in other.');
+        }
+    }
+}
diff --git a/lib/classes/event/enrol_instance_deleted.php b/lib/classes/event/enrol_instance_deleted.php
new file mode 100644 (file)
index 0000000..9c1bf95
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Enrol instance deleted event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Enrol instance deleted event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string enrol: name of enrol method
+ * }
+ *
+ * @package    core
+ * @since      Moodle 2.9
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_instance_deleted extends base {
+
+    /**
+     * Api to Create new event from enrol object.
+     *
+     * @param \stdClass $enrol record from DB table 'enrol'
+     * @return \core\event\base returns instance of new event
+     */
+    public static final function create_from_record($enrol) {
+        $event = static::create(array(
+            'context'  => \context_course::instance($enrol->courseid),
+            'objectid' => $enrol->id,
+            'other'    => array('enrol' => $enrol->enrol)
+        ));
+        $event->add_record_snapshot('enrol', $enrol);
+        return $event;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' deleted the instance of enrolment method '" .
+                $this->other['enrol'] . "' with id '$this->objectid'.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventgroupingdeleted', 'group');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/enrol/instances.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'enrol';
+    }
+
+    /**
+     * custom validations
+     *
+     * Throw \coding_exception notice in case of any problems.
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['enrol'])) {
+            throw new \coding_exception('The \'enrol\' value must be set in other.');
+        }
+    }
+}
diff --git a/lib/classes/event/enrol_instance_updated.php b/lib/classes/event/enrol_instance_updated.php
new file mode 100644 (file)
index 0000000..98732d7
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Enrol instance updated event.
+ *
+ * @package    core
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Enrol instance updated event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string enrol: name of enrol method
+ * }
+ *
+ * @package    core
+ * @since      Moodle 2.9
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_instance_updated extends base {
+
+    /**
+     * Api to Create new event from enrol object.
+     *
+     * @param \stdClass $enrol record from DB table 'enrol'
+     * @return \core\event\base returns instance of new event
+     */
+    public static final function create_from_record($enrol) {
+        $event = static::create(array(
+            'context'  => \context_course::instance($enrol->courseid),
+            'objectid' => $enrol->id,
+            'other'    => array('enrol' => $enrol->enrol)
+        ));
+        $event->add_record_snapshot('enrol', $enrol);
+        return $event;
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated the instance of enrolment method '" .
+                $this->other['enrol'] . "' with id '$this->objectid'.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventenrolinstanceupdated', 'enrol');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/enrol/instances.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'enrol';
+    }
+
+    /**
+     * custom validations
+     *
+     * Throw \coding_exception notice in case of any problems.
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['enrol'])) {
+            throw new \coding_exception('The \'enrol\' value must be set in other.');
+        }
+    }
+}
diff --git a/lib/classes/event/message_deleted.php b/lib/classes/event/message_deleted.php
new file mode 100644 (file)
index 0000000..5d3f71b
--- /dev/null
@@ -0,0 +1,145 @@
+<?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/>.
+
+/**
+ * Message deleted event.
+ *
+ * @package    core
+ * @copyright  2015 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Message deleted event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string $messagetable: the table we marked the message as deleted from (message/message_read).
+ *      - int messageid: the id of the message.
+ *      - int useridfrom: the id of the user who received the message.
+ *      - int useridto: the id of the user who sent the message.
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.0
+ * @copyright  2015 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message_deleted extends base {
+
+    /**
+     * Create event using ids.
+     *
+     * @param int $userfromid the user who the message was from.
+     * @param int $usertoid the user who the message was sent to.
+     * @param int $userdeleted the user who deleted it.
+     * @param string $messagetable the table we are marking the message as deleted in.
+     * @param int $messageid the id of the message that was deleted.
+     * @return message_deleted
+     */
+    public static function create_from_ids($userfromid, $usertoid, $userdeleted, $messagetable, $messageid) {
+        // Check who was deleting the message.
+        if ($userdeleted == $userfromid) {
+            $relateduserid = $usertoid;
+        } else {
+            $relateduserid = $userfromid;
+        }
+
+        // We set the userid to the user who deleted the message, nothing to do
+        // with whether or not they sent or received the message.
+        $event = self::create(array(
+            'userid' => $userdeleted,
+            'context' => \context_system::instance(),
+            'relateduserid' => $relateduserid,
+            'other' => array(
+                'messagetable' => $messagetable,
+                'messageid' => $messageid,
+                'useridfrom' => $userfromid,
+                'useridto' => $usertoid
+            )
+        ));
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventmessagedeleted', 'message');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        // Check if the person who deleted the message received or sent it.
+        if ($this->userid == $this->other['useridto']) {
+            $str = 'from';
+        } else {
+            $str = 'to';
+        }
+
+        return "The user with id '$this->userid' deleted a message sent $str the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['messagetable'])) {
+            throw new \coding_exception('The \'messagetable\' value must be set in other.');
+        }
+
+        if (!isset($this->other['messageid'])) {
+            throw new \coding_exception('The \'messageid\' value must be set in other.');
+        }
+
+        if (!isset($this->other['useridfrom'])) {
+            throw new \coding_exception('The \'useridfrom\' value must be set in other.');
+        }
+
+        if (!isset($this->other['useridto'])) {
+            throw new \coding_exception('The \'useridto\' value must be set in other.');
+        }
+    }
+}
index fcf9c37..9da1f0b 100644 (file)
@@ -245,7 +245,6 @@ abstract class handler {
      * @return array message and message format to use.
      */
     protected static function remove_quoted_text($messagedata) {
-        $linecount = self::get_linecount_to_remove($messagedata);
         if (!empty($messagedata->plain)) {
             $text = $messagedata->plain;
         } else {
@@ -258,8 +257,6 @@ abstract class handler {
             return array($text, $messageformat);
         }
 
-        // Remove extra line. "Xyz wrote on...".
-        $count = 0;
         $i = 0;
         $flag = false;
         foreach ($splitted as $i => $element) {
@@ -271,9 +268,6 @@ abstract class handler {
                     $element = $splitted[$j];
                     if (!empty($element)) {
                         unset($splitted[$j]);
-                        $count++;
-                    }
-                    if ($count == $linecount) {
                         break;
                     }
                 }
@@ -282,10 +276,8 @@ abstract class handler {
         }
         if ($flag) {
             // Quoted text was found.
-            $k = $i - $linecount; // Where to start the chopping process.
-
-            // Remove quoted text.
-            $splitted = array_slice($splitted, 0, $k);
+            // Retrieve everything from the start until the line before the quoted text.
+            $splitted = array_slice($splitted, 0, $i-1);
 
             // Strip out empty lines towards the end, since a lot of clients add a huge chunk of empty lines.
             $reverse = array_reverse($splitted);
@@ -311,24 +303,4 @@ abstract class handler {
         }
         return array($message, $messageformat);
     }
-
-    /**
-     * Try to guess how many lines to remove from the email to delete "xyz wrote on" text. Hard coded numbers for various email
-     * clients.
-     * Gmail uses two
-     * Evolution uses one
-     * Thunderbird uses one
-     *
-     * @param \stdClass $messagedata The Inbound Message record
-     *
-     * @return int number of lines to chop off before the start of quoted text.
-     */
-    protected static function get_linecount_to_remove($messagedata) {
-        $linecount = 1;
-        if (!empty($messagedata->html) && stripos($messagedata->html, 'gmail_quote') !== false) {
-            // Gmail uses two lines.
-            $linecount = 2;
-        }
-        return $linecount;
-    }
 }
index ddd18d4..f6543e8 100644 (file)
@@ -1111,7 +1111,8 @@ class core_plugin_manager {
 
             'qtype' => array(
                 'calculated', 'calculatedmulti', 'calculatedsimple',
-                'description', 'essay', 'match', 'missingtype', 'multianswer',
+                'ddimageortext', 'ddmarker', 'ddwtos', 'description',
+                'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
                 'multichoice', 'numerical', 'random', 'randomsamatch',
                 'shortanswer', 'truefalse'
             ),
index 962a700..ba243f1 100644 (file)
 
 // NOTE: no MOODLE_INTERNAL test here, sometimes we use this before requiring Moodle libs!
 
+/**
+ * Write a text to the given stream
+ *
+ * @param string $text text to be written
+ * @param resource $stream output stream to be written to, defaults to STDOUT
+ */
+function cli_write($text, $stream=STDOUT) {
+    fwrite($stream, $text);
+}
+
+/**
+ * Write a text followed by an end of line symbol to the given stream
+ *
+ * @param string $text text to be written
+ * @param resource $stream output stream to be written to, defaults to STDOUT
+ */
+function cli_writeln($text, $stream=STDOUT) {
+    cli_write($text.PHP_EOL, $stream);
+}
+
 /**
  * Get input from user
  * @param string $prompt text prompt, should include possible options
@@ -35,8 +55,8 @@
  * @return string entered text
  */
 function cli_input($prompt, $default='', array $options=null, $casesensitiveoptions=false) {
-    echo $prompt;
-    echo "\n: ";
+    cli_writeln($prompt);
+    cli_write(': ');
     $input = fread(STDIN, 2048);
     $input = trim($input);
     if ($input === '') {
@@ -47,7 +67,7 @@ function cli_input($prompt, $default='', array $options=null, $casesensitiveopti
             $input = strtolower($input);
         }
         if (!in_array($input, $options)) {
-            echo "Incorrect value, please retry.\n"; // TODO: localize, mark as needed in install
+            cli_writeln(get_string('cliincorrectvalueretry', 'admin'));
             return cli_input($prompt, $default, $options, $casesensitiveoptions);
         }
     }
@@ -131,11 +151,11 @@ function cli_get_params(array $longoptions, array $shortmapping=null) {
  * @return mixed void or string
  */
 function cli_separator($return=false) {
-    $separator = str_repeat('-', 79)."\n";
+    $separator = str_repeat('-', 79).PHP_EOL;
     if ($return) {
         return $separator;
     } else {
-        echo $separator;
+        cli_write($separator);
     }
 }
 
@@ -146,11 +166,11 @@ function cli_separator($return=false) {
  * @return mixed void or string
  */
 function cli_heading($string, $return=false) {
-    $string = "== $string ==\n";
+    $string = "== $string ==".PHP_EOL;
     if ($return) {
         return $string;
     } else {
-        echo $string;
+        cli_write($string);
     }
 }
 
@@ -160,19 +180,18 @@ function cli_heading($string, $return=false) {
  * @return void
  */
 function cli_problem($text) {
-    fwrite(STDERR, $text."\n");
+    cli_writeln($text, STDERR);
 }
 
 /**
- * Write to standard out and error with exit in error.
+ * Write to standard error output and exit with the given code
  *
  * @param string $text
  * @param int $errorcode
  * @return void (does not return)
  */
 function cli_error($text, $errorcode=1) {
-    fwrite(STDERR, $text);
-    fwrite(STDERR, "\n");
+    cli_writeln($text.PHP_EOL, STDERR);
     die($errorcode);
 }
 
@@ -205,6 +224,6 @@ function cli_logo($padding=2, $return=false) {
     if ($return) {
         return $logo;
     } else {
-        echo $logo;
+        cli_write($logo);
     }
 }
index e6e6b46..fa8bb71 100644 (file)
@@ -64,7 +64,8 @@ function cron_run() {
     // Run all scheduled tasks.
     while (!\core\task\manager::static_caches_cleared_since($timenow) &&
            $task = \core\task\manager::get_next_scheduled_task($timenow)) {
-        mtrace("Execute scheduled task: " . $task->get_name());
+        $fullname = $task->get_name() . ' (' . get_class($task) . ')';
+        mtrace('Execute scheduled task: ' . $fullname);
         cron_trace_time_and_memory();
         $predbqueries = null;
         $predbqueries = $DB->perf_get_queries();
@@ -79,7 +80,7 @@ function cron_run() {
                 mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
                 mtrace("... used " . (microtime(1) - $pretime) . " seconds");
             }
-            mtrace("Scheduled task complete: " . $task->get_name());
+            mtrace('Scheduled task complete: ' . $fullname);
             \core\task\manager::scheduled_task_complete($task);
         } catch (Exception $e) {
             if ($DB && $DB->is_transaction_started()) {
@@ -90,7 +91,7 @@ function cron_run() {
                 mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
                 mtrace("... used " . (microtime(1) - $pretime) . " seconds");
             }
-            mtrace("Scheduled task failed: " . $task->get_name() . "," . $e->getMessage());
+            mtrace('Scheduled task failed: ' . $fullname . ',' . $e->getMessage());
             if ($CFG->debugdeveloper) {
                  if (!empty($e->debuginfo)) {
                     mtrace("Debug info:");
index 837a755..9a0f927 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150824" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20150922" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="createdby" TYPE="foreign" FIELDS="createdby" REFTABLE="user" REFFIELDS="id" COMMENT="foreign (createdby) references user (id)"/>
         <KEY NAME="modifiedby" TYPE="foreign" FIELDS="modifiedby" REFTABLE="user" REFFIELDS="id" COMMENT="foreign (modifiedby) references user (id)"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="qtype" UNIQUE="false" FIELDS="qtype"/>
+      </INDEXES>
     </TABLE>
     <TABLE NAME="question_answers" COMMENT="Answers, with a fractional grade (0-1) and feedback">
       <FIELDS>
index 0d279b0..84b9be1 100644 (file)
@@ -742,6 +742,13 @@ $functions = array(
         'type'        => 'write'
     ),
 
+    'core_course_get_course_module' => array(
+        'classname'   => 'core_course_external',
+        'methodname'  => 'get_course_module',
+        'classpath'   => 'course/externallib.php',
+        'description' => 'Return information about a course module',
+        'type'        => 'read'
+    ),
 
     // === course category related functions ===
 
@@ -1192,6 +1199,7 @@ $services = array(
             'mod_forum_view_forum',
             'core_course_view_course',
             'core_course_search_courses',
+            'core_course_get_course_module',
             'core_completion_get_activities_completion_status',
             'core_notes_get_course_notes',
             'core_completion_get_course_completion_status',
@@ -1212,6 +1220,7 @@ $services = array(
             'mod_scorm_insert_scorm_tracks',
             'mod_scorm_get_scorm_sco_tracks',
             'mod_scorm_get_scorm_attempt_count',
+            'mod_scorm_get_scorms_by_courses',
             'mod_page_view_page',
             'mod_resource_view_resource',
             'mod_folder_view_folder',
@@ -1222,11 +1231,14 @@ $services = array(
             'mod_chat_view_chat',
             'mod_chat_get_chats_by_courses',
             'mod_book_view_book',
+            'mod_book_get_books_by_courses',
             'mod_choice_get_choice_results',
             'mod_choice_get_choice_options',
             'mod_choice_submit_choice_response',
             'mod_choice_view_choice',
+            'mod_choice_get_choices_by_courses',
             'mod_imscp_view_imscp',
+            'mod_imscp_get_imscps_by_courses',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index 221943a..56e153f 100644 (file)
@@ -4558,5 +4558,31 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015090801.00);
     }
 
+    if ($oldversion < 2015092200.00) {
+        // Define index qtype (not unique) to be added to question.
+        $table = new xmldb_table('question');
+        $index = new xmldb_index('qtype', XMLDB_INDEX_NOTUNIQUE, array('qtype'));
+
+        // Conditionally launch add index qtype.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015092200.00);
+    }
+
+    if ($oldversion < 2015092900.00) {
+        // Rename backup_auto_keep setting to backup_auto_max_kept.
+        $keep = get_config('backup', 'backup_auto_keep');
+        if ($keep !== false) {
+            set_config('backup_auto_max_kept', $keep, 'backup');
+            unset_config('backup_auto_keep', 'backup');
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015092900.00);
+    }
+
     return true;
 }
index 96df899..ba6dcf6 100644 (file)
@@ -40,6 +40,11 @@ class mssql_native_moodle_database extends moodle_database {
     protected $mssql     = null;
     protected $last_error_reporting; // To handle mssql driver default verbosity
     protected $collation;  // current DB collation cache
+    /**
+     * Does the used db version support ANSI way of limiting (2012 and higher)
+     * @var bool
+     */
+    protected $supportsoffsetfetch;
 
     /**
      * Detects if all needed PHP stuff installed.
@@ -229,6 +234,10 @@ class mssql_native_moodle_database extends moodle_database {
 
         $this->free_result($result);
 
+        $serverinfo = $this->get_server_info();
+        // Fetch/offset is supported staring from SQL Server 2012.
+        $this->supportsoffsetfetch = $serverinfo['version'] > '11';
+
         // Connection stabilised and configured, going to instantiate the temptables controller
         $this->temptables = new mssql_native_moodle_temptables($this);
 
@@ -737,13 +746,28 @@ class mssql_native_moodle_database extends moodle_database {
         list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
 
         if ($limitfrom or $limitnum) {
-            if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later)
-                $fetch = $limitfrom + $limitnum;
-                if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow
-                    $fetch = PHP_INT_MAX;
+            if (!$this->supportsoffsetfetch) {
+                if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later).
+                    $fetch = $limitfrom + $limitnum;
+                    if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow.
+                        $fetch = PHP_INT_MAX;
+                    }
+                    $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
+                                        "\\1SELECT\\2 TOP $fetch", $sql);
+                }
+            } else {
+                $sql = (substr($sql, -1) === ';') ? substr($sql, 0, -1) : $sql;
+                // We need order by to use FETCH/OFFSET.
+                // Ordering by first column shouldn't break anything if there was no order in the first place.
+                if (!strpos(strtoupper($sql), "ORDER BY")) {
+                    $sql .= " ORDER BY 1";
+                }
+
+                $sql .= " OFFSET ".$limitfrom." ROWS ";
+
+                if ($limitnum > 0) {
+                    $sql .= " FETCH NEXT ".$limitnum." ROWS ONLY";
                 }
-                $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
-                                    "\\1SELECT\\2 TOP $fetch", $sql);
             }
         }
 
@@ -754,7 +778,7 @@ class mssql_native_moodle_database extends moodle_database {
         $result = mssql_query($rawsql, $this->mssql);
         $this->query_end($result);
 
-        if ($limitfrom) { // Skip $limitfrom records
+        if ($limitfrom && !$this->supportsoffsetfetch) { // Skip $limitfrom records.
             if (!@mssql_data_seek($result, $limitfrom)) {
                 // Nothing, most probably seek past the end.
                 mssql_free_result($result);
index 9d7b767..70e8a6e 100644 (file)
@@ -1502,10 +1502,13 @@ class mysqli_native_moodle_database extends moodle_database {
     /**
      * Returns 'LIKE' part of a query.
      *
+     * Note that mysql does not support $casesensitive = true and $accentsensitive = false.
+     * More information in http://bugs.mysql.com/bug.php?id=19567.
+     *
      * @param string $fieldname usually name of the table column
      * @param string $param usually bound query parameter (?, :named)
      * @param bool $casesensitive use case sensitive search
-     * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
+     * @param bool $accensensitive use accent sensitive search (ignored if $casesensitive is true)
      * @param bool $notlike true means "NOT LIKE"
      * @param string $escapechar escape char for '%' and '_'
      * @return string SQL code fragment
@@ -1517,14 +1520,24 @@ class mysqli_native_moodle_database extends moodle_database {
         $escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\'
 
         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
+
         if ($casesensitive) {
+            // Current MySQL versions do not support case sensitive and accent insensitive.
             return "$fieldname $LIKE $param COLLATE utf8_bin ESCAPE '$escapechar'";
+
+        } else if ($accentsensitive) {
+            // Case insensitive and accent sensitive, we can force a binary comparison once all texts are using the same case.
+            return "LOWER($fieldname) $LIKE LOWER($param) COLLATE utf8_bin ESCAPE '$escapechar'";
+
         } else {
-            if ($accentsensitive) {
-                return "LOWER($fieldname) $LIKE LOWER($param) COLLATE utf8_bin ESCAPE '$escapechar'";
-            } else {
-                return "$fieldname $LIKE $param ESCAPE '$escapechar'";
+            // Case insensitive and accent insensitive.
+            $collation = '';
+            if ($this->get_dbcollation() == 'utf8_bin') {
+                // Force a case insensitive comparison if using utf8_bin.
+                $collation = 'COLLATE utf8_unicode_ci';
             }
+
+            return "$fieldname $LIKE $param $collation ESCAPE '$escapechar'";
         }
     }
 
index 07d110b..2543336 100644 (file)
@@ -41,6 +41,12 @@ class sqlsrv_native_moodle_database extends moodle_database {
     protected $last_error_reporting; // To handle SQL*Server-Native driver default verbosity
     protected $temptables; // Control existing temptables (sqlsrv_moodle_temptables object)
     protected $collation;  // current DB collation cache
+    /**
+     * Does the used db version support ANSI way of limiting (2012 and higher)
+     * @var bool
+     */
+    protected $supportsoffsetfetch;
+
     /** @var array list of open recordsets */
     protected $recordsets = array();
 
@@ -240,6 +246,10 @@ class sqlsrv_native_moodle_database extends moodle_database {
 
         $this->free_result($result);
 
+        $serverinfo = $this->get_server_info();
+        // Fetch/offset is supported staring from SQL Server 2012.
+        $this->supportsoffsetfetch = $serverinfo['version'] > '11';
+
         // Connection established and configured, going to instantiate the temptables controller
         $this->temptables = new sqlsrv_native_moodle_temptables($this);
 
@@ -809,20 +819,37 @@ class sqlsrv_native_moodle_database extends moodle_database {
     public function get_recordset_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
 
         list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
+        $needscrollable = (bool)$limitfrom; // To determine if we'll need to perform scroll to $limitfrom.
 
         if ($limitfrom or $limitnum) {
-            if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later)
-                $fetch = $limitfrom + $limitnum;
-                if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow
-                    $fetch = PHP_INT_MAX;
+            if (!$this->supportsoffsetfetch) {
+                if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later).
+                    $fetch = $limitfrom + $limitnum;
+                    if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow.
+                        $fetch = PHP_INT_MAX;
+                    }
+                    $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
+                                        "\\1SELECT\\2 TOP $fetch", $sql);
+                }
+            } else {
+                $needscrollable = false; // Using supported fetch/offset, no need to scroll anymore.
+                $sql = (substr($sql, -1) === ';') ? substr($sql, 0, -1) : $sql;
+                // We need order by to use FETCH/OFFSET.
+                // Ordering by first column shouldn't break anything if there was no order in the first place.
+                if (!strpos(strtoupper($sql), "ORDER BY")) {
+                    $sql .= " ORDER BY 1";
+                }
+
+                $sql .= " OFFSET ".$limitfrom." ROWS ";
+
+                if ($limitnum > 0) {
+                    $sql .= " FETCH NEXT ".$limitnum." ROWS ONLY";
                 }
-                $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
-                                    "\\1SELECT\\2 TOP $fetch", $sql);
             }
         }
-        $result = $this->do_query($sql, $params, SQL_QUERY_SELECT, false, (bool)$limitfrom);
+        $result = $this->do_query($sql, $params, SQL_QUERY_SELECT, false, $needscrollable);
 
-        if ($limitfrom) { // Skip $limitfrom records
+        if ($needscrollable) { // Skip $limitfrom records.
             sqlsrv_fetch($result, SQLSRV_SCROLL_ABSOLUTE, $limitfrom - 1);
         }
         return $this->create_recordset($result);
index b064d99..8b16565 100644 (file)
@@ -3870,6 +3870,11 @@ class core_dml_testcase extends database_driver_testcase {
         $records = $DB->get_records_sql($sql, array('aui'));
         $this->assertCount(1, $records);
 
+        // Test LIKE under unusual collations.
+        $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', false, false);
+        $records = $DB->get_records_sql($sql, array("%dup_r%"));
+        $this->assertCount(2, $records);
+
         $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_like('name', '?', true, true, true); // NOT LIKE.
         $records = $DB->get_records_sql($sql, array("%o%"));
         $this->assertCount(3, $records);
index 628f2ce..270e0eb 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js differ
index 05522fd..5740db9 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js differ
index 0350ece..ade5d5f 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js differ
index 864f421..2b30130 100644 (file)
@@ -30,7 +30,6 @@
  * @class Button
  * @extends M.editor_atto.EditorPlugin
  */
-
 var COMPONENTNAME = 'atto_equation',
     LOGNAME = 'atto_equation',
     CSS = {
@@ -229,8 +228,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
 
         tabview.render();
         dialogue.show();
-        // Trigger any JS filters to reprocess the new nodes.
-        Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(dialogue.get('boundingBox')))});
+        // Notify the filters about the modified nodes.
+        require(['core/event'], function(event) {
+            event.notifyFilterContentUpdated(dialogue.get('boundingBox').getDOMNode());
+        });
 
         if (equation) {
             content.one(SELECTORS.EQUATION_TEXT).set('text', equation);
@@ -494,7 +495,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         if (preview.status === 200) {
             previewNode.setHTML(preview.responseText);
 
-            Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(previewNode))});
+            // Notify the filters about the modified nodes.
+            require(['core/event'], function(event) {
+                event.notifyFilterContentUpdated(previewNode.getDOMNode());
+            });
         }
     },
 
index 8ef6171..d1902ba 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index bdcd0dc..4d4f660 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 239dbd9..c98aa83 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 98e25d4..187c493 100644 (file)
@@ -57,6 +57,10 @@ EditorCommand.prototype = {
 
                 // Collapse selection so cursor is at end of inserted material.
                 selection.collapseToEnd();
+
+                // Save save selection and editor contents.
+                this.saveSelection();
+                this.updateOriginal();
             }, this, e, callback, context, args, anchorNode, anchorOffset));
         }
 
@@ -80,6 +84,9 @@ EditorCommand.prototype = {
         // The range is not collapsed; so apply callback method immediately.
         callback.apply(context, [e, args]);
 
+        // Save save selection and editor contents.
+        this.saveSelection();
+        this.updateOriginal();
     },
 
     /**
index 67a1cfd..593cafa 100644 (file)
@@ -1692,7 +1692,11 @@ abstract class enrol_plugin {
             $instance->$field = $value;
         }
 
-        return $DB->insert_record('enrol', $instance);
+        $instance->id = $DB->insert_record('enrol', $instance);
+
+        \core\event\enrol_instance_created::create_from_record($instance)->trigger();
+
+        return $instance->id;
     }
 
     /**
@@ -1723,8 +1727,10 @@ abstract class enrol_plugin {
         $instance->status = $newstatus;
         $DB->update_record('enrol', $instance);
 
-        // invalidate all enrol caches
         $context = context_course::instance($instance->courseid);
+        \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
+
+        // Invalidate all enrol caches.
         $context->mark_dirty();
     }
 
@@ -1756,8 +1762,10 @@ abstract class enrol_plugin {
         // finally drop the enrol row
         $DB->delete_records('enrol', array('id'=>$instance->id));
 
-        // invalidate all enrol caches
         $context = context_course::instance($instance->courseid);
+        \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
+
+        // Invalidate all enrol caches.
         $context->mark_dirty();
     }
 
index dfc2005..8c77cdc 100644 (file)
@@ -466,7 +466,7 @@ class grade_category extends grade_object {
                      WHERE id $usql";
             $items = $DB->get_records_sql($sql, $params);
             foreach ($items as $id => $item) {
-                $items[$id] = new grade_item($item);
+                $items[$id] = new grade_item($item, false);
             }
         }
 
@@ -501,10 +501,13 @@ class grade_category extends grade_object {
             $grademinoverrides = array();
 
             foreach ($rs as $used) {
-                $grade = new grade_grade($used);
+                $grade = new grade_grade($used, false);
                 if (isset($items[$grade->itemid])) {
                     // Prevent grade item to be fetched from DB.
                     $grade->grade_item =& $items[$grade->itemid];
+                } else if ($grade->itemid == $this->grade_item->id) {
+                    // This grade's grade item is not in $items.
+                    $grade->grade_item =& $this->grade_item;
                 }
                 if ($grade->userid != $prevuser) {
                     $this->aggregate_grades($prevuser,
@@ -882,7 +885,7 @@ class grade_category extends grade_object {
                                                        & $weights = null,
                                                        $grademinoverrides = array(),
                                                        $grademaxoverrides = array()) {
-        $category_item = $this->get_grade_item();
+        $category_item = $this->load_grade_item();
         $grademin = $category_item->grademin;
         $grademax = $category_item->grademax;
 
index d470795..6bb860e 100644 (file)
@@ -694,7 +694,7 @@ class grade_item extends grade_object {
         // aggregate the category grade
         } else if ($this->is_category_item() or $this->is_course_item()) {
             // aggregate category grade item
-            $category = $this->get_item_category();
+            $category = $this->load_item_category();
             $category->grade_item =& $this;
             if ($category->generate_grades($userid)) {
                 return true;
index 233c34d..c85b826 100644 (file)
@@ -256,6 +256,10 @@ function install_generate_configphp($database, $cfg) {