Merge branch 'mdl51440-master' of git://github.com/tlock/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 30 Sep 2015 02:48:25 +0000 (10:48 +0800)
committerDavid Monllao <davidm@moodle.com>
Wed, 30 Sep 2015 02:48:25 +0000 (10:48 +0800)
481 files changed:
.jshintrc
Gruntfile.js
admin/cli/install.php
admin/index.php
admin/renderer.php
admin/settings/courses.php
admin/tool/behat/tests/behat/basic_actions.feature [deleted file]
admin/tool/behat/tests/behat/data_generators.feature
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/tool/templatelibrary/classes/external.php
admin/tool/templatelibrary/db/services.php
admin/user.php
auth/email/auth.php
auth/shibboleth/auth.php
auth/upgrade.txt
backup/cc/cc_lib/cc_assesment_truefalse.php
backup/cc/entity.quiz.class.php
backup/cc/entity11.quiz.class.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
blog/edit.php
calendar/export_execute.php
cohort/lib.php
completion/classes/external.php
completion/tests/behat/behat_completion.php
completion/tests/externallib_test.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/recent.php
course/rest.php
course/tests/behat/behat_course.php
course/tests/behat/create_delete_course.feature
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/classes/task/flatfile_sync_task.php [new file with mode: 0644]
enrol/flatfile/cli/sync.php
enrol/flatfile/db/tasks.php [new file with mode: 0644]
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/flatfile/lib.php
enrol/flatfile/tests/flatfile_test.php
enrol/flatfile/version.php
enrol/guest/lib.php
enrol/manual/ajax.php
enrol/manual/db/upgrade.php
enrol/manual/edit.php
enrol/manual/lang/en/enrol_manual.php
enrol/manual/lib.php
enrol/manual/manage.php
enrol/manual/settings.php
enrol/manual/version.php
enrol/manual/yui/quickenrolment/quickenrolment.js
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/edit/tree/index.php
grade/edit/tree/lib.php
grade/export/xml/grade_export_xml.php
grade/grading/lib.php
grade/lib.php
grade/report/overview/lib.php
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_aggregation_changes.feature
grade/tests/behat/grade_average.feature
grade/tests/behat/grade_calculated_grade_items.feature
grade/tests/behat/grade_calculated_grade_items_20150627.feature
grade/tests/behat/grade_calculated_weights.feature
grade/tests/behat/grade_contribution_with_extra_credit.feature
grade/tests/behat/grade_mingrade.feature
grade/tests/behat/grade_minmax.feature
grade/tests/behat/grade_natural_exclude_empty.feature
grade/tests/behat/grade_natural_exclude_empty_20150619.feature
grade/tests/behat/grade_natural_normalisation.feature
grade/tests/behat/grade_natural_normalisation_20150619.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_scales_aggregation.feature
grade/tests/behat/grade_single_item_scales.feature
grade/tests/behat/grade_view.feature
grade/tests/edittreelib_test.php
group/externallib.php
group/tests/externallib_test.php
install/stringnames.txt
lang/en/admin.php
lang/en/backup.php
lang/en/blog.php
lang/en/deprecated.txt
lang/en/enrol.php
lang/en/grades.php
lang/en/message.php
lang/en/moodle.php
lang/en/my.php
lang/en/plugin.php
lang/en/webservice.php
lib/adminlib.php
lib/ajax/service-nologin.php [new file with mode: 0644]
lib/ajax/service.php
lib/amd/build/ajax.min.js
lib/amd/build/event.min.js [new file with mode: 0644]
lib/amd/build/first.min.js
lib/amd/build/str.min.js
lib/amd/build/templates.min.js
lib/amd/src/ajax.js
lib/amd/src/event.js [new file with mode: 0644]
lib/amd/src/first.js
lib/amd/src/str.js
lib/amd/src/templates.js
lib/authlib.php
lib/badgeslib.php
lib/bennu/bennu.class.php
lib/bennu/iCalendar_rfc2445.php
lib/bennu/readme_moodle.txt
lib/blocklib.php
lib/classes/event/calendar_event_created.php
lib/classes/event/calendar_event_deleted.php
lib/classes/event/calendar_event_updated.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/grades_external.php
lib/classes/message/inbound/handler.php
lib/classes/output/external.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/db/upgrade.php [new file with mode: 0644]
lib/editor/atto/plugins/equation/db/upgradelib.php [new file with mode: 0644]
lib/editor/atto/plugins/equation/settings.php
lib/editor/atto/plugins/equation/tests/upgradelib_testcase.php [new file with mode: 0644]
lib/editor/atto/plugins/equation/version.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/external/externallib.php
lib/externallib.php
lib/form/searchableselector.js
lib/grade/grade_category.php
lib/grade/grade_item.php
lib/installlib.php
lib/javascript-static.js
lib/modinfolib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/outputrequirementslib.php
lib/templates/columns-1to1to1.mustache [new file with mode: 0644]
lib/templates/columns-1to2.mustache [new file with mode: 0644]
lib/templates/columns-2to1.mustache [new file with mode: 0644]
lib/templates/columns-autoflow-1to1to1.mustache [new file with mode: 0644]
lib/tests/behat/behat_general.php
lib/tests/blocklib_test.php
lib/tests/externallib_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/tests/other/completion.manualtest.txt
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.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
login/signup_form.php
message/lib.php
message/tests/events_test.php
mod/assign/db/upgrade.php
mod/assign/lang/en/assign.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/book/view.php
mod/choice/classes/external.php
mod/choice/db/services.php
mod/choice/tests/behat/publish_results_anonymously.feature
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/db/services.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/index.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/markposts.php
mod/forum/post.php
mod/forum/renderer.php
mod/forum/settracking.php
mod/forum/styles.css
mod/forum/tests/behat/completion_condition_number_discussions.feature
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/lesson/report.php
mod/lesson/tests/behat/completion_condition_end_reached.feature
mod/lesson/tests/behat/completion_condition_time_spent.feature
mod/lesson/tests/behat/lesson_report.feature [new file with mode: 0644]
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/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/scorm/backup/moodle1/lib.php
mod/scorm/classes/external.php
mod/scorm/db/install.xml
mod/scorm/db/services.php
mod/scorm/db/upgrade.php
mod/scorm/locallib.php
mod/scorm/tests/externallib_test.php
mod/scorm/tests/lib_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/f/FileTypesIcons-LICENSE.txt
pix/f/Oxygen-LICENSE.txt
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]
question/type/match/question.php
question/type/match/tests/question_test.php
rss/file.php
tag/classes/external.php
tag/tests/behat/edit_tag.feature
theme/base/config.php
theme/base/style/core.css
theme/base/style/course.css
theme/base/style/templates.css [new file with mode: 0644]
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/less/moodle/templates.less [new file with mode: 0644]
theme/bootstrapbase/style/moodle.css
user/editlib.php
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
webservice/renderer.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 6237dfa..a3b699e 100644 (file)
@@ -24,6 +24,7 @@
 
 module.exports = function(grunt) {
     var path = require('path'),
+        fs = require('fs'),
         tasks = {},
         cwd = process.env.PWD || process.cwd();
 
@@ -102,22 +103,103 @@ module.exports = function(grunt) {
                 args.push('--lint-stderr');
             }
 
+            var execShifter = function() {
+
+                shifter = exec("node", args, {
+                    cwd: cwd,
+                    stdio: 'inherit',
+                    env: process.env
+                });
+
+                // Tidy up after exec.
+                shifter.on('exit', function (code) {
+                    if (code) {
+                        grunt.fail.fatal('Shifter failed with code: ' + code);
+                    } else {
+                        grunt.log.ok('Shifter build complete.');
+                        done();
+                    }
+                });
+            };
+
             // Actually run shifter.
-            shifter = exec("node", args, {
-                cwd: cwd,
-                stdio: 'inherit',
-                env: process.env
-            });
-
-            // Tidy up after exec.
-            shifter.on('exit', function (code) {
-                if (code) {
-                    grunt.fail.fatal('Shifter failed with code: ' + code);
-                } else {
-                    grunt.log.ok('Shifter build complete.');
-                    done();
-                }
-            });
+            if (!options.recursive) {
+                execShifter();
+            } else {
+                // Check that there are yui modules otherwise shifter ends with exit code 1.
+                var found = false;
+                var hasYuiModules = function(directory, callback) {
+                    fs.readdir(directory, function(err, files) {
+                        if (err) {
+                            return callback(err, null);
+                        }
+
+                        // If we already found a match there is no need to continue scanning.
+                        if (found === true) {
+                            return;
+                        }
+
+                        // We need to track the number of files to know when we return a result.
+                        var pending = files.length;
+
+                        // We first check files, so if there is a match we don't need further
+                        // async calls and we just return a true.
+                        for (var i = 0; i < files.length; i++) {
+                            if (files[i] === 'yui') {
+                                return callback(null, true);
+                            }
+                        }
+
+                        // Iterate through subdirs if there were no matches.
+                        files.forEach(function (file) {
+
+                            var p = path.join(directory, file);
+                            stat = fs.statSync(p);
+                            if (!stat.isDirectory()) {
+                                pending--;
+                            } else {
+
+                                // We defer the pending-1 until we scan the whole dir and subdirs.
+                                hasYuiModules(p, function(err, result) {
+                                    if (err) {
+                                        return callback(err);
+                                    }
+
+                                    if (result === true) {
+                                        // Once we get a true we notify the caller.
+                                        found = true;
+                                        return callback(null, true);
+                                    }
+
+                                    pending--;
+                                    if (pending === 0) {
+                                        // Notify the caller that the whole dir has been scaned and there are no matches.
+                                        return callback(null, false);
+                                    }
+                                });
+                            }
+
+                            // No subdirs here, otherwise the return would be deferred until all subdirs are scanned.
+                            if (pending === 0) {
+                                return callback(null, false);
+                            }
+                        });
+                    });
+                };
+
+                hasYuiModules(cwd, function(err, result) {
+                    if (err) {
+                        grunt.fail.fatal(err.message);
+                    }
+
+                    if (result === true) {
+                        execShifter();
+                    } else {
+                        grunt.log.ok('No YUI modules to build.');
+                        done();
+                    }
+                });
+            }
     };
 
     tasks.startup = function() {
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(
diff --git a/admin/tool/behat/tests/behat/basic_actions.feature b/admin/tool/behat/tests/behat/basic_actions.feature
deleted file mode 100644 (file)
index 86bfdaa..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-@tool @tool_behat
-Feature: Page contents assertions
-  In order to write good tests
-  As a tests writer
-  I need to check the page contents
-
-  @javascript
-  Scenario: Basic contents assertions
-    Given I log in as "admin"
-    And I am on site homepage
-    And I expand "Users" node
-    And I follow "Groups"
-    And I press "Create group"
-    And I set the following fields to these values:
-      | Group name | I'm the name |
-      | Group description | I'm the description |
-    And I press "Save changes"
-    When I follow "Overview"
-    And I wait until the page is ready
-    And I wait "2" seconds
-    And I hover "#region-main .generaltable td span" "css_element"
-    Then I should see "I'm the description"
-    And "Grouping" "select" in the "region-main" "region" should be visible
-    And "Group" "select" should be visible
-    And "Activity report" "link" in the "Administration" "block" should not be visible
-    And "Event monitoring rules" "link" should not be visible
-    And I should see "Filter groups by"
-    And I should not see "Filter groupssss by"
-    And I should see "Group members" in the "#region-main table th.c1" "css_element"
-    And I should not see "Group membersssss" in the "#region-main table th.c1" "css_element"
-    And I follow "Groups"
-    And the "#groupeditform #showcreateorphangroupform" "css_element" should be enabled
-    And the "#groupeditform #showeditgroupsettingsform" "css_element" should be disabled
-
-  @javascript
-  Scenario: Locators inside specific DOM nodes using CSS selectors
-    Given the following "courses" exist:
-      | fullname | shortname | category |
-      | Course 1 | C1 | 0 |
-    And I log in as "admin"
-    And I am on site homepage
-    And I follow "Course 1"
-    When I dock "Administration" block
-    Then I should not see "Question bank" in the ".block-region" "css_element"
-    And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
-
-  @javascript
-  Scenario: Locators inside specific DOM nodes using XPath
-    Given the following "courses" exist:
-      | fullname | shortname | category |
-      | Course 1 | C1 | 0 |
-    And I log in as "admin"
-    When I dock "Administration" block
-    Then I should not see "Turn editing on" in the ".block-region" "css_element"
index 6a15dd4..9853ea5 100644 (file)
@@ -351,7 +351,7 @@ Feature: Set up contextual data for tests
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     Then I should see "Test Grade Item 1"
     And I follow "Edit   Test Grade Item 1"
     And I expand all fieldsets
@@ -434,7 +434,7 @@ Feature: Set up contextual data for tests
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     Then I should see "Test Outcome Grade Item 1"
     And I follow "Edit   Test Outcome Grade Item 1"
     And the field "Outcome" matches value "Grade outcome 1"
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 a5bf8ab..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 062e585..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 1585a18..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) {
@@ -133,7 +131,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
                     component: component,
                     template: name
             }
-        }]);
+        }], true, false);
 
         // When returns a new promise that is resolved when all the passed in promises are resolved.
         // The arguments to the done become the values of each resolved promise.
