Merge branch 'MDL-50207-master' of git://github.com/lameze/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 23 Sep 2015 18:05:10 +0000 (20:05 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 24 Sep 2015 13:38:19 +0000 (15:38 +0200)
471 files changed:
.jshintrc
Gruntfile.js
admin/cli/install.php
admin/roles/classes/capability_table_with_risks.php
admin/roles/classes/define_role_table_advanced.php
admin/roles/classes/define_role_table_basic.php
admin/roles/classes/override_permissions_table_advanced.php
admin/roles/classes/view_role_definition_table.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
blocks/tags/block_tags.php
blocks/tags/coursetags.js [deleted file]
blocks/tags/lang/en/block_tags.php
blocks/tags/lang/en/deprecated.txt [new file with mode: 0644]
blocks/tags/styles.css [deleted file]
blocks/tags/tests/behat/coursetags.feature [deleted file]
blog/edit.php
calendar/export_execute.php
completion/classes/external.php
completion/cron.php
completion/tests/behat/behat_completion.php
completion/tests/externallib_test.php
course/delete.php
course/edit.php
course/edit_form.php
course/editsection.php
course/externallib.php
course/format/topics/tests/behat/edit_delete_sections.feature
course/format/weeks/tests/behat/edit_delete_sections.feature
course/lib.php
course/recent.php
course/rest.php
course/tags.php [new file with mode: 0644]
course/tags_form.php [new file with mode: 0644]
course/tests/behat/coursetags.feature [new file with mode: 0644]
course/tests/behat/create_delete_course.feature
course/tests/externallib_test.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/manual/ajax.php
enrol/manual/db/upgrade.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/locallib.php
enrol/meta/tests/plugin_test.php
enrol/self/classes/empty_form.php [new file with mode: 0644]
enrol/self/lib.php
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/overview.php
group/tests/externallib_test.php
install/lang/xct/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/blog.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/grades.php
lang/en/my.php
lang/en/role.php
lang/en/tag.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/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/grades_external.php
lib/classes/message/inbound/handler.php
lib/classes/output/external.php
lib/classes/plugin_manager.php
lib/classes/task/completion_daily_task.php [moved from lib/classes/task/completion_cron_task.php with 77% similarity]
lib/classes/task/completion_regular_task.php [new file with mode: 0644]
lib/clilib.php
lib/cronlib.php
lib/db/access.php
lib/db/install.xml
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/deprecatedlib.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/lib.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/tinymce/lib.php
lib/external/externallib.php
lib/externallib.php
lib/form/editor.php
lib/form/searchableselector.js
lib/formslib.php
lib/grade/grade_category.php
lib/grade/grade_item.php
lib/javascript-static.js
lib/modinfolib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/outputrenderers.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/moodlelib_test.php
lib/tests/navigationlib_test.php
lib/tests/other/completion.manualtest.txt
lib/tests/upgradelib_test.php
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-event/moodle-core-event-debug.js
lib/yui/build/moodle-core-event/moodle-core-event-min.js
lib/yui/build/moodle-core-event/moodle-core-event.js
lib/yui/src/dragdrop/js/dragdrop.js
lib/yui/src/event/js/event.js
login/signup_form.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 [new file with mode: 0644]
mod/book/db/services.php [new file with mode: 0644]
mod/book/lib.php
mod/book/tests/externallib_test.php [new file with mode: 0644]
mod/book/tests/lib_test.php
mod/book/version.php
mod/book/view.php
mod/chat/chat_ajax.php
mod/chat/classes/external.php [new file with mode: 0644]
mod/chat/db/services.php [new file with mode: 0644]
mod/chat/lib.php
mod/chat/tests/externallib_test.php [new file with mode: 0644]
mod/chat/version.php
mod/chat/view.php
mod/choice/classes/external.php [new file with mode: 0644]
mod/choice/db/services.php [new file with mode: 0644]
mod/choice/lib.php
mod/choice/tests/behat/publish_results_anonymously.feature
mod/choice/tests/externallib_test.php [new file with mode: 0644]
mod/choice/tests/lib_test.php [new file with mode: 0644]
mod/choice/version.php
mod/choice/view.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/externallib.php
mod/forum/index.php
mod/forum/lib.php
mod/forum/markposts.php
mod/forum/post.php
mod/forum/settracking.php
mod/forum/tests/behat/completion_condition_number_discussions.feature
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/upgrade.txt
mod/forum/version.php
mod/imscp/classes/external.php [new file with mode: 0644]
mod/imscp/db/services.php [new file with mode: 0644]
mod/imscp/lib.php
mod/imscp/tests/externallib_test.php [new file with mode: 0644]
mod/imscp/tests/lib_test.php
mod/imscp/version.php
mod/imscp/view.php
mod/lesson/continue.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.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_navigation.feature
mod/lesson/tests/behat/lesson_question_attempts.feature [new file with mode: 0644]
mod/lesson/tests/behat/lesson_report.feature [new file with mode: 0644]
mod/lesson/view.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/manually_mark_question.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/upgrade.txt
mod/wiki/pagelib.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]
notes/externallib.php
pix/f/FileTypesIcons-LICENSE.txt
pix/f/Oxygen-LICENSE.txt
question/classes/bank/view.php
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: 0755]
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 [moved from blocks/tags/settings.php with 66% similarity]
question/type/match/question.php
question/type/match/tests/question_test.php
rating/classes/external.php
rating/index.php
rating/tests/externallib_test.php
rss/file.php
tag/classes/external.php
tag/coursetags_add.php [deleted file]
tag/coursetags_edit.php [deleted file]
tag/coursetags_more.php [deleted file]
tag/coursetagslib.php [deleted file]
tag/index.php
tag/tag.js
tag/tests/behat/edit_tag.feature
tag/tests/events_test.php
tag/upgrade.txt
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/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..51364b9 100644 (file)
@@ -273,7 +273,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 +296,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;
index b0480b0..acf7257 100644 (file)
@@ -163,9 +163,8 @@ abstract class core_role_capability_table_with_risks extends core_role_capabilit
     protected abstract function add_permission_cells($capability);
 
     protected function add_row_cells($capability) {
-        $this->add_permission_cells($capability);
+        $cells = $this->add_permission_cells($capability);
         // One cell for each possible risk.
-        $cells = '';
         foreach ($this->allrisks as $riskname => $risk) {
             $cells .= '<td class="risk ' . str_replace('risk', '', $riskname) . '">';
             if ($risk & (int)$capability->riskbitmask) {
index 8b4cc51..06f565d 100644 (file)
@@ -628,6 +628,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
 
     protected function add_permission_cells($capability) {
         // One cell for each possible permission.
+        $content = '';
         foreach ($this->displaypermissions as $perm => $permname) {
             $strperm = $this->strperms[$permname];
             $extraclass = '';
@@ -638,11 +639,12 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             if ($this->permissions[$capability->name] == $perm) {
                 $checked = 'checked="checked" ';
             }
-            echo '<td class="' . $permname . $extraclass . '">';
-            echo '<label><input type="radio" name="' . $capability->name .
+            $content .= '<td class="' . $permname . $extraclass . '">';
+            $content .= '<label><input type="radio" name="' . $capability->name .
                 '" value="' . $perm . '" ' . $checked . '/> ';
-            echo '<span class="note">' . $strperm . '</span>';
-            echo '</label></td>';
+            $content .= '<span class="note">' . $strperm . '</span>';
+            $content .= '</label></td>';
         }
+        return $content;
     }
 }
index 14393e7..0c3d692 100644 (file)
@@ -45,19 +45,20 @@ class core_role_define_role_table_basic extends core_role_define_role_table_adva
         $perm = $this->permissions[$capability->name];
         $permname = $this->allpermissions[$perm];
         $defaultperm = $this->allpermissions[$this->parentpermissions[$capability->name]];
-        echo '<td class="' . $permname . '">';
+        $content = '<td class="' . $permname . '">';
         if ($perm == CAP_ALLOW || $perm == CAP_INHERIT) {
             $checked = '';
             if ($perm == CAP_ALLOW) {
                 $checked = 'checked="checked" ';
             }
-            echo '<input type="hidden" name="' . $capability->name . '" value="' . CAP_INHERIT . '" />';
-            echo '<label><input type="checkbox" name="' . $capability->name .
+            $content .= '<input type="hidden" name="' . $capability->name . '" value="' . CAP_INHERIT . '" />';
+            $content .= '<label><input type="checkbox" name="' . $capability->name .
                 '" value="' . CAP_ALLOW . '" ' . $checked . '/> ' . $this->strallow . '</label>';
         } else {
-            echo '<input type="hidden" name="' . $capability->name . '" value="' . $perm . '" />';
-            echo $this->strperms[$permname] . '<span class="note">' . $this->stradvmessage . '</span>';
+            $content .= '<input type="hidden" name="' . $capability->name . '" value="' . $perm . '" />';
+            $content .= $this->strperms[$permname] . '<span class="note">' . $this->stradvmessage . '</span>';
         }
-        echo '</td>';
+        $content .= '</td>';
+        return $content;
     }
 }