index d4eefe6..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);
     };
 
@@ -53,7 +53,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
               args: { component: componentStr, search: searchStr },
               done: reloadListTemplate,
               fail: notification.exception }
-        ]);
+        ], true, false);
     };
 
     var throttle = null;
index 100172b..be583a2 100644 (file)
@@ -63,14 +63,6 @@ class external extends external_api {
         return new external_function_parameters($params);
     }
 
-    /**
-     * Expose to AJAX
-     * @return boolean
-     */
-    public static function list_templates_is_allowed_from_ajax() {
-        return true;
-    }
-
     /**
      * Loads the list of templates.
      * @param string $component Limit the search to a component.
@@ -108,16 +100,6 @@ class external extends external_api {
             );
     }
 
-    /**
-     * Can this function be called directly from ajax?
-     *
-     * @return boolean
-     * @since Moodle 2.9
-     */
-    public static function load_canonical_template_is_allowed_from_ajax() {
-        return true;
-    }
-
     /**
      * Return a mustache template.
      * Note - this function differs from the function core_output_load_template
index c1a9479..9dda647 100644 (file)
@@ -32,12 +32,16 @@ $functions = array(
         'description' => 'List/search templates by component.',
         'type'        => 'read',
         'capabilities'=> '',
+        'ajax'        => true,
+        'loginrequired' => false,
     ),
     'tool_templatelibrary_load_canonical_template' => array(
         'classname'   => 'tool_templatelibrary\external',
         'methodname'  => 'load_canonical_template',
         'description' => 'Load a canonical template by name (not the theme overidden one).',
-        'type'        => 'read'
+        'type'        => 'read',
+        'ajax'        => true,
+        'loginrequired' => false,
     ),
 
 );
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 3f037d7..0a0827d 100644 (file)
@@ -235,12 +235,11 @@ class auth_plugin_email extends auth_plugin_base {
     }
 
     /**
-     * Returns whether or not the captcha element is enabled, and the admin settings fulfil its requirements.
+     * Returns whether or not the captcha element is enabled.
      * @return bool
      */
     function is_captcha_enabled() {
-        global $CFG;
-        return isset($CFG->recaptchapublickey) && isset($CFG->recaptchaprivatekey) && get_config("auth/{$this->authtype}", 'recaptcha');
+        return get_config("auth/{$this->authtype}", 'recaptcha');
     }
 
 }
index ddfe96f..a4cebd8 100644 (file)
@@ -210,7 +210,8 @@ class auth_plugin_shibboleth extends auth_plugin_base {
             }
 
             // Overwrite redirect in order to send user to Shibboleth logout page and let him return back
-            $redirect = $this->config->logout_handler.'?return='.urlencode($temp_redirect);
+            $redirecturl = new moodle_url($this->config->logout_handler, array('return' => $temp_redirect));
+            $redirect = $redirecturl->out();
         }
     }
 
index d867a24..0024b81 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /auth/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.0 ===
+
+* login_signup_form::signup_captcha_enabled() now calls is_captcha_enabled() from the current auth plugin instead of from auth_email
+
 === 2.9 ===
 
 * Do not update user->firstaccess from any auth plugin, the complete_user_login() does it automatically.
index 64d0e79..8a14e91 100644 (file)
@@ -27,8 +27,18 @@ class cc_assesment_question_truefalse extends cc_assesment_question_proc_base {
     public function __construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir) {
         parent::__construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir);
         $this->qtype = cc_qti_profiletype::true_false;
-        $this->correct_answer_node_id = $this->questions->nodeValue(
-            'plugin_qtype_truefalse_question/truefalse/trueanswer', $this->question_node);
+
+        // Determine the correct answer by finding out which answer has the non zero fraction...
+        // This is because a true / false question type can have 'false' as the correct answer.
+        $answers = $this->questions->nodeList('plugin_qtype_truefalse_question/answers/answer', $this->question_node);
+        foreach ($answers as $answer) {
+            $fraction = $this->questions->nodeValue('fraction', $answer);
+
+            if ($fraction != 0) {
+                $this->correct_answer_node_id = (int)$this->questions->nodeValue('@id', $answer);
+            }
+        }
+
         $maximum_quiz_grade = (int)$this->quiz->nodeValue('/activity/quiz/grade');
         $this->total_grade_value = ($maximum_quiz_grade + 1).'.0000000';
     }
index 56c42de..465422a 100644 (file)
@@ -885,27 +885,30 @@ class cc_quiz extends entities {
 
         $sheet_question_categories_question = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_TRUE_FALSE);
 
-        $max_score = 0;
-        $true_answer_id = 0;
-        $false_answer_id = 0;
+        $trueanswer  = null;
+        $falseanswer = null;
 
         if (!empty($question['answers'])) {
 
+            // Identify the true and false answers.
             foreach ($question['answers'] as $answer) {
-                if ($answer['score'] > $max_score) {
-                    $max_score = $answer['score'];
-                    $true_answer_id = $answer['id'];
+                if ($answer['identifier'] == 'true') {
+                    $trueanswer = $answer;
+                } else if ($answer['identifier'] == 'false') {
+                    $falseanswer = $answer;
+                } else {
+                    // Should not happen, but just in case.
+                    throw new coding_exception("Unknown answer identifier detected" .
+                            " in true/false quiz question with id {$question['id']}.");
                 }
 
                 $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer);
             }
 
-            foreach ($question['answers'] as $answer) {
-
-                if ($answer['id'] != $true_answer_id) {
-                    $max_score = $answer['score'];
-                    $false_answer_id = $answer['id'];
-                }
+            // Make sure the true and false answer was found.
+            if (is_null($trueanswer) || is_null($falseanswer)) {
+                throw new coding_exception("Unable to correctly identify the " .
+                        "true and false answers in the question with id {$question['id']}.");
             }
         }
 
@@ -914,8 +917,8 @@ class cc_quiz extends entities {
                            '[#false_answer_id#]');
 
         $replace_values = array($node_course_question_categories_question_answer,
-                                $true_answer_id,
-                                $false_answer_id);
+                                $trueanswer['id'],
+                                $falseanswer['id']);
 
         $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question);
 
index 5410caa..fb34758 100644 (file)
@@ -908,27 +908,30 @@ class cc11_quiz extends entities11 {
 
         $sheet_question_categories_question = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_TRUE_FALSE);
 