index 7da1239..a872b96 100644 (file)
@@ -73,6 +73,7 @@ class core_role_override_permissions_table_advanced extends core_role_capability
         }
 
         // One cell for each possible permission.
+        $content = '';
         foreach ($this->displaypermissions as $perm => $permname) {
             $strperm = $this->strperms[$permname];
             $extraclass = '';
@@ -83,8 +84,8 @@ class core_role_override_permissions_table_advanced extends core_role_capability
             if ($this->permissions[$capability->name] == $perm) {
                 $checked = 'checked="checked" ';
             }
-            echo '<td class="' . $permname . $extraclass . '">';
-            echo '<label><input type="radio" name="' . $capability->name .
+            $content .= '<td class="' . $permname . $extraclass . '">';
+            $content .= '<label><input type="radio" name="' . $capability->name .
                 '" value="' . $perm . '" ' . $checked . $disabled . '/> ';
             if ($perm == CAP_INHERIT) {
                 $inherited = $this->parentpermissions[$capability->name];
@@ -95,8 +96,9 @@ class core_role_override_permissions_table_advanced extends core_role_capability
                 }
                 $strperm .= ' (' . $inherited . ')';
             }
-            echo '<span class="note">' . $strperm . '</span>';
-            echo '</label></td>';
+            $content .= '<span class="note">' . $strperm . '</span>';
+            $content .= '</label></td>';
         }
+        return $content;
     }
 }
index a39ff0c..0859969 100644 (file)
@@ -129,7 +129,7 @@ class core_role_view_role_definition_table extends core_role_define_role_table_a
         } else {
             $default = "&#xa0;";
         }
-        echo '<td class="' . $permname . '">' . $this->strperms[$permname] . '<span class="note">' .
+        return '<td class="' . $permname . '">' . $this->strperms[$permname] . '<span class="note">' .
             $default . '</span></td>';
 
     }
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 66a4be8..ff9e88b 100644 (file)
@@ -95,106 +95,7 @@ class block_tags extends block_base {
 
         require_once($CFG->dirroot.'/tag/locallib.php');
 
-        if (empty($CFG->block_tags_showcoursetags) or !$CFG->block_tags_showcoursetags) {
-
-            $this->content->text = tag_print_cloud(null, $this->config->numberoftags, true);
-
-        } else {
-            // Start of show course tags section.
-            require_once($CFG->dirroot.'/tag/coursetagslib.php');
-
-            // Page awareness.
-            $tagtype = 'all';
-            if ($SCRIPT == '/my/index.php') {
-                $tagtype = 'my';
-            } else if (isset($this->page->course->id)) {
-                if ($this->page->course->id != SITEID) {
-                    $tagtype = 'course';
-                }
-            }
-
-            // DB hits to get groups of marked up tags (if available).
-            // TODO check whether time limited personal tags are required.
-            $content = '';
-            $moretags = new moodle_url('/tag/coursetags_more.php', array('show'=>$tagtype));
-            if ($tagtype == 'all') {
-                $tags = coursetag_get_tags(0, 0, $this->config->tagtype, $this->config->numberoftags);
-            } else if ($tagtype == 'course') {
-                $tags = coursetag_get_tags($this->page->course->id, 0, $this->config->tagtype, $this->config->numberoftags);
-                $moretags->param('courseid', $this->page->course->id);
-            } else if ($tagtype == 'my') {
-                $tags = coursetag_get_tags(0, $USER->id, $this->config->tagtype, $this->config->numberoftags);
-            }
-            $tagcloud = tag_print_cloud($tags, 150, true);
-            if (!$tagcloud) {
-                $tagcloud = get_string('notagsyet', 'block_tags');
-            }
-
-            // Prepare the divs that display the groups of tags.
-            $content = get_string($tagtype."tags", 'block_tags').
-                    '<div class="coursetag_list">'.$tagcloud.'</div>
-                    <div class="coursetag_morelink">
-                        <a href="'.$moretags->out().'" title="'.get_string('moretags', 'block_tags').'">'
-                        .get_string('more', 'block_tags').'</a>
-                    </div>';
-            // Add javascript.
-            coursetag_get_jscript();
-
-            // Add the divs (containing the tags) to the block's content.
-            $this->content->text .= $content;
-
-            // Add the input form section (allowing a user to tag the current course) and navigation, or login message.
-            if (isloggedin() && !isguestuser()) {
-                // Only show the input form on course pages for those allowed (or not barred).
-                if ($tagtype == 'course' &&
-                                has_capability('moodle/tag:create', context_course::instance($this->page->course->id))) {
-                    $buttonadd = get_string('add', 'block_tags');
-                    $arrowtitle = get_string('arrowtitle', 'block_tags');
-                    $edittags = get_string('edittags', 'block_tags');
-                    $sesskey = sesskey();
-                    $arrowright = $OUTPUT->pix_url('t/arrow_left');
-                    $redirect = $this->page->url->out();
-                    $this->content->footer .= <<<EOT
-                        <hr />
-                        <form action="{$CFG->wwwroot}/tag/coursetags_add.php" method="post" id="coursetag"
-                                onsubmit="return ctags_checkinput(this.coursetag_new_tag.value)">
-                            <div style="display: none;">
-                                <input type="hidden" name="entryid" value="$COURSE->id" />
-                                <input type="hidden" name="userid" value="$USER->id" />
-                                <input type="hidden" name="sesskey" value="$sesskey" />
-                                <input type="hidden" name="returnurl" value="$redirect" />
-                                </div>
-                            <div class="coursetag_form_wrapper">
-                                <div class="coursetag_form_positioner">
-                                    <div class="coursetag_form_input1">
-                                        <input type="text" name="coursetag_sug_keyword" class="coursetag_form_input1a" disabled="disabled" />
-                                    </div>
-                                    <div class="coursetag_form_input2">
-                                        <input type="text" name="coursetag_new_tag" id="coursetag_new_tag"
-                                        class="coursetag_form_input2a" onfocus="ctags_getKeywords()" onkeyup="ctags_getKeywords()" maxlength="50" />
-                                    </div>
-                                    <div class="coursetag_form_input3" id="coursetag_sug_btn">
-                                        <a title="$arrowtitle">
-                                            <img src="$arrowright" width="10" height="10" alt="enter" onclick="ctags_setKeywords()" />
-                                        </a>
-                                    </div>
-                                </div>
-                                <div style="display: inline;">
-                                    <button type="submit">$buttonadd</button>
-                                    <a href="$CFG->wwwroot/tag/coursetags_edit.php?courseid=$COURSE->id" title="$edittags">$edittags</a>
-                                </div>
-                            </div>
-                        </form>
-EOT;
-                }
-            } else {
-                // If not logged in.
-                $this->content->footer = '<hr />'.get_string('please', 'block_tags').'
-                    <a href="'.get_login_url().'">'.get_string('login', 'block_tags').'
-                        </a> '.get_string('tagunits', 'block_tags');
-            }
-        }
-        // End of show course tags section.
+        $this->content->text = tag_print_cloud(null, $this->config->numberoftags, true);
 
         return $this->content;
     }
diff --git a/blocks/tags/coursetags.js b/blocks/tags/coursetags.js
deleted file mode 100644 (file)
index 783146f..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * coursetags.js
- * @author j.beedell@open.ac.uk July07
- *
- * getKeywords modified from an original script (Auto Complete Textfield)
- * from The JavaScript Source http://javascript.internet.com
- * originally created by: Timothy Groves http://www.brandspankingnew.net/
- */
-
-
-function ctags_show_div(mydiv) {
-    for(x in coursetagdivs) {
-        if(mydiv == coursetagdivs[x]) {
-            document.getElementById(coursetagdivs[x]).style.display="block";
-        } else {
-            document.getElementById(coursetagdivs[x]).style.display="none";
-        }
-    }
-    return false;
-}
-
-var sug = "";
-var sug_disp = "";
-
-function ctags_getKeywords() {
-  /*
-  // This 'workaround' removing the xhtml strict form autocomplete="off" needs to
-  // be added to the body onload() script to work - but decided not to include
-  // (having the browser list might help with screen readers more than this script)
-  // document.forms['coursetag'].setAttribute("autocomplete", "off");
-  */
-  var input = document.forms['coursetag'].coursetag_new_tag.value;
-  var len = input.length;
-  sug_disp = ""; sug = "";
-
-  if (input.length) {
-    for (ele in coursetag_tags)
-    {
-      if (coursetag_tags[ele].substr(0,len).toLowerCase() == input.toLowerCase())
-      {
-        sug_disp = input + coursetag_tags[ele].substr(len);
-        sug = coursetag_tags[ele];
-        break;
-      }
-    }
-  }
-  document.forms['coursetag'].coursetag_sug_keyword.value = sug_disp;
-  if (!sug.length || input == sug_disp) {
-    document.getElementById('coursetag_sug_btn').style.display = "none";
-  } else {
-    document.getElementById('coursetag_sug_btn').style.display = "block";
-  }
-}
-
-function ctags_setKeywords() {
-  document.forms['coursetag'].coursetag_new_tag.value = sug;
-  ctags_hideSug();
-}
-
-function ctags_hideSug() {
-  document.forms['coursetag'].coursetag_sug_keyword.value = "";
-  document.getElementById('coursetag_sug_btn').style.display = "none";
-}
index c63f492..edf9669 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['configtitle'] = 'Block title';
+$string['disabledtags'] = 'Tags are disabled';
+$string['defaultdisplay'] = 'Tag type to display';
+$string['pluginname'] = 'Tags';
+$string['tags:addinstance'] = 'Add a new tags block';
+$string['tags:myaddinstance'] = 'Add a new tags block to Dashboard';
+
+// Deprecated since 3.0
+
 $string['add'] = 'Add';
 $string['alltags'] = 'All tags:';
 $string['arrowtitle'] = 'Click here to enter the suggested text (grey letters).';
-$string['configtitle'] = 'Block title';
 $string['coursetags'] = 'Course tags:';
-$string['disabledtags'] = 'Tags are disabled';
-$string['defaultdisplay'] = 'Tag type to display';
 $string['edit'] = 'edit...';
 $string['editdeletemytag'] = 'Delete tag from this course:';
 $string['editmytags'] = 'My tags - shortcuts to all your tagged courses.';
@@ -65,13 +71,10 @@ $string['mycoursetags'] = 'My course tags:';
 $string['mytags'] = 'My tags:';
 $string['notagsyet'] = 'No tags yet';
 $string['please'] = 'Please';
-$string['pluginname'] = 'Tags';
 $string['select'] = 'Select...';
 $string['showcoursetags'] = 'Show course tags';
 $string['showcoursetagsdef'] = 'Display the course tagging features in the tags block, allowing students to tag courses.';
 $string['suggestedtagthisunit'] = 'Suggested tag to this course:';
 $string['tags'] = 'tags';
-$string['tags:addinstance'] = 'Add a new tags block';
-$string['tags:myaddinstance'] = 'Add a new tags block to Dashboard';
 $string['tagthisunit'] = 'Tag this course:';
 $string['tagunits'] = 'to tag your favourite courses';
diff --git a/blocks/tags/lang/en/deprecated.txt b/blocks/tags/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..7c83a9d
--- /dev/null
@@ -0,0 +1,42 @@
+add,block_tags
+alltags,block_tags
+arrowtitle,block_tags
+coursetags,block_tags
+edit,block_tags
+editdeletemytag,block_tags
+editmytags,block_tags
+editmytagsfor,block_tags
+editnopersonaltags,block_tags
+edittags,block_tags
+edittagthisunit,block_tags
+editthiscoursetags,block_tags
+edittitle,block_tags
+entries,block_tags
+entry,block_tags
+jserror1,block_tags
+jserror2,block_tags
+login,block_tags
+more,block_tags
+moreorder,block_tags
+moreorderalpha,block_tags
+moreorderdate,block_tags
+moreorderpop,block_tags
+moreshow,block_tags
+moreshowalltags,block_tags
+moreshowcommtags,block_tags
+moreshowcoursetags,block_tags
+moreshowmytags,block_tags
+moreshowofficialtags,block_tags
+moretags,block_tags
+moretitle,block_tags
+morewelcome,block_tags
+mytags,block_tags
+notagsyet,block_tags
+please,block_tags
+select,block_tags
+showcoursetags,block_tags
+showcoursetagsdef,block_tags
+suggestedtagthisunit,block_tags
+tags,block_tags
+tagthisunit,block_tags
+tagunits,block_tags
diff --git a/blocks/tags/styles.css b/blocks/tags/styles.css
deleted file mode 100644 (file)
index 59b1afc..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-.block_tags {}
-.block_tags #coursetag {}
-.block_tags #coursetag .coursetag_form_wrapper {}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner {position: relative;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input1 {position: relative;top: 0;left: 0;z-index: 1;width:100%;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input2 {position: absolute;top: 0;left: 0;z-index: 2;width:100%;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input3 {position: absolute;top: 3px;left: 12.8em;display: none;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input1a {background-color: white; border: 1px solid #999;width: 12em;padding: 2px;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input2a {background-color: transparent; border: 1px solid #999;width: 12em;color: #669954;padding: 2px;}
-.block_tags .coursetag_morelink {}
-.block_tags .coursetag_list {}
\ No newline at end of file
diff --git a/blocks/tags/tests/behat/coursetags.feature b/blocks/tags/tests/behat/coursetags.feature
deleted file mode 100644 (file)
index c5f9c55..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-@block @block_tags @core_tag
-Feature: Block tags displaying course tags
-  In order to tag courses
-  As a user
-  I need to be able to use the block tags
-
-  Background:
-    Given the following "users" exist:
-      | username | firstname | lastname | email |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
-      | student1 | Student | 1 | student1@example.com |
-      | student2 | Student | 2 | student2@example.com |
-    And the following "courses" exist:
-      | fullname  | shortname |
-      | Course 1  | c1        |
-    And the following "tags" exist:
-      | name         | tagtype  |
-      | Neverusedtag | official |
-    And the following "course enrolments" exist:
-      | user     | course | role           |
-      | teacher1 | c1     | editingteacher |
-      | student1 | c1     | student        |
-      | student2 | c1     | student        |
-    And I log in as "admin"
-    And I set the following administration settings values:
-      | Show course tags | 1 |
-    And I log out
-
-  Scenario: Add Tags block to tag courses in a course
-    When I log in as "teacher1"
-    And I follow "Course 1"
-    And I turn editing mode on
-    And I add the "Tags" block
-    And I log out
-    And I log in as "student1"
-    And I follow "Course 1"
-    And I should not see "Neverusedtag" in the "Tags" "block"
-    And I click on "more..." "link" in the "Tags" "block"
-    And I should not see "Neverusedtag"
-    And I follow "c1"
-    And I set the field "coursetag_new_tag" to "Dogs, Mice"
-    And I press "Add"
-    And I should see "Dogs" in the "Tags" "block"
-    And I should see "Mice" in the "Tags" "block"
-    And I log out
-    And I log in as "student2"
-    And I follow "Course 1"
-    And I should see "Dogs" in the "Tags" "block"
-    And I set the field "coursetag_new_tag" to "Cats, Dogs"
-    And I press "Add"
-    And I should see "Dogs" in the "Tags" "block"
-    And I should see "Cats" in the "Tags" "block"
-    And I click on "more..." "link" in the "Tags" "block"
-    And "Cats" "link" should appear before "Dogs" "link"
-    And "Dogs" "link" should appear before "Mice" "link"
-    And I follow "My tags"
-    And I should see "Dogs"
-    And I should see "Cats"
-    And I should not see "Mice"
-    And I follow "All tags"
-    And I follow "Popularity"
-    And "Mice" "link" should appear before "Dogs" "link"
-    And I should not see "Neverusedtag"
-    And I log out
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 d464901..dcbad6b 100644 (file)
@@ -365,7 +365,7 @@ class core_completion_external extends external_api {
                                          'type' => new external_value(PARAM_TEXT, 'Type description'),
                                          'criteria' => new external_value(PARAM_RAW, 'Criteria description'),
                                          'requirement' => new external_value(PARAM_TEXT, 'Requirement description'),
-                                         'status' => new external_value(PARAM_TEXT, 'Status description'),
+                                         'status' => new external_value(PARAM_RAW, 'Status description, can be anything'),
                                          ), 'details'),
                                  ), 'Completions'
                             ), ''
@@ -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 23e682c..f56e5dd 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Cron job for reviewing and aggregating course completion criteria
+ * Code used by scheduled tasks for reviewing and aggregating course completion criteria.
  *
  * @package core_completion
  * @category completion
 defined('MOODLE_INTERNAL') || die();
 require_once($CFG->libdir.'/completionlib.php');
 