-        $max_score = 0;
-        $true_answer_id = 0;
-        $false_answer_id = 0;
+        $trueanswer  = null;
+        $falseanswer = null;
 
         if (!empty($question['answers'])) {
 
+            // Identify the true and false answers.
             foreach ($question['answers'] as $answer) {
-                if ($answer['score'] > $max_score) {
-                    $max_score = $answer['score'];
-                    $true_answer_id = $answer['id'];
+                if ($answer['identifier'] == 'true') {
+                    $trueanswer = $answer;
+                } else if ($answer['identifier'] == 'false') {
+                    $falseanswer = $answer;
+                } else {
+                    // Should not happen, but just in case.
+                    throw new coding_exception("Unknown answer identifier detected " .
+                            "in true/false quiz question with id {$question['id']}.");
                 }
 
                 $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer);
             }
 
-            foreach ($question['answers'] as $answer) {
-
-                if ($answer['id'] != $true_answer_id) {
-                    $max_score = $answer['score'];
-                    $false_answer_id = $answer['id'];
-                }
+            // Make sure the true and false answer was found.
+            if (is_null($trueanswer) || is_null($falseanswer)) {
+                throw new coding_exception("Unable to correctly identify the " .
+                        "true and false answers in the question with id {$question['id']}.");
             }
         }
 
@@ -937,8 +940,8 @@ class cc11_quiz extends entities11 {
                            '[#false_answer_id#]');
 
         $replace_values = array($node_course_question_categories_question_answer,
-                                $true_answer_id,
-                                $false_answer_id);
+                                $trueanswer['id'],
+                                $falseanswer['id']);
 
         $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question);
 
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 24612dd..d82b244 100644 (file)
@@ -134,6 +134,9 @@ if ($action === 'delete') {
         $PAGE->set_heading($SITE->fullname);
         echo $OUTPUT->header();
 
+        // Output edit mode title.
+        echo $OUTPUT->heading($strblogs . ': ' . get_string('deleteentry', 'blog'), 2);
+
         // Output the entry.
         $entry->prepare_render();
         echo $output->render($entry);
@@ -146,10 +149,12 @@ if ($action === 'delete') {
         die;
     }
 } else if ($action == 'add') {
-    $PAGE->set_title("$SITE->shortname: $strblogs: " . get_string('addnewentry', 'blog'));
+    $editmodetitle = $strblogs . ': ' . get_string('addnewentry', 'blog');
+    $PAGE->set_title("$SITE->shortname: $editmodetitle");
     $PAGE->set_heading(fullname($USER));
 } else if ($action == 'edit') {
-    $PAGE->set_title("$SITE->shortname: $strblogs: " . get_string('editentry', 'blog'));
+    $editmodetitle = $strblogs . ': ' . get_string('editentry', 'blog');
+    $PAGE->set_title("$SITE->shortname: $editmodetitle");
     $PAGE->set_heading(fullname($USER));
 }
 
@@ -270,6 +275,10 @@ $entry->modid = $modid;
 $entry->courseid = $courseid;
 
 echo $OUTPUT->header();
+// Output title for editing mode.
+if (isset($editmodetitle)) {
+    echo $OUTPUT->heading($editmodetitle, 2);
+}
 $blogeditform->display();
 echo $OUTPUT->footer();
 
index 9f2700e..b999205 100644 (file)
@@ -189,10 +189,14 @@ foreach($events as $event) {
     $ev->add_property('class', 'PUBLIC'); // PUBLIC / PRIVATE / CONFIDENTIAL
     $ev->add_property('last-modified', Bennu::timestamp_to_datetime($event->timemodified));
     $ev->add_property('dtstamp', Bennu::timestamp_to_datetime()); // now
-    $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // when event starts
     if ($event->timeduration > 0) {
         //dtend is better than duration, because it works in Microsoft Outlook and works better in Korganizer
+        $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // when event starts.
         $ev->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart + $event->timeduration));
+    } else {
+        // When no duration is present, ie an all day event, VALUE should be date instead of time and dtend = dtstart + 1 day.
+        $ev->add_property('dtstart', Bennu::timestamp_to_date($event->timestart), array('value' => 'DATE')); // All day event.
+        $ev->add_property('dtend', Bennu::timestamp_to_date($event->timestart + DAYSECS), array('value' => 'DATE')); // All day event.
     }
     if ($event->courseid != 0) {
         $coursecontext = context_course::instance($event->courseid);
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 dc57b66..dcbad6b 100644 (file)
@@ -377,4 +377,84 @@ class core_completion_external extends external_api {
         );
     }
 
+    /**
+     * Describes the parameters for mark_course_self_completed.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function mark_course_self_completed_parameters() {
+        return new external_function_parameters (
+            array(
+                'courseid' => new external_value(PARAM_INT, 'Course ID')
+            )
+        );
+    }
+
+    /**
+     * Update the course completion status for the current user (if course self-completion is enabled).
+     *
+     * @param  int $courseid    Course id
+     * @return array            Result and possible warnings
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function mark_course_self_completed($courseid) {
+        global $USER;
+
+        $warnings = array();
+        $params = self::validate_parameters(self::mark_course_self_completed_parameters(),
+                                            array('courseid' => $courseid));
+
+        $course = get_course($params['courseid']);
+        $context = context_course::instance($course->id);
+        self::validate_context($context);
+
+        // Set up completion object and check it is enabled.
+        $completion = new completion_info($course);
+        if (!$completion->is_enabled()) {
+            throw new moodle_exception('completionnotenabled', 'completion');
+        }
+
+        if (!$completion->is_tracked_user($USER->id)) {
+            throw new moodle_exception('nottracked', 'completion');
+        }
+
+        $completion = $completion->get_completion($USER->id, COMPLETION_CRITERIA_TYPE_SELF);
+
+        // Self completion criteria not enabled.
+        if (!$completion) {
+            throw new moodle_exception('noselfcompletioncriteria', 'completion');
+        }
+
+        // Check if the user has already marked himself as complete.
+        if ($completion->is_complete()) {
+            throw new moodle_exception('useralreadymarkedcomplete', 'completion');
+        }
+
+        // Mark the course complete.
+        $completion->mark_complete();
+
+        $result = array();
+        $result['status'] = true;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the mark_course_self_completed return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.0
+     */
+    public static function mark_course_self_completed_returns() {
+
+        return new external_single_structure(
+            array(
+                'status'    => new external_value(PARAM_BOOL, 'status, true if success'),
+                'warnings'  => new external_warnings(),
+            )
+        );
+    }
+
 }
index 6790dbf..3762290 100644 (file)
@@ -122,4 +122,40 @@ class behat_completion extends behat_base {
             new Given('I press "'.get_string('savechangesanddisplay').'"')
         );
     }