-/**
- * Update user's course completion statuses
- *
- * First update all criteria completions, then aggregate all criteria completions
- * and update overall course completions
- */
-function completion_cron() {
-
-    completion_cron_mark_started();
-
-    completion_cron_criteria();
-
-    completion_cron_completions();
-}
-
 /**
  * Mark users as started if the config option is set
  *
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 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 6f41d19..0189fae 100644 (file)
@@ -122,6 +122,12 @@ if (!empty($course)) {
         $course->{'role_'.$alias->roleid} = $alias->name;
     }
 
+    // Populate course tags.
+    if (!empty($CFG->usetags)) {
+        include_once($CFG->dirroot.'/tag/lib.php');
+        $course->tags = tag_get_tags_array('course', $course->id);
+    }
+
 } else {
     // Editor should respect category context if course context is not set.
     $editoroptions['context'] = $catcontext;
index 54359aa..3faea92 100644 (file)
@@ -301,6 +301,13 @@ class course_edit_form extends moodleform {
             }
         }
 
+        if (!empty($CFG->usetags) &&
+                ((empty($course->id) && guess_if_creator_will_have_course_capability('moodle/course:tag', $categorycontext))
+                || (!empty($course->id) && has_capability('moodle/course:tag', $coursecontext)))) {
+            $mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
+            $mform->addElement('tags', 'tags', get_string('tags'));
+        }
+
         // When two elements we need a group.
         $buttonarray = array();
         if ($returnto !== 0) {
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 6b6cf0b..4b036d8 100644 (file)
@@ -43,7 +43,7 @@ Feature: Sections can be edited and deleted in topics format
   Scenario: Deleting the last section in topics format
     When I click on "Delete topic" "link" in the "li#section-5" "css_element"
     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
@@ -51,7 +51,7 @@ Feature: Sections can be edited and deleted in topics format
 
   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"
+    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"
@@ -63,7 +63,7 @@ Feature: Sections can be edited and deleted 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 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
@@ -77,7 +77,7 @@ Feature: Sections can be edited and deleted in topics format
     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 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 76d4004..c8b1022 100644 (file)
@@ -45,7 +45,7 @@ Feature: Sections can be edited and deleted 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"
     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
@@ -54,7 +54,7 @@ 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"
+    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"
@@ -66,7 +66,7 @@ Feature: Sections can be edited and deleted 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 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
@@ -80,7 +80,7 @@ Feature: Sections can be edited and deleted in weeks format
     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 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 7c89127..5cbc191 100644 (file)
@@ -2547,7 +2547,8 @@ function course_overviewfiles_options($course) {
  * @return object new course instance
  */
 function create_course($data, $editoroptions = NULL) {
-    global $DB;
+    global $DB, $CFG;
+    require_once($CFG->dirroot.'/tag/lib.php');
 
     //check the categoryid - must be given for all new courses
     $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
@@ -2623,6 +2624,11 @@ function create_course($data, $editoroptions = NULL) {
     // set up enrolments
     enrol_course_updated(true, $course, $data);
 
+    // Update course tags.
+    if ($CFG->usetags && isset($data->tags)) {
+        tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id);
+    }
+
     // Trigger a course created event.
     $event = \core\event\course_created::create(array(
         'objectid' => $course->id,
@@ -2646,7 +2652,8 @@ function create_course($data, $editoroptions = NULL) {
  * @return void
  */
 function update_course($data, $editoroptions = NULL) {
-    global $DB;
+    global $DB, $CFG;
+    require_once($CFG->dirroot.'/tag/lib.php');
 
     $data->timemodified = time();
 
@@ -2733,6 +2740,11 @@ function update_course($data, $editoroptions = NULL) {
     // update enrol settings
     enrol_course_updated(false, $course, $data);
 
+    // Update course tags.
+    if ($CFG->usetags && isset($data->tags)) {
+        tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id);
+    }
+
     // Trigger a course updated event.
     $event = \core\event\course_updated::create(array(
         'objectid' => $course->id,
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();
diff --git a/course/tags.php b/course/tags.php
new file mode 100644 (file)
index 0000000..8e81fb1
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Edit course tags
+ *
+ * @package    core_course
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once("../config.php");
+require_once($CFG->dirroot . '/tag/lib.php');
+require_once($CFG->dirroot . '/course/tags_form.php');
+
+$id = required_param('id', PARAM_INT); // Course id.
+$returnurl = optional_param('return', null, PARAM_LOCALURL);
+$course = get_course($id);
+
+require_login();
+
+// Check capabilities but do not call require_login($course) - the user does not have to be enrolled.
+$context = context_course::instance($course->id);
+if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
+    print_error('coursehidden', '', $CFG->wwwroot .'/');
+}
+require_capability('moodle/course:tag', $context);
+if (empty($CFG->usetags)) {
+    print_error('tagsaredisabled', 'tag');
+}
+
+$PAGE->set_course($course);
+$PAGE->set_pagelayout('incourse');
+$PAGE->set_url('/course/tags.php', array('id' => $course->id));
+$PAGE->set_title(get_string('coursetags', 'tag'));
+$PAGE->set_heading($course->fullname);
+
+$form = new coursetags_form();
+$data = array('id' => $course->id, 'tags' => tag_get_tags_array('course', $course->id));
+$form->set_data($data);
+
+$redirecturl = $returnurl ? new moodle_url($returnurl) : course_get_url($course);
+if ($form->is_cancelled()) {
+    redirect($redirecturl);
+} else if ($data = $form->get_data()) {
+    tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id);
+    redirect($redirecturl);
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('coursetags', 'tag'));
+
+$form->display();
+
+echo $OUTPUT->footer();
diff --git a/course/tags_form.php b/course/tags_form.php
new file mode 100644 (file)
index 0000000..08cacc0
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Edit course tags form
+ *
+ * @package    core_course
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Edit course tags form
+ *
+ * @package    core_course
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class coursetags_form extends moodleform {
+
+    /**
+     * Form definition
+     */
+    public function definition() {
+        $mform    = $this->_form;
+
+        $mform->addElement('tags', 'tags', get_string('tags'));
+
+        $mform->addElement('hidden', 'id', null);
+        $mform->setType('id', PARAM_INT);
+
+        $this->add_action_buttons();
+
+    }
+}
diff --git a/course/tests/behat/coursetags.feature b/course/tests/behat/coursetags.feature
new file mode 100644 (file)
index 0000000..425efa0
--- /dev/null
@@ -0,0 +1,98 @@
+@core @core_course @core_tag
+Feature: Tagging courses
+  In order to search courses
+  As a teacher
+  I need to be able to tag courses
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | teacher2 | Teacher | 2 | teacher2@example.com |
+      | user1    | User    | 1 | user1@example.com |
+    And the following "courses" exist:
+      | fullname  | shortname |
+      | Course 1  | c1        |
+      | Course 2  | c2        |
+    And the following "tags" exist:
+      | name         | tagtype  |
+      | Neverusedtag | official |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | c1     | editingteacher |
+      | teacher2 | c1     | teacher        |
+      | teacher1 | c2     | editingteacher |
+      | teacher2 | c2     | teacher        |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I click on "Edit settings" "link" in the "Administration" "block"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics |
+    And I press "Save and display"
+    And I log out
+
+  Scenario: Set course tags using the course edit form
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And "Course tags" "link" should not exist in the "Administration" "block"
+    And I click on "Edit settings" "link" in the "Administration" "block"
+    And the field "Other tags (enter tags separated by commas)" matches value "Mathematics"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics, Algebra |
+    And I press "Save and display"
+    And I click on "Dashboard" "link" in the "Navigation" "block"
+    And I follow "Course 2"
+    And I click on "Edit settings" "link" in the "Administration" "block"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics, Geometry |
+    And I press "Save and display"
+    And I log out
+    And I log in as "user1"
+    And I navigate to "Tags" node in "Site pages"
+    And I follow "Mathematics"
+    Then I should see "Course 1"
+    And I should see "Course 2"
+    And I follow "Tags"
+    And I follow "Algebra"
+    And I should see "Course 1"
+    And I should not see "Course 2"
+    And I follow "Tags"
+    And I follow "Geometry"
+    And I should not see "Course 1"
+    And I should see "Course 2"
+    And I log out
+
+  Scenario: User can set course tags using separate form
+    Given I log in as "admin"
+    And I set the following system permissions of "Non-editing teacher" role:
+      | moodle/course:tag | Allow |
+    And I log out
+    When I log in as "teacher2"
+    And I follow "Course 1"
+    And "Edit settings" "link" should not exist in the "Administration" "block"
+    And I click on "Course tags" "link" in the "Administration" "block"
+    And the field "Other tags (enter tags separated by commas)" matches value "Mathematics"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics, Algebra |
+    And I press "Save changes"
+    And I click on "Dashboard" "link" in the "Navigation" "block"
+    And I follow "Course 2"
+    And I click on "Course tags" "link" in the "Administration" "block"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics, Geometry |
+    And I press "Save changes"
+    And I log out
+    And I log in as "user1"
+    And I navigate to "Tags" node in "Site pages"
+    And I follow "Mathematics"
+    Then I should see "Course 1"
+    And I should see "Course 2"
+    And I follow "Tags"
+    And I follow "Algebra"
+    And I should see "Course 1"
+    And I should not see "Course 2"
+    And I follow "Tags"
+    And I follow "Geometry"
+    And I should not see "Course 1"
+    And I should see "Course 2"
+    And I log out
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']);
+
+    }
 }
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 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 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;
index dbfc766..2056c19 100644 (file)
@@ -66,6 +66,11 @@ if ($ADMIN->fulltree) {
             get_string('defaultrole', 'role'), '', $student->id, $options));
     }
 
+    $options = array(2 => get_string('coursestart'), 3 => get_string('today'), 4 => get_string('now', 'enrol_manual'));
+    $settings->add(
+        new admin_setting_configselect('enrol_manual/enrolstart', get_string('defaultstart', 'enrol_manual'), '', 4, $options)
+    );
+
     $settings->add(new admin_setting_configduration('enrol_manual/enrolperiod',
         get_string('defaultperiod', 'enrol_manual'), get_string('defaultperiod_desc', 'enrol_manual'), 0));
 
index bb592b3..ebb6db1 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015091500;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'enrol_manual';    // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 600;
index f3de1bf..d05b1cb 100644 (file)
@@ -215,12 +215,12 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             var options = this.get(UEP.OPTIONSTARTDATE);
             var index = 0, count = 0;
             for (var i in options) {
-                count++;
                 var option = create('<option value="'+i+'">'+options[i]+'</option>');
                 if (i == defaultvalue) {
                     index = count;
                 }
                 select.append(option);
+                count++;
             }
             select.set('selectedIndex', index);
         },