+
+    /**
+     * Checks if the activity with specified name is maked as complete.
+     *
+     * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion should be marked as complete$/
+     * @return array
+     */
+    public function activity_marked_as_complete($activityname, $activitytype, $completiontype) {
+        if ($completiontype == "manual") {
+            $imgalttext = get_string("completion-alt-manual-y", 'core_completion', $activityname);
+        } else {
+            $imgalttext = get_string("completion-alt-auto-y", 'core_completion', $activityname);
+        }
+        $csselementforactivitytype = "li.modtype_".strtolower($activitytype);
+
+        return new Given('"//img[contains(@alt, \''.$imgalttext.'\')]" "xpath_element" ' .
+            'should exist in the "'.$csselementforactivitytype.'" "css_element"');
+    }
+
+    /**
+     * Checks if the activity with specified name is maked as complete.
+     *
+     * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion should be marked as not complete$/
+     * @return array
+     */
+    public function activity_marked_as_not_complete($activityname, $activitytype, $completiontype) {
+        if ($completiontype == "manual") {
+            $imgalttext = get_string("completion-alt-manual-n", 'core_completion', $activityname);
+        } else {
+            $imgalttext = get_string("completion-alt-auto-n", 'core_completion', $activityname);
+        }
+        $csselementforactivitytype = "li.modtype_".strtolower($activitytype);
+
+        return new Given('"//img[contains(@alt, \''.$imgalttext.'\')]" "xpath_element" ' .
+            'should exist in the "'.$csselementforactivitytype.'" "css_element"');
+    }
 }
index c16b29c..ebcedc0 100644 (file)
@@ -317,4 +317,71 @@ class core_completion_externallib_testcase extends externallib_advanced_testcase
 
     }
 