@@ -608,7 +608,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 value : 0
             },
             defaultStartDate : {
-                value : 2,
+                value : 4,
                 validator : Y.Lang.isNumber
             },
             defaultDuration : {
index 08c50c8..a7af91c 100644 (file)
@@ -95,7 +95,7 @@ class enrol_meta_handler {
         list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
         $params['userid'] = $userid;
         $params['parentcourse'] = $instance->customint1;
-        $sql = "SELECT ue.*
+        $sql = "SELECT ue.*, e.status AS enrolstatus
                   FROM {user_enrolments} ue
                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol <> 'meta' AND e.courseid = :parentcourse AND e.enrol $enabled)
                  WHERE ue.userid = :userid";
@@ -142,23 +142,33 @@ class enrol_meta_handler {
             return;
         }
 
-        // is parent enrol active? (we ignore enrol starts and ends, sorry it would be too complex)
+        // Is parent enrol active? Find minimum timestart and maximum timeend of all active enrolments.
         $parentstatus = ENROL_USER_SUSPENDED;
+        $parenttimeend = null;
+        $parenttimestart = null;
         foreach ($parentues as $pue) {
-            if ($pue->status == ENROL_USER_ACTIVE) {
+            if ($pue->status == ENROL_USER_ACTIVE && $pue->enrolstatus == ENROL_INSTANCE_ENABLED) {
                 $parentstatus = ENROL_USER_ACTIVE;
-                break;
+                if ($parenttimeend === null || $pue->timeend == 0 || ($parenttimeend && $parenttimeend < $pue->timeend)) {
+                    $parenttimeend = $pue->timeend;
+                }
+                if ($parenttimestart === null || $parenttimestart > $pue->timestart) {
+                    $parenttimestart = $pue->timestart;
+                }
             }
         }
 
-        // enrol user if not enrolled yet or fix status
+        // Enrol user if not enrolled yet or fix status/timestart/timeend. Use the minimum timestart and maximum timeend found above.
         if ($ue) {
-            if ($parentstatus != $ue->status) {
-                $plugin->update_user_enrol($instance, $userid, $parentstatus);
+            if ($parentstatus != $ue->status ||
+                    ($parentstatus == ENROL_USER_ACTIVE && ($parenttimestart != $ue->timestart || $parenttimeend != $ue->timeend))) {
+                $plugin->update_user_enrol($instance, $userid, $parentstatus, $parenttimestart, $parenttimeend);
                 $ue->status = $parentstatus;
+                $ue->timestart = $parenttimestart;
+                $ue->timeend = $parenttimeend;
             }
         } else {
-            $plugin->enrol_user($instance, $userid, NULL, 0, 0, $parentstatus);
+            $plugin->enrol_user($instance, $userid, NULL, (int)$parenttimestart, (int)$parenttimeend, $parentstatus);
             $ue = new stdClass();
             $ue->userid = $userid;
             $ue->enrolid = $instance->id;
@@ -170,11 +180,13 @@ class enrol_meta_handler {
 
         $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 
-        // only active users in enabled instances are supposed to have roles (we can reassign the roles any time later)
-        if ($ue->status != ENROL_USER_ACTIVE or $instance->status != ENROL_INSTANCE_ENABLED) {
+        // Only active users in enabled instances are supposed to have roles (we can reassign the roles any time later).
+        if ($ue->status != ENROL_USER_ACTIVE or $instance->status != ENROL_INSTANCE_ENABLED or
+                ($parenttimeend and $parenttimeend < time()) or ($parenttimestart > time())) {
             if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
                 // Always keep the roles.
             } else if ($roles) {
+                // This will only unassign roles that were assigned in this enrolment method, leaving all manual role assignments intact.
                 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
             }
             return;
@@ -279,17 +291,27 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
     $allroles = get_all_roles();
 
 
-    // iterate through all not enrolled yet users
+    // Iterate through all not enrolled yet users. For each active enrolment of each user find the minimum
+    // enrolment startdate and maximum enrolment enddate.
+    // This SQL relies on the fact that ENROL_USER_ACTIVE < ENROL_USER_SUSPENDED
+    // and ENROL_INSTANCE_ENABLED < ENROL_INSTANCE_DISABLED. Condition "pue.status + pe.status = 0" means
+    // that enrolment is active. When MIN(pue.status + pe.status)=0 it means there exists an active
+    // enrolment.
     $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
     list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
     $params['courseid'] = $courseid;
-    $sql = "SELECT pue.userid, e.id AS enrolid, pue.status
+    $sql = "SELECT pue.userid, e.id AS enrolid, MIN(pue.status + pe.status) AS status,
+                      MIN(CASE WHEN (pue.status + pe.status = 0) THEN pue.timestart ELSE 9999999999 END) AS timestart,
+                      MAX(CASE WHEN (pue.status + pe.status = 0) THEN
+                                (CASE WHEN pue.timeend = 0 THEN 9999999999 ELSE pue.timeend END)
+                                ELSE 0 END) AS timeend
               FROM {user_enrolments} pue
               JOIN {enrol} pe ON (pe.id = pue.enrolid AND pe.enrol <> 'meta' AND pe.enrol $enabled)
               JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' $onecourse)
               JOIN {user} u ON (u.id = pue.userid AND u.deleted = 0)
          LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = pue.userid)
-             WHERE ue.id IS NULL";
+             WHERE ue.id IS NULL
+             GROUP BY pue.userid, e.id";
 
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $ue) {
@@ -314,7 +336,15 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
             }
         }
 
-        $meta->enrol_user($instance, $ue->userid, $ue->status);
+        // So now we have aggregated values that we will use for the meta enrolment status, timeend and timestart.
+        // Again, we use the fact that active=0 and disabled/suspended=1. Only when MIN(pue.status + pe.status)=0 the enrolment is active:
+        $ue->status = ($ue->status == ENROL_USER_ACTIVE + ENROL_INSTANCE_ENABLED) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED;
+        // Timeend 9999999999 was used instead of 0 in the "MAX()" function:
+        $ue->timeend = ($ue->timeend == 9999999999) ? 0 : (int)$ue->timeend;
+        // Timestart 9999999999 is only possible when there are no active enrolments:
+        $ue->timestart = ($ue->timestart == 9999999999) ? 0 : (int)$ue->timestart;
+
+        $meta->enrol_user($instance, $ue->userid, null, $ue->timestart, $ue->timeend, $ue->status);
         if ($instance->customint2) {
             groups_add_member($instance->customint2, $ue->userid, 'enrol_meta', $instance->id);
         }
@@ -371,29 +401,38 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
     $rs->close();
 
 
-    // update status - meta enrols + start and end dates are ignored, sorry
-    // note the trick here is that the active enrolment and instance constants have value 0
+    // Update status - meta enrols are ignored to avoid recursion.
+    // Note the trick here is that the active enrolment and instance constants have value 0.
     $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
     list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
     $params['courseid'] = $courseid;
-    $sql = "SELECT ue.userid, ue.enrolid, pue.pstatus
+    $sql = "SELECT ue.userid, ue.enrolid, pue.pstatus, pue.ptimestart, pue.ptimeend
               FROM {user_enrolments} ue
               JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse)
-              JOIN (SELECT xpue.userid, xpe.courseid, MIN(xpue.status + xpe.status) AS pstatus
+              JOIN (SELECT xpue.userid, xpe.courseid, MIN(xpue.status + xpe.status) AS pstatus,
+                      MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END) AS ptimestart,
+                      MAX(CASE WHEN (xpue.status + xpe.status = 0) THEN
+                                (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
+                                ELSE 0 END) AS ptimeend
                       FROM {user_enrolments} xpue
                       JOIN {enrol} xpe ON (xpe.id = xpue.enrolid AND xpe.enrol <> 'meta' AND xpe.enrol $enabled)
                   GROUP BY xpue.userid, xpe.courseid
                    ) pue ON (pue.courseid = e.customint1 AND pue.userid = ue.userid)
-             WHERE (pue.pstatus = 0 AND ue.status > 0) OR (pue.pstatus > 0 and ue.status = 0)";
+             WHERE (pue.pstatus = 0 AND ue.status > 0) OR (pue.pstatus > 0 and ue.status = 0)
+             OR ((CASE WHEN pue.ptimestart = 9999999999 THEN 0 ELSE pue.ptimestart END) <> ue.timestart)
+             OR ((CASE WHEN pue.ptimeend = 9999999999 THEN 0 ELSE pue.ptimeend END) <> ue.timeend)";
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $ue) {
         if (!isset($instances[$ue->enrolid])) {
             $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
         }
         $instance = $instances[$ue->enrolid];
-        $ue->pstatus = ($ue->pstatus == ENROL_USER_ACTIVE) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED;
+        $ue->pstatus = ($ue->pstatus == ENROL_USER_ACTIVE + ENROL_INSTANCE_ENABLED) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED;
+        $ue->ptimeend = ($ue->ptimeend == 9999999999) ? 0 : (int)$ue->ptimeend;
+        $ue->ptimestart = ($ue->ptimestart == 9999999999) ? 0 : (int)$ue->ptimestart;
 
-        if ($ue->pstatus == ENROL_USER_ACTIVE and !$syncall and $unenrolaction != ENROL_EXT_REMOVED_UNENROL) {
+        if ($ue->pstatus == ENROL_USER_ACTIVE and (!$ue->ptimeend || $ue->ptimeend > time())
+                and !$syncall and $unenrolaction != ENROL_EXT_REMOVED_UNENROL) {
             // this may be slow if very many users are ignored in sync
             $parentcontext = context_course::instance($instance->customint1);
             list($ignoreroles, $params) = $DB->get_in_or_equal($skiproles, SQL_PARAMS_NAMED, 'ri', false, -1);
@@ -409,7 +448,7 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
             }
         }
 
-        $meta->update_user_enrol($instance, $ue->userid, $ue->pstatus);
+        $meta->update_user_enrol($instance, $ue->userid, $ue->pstatus, $ue->ptimestart, $ue->ptimeend);
         if ($verbose) {
             if ($ue->pstatus == ENROL_USER_ACTIVE) {
                 mtrace("  unsuspending: $ue->userid ==> $instance->courseid");
index 772c8d0..4d3b549 100644 (file)
@@ -743,4 +743,117 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         // Check that the group name has been changed.
         $this->assertEquals('Physics course (3)', $groupinfo->name);
     }
+
+    /**
+     * Test that enrolment timestart-timeend is respected in meta course.
+     */
+    public function test_timeend() {
+        global $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $timeinfuture = time() + DAYSECS;
+        $timeinpast = time() - DAYSECS;
+
+        $metalplugin = enrol_get_plugin('meta');
+        $manplugin = enrol_get_plugin('manual');
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        $user5 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+        $manual1 = $DB->get_record('enrol', array('courseid' => $course1->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+
+        $student = $DB->get_record('role', array('shortname' => 'student'));
+
+        $this->enable_plugin();
+
+        // Create instance of enrol_meta in course2 when there are no enrolments present.
+        $meta2id = $metalplugin->add_instance($course2, array('customint1' => $course1->id));
+
+        $expectedenrolments = array(
+            $user1->id => array(0, 0, ENROL_USER_ACTIVE),
+            $user2->id => array($timeinpast, 0, ENROL_USER_ACTIVE),
+            $user3->id => array(0, $timeinfuture, ENROL_USER_ACTIVE),
+            $user4->id => array($timeinpast, $timeinfuture, ENROL_USER_ACTIVE),
+            $user5->id => array(0, 0, ENROL_USER_SUSPENDED),
+        );
+        foreach ($expectedenrolments as $userid => $data) {
+            $expectedenrolments[$userid] = (object)(array('userid' => $userid) +
+                    array_combine(array('timestart', 'timeend', 'status'), $data));
+        }
+
+        // Enrol users manually in course 1.
+        foreach ($expectedenrolments as $e) {
+            $manplugin->enrol_user($manual1, $e->userid, $student->id, $e->timestart, $e->timeend, $e->status);
+        }
+
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $manual1->id), 'userid', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Make sure that the same enrolments are now present in course2 under meta enrolment.
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta2id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Create instance of enrol_meta in course3 and run sync.
+        $meta3id = $metalplugin->add_instance($course3, array('customint1' => $course1->id));
+        enrol_meta_sync($course3->id);
+
+        // Make sure that the same enrolments are now present in course3 under meta enrolment.
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Update some of the manual enrolments.
+        $expectedenrolments[$user2->id]->timestart = $timeinpast - 60;
+        $expectedenrolments[$user3->id]->timeend = $timeinfuture + 60;
+        $expectedenrolments[$user4->id]->status = ENROL_USER_SUSPENDED;
+        $expectedenrolments[$user5->id]->status = ENROL_USER_ACTIVE;
+        foreach ($expectedenrolments as $e) {
+            $manplugin->update_user_enrol($manual1, $e->userid, $e->status, $e->timestart, $e->timeend);
+        }
+
+        // Make sure meta courses are also updated.
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta2id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Test meta sync. Imagine events are not working.
+        $sink = $this->redirectEvents();
+        $expectedenrolments[$user2->id]->timestart = $timeinpast;
+        $expectedenrolments[$user3->id]->timeend = $timeinfuture;
+        $expectedenrolments[$user4->id]->status = ENROL_USER_ACTIVE;
+        $expectedenrolments[$user5->id]->status = ENROL_USER_SUSPENDED;
+        foreach ($expectedenrolments as $e) {
+            $manplugin->update_user_enrol($manual1, $e->userid, $e->status, $e->timestart, $e->timeend);
+        }
+
+        // Make sure meta courses are updated only for the course that was synced.
+        enrol_meta_sync($course3->id);
+
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta2id), '', 'userid, timestart, timeend, status');
+        $this->assertNotEquals($expectedenrolments, $enrolments);
+
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        $sink->close();
+
+        // Disable manual enrolment in course1 and make sure all user enrolments in course2 are suspended.
+        $manplugin->update_status($manual1, ENROL_INSTANCE_DISABLED);
+        $allsuspendedenrolemnts = array_combine(array_keys($expectedenrolments), array_fill(0, 5, ENROL_USER_SUSPENDED));
+        enrol_meta_sync($course3->id);
+        $enrolmentstatuses = $DB->get_records_menu('user_enrolments', array('enrolid' => $meta3id), '', 'userid, status');
+        $this->assertEquals($allsuspendedenrolemnts, $enrolmentstatuses);
+
+        $manplugin->update_status($manual1, ENROL_INSTANCE_ENABLED);
+        enrol_meta_sync($course3->id);
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+    }
 }
diff --git a/enrol/self/classes/empty_form.php b/enrol/self/classes/empty_form.php
new file mode 100644 (file)
index 0000000..a54d70d
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Empty enrol_self form.
+ *
+ * Useful to mimic valid enrol instances UI when the enrolment instance is not available.
+ *
+ * @package enrol_self
+ * @copyright 2015 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+class enrol_self_empty_form extends moodleform {
+
+    /**
+     * Form definition.
+     * @return void
+     */
+    public function definition() {
+        $this->_form->addElement('header', 'selfheader', $this->_customdata->header);
+        $this->_form->addElement('static', 'info', '', $this->_customdata->info);
+    }
+}
index 5c668ef..6c1364b 100644 (file)
@@ -234,8 +234,8 @@ class enrol_self_plugin extends enrol_plugin {
 
         $enrolstatus = $this->can_self_enrol($instance);
 
-        // Don't show enrolment instance form, if user can't enrol using it.
         if (true === $enrolstatus) {
+            // This user can self enrol using this instance.
             $form = new enrol_self_enrol_form(NULL, $instance);
             $instanceid = optional_param('instance', 0, PARAM_INT);
             if ($instance->id == $instanceid) {
@@ -243,14 +243,23 @@ class enrol_self_plugin extends enrol_plugin {
                     $this->enrol_self($instance, $data);
                 }
             }
-
-            ob_start();
-            $form->display();
-            $output = ob_get_clean();
-            return $OUTPUT->box($output);
         } else {
-            return $OUTPUT->box($enrolstatus);
-        }
+            // This user can not self enrol using this instance. Using an empty form to keep
+            // the UI consistent with other enrolment plugins that returns a form.
+            $data = new stdClass();
+            $data->header = $this->get_instance_name($instance);
+            $data->info = $enrolstatus;
+
+            // The can_self_enrol call returns a button to the login page if the user is a
+            // guest, setting the login url to the form if that is the case.
+            $url = isguestuser() ? get_login_url() : null;
+            $form = new enrol_self_empty_form($url, $data);
+        }
+
+        ob_start();
+        $form->display();
+        $output = ob_get_clean();
+        return $OUTPUT->box($output);
     }
 
     /**
index b932ef7..ecffc3c 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js differ
index 34d316d..46b748d 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js differ
index b932ef7..ecffc3c 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js differ
index 564f348..3689b4a 100644 (file)
@@ -20,46 +20,53 @@ Y.extend(AUTOLINKER, Y.Base, {
     alertpanels: {},
     initializer : function() {
         var self = this;
-        Y.delegate('click', function(e){
-            e.preventDefault();
+        require(['core/event'], function(event) {
+            Y.delegate('click', function(e){
+                e.preventDefault();
 
-            //display a progress indicator
-            var title = '',
-                content = Y.Node.create('<div id="glossaryfilteroverlayprogress">' +
-                                        '<img src="' + M.cfg.loadingicon + '" class="spinner" />' +
-                                        '</div>'),
-                o = new Y.Overlay({
-                    headerContent :  title,
-                    bodyContent : content
-                }),
-                fullurl,
-                cfg;
-            self.overlay = o;
-            o.render(Y.one(document.body));
+                //display a progress indicator
+                var title = '',
+                    content = Y.Node.create('<div id="glossaryfilteroverlayprogress">' +
+                                            '<img src="' + M.cfg.loadingicon + '" class="spinner" />' +
+                                            '</div>'),
+                    o = new Y.Overlay({
+                        headerContent :  title,
+                        bodyContent : content
+                    }),
+                    fullurl,
+                    cfg;
+                self.overlay = o;
+                o.render(Y.one(document.body));
 
-            //Switch over to the ajax url and fetch the glossary item
-            fullurl = this.getAttribute('href').replace('showentry.php','showentry_ajax.php');
-            cfg = {
-                method: 'get',
-                context : self,
-                on: {
-                    success: function(id, o) {
-                        this.display_callback(o.responseText);
-                    },
-                    failure: function(id, o) {
-                        var debuginfo = o.statusText;
-                        if (M.cfg.developerdebug) {
-                            o.statusText += ' (' + fullurl + ')';
+                //Switch over to the ajax url and fetch the glossary item
+                fullurl = this.getAttribute('href').replace('showentry.php','showentry_ajax.php');
+                cfg = {
+                    method: 'get',
+                    context : self,
+                    on: {
+                        success: function(id, o) {
+                            this.display_callback(o.responseText, event);
+                        },
+                        failure: function(id, o) {
+                            var debuginfo = o.statusText;
+                            if (M.cfg.developerdebug) {
+                                o.statusText += ' (' + fullurl + ')';
+                            }
+                            new M.core.exception({ message: debuginfo });
                         }
-                        this.display_callback('bodyContent',debuginfo);
                     }
-                }
-            };
-            Y.io(fullurl, cfg);
+                };
+                Y.io(fullurl, cfg);
 
-        }, Y.one(document.body), 'a.glossary.autolink.concept');
+            }, Y.one(document.body), 'a.glossary.autolink.concept');
+        });
     },
-    display_callback : function(content) {
+    /**
+     * @method display_callback
+     * @param {String} content - Content to display
+     * @param {Object} event The amd event module used to fire events for jquery and yui.
+     */
+    display_callback : function(content, event) {
         var data,
             key,
             alertpanel,
@@ -76,7 +83,8 @@ Y.extend(AUTOLINKER, Y.Base, {
                     definition = data.entries[key].definition + data.entries[key].attachments;
                     alertpanel = new M.core.alert({title:data.entries[key].concept, draggable: true,
                         message:definition, modal:false, yesLabel: M.util.get_string('ok', 'moodle')});
-                    Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(alertpanel.get('boundingBox')))});
+                    // Notify the filters about the modified nodes.
+                    event.notifyFilterContentUpdated(alertpanel.get('boundingBox').getDOMNode());
                     Y.Node.one('#id_yuialertconfirm-' + alertpanel.get('COUNT')).focus();
 
                     // Register alertpanel for stacking.
index e73bc93..672888a 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Edit and review page for grade categories and items
+ * The Gradebook setup page.
  *
  * @package   core_grades
  * @copyright 2008 Nicolas Connault
@@ -244,7 +244,7 @@ if ($data = data_submitted() and confirm_sesskey()) {
     }
 }
 