+    /**
+     * Test mark_course_self_completed
+     */
+    public function test_mark_course_self_completed() {
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
+
+        $this->resetAfterTest(true);
+
+        $CFG->enablecompletion = true;
+        $student = $this->getDataGenerator()->create_user();
+        $teacher = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
+
+        // Set completion rules.
+        $completion = new completion_info($course);
+
+        $criteriadata = new stdClass();
+        $criteriadata->id = $course->id;
+        $criteriadata->criteria_activity = array();
+
+        // Self completion.
+        $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
+        $class = 'completion_criteria_self';
+        $criterion = new $class();
+        $criterion->update_config($criteriadata);
+
+        // Handle overall aggregation.
+        $aggdata = array(
+            'course'        => $course->id,
+            'criteriatype'  => null
+        );
+        $aggregation = new completion_aggregation($aggdata);
+        $aggregation->setMethod(COMPLETION_AGGREGATION_ALL);
+        $aggregation->save();
+
+        $this->setUser($student);
+
+        $result = core_completion_external::mark_course_self_completed($course->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(
+            core_completion_external::mark_course_self_completed_returns(), $result);
+
+        // We expect a valid result.
+        $this->assertEquals(true, $result['status']);
+
+        $result = core_completion_external::get_course_completion_status($course->id, $student->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(
+            core_completion_external::get_course_completion_status_returns(), $result);
+
+        // Course must be completed.
+        $this->assertEquals(COMPLETION_COMPLETE, $result['completionstatus']['completions'][0]['complete']);
+
+        try {
+            $result = core_completion_external::mark_course_self_completed($course->id);
+            $this->fail('Exception expected due course already self completed.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('useralreadymarkedcomplete', $e->errorcode);
+        }
+
+    }
+
 }
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 18633bd..e36b268 100644 (file)
@@ -2122,6 +2122,302 @@ class core_course_external extends external_api {
         );
     }
 
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function search_courses_parameters() {
+        return new external_function_parameters(
+            array(
+                'criterianame'  => new external_value(PARAM_ALPHA, 'criteria name
+                                                        (search, modulelist (only admins), blocklist (only admins), tagid)'),
+                'criteriavalue' => new external_value(PARAM_RAW, 'criteria value'),
+                'page'          => new external_value(PARAM_INT, 'page number (0 based)', VALUE_DEFAULT, 0),
+                'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0)
+            )
+        );
+    }
+
+    /**
+     * Search courses following the specified criteria.
+     *
+     * @param string $criterianame  Criteria name (search, modulelist (only admins), blocklist (only admins), tagid)
+     * @param string $criteriavalue Criteria value
+     * @param int $page             Page number (for pagination)
+     * @param int $perpage          Items per page
+     * @return array of course objects and warnings
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function search_courses($criterianame, $criteriavalue, $page=0, $perpage=0) {
+        global $CFG;
+        require_once($CFG->libdir . '/coursecatlib.php');
+
+        $warnings = array();
+
+        $parameters = array(
+            'criterianame'  => $criterianame,
+            'criteriavalue' => $criteriavalue,
+            'page'          => $page,
+            'perpage'       => $perpage
+        );
+        $params = self::validate_parameters(self::search_courses_parameters(), $parameters);
+
+        $allowedcriterianames = array('search', 'modulelist', 'blocklist', 'tagid');
+        if (!in_array($params['criterianame'], $allowedcriterianames)) {
+            throw new invalid_parameter_exception('Invalid value for criterianame parameter (value: '.$params['criterianame'].'),' .
+                'allowed values are: '.implode(',', $allowedcriterianames));
+        }
+
+        if ($params['criterianame'] == 'modulelist' or $params['criterianame'] == 'blocklist') {
+            require_capability('moodle/site:config', context_system::instance());
+        }
+
+        $paramtype = array(
+            'search' => PARAM_RAW,
+            'modulelist' => PARAM_PLUGIN,
+            'blocklist' => PARAM_INT,
+            'tagid' => PARAM_INT
+        );
+        $params['criteriavalue'] = clean_param($params['criteriavalue'], $paramtype[$params['criterianame']]);
+
+        // Prepare the search API options.
+        $searchcriteria = array();
+        $searchcriteria[$params['criterianame']] = $params['criteriavalue'];
+
+        $options = array();
+        if ($params['perpage'] != 0) {
+            $offset = $params['page'] * $params['perpage'];
+            $options = array('offset' => $offset, 'limit' => $params['perpage']);
+        }
+
+        // Search the courses.
+        $courses = coursecat::search_courses($searchcriteria, $options);
+        $totalcount = coursecat::search_courses_count($searchcriteria);
+
+        $finalcourses = array();
+        $categoriescache = array();
+
+        foreach ($courses as $course) {
+
+            $coursecontext = context_course::instance($course->id);
+
+            // Category information.
+            if (!isset($categoriescache[$course->category])) {
+                $categoriescache[$course->category] = coursecat::get($course->category);
+            }
+            $category = $categoriescache[$course->category];
+
+            // Retrieve course overfiew used files.
+            $files = array();
+            foreach ($course->get_course_overviewfiles() as $file) {
+                $fileurl = moodle_url::make_webservice_pluginfile_url($file->get_contextid(), $file->get_component(),
+                                                                        $file->get_filearea(), null, $file->get_filepath(),
+                                                                        $file->get_filename())->out(false);
+                $files[] = array(
+                    'filename' => $file->get_filename(),
+                    'fileurl' => $fileurl,
+                    'filesize' => $file->get_filesize()
+                );
+            }
+
+            // Retrieve the course contacts,
+            // we need here the users fullname since if we are not enrolled can be difficult to obtain them via other Web Services.
+            $coursecontacts = array();
+            foreach ($course->get_course_contacts() as $contact) {
+                 $coursecontacts[] = array(
+                    'id' => $contact['user']->id,
+                    'fullname' => $contact['username']
+                );
+            }
+
+            // Allowed enrolment methods (maybe we can self-enrol).
+            $enroltypes = array();
+            $instances = enrol_get_instances($course->id, true);
+            foreach ($instances as $instance) {
+                $enroltypes[] = $instance->enrol;
+            }
+
+            // Format summary.
+            list($summary, $summaryformat) =
+                external_format_text($course->summary, $course->summaryformat, $coursecontext->id, 'course', 'summary', null);
+
+            $coursereturns = array();
+            $coursereturns['id']                = $course->id;
+            $coursereturns['fullname']          = $course->get_formatted_fullname();
+            $coursereturns['shortname']         = $course->get_formatted_shortname();
+            $coursereturns['categoryid']        = $course->category;
+            $coursereturns['categoryname']      = $category->name;
+            $coursereturns['summary']           = $summary;
+            $coursereturns['summaryformat']     = $summaryformat;
+            $coursereturns['overviewfiles']     = $files;
+            $coursereturns['contacts']          = $coursecontacts;
+            $coursereturns['enrollmentmethods'] = $enroltypes;
+            $finalcourses[] = $coursereturns;
+        }
+
+        return array(
+            'total' => $totalcount,
+            'courses' => $finalcourses,
+            'warnings' => $warnings
+        );
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function search_courses_returns() {
+
+        return new external_single_structure(
+            array(
+                'total' => new external_value(PARAM_INT, 'total course count'),
+                'courses' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'course id'),
+                            'fullname' => new external_value(PARAM_TEXT, 'course full name'),
+                            'shortname' => new external_value(PARAM_TEXT, 'course short name'),
+                            'categoryid' => new external_value(PARAM_INT, 'category id'),
+                            'categoryname' => new external_value(PARAM_TEXT, 'category name'),
+                            'summary' => new external_value(PARAM_RAW, 'summary'),
+                            'summaryformat' => new external_format_value('summary'),
+                            'overviewfiles' => new external_multiple_structure(
+                                new external_single_structure(
+                                    array(
+                                        'filename' => new external_value(PARAM_FILE, 'overview file name'),
+                                        'fileurl'  => new external_value(PARAM_URL, 'overview file url'),
+                                        'filesize'  => new external_value(PARAM_INT, 'overview file size'),
+                                    )
+                                ),
+                                'additional overview files attached to this course'
+                            ),
+                            'contacts' => new external_multiple_structure(
+                                new external_single_structure(
+                                    array(
+                                        'id' => new external_value(PARAM_INT, 'contact user id'),
+                                        'fullname'  => new external_value(PARAM_NOTAGS, 'contact user fullname'),
+                                    )
+                                ),
+                                'contact users'
+                            ),
+                            'enrollmentmethods' => new external_multiple_structure(
+                                new external_value(PARAM_PLUGIN, 'enrollment method'),
+                                'enrollment methods list'
+                            ),
+                        )
+                    ), 'course'
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * 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 72bb761..9738ac4 100644 (file)
@@ -212,7 +212,7 @@ if (!empty($activities)) {
                 echo $OUTPUT->spacer(array('height'=>30, 'br'=>true)); // should be done with CSS instead
             }
             echo $OUTPUT->box_start();
-            if (!empty($activity->name)) {
+            if (strval($activity->name) !== '') {
                 echo html_writer::tag('h2', $activity->name);
             }
             $inbox = true;
index eed9a3d..675f367 100644 (file)
@@ -173,7 +173,7 @@ switch($requestmethod) {
                             $module->name = clean_param($title, PARAM_CLEANHTML);
                         }
 
-                        if (!empty($module->name)) {
+                        if (strval($module->name) !== '') {
                             $DB->update_record($cm->modname, $module);
                             $cm->name = $module->name;
                             \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
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 1b8bd90..875d882 100644 (file)
@@ -600,6 +600,71 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($DB->count_records('course'), count($courses));
     }
 
+    /**
+     * Test search_courses
+     */
+    public function test_search_courses () {
+
+        global $DB, $CFG;
+        require_once($CFG->dirroot . '/tag/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generatedcourses = array();
+        $coursedata1['fullname'] = 'FIRST COURSE';
+        $course1  = self::getDataGenerator()->create_course($coursedata1);
+        $coursedata2['fullname'] = 'SECOND COURSE';
+        $course2  = self::getDataGenerator()->create_course($coursedata2);
+        // Search by name.
+        $results = core_course_external::search_courses('search', 'FIRST');
+        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+        $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
+        $this->assertCount(1, $results['courses']);
+
+        // Create the forum.
+        $record = new stdClass();
+        $record->introformat = FORMAT_HTML;
+        $record->course = $course2->id;
+        // Set Aggregate type = Average of ratings.
+        $forum = self::getDataGenerator()->create_module('forum', $record);
+
+        // Search by module.
+        $results = core_course_external::search_courses('modulelist', 'forum');
+        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+        $this->assertEquals(1, $results['total']);
+
+        // Enable coursetag option.
+        set_config('block_tags_showcoursetags', true);
+        // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
+        tag_set('course', $course2->id, array('TAG-LABEL ON SECOND COURSE'), 'core', context_course::instance($course2->id)->id);
+        $taginstance = $DB->get_record('tag_instance', array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
+        // Search by tagid.
+        $results = core_course_external::search_courses('tagid', $taginstance->tagid);
+        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+        $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
+
+        // Search by block (use news_items default block).
+        $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
+        $results = core_course_external::search_courses('blocklist', $blockid);
+        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+        $this->assertEquals(2, $results['total']);
+
+        // Now as a normal user.
+        $user = self::getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $results = core_course_external::search_courses('search', 'FIRST');
+        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+        $this->assertCount(1, $results['courses']);
+        $this->assertEquals(1, $results['total']);
+        $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
+
+        // Search by block (use news_items default block). Should fail (only admins allowed).
+        $this->setExpectedException('required_capability_exception');
+        $results = core_course_external::search_courses('blocklist', $blockid);
+
+    }
+
     /**
      * Create a course with contents
      * @return array A list with the course object and course modules objects
@@ -1457,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) {
diff --git a/enrol/flatfile/classes/task/flatfile_sync_task.php b/enrol/flatfile/classes/task/flatfile_sync_task.php
new file mode 100644 (file)
index 0000000..a34da88
--- /dev/null
@@ -0,0 +1,66 @@
+<?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/>.
+
+/**
+ * Scheduled task for processing flatfile enrolments.
+ *
+ * @package    enrol_flatfile
+ * @copyright  2014 Troy Williams
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace enrol_flatfile\task;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Simple task to run sync enrolments.
+ *
+ * @copyright  2014 Troy Williams
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class flatfile_sync_task extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('flatfilesync', 'enrol_flatfile');
+    }
+
+    /**
+     * Do the job.
+     * Throw exceptions on errors (the job will be retried).
+     */
+    public function execute() {
+        global $CFG;
+
+        require_once($CFG->dirroot . '/enrol/flatfile/lib.php');
+
+        if (!enrol_is_enabled('flatfile')) {
+            return;
+        }
+
+        // Instance of enrol_flatfile_plugin.
+        $plugin = enrol_get_plugin('flatfile');
+        $result = $plugin->sync(new \null_progress_trace());
+        return $result;
+
+    }
+
+}
index 59df7e0..2b248c1 100644 (file)
  *   - you need to change the "www-data" to match the apache user account
  *   - use "su" if "sudo" not available
  *
+ * Update
+ *
+ * This plugin now has a enrolment sync scheduled task. Scheduled tasks were
+ * introduced in Moodle 2.7.  It is possible to override the scheduled tasks
+ * configuration and run a single scheduled task immediately using the
+ * admin/tool/task/cli/schedule_task.php script. This is the recommended
+ * method to use for immediate enrollment synchronisation.
+ *
+ * Usage help:
+ * $ php admin/tool/task/cli/schedule_task.php -h
+ *
+ * Execute task:
+ * $ sudo -u www-data /usr/bin/php admin/tool/task/cli/schedule_task.php /
+ * --execute=\\enrol_flatfile\\task\\flatfile_sync_task
+ *
  * @package    enrol_flatfile
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
diff --git a/enrol/flatfile/db/tasks.php b/enrol/flatfile/db/tasks.php
new file mode 100644 (file)
index 0000000..a78fd91
--- /dev/null
@@ -0,0 +1,37 @@
+<?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/>.
+
+/**
+ * Definition of flatfile enrolment scheduled tasks.
+ *
+ * @package    enrol_flatfile
+ * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+    array(
+        'classname' => '\enrol_flatfile\task\flatfile_sync_task',
+        'blocking' => 0,
+        'minute' => '15',
+        'hour' => '*',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    )
+);
index 7fd1901..a1c9922 100644 (file)
@@ -29,6 +29,7 @@ $string['filelockedmail'] = 'The text file you are using for file-based enrolmen
 $string['filelockedmailsubject'] = 'Important error: Enrolment file';
 $string['flatfile:manage'] = 'Manage user enrolments manually';
 $string['flatfile:unenrol'] = 'Unenrol users from the course manually';
+$string['flatfilesync'] = 'Flat file enrolment sync';
 $string['location'] = 'File location';
 $string['location_desc'] = 'Specify full path to the enrolment file. The file is automatically deleted after processing.';
 $string['notifyadmin'] = 'Notify administrator';
index 41e0577..57438fb 100644 (file)
@@ -161,11 +161,6 @@ class enrol_flatfile_plugin extends enrol_plugin {
         }
     }
 
-    public function cron() {
-        $trace = new text_progress_trace();
-        $this->sync($trace);
-    }
-
     /**
      * Execute synchronisation.
      * @param progress_trace
@@ -446,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 93ed0ef..0ab21c9 100644 (file)
@@ -467,4 +467,35 @@ class enrol_flatfile_testcase extends advanced_testcase {
         $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
         $this->assertEquals(0, $DB->count_records('role_assignments', array('roleid'=>$managerrole->id)));
     }
+
+    /**
+     * Flatfile enrolment sync task test.
+     */
+    public function test_flatfile_sync_task() {
+        global $CFG, $DB;
+        $this->resetAfterTest();
+
+        $flatfileplugin = enrol_get_plugin('flatfile');
+
+        $trace = new null_progress_trace();
+        $this->enable_plugin();
+        $file = "$CFG->dataroot/enrol.txt";
+        $flatfileplugin->set_config('location', $file);
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->assertNotEmpty($studentrole);
+
+        $user1 = $this->getDataGenerator()->create_user(array('idnumber' => 'u1'));
+        $course1 = $this->getDataGenerator()->create_course(array('idnumber' => 'c1'));
+        $context1 = context_course::instance($course1->id);
+
+        $data =
+            "add,student,u1,c1";
+        file_put_contents($file, $data);
+
+        $task = new enrol_flatfile\task\flatfile_sync_task;
+        $task->execute();
+
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid' => $studentrole->id)));
+    }
 }
index efc367c..e7e8b86 100644 (file)
@@ -25,7 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015090700;        // The current plugin version (Date: YYYYMMDDRR)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'enrol_flatfile';  // Full name of the plugin (used for diagnostics)
-$plugin->cron      = 60;
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 86bc7a0..94ccd7b 100644 (file)
@@ -128,10 +128,22 @@ switch ($action) {
             $roleid = null;
         }
 
+        if (empty($startdate)) {
+            if (!$startdate = get_config('enrol_manual', 'enrolstart')) {
+                // Default to now if there is no system setting.
+                $startdate = 4;
+            }
+        }
+
         switch($startdate) {
             case 2:
                 $timestart = $course->startdate;
                 break;
+            case 4:
+                // We mimic get_enrolled_sql round(time(), -2) but always floor as we want users to always access their
+                // courses once they are enrolled.
+                $timestart = intval(substr(time(), 0, 8) . '00') - 1;
+                break;
             case 3:
             default:
                 $today = time();
index 3e58900..db7d182 100644 (file)
@@ -66,6 +66,12 @@ function xmldb_enrol_manual_upgrade($oldversion) {
     // Moodle v2.9.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2015091500) {
+        // We keep today as default enrolment start time on upgrades.
+        set_config('enrolstart', 3, 'enrol_manual');
+        upgrade_plugin_savepoint(true, 2015091500, 'enrol', 'manual');
+    }
+
     return true;
 }
 
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 7c5f941..b325f4a 100644 (file)
@@ -29,6 +29,7 @@ $string['assignrole'] = 'Assign role';
 $string['browseusers'] = 'Browse users';
 $string['browsecohorts'] = 'Browse cohorts';
 $string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
+$string['defaultstart'] = 'Default enrolment start';
 $string['defaultperiod'] = 'Default enrolment duration';
 $string['defaultperiod_desc'] = 'Default length of time that the enrolment is valid. If set to zero, the enrolment duration will be unlimited by default.';
 $string['defaultperiod_help'] = 'Default length of time that the enrolment is valid, starting with the moment the user is enrolled. If disabled, the enrolment duration will be unlimited by default.';
@@ -56,6 +57,7 @@ $string['manual:manage'] = 'Manage user enrolments';
 $string['manual:unenrol'] = 'Unenrol users from the course';
 $string['manual:unenrolself'] = 'Unenrol self from the course';
 $string['messageprovider:expiry_notification'] = 'Manual enrolment expiry notifications';
+$string['now'] = 'Now';
 $string['pluginname'] = 'Manual enrolments';
 $string['pluginname_desc'] = 'The manual enrolments plugin allows users to be enrolled manually via a link in the course administration settings, by a user with appropriate permissions such as a teacher. The plugin should normally be enabled, since certain other enrolment plugins, such as self enrolment, require it.';
 $string['status'] = 'Enable manual enrolments';
@@ -71,4 +73,4 @@ $string['unenrolusers'] = 'Unenrol users';
 $string['wscannotenrol'] = 'Plugin instance cannot manually enrol a user in the course id = {$a->courseid}';
 $string['wsnoinstance'] = 'Manual enrolment plugin instance doesn\'t exist or is disabled for the course (id = {$a->courseid})';
 $string['wsusercannotassign'] = 'You don\'t have the permission to assign this role ({$a->roleid}) to this user ({$a->userid}) in this course({$a->courseid}).';
-$string['manualpluginnotinstalled'] = 'The "Manual" plugin has not yet been installed';
\ No newline at end of file
+$string['manualpluginnotinstalled'] = 'The "Manual" plugin has not yet been installed';
index 464d76b..b50e3cc 100644 (file)
@@ -226,14 +226,19 @@ class enrol_manual_plugin extends enrol_plugin {
         $button->class .= ' enrol_manual_plugin';
 
         $startdate = $manager->get_course()->startdate;
+        if (!$defaultstart = get_config('enrol_manual', 'enrolstart')) {
+            // Default to now if there is no system setting.
+            $defaultstart = 4;
+        }
         $startdateoptions = array();
-        $timeformat = get_string('strftimedatefullshort');
+        $dateformat = get_string('strftimedatefullshort');
         if ($startdate > 0) {
-            $startdateoptions[2] = get_string('coursestart') . ' (' . userdate($startdate, $timeformat) . ')';
+            $startdateoptions[2] = get_string('coursestart') . ' (' . userdate($startdate, $dateformat) . ')';
         }
-        $today = time();
-        $today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0);
-        $startdateoptions[3] = get_string('today') . ' (' . userdate($today, $timeformat) . ')' ;
+        $now = time();
+        $today = make_timestamp(date('Y', $now), date('m', $now), date('d', $now), 0, 0, 0);
+        $startdateoptions[3] = get_string('today') . ' (' . userdate($today, $dateformat) . ')';
+        $startdateoptions[4] = get_string('now', 'enrol_manual') . ' (' . userdate($now, get_string('strftimedatetimeshort')) . ')';
         $defaultduration = $instance->enrolperiod > 0 ? $instance->enrolperiod / 86400 : '';
 
         $modules = array('moodle-enrol_manual-quickenrolment', 'moodle-enrol_manual-quickenrolment-skin');
@@ -245,6 +250,7 @@ class enrol_manual_plugin extends enrol_plugin {
             'optionsStartDate'    => $startdateoptions,
             'defaultRole'         => $instance->roleid,
             'defaultDuration'     => $defaultduration,
+            'defaultStartDate'    => (int)$defaultstart,
             'disableGradeHistory' => $CFG->disablegradehistory,
             'recoverGradesDefault'=> '',
             'cohortsAvailable'    => cohort_get_available_cohorts($manager->get_context(), COHORT_WITH_NOTENROLLED_MEMBERS_ONLY, 0, 1) ? true : false
index 6c86e6f..0b9ca4c 100644 (file)
@@ -28,7 +28,7 @@ require_once($CFG->dirroot.'/enrol/manual/locallib.php');
 $enrolid      = required_param('enrolid', PARAM_INT);
 $roleid       = optional_param('roleid', -1, PARAM_INT);
 $extendperiod = optional_param('extendperiod', 0, PARAM_INT);
-$extendbase   = optional_param('extendbase', 3, PARAM_INT);
+$extendbase   = optional_param('extendbase', 0, PARAM_INT);
 
 $instance = $DB->get_record('enrol', array('id'=>$enrolid, 'enrol'=>'manual'), '*', MUST_EXIST);
 $course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_EXIST);
@@ -83,24 +83,31 @@ for ($i=1; $i<=365; $i++) {
     $seconds = $i * 86400;
     $periodmenu[$seconds] = get_string('numdays', '', $i);
 }
-// Work out the apropriate default setting.
+// Work out the apropriate default settings.
 if ($extendperiod) {
     $defaultperiod = $extendperiod;
 } else {
     $defaultperiod = $instance->enrolperiod;
 }
+if (empty($extendbase)) {
+    if (!$extendbase = get_config('enrol_manual', 'enrolstart')) {
+        // Default to now if there is no system setting.
+        $extendbase = 4;
+    }
+}
 
 // Build the list of options for the starting from dropdown.
-$timeformat = get_string('strftimedatefullshort');
-$today = time();
-$today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0);
+$now = time();
+$today = make_timestamp(date('Y', $now), date('m', $now), date('d', $now), 0, 0, 0);
+$dateformat = get_string('strftimedatefullshort');
 
 // Enrolment start.
 $basemenu = array();
 if ($course->startdate > 0) {
-    $basemenu[2] = get_string('coursestart') . ' (' . userdate($course->startdate, $timeformat) . ')';
+    $basemenu[2] = get_string('coursestart') . ' (' . userdate($course->startdate, $dateformat) . ')';
 }
-$basemenu[3] = get_string('today') . ' (' . userdate($today, $timeformat) . ')' ;
+$basemenu[3] = get_string('today') . ' (' . userdate($today, $dateformat) . ')';
+$basemenu[4] = get_string('now', 'enrol_manual') . ' (' . userdate($now, get_string('strftimedatetimeshort')) . ')';
 
 // Process add and removes.
 if ($canenrol && optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
@@ -111,6 +118,11 @@ if ($canenrol && optional_param('add', false, PARAM_BOOL) && confirm_sesskey())
                 case 2:
                     $timestart = $course->startdate;
                     break;
+                case 4:
+                    // We mimic get_enrolled_sql round(time(), -2) but always floor as we want users to always access their
+                    // courses once they are enrolled.
+                    $timestart = intval(substr($now, 0, 8) . '00') - 1;
+                    break;
                 case 3:
                 default:
                     $timestart = $today;