-print_grade_page_head($courseid, 'settings', 'setup', get_string('categoriesanditems', 'grades'));
+print_grade_page_head($courseid, 'settings', 'setup', get_string('gradebooksetup', 'grades'));
 
 // Print Table of categories and items
 echo $OUTPUT->box_start('gradetreebox generalbox');
index 0efde54..b6dc052 100644 (file)
@@ -435,9 +435,9 @@ class grade_edit_tree {
         return $str;
     }
 
-    //Trims trailing zeros
-    //Used on the 'categories and items' page for grade items settings like aggregation co-efficient
-    //Grader report has its own decimal place settings so they are handled elsewhere
+    // Trims trailing zeros.
+    // Used on the 'Gradebook setup' page for grade items settings like aggregation co-efficient.
+    // Grader report has its own decimal place settings so they are handled elsewhere.
     static function format_number($number) {
         $formatted = rtrim(format_float($number, 4),'0');
         if (substr($formatted, -1)==get_string('decsep', 'langconfig')) { //if last char is the decimal point
index 8b25939..c91122b 100644 (file)
@@ -68,7 +68,6 @@ class grade_export_xml extends grade_export {
             foreach ($userdata->grades as $itemid => $grade) {
                 $grade_item = $this->grade_items[$itemid];
                 $grade->grade_item =& $grade_item;
-                $gradestr = $this->format_grade($grade, $this->displaytype); // no formating for now
 
                 // MDL-11669, skip exported grades or bad grades (if setting says so)
                 if ($export_tracking) {
@@ -88,7 +87,19 @@ class grade_export_xml extends grade_export {
                 fwrite($handle,  "\t\t<assignment>{$grade_item->idnumber}</assignment>\n");
                 // this column should be customizable to use either student id, idnumber, uesrname or email.
                 fwrite($handle,  "\t\t<student>{$user->idnumber}</student>\n");
-                fwrite($handle,  "\t\t<score>$gradestr</score>\n");
+                // Format and display the grade in the selected display type (real, letter, percentage).
+                if (is_array($this->displaytype)) {
+                    // Grades display type came from the return of export_bulk_export_data() on grade publishing.
+                    foreach ($this->displaytype as $gradedisplayconst) {
+                        $gradestr = $this->format_grade($grade, $gradedisplayconst);
+                        fwrite($handle,  "\t\t<score>$gradestr</score>\n");
+                    }
+                } else {
+                    // Grade display type submitted directly from the grade export form.
+                    $gradestr = $this->format_grade($grade, $this->displaytype);
+                    fwrite($handle,  "\t\t<score>$gradestr</score>\n");
+                }
+
                 if ($this->export_feedback) {
                     $feedbackstr = $this->format_feedback($userdata->feedbacks[$itemid]);
                     fwrite($handle,  "\t\t<feedback>$feedbackstr</feedback>\n");
index 215d346..46fd52a 100644 (file)
@@ -185,7 +185,7 @@ class grading_manager {
         } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
             list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
 
-            if (!empty($cm->name)) {
+            if (strval($cm->name) !== '') {
                 $title = $cm->name;
             } else {
                 debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
index fbfe359..2216d57 100644 (file)
@@ -2832,9 +2832,9 @@ abstract class grade_helper {
         $context = context_course::instance($courseid);
         self::$managesetting = array();
         if ($courseid != SITEID && has_capability('moodle/grade:manage', $context)) {
-            self::$managesetting['categoriesanditems'] = new grade_plugin_info('setup',
+            self::$managesetting['gradebooksetup'] = new grade_plugin_info('setup',
                 new moodle_url('/grade/edit/tree/index.php', array('id' => $courseid)),
-                get_string('categoriesanditems', 'grades'));
+                get_string('gradebooksetup', 'grades'));
             self::$managesetting['coursesettings'] = new grade_plugin_info('coursesettings',
                 new moodle_url('/grade/edit/settings/index.php', array('id'=>$courseid)),
                 get_string('coursegradesettings', 'grades'));
index 3b04d7e..923fc2f 100644 (file)
@@ -350,10 +350,10 @@ function grade_report_overview_settings_definition(&$mform) {
                       GRADE_REPORT_SHOW_TOTAL_IF_CONTAINS_HIDDEN => get_string('hidetotalshowexhiddenitems', 'grades'),
                       GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN => get_string('hidetotalshowinchiddenitems', 'grades') );
 
-    if (empty($CFG->grade_report_overview_showtotalsifcontainhidden)) {
+    if (!array_key_exists($CFG->grade_report_overview_showtotalsifcontainhidden, $options)) {
         $options[-1] = get_string('defaultprev', 'grades', $options[0]);
     } else {
-        $options[-1] = get_string('defaultprev', 'grades', $options[1]);
+        $options[-1] = get_string('defaultprev', 'grades', $options[$CFG->grade_report_overview_showtotalsifcontainhidden]);
     }
 
     $mform->addElement('select', 'report_overview_showtotalsifcontainhidden', get_string('hidetotalifhiddenitems', 'grades'), $options);
index e9cbcbd..727aeb0 100644 (file)
@@ -80,7 +80,8 @@ class behat_grade extends behat_base {
     }
 
     /**
-     * Sets a calculated manual grade item. Needs a table with item name - idnumber relation. The step requires you to be in categories and items page.
+     * Sets a calculated manual grade item. Needs a table with item name - idnumber relation.
+     * The step requires you to be in the 'Gradebook setup' page.
      *
      * @Given /^I set "(?P<calculation_string>(?:[^"]|\\")*)" calculation for grade item "(?P<grade_item_string>(?:[^"]|\\")*)" with idnumbers:$/
      * @param string $calculation The calculation.
@@ -139,7 +140,7 @@ class behat_grade extends behat_base {
 
     /**
      * Sets a calculated manual grade category total. Needs a table with item name - idnumber relation.
-     * The step requires you to be in categories and items page.
+     * The step requires you to be in the 'Gradebook setup' page.
      *
      * @Given /^I set "(?P<calculation_string>(?:[^"]|\\")*)" calculation for grade category "(?P<grade_item_string>(?:[^"]|\\")*)" with idnumbers:$/
      * @param string $calculation The calculation.
index d10d14a..09f1b04 100644 (file)
@@ -235,7 +235,7 @@ Feature: We can use calculated grade totals
       | itemname              | course | outcome | gradetype | scale      |
       | Test outcome item one | C1     | OT1     | Scale     | Test Scale |
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     And I set the following settings for grade item "Course 1":
       | Aggregation                     | Natural |
       | Include outcomes in aggregation | 1       |
@@ -258,7 +258,7 @@ Feature: We can use calculated grade totals
     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"
     And I set the following settings for grade item "Test outcome item one":
      | Extra credit     | 1   |
     And I log out
@@ -272,7 +272,7 @@ Feature: We can use calculated grade totals
     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"
     And I set the following settings for grade item "Course 1":
       | Aggregation                     | Natural |
       | Include outcomes in aggregation | 0       |
@@ -297,7 +297,7 @@ Feature: We can use calculated grade totals
       | Test outcome item one | C1     | OT1     | Scale     | Test Scale |
     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"
     And I set the following settings for grade item "Course 1":
       | Aggregation                     | Natural |
       | Include outcomes in aggregation | 1       |
@@ -387,7 +387,7 @@ Feature: We can use calculated grade totals
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add category"
     And I set the following fields to these values:
       | Category name | Sub category 3 |
@@ -441,7 +441,7 @@ Feature: We can use calculated grade totals
     And I press "Save changes"
     And I turn editing mode off
     And I should see "250.00 (25.25 %)" in the ".course" "css_element"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add category"
     And I set the following fields to these values:
       | Category name | Sub sub category 1 |
@@ -452,7 +452,7 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Natural aggregation from the setup screen
-    And I select "Categories and items" from the "Grade report" singleselect
+    And I select "Gradebook setup" from the "Grade report" singleselect
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Natural |
     And I set the following settings for grade item "Sub category 1":
@@ -514,7 +514,7 @@ Feature: We can use calculated grade totals
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
     And I turn editing mode off
-    And I select "Categories and items" from the "Grade report" singleselect
+    And I select "Gradebook setup" from the "Grade report" singleselect
     And I set the field "Override weight of Test assignment one" to "1"
     And I set the field "Weight of Test assignment one" to "0"
     And I set the field "Override weight of Test assignment six" to "1"
index 7453a73..fd792e0 100644 (file)
@@ -336,7 +336,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
   @javascript
   Scenario: Switching grade items between categories
     # Move to same aggregation (Natural).
-    Given I navigate to "Categories and items" node in "Grade administration > Setup"
+    Given I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -362,7 +362,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And the field "Extra credit" matches value "1"
     And I press "Cancel"
     # Move to Mean of grades (with extra credit).
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -382,7 +382,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And the field "Extra credit" matches value "1"
     And I press "Cancel"
     # Move to Simple weight mean of grades.
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -402,7 +402,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And the field "Extra credit" matches value "1"
     And I press "Cancel"
     # Move to Weighted mean of grades.
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -426,7 +426,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And I set the field "Item weight" to "11"
     And I press "Save changes"
     # Move to same (Weighted mean of grades).
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -447,7 +447,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And the field "Item weight" matches value "11"
     And I press "Save changes"
     # Move back to Natural.
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
index 1899e4d..e52ab6a 100644 (file)
@@ -30,7 +30,7 @@ Feature: Average grades are displayed in the gradebook
       | Show average | Show |
     And I press "Save changes"
     # Add a manual grade item
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | Manual item 1 |
index e295c02..145a39e 100644 (file)
@@ -2,7 +2,7 @@
 Feature: Calculated grade items can be used in the gradebook
   In order to use calculated grade items in the gradebook
   As a teacher
-  I need setup calculated grade items in the categories and items page.
+  I need setup calculated grade items in the 'Gradebook setup' page.
 
   Background:
     Given the following "courses" exist:
@@ -22,7 +22,7 @@ Feature: Calculated grade items can be used in the gradebook
     And I am on site homepage
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
 
   @javascript
   Scenario: The max grade for a category item, with a calculation using Natural aggregation, can be changed
@@ -77,7 +77,7 @@ Feature: Calculated grade items can be used in the gradebook
       | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
       | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–50  | 75.00 %    | -                            |
       | Course total                        | -                 | 37.50  | 0–50  | 75.00 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "Calc cat":
       | Maximum grade | 40 |
     And I follow "Grader report"
@@ -143,7 +143,7 @@ Feature: Calculated grade items can be used in the gradebook
       | grade item 1 | 66.67 %           | 75.00  | 0–100 | 75.00 %    | 50.00 %                      |
       | calc item    | 33.33 %           | 37.50  | 0–50  | 75.00 %    | 25.00 %                      |
       | Course total | -                 | 112.50 | 0–150 | 75.00 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "calc item":
       | Maximum grade | 40 |
     And I follow "Grader report"
index 62d5e3e..cd8d791 100644 (file)
@@ -23,7 +23,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
     And I am on site homepage
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
 
   @javascript
   Scenario: The max grade for a category item, with a calculation using Natural aggregation, can be changed
@@ -78,7 +78,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
       | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
       | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–100 | 37.50 %    | -                            |
       | Course total                        | -                 | 37.50  | 0–100 | 37.50 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "Calc cat":
       | Maximum grade | 40 |
     And I follow "Grader report"
@@ -144,7 +144,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
       | grade item 1 | 50.00 %           | 75.00  | 0–100 | 75.00 %    | 37.50 %                      |
       | calc item    | 50.00 %           | 37.50  | 0–100 | 37.50 %    | 18.75 %                      |
       | Course total | -                 | 112.50 | 0–200 | 56.25 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "calc item":
       | Maximum grade | 40 |
     And I follow "Grader report"
index e914de5..e899835 100644 (file)
@@ -43,7 +43,7 @@ Feature: We can understand the gradebook user report
     And I set the field "Show weightings" to "Show"
     And I set the field "Show contribution to course total" to "Show"
     And I press "Save changes"
-    And I set the field "Grade report" to "Categories and items"
+    And I set the field "Grade report" to "Gradebook setup"
     And I press "Add category"
     And I set the field "Category name" to "Sub category"
     And I press "Save changes"
index 896c567..7f60a15 100644 (file)
@@ -22,7 +22,7 @@ Feature: Extra credit contributions are normalised when going out of bounds
     And I am on site homepage
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | Manual item 1 |
@@ -56,7 +56,7 @@ Feature: Extra credit contributions are normalised when going out of bounds
     And I press "Save changes"
 
   Scenario Outline: The contribution of extra credit items is normalised
-    Given I set the field "Grade report" to "Categories and items"
+    Given I set the field "Grade report" to "Gradebook setup"
     When I set the following settings for grade item "Course 1":
       | Aggregation | <aggregation> |
     And I set the following settings for grade item "Manual item 2":
index 919b745..a886c39 100644 (file)
@@ -28,7 +28,7 @@ Feature: We can use a minimum grade different than zero
     And I am on site homepage
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | Manual item 1 |
@@ -73,7 +73,7 @@ Feature: We can use a minimum grade different than zero
 
   @javascript
   Scenario: Natural aggregation with negative and positive grade
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "Sub category 1":
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
index b956a5a..cb1aa15 100644 (file)
@@ -27,7 +27,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
     And I am on site homepage
     And I follow "C1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | MI 1 |
@@ -62,7 +62,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
     And I set the field "Show weightings" to "Show"
     And I set the field "Show contribution to course total" to "Show"
     And I press "Save changes"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "CAT1":
       | Aggregation          | Natural |
     And I log out
@@ -98,7 +98,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
       | MI 5         | 33.33 %           | 30.00  | 0–100 | 30.00 %    | 10.00 %                      |
       | CAT1 total   | 33.33 %           | 10.00  | 0–100 | 10.00 %    | -                            |
       | Course total | -                 | 60.00  | 0–300 | 20.00 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "MI 1":
       | Maximum grade          | 50.00 |
       | Minimum grade          | 5.00 |
@@ -126,7 +126,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
       | MI 5         | 50.00 %           | 30.00  | 0–100 | 30.00 %    | 15.00 %                      |