Merge branch 'MDL-64590' of https://github.com/timhunt/moodle
authorDavid Monllaó <davidm@moodle.com>
Tue, 12 Feb 2019 11:45:48 +0000 (12:45 +0100)
committerDavid Monllaó <davidm@moodle.com>
Tue, 12 Feb 2019 11:45:48 +0000 (12:45 +0100)
475 files changed:
admin/cli/uninstall_plugins.php [new file with mode: 0644]
admin/customfields.php [new file with mode: 0644]
admin/index.php
admin/renderer.php
admin/searchareas.php
admin/settings/courses.php
admin/settings/plugins.php
admin/tool/behat/cli/util.php
admin/tool/behat/cli/util_single_run.php
admin/tool/dataprivacy/classes/expired_contexts_manager.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/templates/form-user-selector-suggestion.mustache
admin/tool/dataprivacy/tests/behat/dataexport.feature
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/dataprivacy/tests/external_test.php
admin/tool/dataprivacy/version.php
analytics/classes/local/indicator/base.php
analytics/classes/local/indicator/binary.php
analytics/classes/local/indicator/discrete.php
analytics/tests/fixtures/test_indicator_discrete.php [new file with mode: 0644]
analytics/tests/fixtures/test_indicator_random.php
analytics/tests/indicator_test.php [new file with mode: 0644]
analytics/tests/prediction_test.php
auth/cas/auth.php
auth/cas/cas_form.html [deleted file]
auth/cas/lang/en/auth_cas.php
auth/cas/lang/en/deprecated.txt [new file with mode: 0644]
auth/cas/lib.php [new file with mode: 0644]
auth/cas/settings.php
auth/cas/version.php
auth/ldap/auth.php
auth/ldap/lang/en/auth_ldap.php
auth/mnet/classes/privacy/provider.php
auth/oauth2/classes/privacy/provider.php
auth/shibboleth/auth.php
auth/shibboleth/index_form.html [deleted file]
auth/shibboleth/lang/en/auth_shibboleth.php
auth/shibboleth/login.php
auth/shibboleth/templates/login_form.mustache [new file with mode: 0644]
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
badges/criteria/award_criteria_activity.php
badges/renderer.php
badges/tests/behat/criteria_activity.feature [new file with mode: 0644]
blocks/community/classes/privacy/provider.php
blocks/html/classes/privacy/provider.php
blocks/lp/db/access.php
blocks/lp/lang/en/block_lp.php
blocks/lp/upgrade.txt [new file with mode: 0644]
blocks/lp/version.php
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/src/view.js
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lang/en/deprecated.txt
blocks/myoverview/templates/placeholders.mustache
blocks/private_files/module.js
blocks/recentlyaccessedcourses/amd/build/main.min.js
blocks/recentlyaccessedcourses/amd/src/main.js
blocks/recentlyaccessedcourses/classes/output/main.php
blocks/recentlyaccessedcourses/lang/en/block_recentlyaccessedcourses.php
blocks/recentlyaccessedcourses/templates/main.mustache
blocks/recentlyaccessedcourses/templates/no-courses.mustache
blocks/recentlyaccessedcourses/templates/recentlyaccessedcourses-view.mustache
blocks/rss_client/classes/privacy/provider.php
blocks/starredcourses/amd/build/main.min.js
blocks/starredcourses/amd/src/main.js
blocks/starredcourses/lang/en/block_starredcourses.php
blocks/starredcourses/templates/no-courses.mustache
blocks/starredcourses/templates/placeholder-course.mustache [deleted file]
blocks/starredcourses/templates/view-cards.mustache [deleted file]
blocks/starredcourses/templates/view.mustache
cohort/tests/behat/upload_cohort_users.feature
comment/locallib.php
completion/classes/external.php
completion/classes/privacy/provider.php
completion/tests/externallib_test.php
completion/tests/privacy_test.php
completion/upgrade.txt
composer.json
config-dist.php
course/classes/category.php
course/classes/customfield/course_handler.php [new file with mode: 0644]
course/classes/list_element.php
course/classes/search/customfield.php [new file with mode: 0644]
course/classes/search/mycourse.php
course/classes/search/section.php
course/completion.js
course/customfield.php [new file with mode: 0644]
course/edit_form.php
course/externallib.php
course/format/lib.php
course/lib.php
course/renderer.php
course/templates/no-courses.mustache [moved from blocks/myoverview/templates/no-courses.mustache with 68% similarity]
course/templates/placeholder-course.mustache [moved from blocks/recentlyaccessedcourses/templates/placeholder-course.mustache with 91% similarity]
course/templates/view-cards.mustache [moved from blocks/recentlyaccessedcourses/templates/view-cards.mustache with 91% similarity]
course/tests/behat/customfields_locked.feature [new file with mode: 0644]
course/tests/behat/customfields_visibility.feature [new file with mode: 0644]
course/tests/customfield_test.php [new file with mode: 0644]
course/tests/externallib_test.php
course/tests/search_test.php
course/togglecompletion.php
course/upgrade.txt
customfield/amd/build/form.min.js [new file with mode: 0644]
customfield/amd/src/form.js [new file with mode: 0644]
customfield/classes/api.php [new file with mode: 0644]
customfield/classes/category.php [new file with mode: 0644]
customfield/classes/category_controller.php [new file with mode: 0644]
customfield/classes/data.php [new file with mode: 0644]
customfield/classes/data_controller.php [new file with mode: 0644]
customfield/classes/event/category_created.php [new file with mode: 0644]
customfield/classes/event/category_deleted.php [new file with mode: 0644]
customfield/classes/event/category_updated.php [new file with mode: 0644]
customfield/classes/event/field_created.php [new file with mode: 0644]
customfield/classes/event/field_deleted.php [new file with mode: 0644]
customfield/classes/event/field_updated.php [new file with mode: 0644]
customfield/classes/field.php [new file with mode: 0644]
customfield/classes/field_config_form.php [new file with mode: 0644]
customfield/classes/field_controller.php [new file with mode: 0644]
customfield/classes/handler.php [new file with mode: 0644]
customfield/classes/output/field_data.php [new file with mode: 0644]
customfield/classes/output/management.php [new file with mode: 0644]
customfield/classes/output/renderer.php [new file with mode: 0644]
customfield/classes/privacy/customfield_provider.php [new file with mode: 0644]
customfield/classes/privacy/provider.php [new file with mode: 0644]
customfield/edit.php [new file with mode: 0644]
customfield/externallib.php [new file with mode: 0644]
customfield/field/checkbox/classes/data_controller.php [new file with mode: 0644]
customfield/field/checkbox/classes/field_controller.php [new file with mode: 0644]
customfield/field/checkbox/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/checkbox/lang/en/customfield_checkbox.php [new file with mode: 0644]
customfield/field/checkbox/tests/behat/field.feature [new file with mode: 0644]
customfield/field/checkbox/tests/plugin_test.php [new file with mode: 0644]
customfield/field/checkbox/version.php [new file with mode: 0644]
customfield/field/date/classes/data_controller.php [new file with mode: 0644]
customfield/field/date/classes/field_controller.php [new file with mode: 0644]
customfield/field/date/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/date/lang/en/customfield_date.php [new file with mode: 0644]
customfield/field/date/lib.php [new file with mode: 0644]
customfield/field/date/tests/behat/field.feature [new file with mode: 0644]
customfield/field/date/tests/plugin_test.php [new file with mode: 0644]
customfield/field/date/version.php [new file with mode: 0644]
customfield/field/select/classes/data_controller.php [new file with mode: 0644]
customfield/field/select/classes/field_controller.php [new file with mode: 0644]
customfield/field/select/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/select/lang/en/customfield_select.php [new file with mode: 0644]
customfield/field/select/tests/behat/field.feature [new file with mode: 0644]
customfield/field/select/tests/plugin_test.php [new file with mode: 0644]
customfield/field/select/version.php [new file with mode: 0644]
customfield/field/text/classes/data_controller.php [new file with mode: 0644]
customfield/field/text/classes/field_controller.php [new file with mode: 0644]
customfield/field/text/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/text/lang/en/customfield_text.php [new file with mode: 0644]
customfield/field/text/tests/behat/field.feature [new file with mode: 0644]
customfield/field/text/tests/plugin_test.php [new file with mode: 0644]
customfield/field/text/version.php [new file with mode: 0644]
customfield/field/textarea/classes/data_controller.php [new file with mode: 0644]
customfield/field/textarea/classes/field_controller.php [new file with mode: 0644]
customfield/field/textarea/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/textarea/lang/en/customfield_textarea.php [new file with mode: 0644]
customfield/field/textarea/lib.php [new file with mode: 0644]
customfield/field/textarea/tests/behat/default_value.feature [new file with mode: 0644]
customfield/field/textarea/tests/behat/field.feature [new file with mode: 0644]
customfield/field/textarea/tests/plugin_test.php [new file with mode: 0644]
customfield/field/textarea/version.php [new file with mode: 0644]
customfield/lib.php [new file with mode: 0644]
customfield/templates/field_data.mustache [new file with mode: 0644]
customfield/templates/list.mustache [new file with mode: 0644]
customfield/tests/api_test.php [new file with mode: 0644]
customfield/tests/behat/edit_categories.feature [new file with mode: 0644]
customfield/tests/behat/edit_fields_settings.feature [new file with mode: 0644]
customfield/tests/behat/required_field.feature [new file with mode: 0644]
customfield/tests/behat/unique_field.feature [new file with mode: 0644]
customfield/tests/category_controller_test.php [new file with mode: 0644]
customfield/tests/data_controller_test.php [new file with mode: 0644]
customfield/tests/field_controller_test.php [new file with mode: 0644]
customfield/tests/fixtures/test_instance_form.php [new file with mode: 0644]
customfield/tests/generator/lib.php [new file with mode: 0644]
customfield/tests/generator_test.php [new file with mode: 0644]
customfield/tests/privacy_test.php [new file with mode: 0644]
enrol/classes/privacy/provider.php
enrol/database/classes/task/sync_enrolments.php [new file with mode: 0644]
enrol/database/cli/sync.php
enrol/database/db/tasks.php [new file with mode: 0644]
enrol/database/lang/en/enrol_database.php
enrol/database/upgrade.txt [new file with mode: 0644]
enrol/database/version.php
enrol/manual/amd/build/form-potential-user-selector.min.js
enrol/manual/amd/src/form-potential-user-selector.js
enrol/manual/classes/enrol_users_form.php
enrol/manual/tests/behat/quickenrolment.feature
grade/edit/tree/calculation.php
group/externallib.php
group/overview.php
group/tests/externallib_test.php
install/lang/el/langconfig.php
install/lang/pt/admin.php
install/lang/ro/install.php
lang/en/admin.php
lang/en/backup.php
lang/en/course.php
lang/en/customfield.php [new file with mode: 0644]
lang/en/moodle.php
lang/en/plugin.php
lang/en/role.php
lang/en/search.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/icon_system_fontawesome.min.js
lib/amd/build/inplace_editable.min.js
lib/amd/src/form-autocomplete.js
lib/amd/src/icon_system_fontawesome.js
lib/amd/src/inplace_editable.js
lib/behat/behat_base.php
lib/behat/form_field/behat_form_autocomplete.php
lib/behat/form_field/behat_form_field.php
lib/classes/analytics/analyser/courses.php
lib/classes/analytics/analyser/site_courses.php
lib/classes/component.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/customfield.php [new file with mode: 0644]
lib/classes/task/logging_trait.php [new file with mode: 0644]
lib/classes/task/manager.php
lib/cronlib.php
lib/db/access.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/editor/atto/classes/privacy/provider.php
lib/editor/atto/tests/privacy_provider_test.php [moved from lib/editor/atto/tests/privacy_provider.php with 100% similarity]
lib/evalmath/evalmath.class.php
lib/evalmath/readme_moodle.txt
lib/formslib.php
lib/gradelib.php
lib/grouplib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/phpmailer/README_MOODLE.txt
lib/phpmailer/src/PHPMailer.php
lib/phpunit/classes/arraydataset.php
lib/requirejs/moodle-config.js
lib/setuplib.php
lib/templates/form_autocomplete_input.mustache
lib/templates/pix_icon.mustache
lib/templates/pix_icon_fontawesome.mustache
lib/templates/popover_region.mustache
lib/templates/url_select.mustache
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/tests/adhoc_task_test.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_general.php
lib/tests/component_test.php
lib/tests/gradelib_test.php
lib/tests/grouplib_test.php
lib/tests/mathslib_test.php
lib/tests/moodlelib_test.php
lib/tests/outputcomponents_test.php
lib/typo3/class.t3lib_div.php
lib/typo3/readme_moodle.txt
lib/upgrade.txt
lib/userkey/tests/privacy_provider_test.php [moved from lib/userkey/tests/privacy_provider.php with 100% similarity]
message/classes/api.php
message/classes/helper.php
message/classes/search/base_message.php
message/classes/task/migrate_message_data.php
message/lib.php
message/output/airnotifier/classes/privacy/provider.php
message/templates/message_drawer_contacts_list.mustache
message/templates/message_drawer_messages_list.mustache
message/templates/message_drawer_non_contacts_list.mustache
message/tests/externallib_test.php
message/tests/helper_test.php [new file with mode: 0644]
message/tests/migrate_message_data_task_test.php
message/tests/search_received_test.php
message/tests/search_sent_test.php
mod/assign/amd/build/grading_navigation_user_info.min.js
mod/assign/amd/src/grading_navigation_user_info.js
mod/assign/classes/event/remove_submission_form_viewed.php [new file with mode: 0644]
mod/assign/classes/output/grading_app.php
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/globals.js
mod/assign/gradingbatchoperationsform.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/module.js
mod/assign/renderer.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/submissionplugin.php
mod/assign/templates/grading_actions.mustache
mod/assign/tests/behat/allow_another_attempt.feature
mod/assign/tests/behat/edit_student_submission.feature
mod/assign/tests/behat/group_submission.feature
mod/assign/tests/behat/page_titles.feature [new file with mode: 0644]
mod/assign/tests/behat/remove_submission.feature [new file with mode: 0644]
mod/assign/upgrade.txt
mod/book/edit.php
mod/book/edit_form.php
mod/book/lang/en/book.php
mod/book/locallib.php
mod/book/tests/behat/create_chapters.feature
mod/book/tests/behat/reorganize_chapters.feature
mod/book/tests/behat/show_hide_chapters.feature
mod/book/tool/print/classes/output/print_book_chapter_page.php [new file with mode: 0644]
mod/book/tool/print/classes/output/print_book_page.php [new file with mode: 0644]
mod/book/tool/print/classes/output/renderer.php [new file with mode: 0644]
mod/book/tool/print/index.php
mod/book/tool/print/locallib.php
mod/book/tool/print/print.css
mod/book/tool/print/templates/print_book.mustache [new file with mode: 0644]
mod/book/tool/print/templates/print_book_chapter.mustache [new file with mode: 0644]
mod/data/lib.php
mod/feedback/tests/behat/coursemapping.feature
mod/folder/lib.php
mod/folder/module.js
mod/forum/classes/task/cron_task.php
mod/forum/classes/task/send_user_digests.php [new file with mode: 0644]
mod/forum/classes/task/send_user_notifications.php [new file with mode: 0644]
mod/forum/deprecatedlib.php
mod/forum/index.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/templates/forum_post_emaildigestfull_textemail.mustache
mod/forum/tests/cron_trait.php [new file with mode: 0644]
mod/forum/tests/generator_trait.php [new file with mode: 0644]
mod/forum/tests/lib_test.php
mod/forum/tests/mail_group_test.php [new file with mode: 0644]
mod/forum/tests/mail_test.php
mod/forum/tests/maildigest_test.php
mod/forum/tests/qanda_test.php [new file with mode: 0644]
mod/glossary/editcategories.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/tests/behat/behat_mod_glossary.php
mod/glossary/tests/behat/categories.feature
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/pagetypes/branchtable.php
mod/lesson/pagetypes/cluster.php
mod/lesson/pagetypes/endofbranch.php
mod/lesson/pagetypes/endofcluster.php
mod/lesson/tests/lib_test.php
mod/lesson/tests/locallib_test.php
mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php
mod/quiz/amd/build/repaginate.min.js [new file with mode: 0644]
mod/quiz/amd/src/repaginate.js [new file with mode: 0644]
mod/quiz/attemptlib.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/settings.php
mod/quiz/styles.css
mod/quiz/tests/attempt_test.php
mod/quiz/tests/lib_test.php
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate-debug.js [deleted file]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate-min.js [deleted file]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate.js [deleted file]
mod/quiz/yui/src/repaginate/build.json [deleted file]
mod/quiz/yui/src/repaginate/js/repaginate.js [deleted file]
mod/quiz/yui/src/repaginate/meta/repaginate.json [deleted file]
mod/resource/locallib.php
mod/scorm/db/install.xml
mod/scorm/db/upgrade.php
mod/scorm/lib.php
mod/scorm/locallib.php
mod/scorm/tests/lib_test.php
mod/scorm/version.php
mod/wiki/module.js
mod/workshop/classes/external.php
mod/workshop/lib.php
mod/workshop/tests/external_test.php
phpunit.xml.dist
pix/i/customfield.png [new file with mode: 0644]
pix/i/customfield.svg [new file with mode: 0644]
pix/i/flagged.svg [new file with mode: 0644]
pix/i/unflagged.svg [new file with mode: 0644]
portfolio/classes/privacy/provider.php
privacy/classes/local/request/moodle_content_writer.php
question/behaviour/behaviourbase.php
question/behaviour/manualgraded/tests/walkthrough_test.php
question/category_class.php
question/classes/bank/tags_action_column.php
question/engine/lib.php
question/engine/tests/helpers.php
question/engine/tests/questionutils_test.php
question/tests/behat/question_categories.feature
question/tests/behat/question_categories_idnumber.feature
question/type/calculated/edit_calculated_form.php
question/type/calculatedmulti/edit_calculatedmulti_form.php
question/type/ddimageortext/amd/build/form.min.js
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/src/form.js
question/type/ddimageortext/amd/src/question.js
question/type/ddimageortext/edit_ddimageortext_form.php
question/type/ddimageortext/lang/en/qtype_ddimageortext.php
question/type/ddimageortext/tests/edit_form_test.php [new file with mode: 0644]
question/type/ddimageortext/tests/helper.php
question/type/ddimageortext/tests/walkthrough_test.php
question/type/ddmarker/amd/build/form.min.js
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/src/form.js
question/type/ddmarker/amd/src/question.js
question/type/ddmarker/edit_ddmarker_form.php
question/type/ddmarker/tests/edit_form_test.php [new file with mode: 0644]
question/type/ddwtos/tests/behat/edit.feature
question/type/ddwtos/tests/edit_form_test.php
question/type/gapselect/edit_form_base.php
question/type/gapselect/lang/en/qtype_gapselect.php
question/type/gapselect/renderer.php
question/type/gapselect/rendererbase.php
question/type/gapselect/tests/edit_form_test.php
question/type/gapselect/tests/helper.php
question/type/gapselect/tests/walkthrough_test.php
question/type/match/renderer.php
report/completion/index.php
report/insights/templates/insight.mustache
report/insights/templates/insight_details.mustache
report/insights/templates/insights_list.mustache
report/progress/index.php
report/security/locallib.php
repository/classes/privacy/provider.php
repository/equella/lib.php
repository/onedrive/classes/privacy/provider.php
search/classes/area_category.php [new file with mode: 0644]
search/classes/base.php
search/classes/base_block.php
search/classes/base_mod.php
search/classes/manager.php
search/classes/output/form/search.php
search/classes/output/renderer.php
search/engine/solr/settings.php
search/index.php
search/tests/area_category_test.php [new file with mode: 0644]
search/tests/base_test.php
search/tests/manager_test.php
theme/boost/config.php
theme/boost/scss/editor.scss [new file with mode: 0644]
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/core/form_autocomplete_input.mustache
theme/boost/templates/mod_assign/grading_actions.mustache
theme/boost/templates/navbar.mustache
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/style/moodle.css
theme/upgrade.txt
user/classes/privacy/provider.php
user/classes/search/user.php
user/index.php
user/profile/field/checkbox/classes/privacy/provider.php
user/profile/field/datetime/classes/privacy/provider.php
user/profile/field/menu/classes/privacy/provider.php
user/profile/field/text/classes/privacy/provider.php
user/profile/field/textarea/classes/privacy/provider.php
user/tests/search_test.php
version.php
webservice/classes/privacy/provider.php

diff --git a/admin/cli/uninstall_plugins.php b/admin/cli/uninstall_plugins.php
new file mode 100644 (file)
index 0000000..e093d89
--- /dev/null
@@ -0,0 +1,163 @@
+<?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/>.
+
+/**
+ * CLI script to uninstall plugins.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2018 Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once($CFG->libdir . '/clilib.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+$help = "Command line tool to uninstall plugins.
+
+Options:
+    -h --help                   Print this help.
+    --show-all                  Displays a list of all installed plugins.
+    --show-missing              Displays a list of plugins missing from disk.
+    --purge-missing             Uninstall all missing from disk plugins.
+    --plugins=<plugin name>     A comma separated list of plugins to be uninstalled. E.g. mod_assign,mod_forum
+    --run                       Execute uninstall. If this option is not set, then the script will be run in a dry mode.
+
+Examples:
+
+    # php uninstall_plugins.php  --show-all
+        Prints tab-separated list of all installed plugins.
+
+    # php uninstall_plugins.php  --show-missing
+        Prints tab-separated list of all missing from disk plugins.
+
+    # php uninstall_plugins.php  --purge-missing
+        A dry run of uninstalling all missing plugins.
+
+    # php uninstall_plugins.php  --purge-missing --run
+        Run uninstall of all missing plugins.
+
+    # php uninstall_plugins.php  --plugins=mod_assign,mod_forum
+        A dry run of uninstalling mod_assign and mod_forum plugins.
+
+    # php uninstall_plugins.php  --plugins=mod_assign,mod_forum --run
+        Run uninstall for mod_assign and mod_forum plugins.
+";
+
+list($options, $unrecognised) = cli_get_params([
+    'help' => false,
+    'show-all' => false,
+    'show-missing' => false,
+    'purge-missing' => false,
+    'plugins' => false,
+    'run' => false,
+], [
+    'h' => 'help'
+]);
+
+if ($unrecognised) {
+    $unrecognised = implode(PHP_EOL.'  ', $unrecognised);
+    cli_error(get_string('cliunknowoption', 'core_admin', $unrecognised));
+}
+
+if ($options['help']) {
+    cli_writeln($help);
+    exit(0);
+}
+
+$pluginman = core_plugin_manager::instance();
+$plugininfo = $pluginman->get_plugins();
+
+if ($options['show-all'] || $options['show-missing']) {
+    foreach ($plugininfo as $type => $plugins) {
+        foreach ($plugins as $name => $plugin) {
+            $pluginstring = $plugin->component . "\t" . $plugin->displayname;
+
+            if ($options['show-all']) {
+                cli_writeln($pluginstring);
+            } else {
+                if ($plugin->get_status() === core_plugin_manager::PLUGIN_STATUS_MISSING) {
+                    cli_writeln($pluginstring);
+                }
+            }
+        }
+    }
+
+    exit(0);
+}
+
+if ($options['purge-missing']) {
+    foreach ($plugininfo as $type => $plugins) {
+        foreach ($plugins as $name => $plugin) {
+            if ($plugin->get_status() === core_plugin_manager::PLUGIN_STATUS_MISSING) {
+
+                $pluginstring = $plugin->component . "\t" . $plugin->displayname;
+
+                if ($pluginman->can_uninstall_plugin($plugin->component)) {
+                    if ($options['run']) {
+                        cli_writeln('Uninstalling: ' . $pluginstring);
+
+                        $progress = new progress_trace_buffer(new text_progress_trace(), true);
+                        $pluginman->uninstall_plugin($plugin->component, $progress);
+                        $progress->finished();
+                        cli_write($progress->get_buffer());
+                    } else {
+                        cli_writeln('Will be uninstalled: ' . $pluginstring);
+                    }
+                } else {
+                    cli_writeln('Can not be uninstalled: ' . $pluginstring);
+                }
+            }
+        }
+    }
+
+    exit(0);
+}
+
+if ($options['plugins']) {
+    $components = explode(',', $options['plugins']);
+    foreach ($components as $component) {
+        $plugin = $pluginman->get_plugin_info($component);
+
+        if (is_null($plugin)) {
+            cli_writeln('Unknown plugin: ' . $component);
+        } else {
+            $pluginstring = $plugin->component . "\t" . $plugin->displayname;
+
+            if ($pluginman->can_uninstall_plugin($plugin->component)) {
+                if ($options['run']) {
+                    cli_writeln('Uninstalling: ' . $pluginstring);
+                    $progress = new progress_trace_buffer(new text_progress_trace(), true);
+                    $pluginman->uninstall_plugin($plugin->component, $progress);
+                    $progress->finished();
+                    cli_write($progress->get_buffer());
+                } else {
+                    cli_writeln('Will be uninstalled: ' . $pluginstring);
+                }
+            } else {
+                cli_writeln('Can not be uninstalled: ' . $pluginstring);
+            }
+        }
+    }
+
+    exit(0);
+}
+
+cli_writeln($help);
+exit(0);
diff --git a/admin/customfields.php b/admin/customfields.php
new file mode 100644 (file)
index 0000000..2f7681e
--- /dev/null
@@ -0,0 +1,62 @@
+<?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/>.
+
+/**
+ * Allows the admin to enable, disable and uninstall custom fields
+ *
+ * @package    core_admin
+ * @copyright  2018 Daniel Neis Araujo
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$action  = required_param('action', PARAM_ALPHANUMEXT);
+$customfieldname = required_param('field', PARAM_PLUGIN);
+
+$syscontext = context_system::instance();
+$PAGE->set_url('/admin/customfields.php');
+$PAGE->set_context($syscontext);
+
+require_login();
+require_capability('moodle/site:config', $syscontext);
+require_sesskey();
+
+$return = new moodle_url('/admin/settings.php', array('section' => 'managecustomfields'));
+
+$customfieldplugins = core_plugin_manager::instance()->get_plugins_of_type('customfield');
+$sortorder = array_flip(array_keys($customfieldplugins));
+
+if (!isset($customfieldplugins[$customfieldname])) {
+    print_error('customfieldnotfound', 'error', $return, $customfieldname);
+}
+
+switch ($action) {
+    case 'disable':
+        if ($customfieldplugins[$customfieldname]->is_enabled()) {
+            set_config('disabled', 1, 'customfield_'. $customfieldname);
+            core_plugin_manager::reset_caches();
+        }
+        break;
+    case 'enable':
+        if (!$customfieldplugins[$customfieldname]->is_enabled()) {
+            unset_config('disabled', 'customfield_'. $customfieldname);
+            core_plugin_manager::reset_caches();
+        }
+        break;
+}
+redirect($return);
index e04c319..c34a437 100644 (file)
@@ -823,9 +823,11 @@ if (isset($SESSION->pluginuninstallreturn)) {
 // Print default admin page with notifications.
 $errorsdisplayed = defined('WARN_DISPLAY_ERRORS_ENABLED');
 
-// We make the assumption that at least one schedule task should run once per day.
-$lastcron = $DB->get_field_sql('SELECT MAX(lastruntime) FROM {task_scheduled}');
+$lastcron = get_config('tool_task', 'lastcronstart');
 $cronoverdue = ($lastcron < time() - 3600 * 24);
+$lastcroninterval = get_config('tool_task', 'lastcroninterval');
+$expectedfrequency = $CFG->expectedcronfrequency ?? 200;
+$croninfrequent = !$cronoverdue && ($lastcroninterval > $expectedfrequency || $lastcron < time() - $expectedfrequency);
 $dbproblems = $DB->diagnose();
 $maintenancemode = !empty($CFG->maintenance_enabled);
 
@@ -886,4 +888,4 @@ $output = $PAGE->get_renderer('core', 'admin');
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
                                        $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
-                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl);
+                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent);
index da59ef6..57eaa0e 100644 (file)
@@ -281,6 +281,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param bool $mobileconfigured Whether the mobile web services have been enabled
      * @param bool $overridetossl Whether or not ssl is being forced.
      * @param bool $invalidforgottenpasswordurl Whether the forgotten password URL does not link to a valid URL.
+     * @param bool $croninfrequent If true, warn that cron hasn't run in the past few minutes
      *
      * @return string HTML to output.
      */
@@ -288,7 +289,7 @@ class core_admin_renderer extends plugin_renderer_base {
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
             $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
             $themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
-            $overridetossl = false, $invalidforgottenpasswordurl = false) {
+            $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false) {
         global $CFG;
         $output = '';
 
@@ -302,6 +303,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->display_errors_warning($errorsdisplayed);
         $output .= $this->buggy_iconv_warning($buggyiconvnomb);
         $output .= $this->cron_overdue_warning($cronoverdue);
+        $output .= $this->cron_infrequent_warning($croninfrequent);
         $output .= $this->db_problems($dbproblems);
         $output .= $this->maintenance_mode_warning($maintenancemode);
         $output .= $this->overridetossl_warning($overridetossl);
@@ -614,6 +616,24 @@ class core_admin_renderer extends plugin_renderer_base {
                 $this->help_icon('cron', 'admin'));
     }
 
+    /**
+     * Render an appropriate message if cron is not being run frequently (recommended every minute).
+     *
+     * @param bool $croninfrequent
+     * @return string HTML to output.
+     */
+    public function cron_infrequent_warning(bool $croninfrequent) : string {
+        global $CFG;
+
+        if (!$croninfrequent) {
+            return '';
+        }
+
+        $expectedfrequency = $CFG->expectedcronfrequency ?? 200;
+        return $this->warning(get_string('croninfrequent', 'admin', $expectedfrequency) . '&nbsp;' .
+                $this->help_icon('cron', 'admin'));
+    }
+
     /**
      * Render an appropriate message if there are any problems with the DB set-up.
      * @param bool $dbproblems
index 911a86b..fb5b0a3 100644 (file)
@@ -153,6 +153,7 @@ $table = new html_table();
 $table->id = 'core-search-areas';
 $table->head = [
     get_string('searcharea', 'search'),
+    get_string('searchareacategories', 'search'),
     get_string('enable'),
     get_string('newestdocindexed', 'admin'),
     get_string('searchlastrun', 'admin'),
@@ -165,6 +166,14 @@ foreach ($searchareas as $area) {
     $areaid = $area->get_area_id();
     $columns = array(new html_table_cell($area->get_visible_name()));
 
+    $areacategories = [];
+    foreach (\core_search\manager::get_search_area_categories() as $category) {
+        if (key_exists($areaid, $category->get_areas())) {
+            $areacategories[] = $category->get_visiblename();
+        }
+    }
+    $columns[] = new html_table_cell(implode(', ', $areacategories));
+
     if ($area->is_enabled()) {
         $columns[] = $OUTPUT->action_icon(admin_searcharea_action_url('disable', $areaid),
             new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
index f23bf5f..739c96e 100644 (file)
@@ -37,6 +37,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
             array('moodle/category:manage', 'moodle/course:create')
         )
     );
+    $ADMIN->add('courses',
+        new admin_externalpage('course_customfield', new lang_string('course_customfield', 'admin'),
+            $CFG->wwwroot . '/course/customfield.php',
+            array('moodle/course:configurecustomfields')
+        )
+    );
     $ADMIN->add('courses',
         new admin_externalpage('addcategory', new lang_string('addcategory', 'admin'),
             new moodle_url('/course/editcategory.php', array('parent' => 0)),
index 9a2202f..616d10e 100644 (file)
@@ -59,6 +59,18 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'formatsettings', $hassiteconfig);
     }
 
+    // Custom fields.
+    $ADMIN->add('modules', new admin_category('customfieldsettings', new lang_string('customfields', 'core_customfield')));
+    $temp = new admin_settingpage('managecustomfields', new lang_string('managecustomfields', 'core_admin'));
+    $temp->add(new admin_setting_managecustomfields());
+    $ADMIN->add('customfieldsettings', $temp);
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('customfield');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
+        /** @var \core\plugininfo\customfield $plugin */
+        $plugin->load_settings($ADMIN, 'customfieldsettings', $hassiteconfig);
+    }
+
     // blocks
     $ADMIN->add('modules', new admin_category('blocksettings', new lang_string('blocks')));
     $ADMIN->add('blocksettings', new admin_page_manageblocks());
@@ -573,6 +585,25 @@ if ($hassiteconfig) {
             new lang_string('searchallavailablecourses_desc', 'admin'),
             0, $options));
 
+    // Search display options.
+    $temp->add(new admin_setting_heading('searchdisplay', new lang_string('searchdisplay', 'admin'), ''));
+    $temp->add(new admin_setting_configcheckbox('searchenablecategories',
+        new lang_string('searchenablecategories', 'admin'),
+        new lang_string('searchenablecategories_desc', 'admin'),
+        0));
+    $options = [];
+    foreach (\core_search\manager::get_search_area_categories() as $category) {
+        $options[$category->get_name()] = $category->get_visiblename();
+    }
+    $temp->add(new admin_setting_configselect('searchdefaultcategory',
+        new lang_string('searchdefaultcategory', 'admin'),
+        new lang_string('searchdefaultcategory_desc', 'admin'),
+        \core_search\manager::SEARCH_AREA_CATEGORY_ALL, $options));
+    $temp->add(new admin_setting_configcheckbox('searchhideallcategory',
+        new lang_string('searchhideallcategory', 'admin'),
+        new lang_string('searchhideallcategory_desc', 'admin'),
+        0));
+
     $ADMIN->add('searchplugins', $temp);
     $ADMIN->add('searchplugins', new admin_externalpage('searchareas', new lang_string('searchareas', 'admin'),
         new moodle_url('/admin/searchareas.php')));
index e88125b..83a19af 100644 (file)
@@ -109,6 +109,17 @@ require_once(__DIR__ . '/../../../../lib/behat/lib.php');
 require_once(__DIR__ . '/../../../../lib/behat/classes/behat_command.php');
 require_once(__DIR__ . '/../../../../lib/behat/classes/behat_config_manager.php');
 
+// Remove error handling overrides done in config.php. This is consistent with admin/tool/behat/cli/util_single_run.php.
+$CFG->debug = (E_ALL | E_STRICT);
+$CFG->debugdisplay = 1;
+error_reporting($CFG->debug);
+ini_set('display_errors', '1');
+ini_set('log_errors', '1');
+
+// Import the necessary libraries.
+require_once($CFG->libdir . '/setuplib.php');
+require_once($CFG->libdir . '/behat/classes/util.php');
+
 // For drop option check if parallel site.
 if ((empty($options['parallel'])) && ($options['drop']) || $options['updatesteps']) {
     $options['parallel'] = behat_config_manager::get_behat_run_config_value('parallel');
index 2e98656..967fde3 100644 (file)
@@ -218,7 +218,7 @@ if ($options['install']) {
     // Run behat command to get steps in feature files.
     $featurestepscmd = behat_command::get_behat_command(true);
     $featurestepscmd .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
-    $featurestepscmd .= ' --dry-run --format=moodle_step_count';
+    $featurestepscmd .= ' --dry-run --format=moodle_stepcount';
     $processes = cli_execute_parallel(array($featurestepscmd), __DIR__ . "/../../../../");
     $status = print_update_step_output(array_pop($processes), $behatstepfile);
 
index 90e7b2a..7672da3 100644 (file)
@@ -920,9 +920,17 @@ class expired_contexts_manager {
      * @return  bool
      */
     protected static function is_course_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) {
-        $expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
 
-        $info = $expiryrecords[$context->path]->info;
+        if ($context->get_course_context()->instanceid == SITEID) {
+            // The is an activity in the site course (front page).
+            $purpose = data_registry::get_effective_contextlevel_value(CONTEXT_SYSTEM, 'purpose');
+            $info = static::get_expiry_info($purpose);
+
+        } else {
+            $expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
+            $info = $expiryrecords[$context->path]->info;
+        }
+
         if ($info->is_fully_expired()) {
             // This context is fully expired.
             return true;
index 3dd839e..20c28f9 100644 (file)
@@ -689,6 +689,7 @@ class external extends external_api {
      * @throws restricted_context_exception
      */
     public static function get_users($query) {
+        global $DB;
         $params = external_api::validate_parameters(self::get_users_parameters(), [
             'query' => $query
         ]);
@@ -703,15 +704,30 @@ class external extends external_api {
         // Exclude admins and guest user.
         $excludedusers = array_keys(get_admins()) + [guest_user()->id];
         $sort = 'lastname ASC, firstname ASC';
-        $fields = 'id, email, ' . $allusernames;
-        $users = get_users(true, $query, true, $excludedusers, $sort, '', '', 0, 30, $fields);
+        $fields = 'id,' . $allusernames;
+
+        $extrafields = get_extra_user_fields($context);
+        if (!empty($extrafields)) {
+            $fields .= ',' . implode(',', $extrafields);
+        }
+
+        list($sql, $params) = users_search_sql($query, '', false, $extrafields, $excludedusers);
+        $users = $DB->get_records_select('user', $sql, $params, $sort, $fields, 0, 30);
+
         $useroptions = [];
         foreach ($users as $user) {
-            $useroptions[$user->id] = (object)[
+            $useroption = (object)[
                 'id' => $user->id,
-                'fullname' => fullname($user),
-                'email' => $user->email
+                'fullname' => fullname($user)
             ];
+            $useroption->extrafields = [];
+            foreach ($extrafields as $extrafield) {
+                $useroption->extrafields[] = (object)[
+                    'name' => $extrafield,
+                    'value' => $user->$extrafield
+                ];
+            }
+            $useroptions[$user->id] = $useroption;
         }
 
         return $useroptions;
@@ -729,7 +745,13 @@ class external extends external_api {
             [
                 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
                 'fullname' => new external_value(core_user::get_property_type('firstname'), 'The fullname of the user'),
-                'email' => new external_value(core_user::get_property_type('email'), 'The user\'s email address', VALUE_OPTIONAL),
+                'extrafields' => new external_multiple_structure(
+                    new external_single_structure([
+                            'name' => new external_value(PARAM_TEXT, 'Name of the extrafield.'),
+                            'value' => new external_value(PARAM_TEXT, 'Value of the extrafield.')
+                        ]
+                    ), 'List of extra fields', VALUE_OPTIONAL
+                )
             ]
         ));
     }
index 759650c..0f8443d 100644 (file)
     Example context (json):
     {
         "fullname": "Admin User",
-        "email": "admin@example.com"
+        "extrafields": [
+            {
+                "name": "email",
+                "value": "admin@example.com"
+            },
+            {
+                "name": "phone1",
+                "value": "0123456789"
+            }
+        ]
     }
 }}
 <span>
     <span>{{fullname}}</span>
-    <span><small>{{email}}</small></span>
+    {{#extrafields}}
+        <span><small>{{value}}</small></span>
+    {{/extrafields}}
 </span>
index 1947f62..78751cc 100644 (file)
@@ -6,21 +6,29 @@ Feature: Data export from the privacy API
 
   Background:
     Given the following "users" exist:
-      | username | firstname      | lastname |
-      | victim   | Victim User    | 1        |
-      | parent   | Long-suffering | Parent   |
+      | username  | firstname      | lastname  | institution |
+      | victim    | Victim User    | 1         | University1 |
+      | victim2   | Victim User    | 2         | University2 |
+      | requester | The            | Requester | University3 |
+      | parent    | Long-suffering | Parent    |             |
     And the following "roles" exist:
       | shortname | name  | archetype |
       | tired     | Tired |           |
     And the following "permission overrides" exist:
-      | capability                                   | permission | role  | contextlevel | reference |
-      | tool/dataprivacy:makedatarequestsforchildren | Allow      | tired | System       |           |
+      | capability                                   | permission | role    | contextlevel | reference |
+      | tool/dataprivacy:makedatarequestsforchildren | Allow      | tired   | System       |           |
+      | tool/dataprivacy:managedatarequests          | Allow      | manager | System       |           |
+      | moodle/site:viewuseridentity                 | Prevent    | manager | System       |           |
     And the following "role assigns" exist:
       | user   | role  | contextlevel | reference |
       | parent | tired | User         | victim    |
+    And the following "system role assigns" exist:
+      | user      | role    | contextlevel |
+      | requester | manager | User         |
     And the following config values are set as admin:
       | contactdataprotectionofficer | 1  | tool_dataprivacy |
       | privacyrequestexpiry         | 55 | tool_dataprivacy |
+      | dporoles                     | 1  | tool_dataprivacy |
     And the following data privacy "categories" exist:
       | name          |
       | Site category |
@@ -127,3 +135,19 @@ Feature: Data export from the privacy API
 
     And I should see "Expired" in the "Victim User 1" "table_row"
     And I should not see "Actions"
+
+  @javascript
+  Scenario: Test search for user using extra field.
+    Given the following "permission overrides" exist:
+      | capability                   | permission | role    | contextlevel | reference |
+      | moodle/site:viewuseridentity | Allow      | manager | System       |           |
+    And the following config values are set as admin:
+      | showuseridentity | institution |
+    And I log in as "requester"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "New request"
+    And I set the field "Search" to "University1"
+    Then I should see "Victim User 1"
+    When I reload the page
+    And I set the field "Search" to "University2"
+    Then I should see "Victim User 2"
index 08ffb80..6deb919 100644 (file)
@@ -2222,6 +2222,40 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
     }
 
+    /**
+     * Test the is_context_expired functions when supplied with the front page course.
+     */
+    public function test_is_context_expired_frontpage() {
+        $this->resetAfterTest();
+
+        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
+
+        $frontcourse = get_site();
+        $frontcoursecontext = \context_course::instance($frontcourse->id);
+
+        $sitenews = $this->getDataGenerator()->create_module('forum', ['course' => $frontcourse->id]);
+        $cm = get_coursemodule_from_instance('forum', $sitenews->id);
+        $sitenewscontext = \context_module::instance($cm->id);
+
+        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+
+        $this->assertFalse(expired_contexts_manager::is_context_expired($frontcoursecontext));
+        $this->assertFalse(expired_contexts_manager::is_context_expired($sitenewscontext));
+
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
+
+        // Protecting the course contextlevel does not impact the front page.
+        $purposes->course->set('protected', 1)->save();
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
+
+        // Protecting the system contextlevel affects the front page, too.
+        $purposes->system->set('protected', 1)->save();
+        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
+        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
+    }
+
     /**
      * Test the is_context_expired functions when supplied with an expired course.
      */
index b17811a..7cb7df7 100644 (file)
@@ -970,4 +970,143 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
         $this->expectException(required_capability_exception::class);
         $result = external::bulk_deny_data_requests([$requestid1]);
     }
+
+    /**
+     * Test for external::get_users(), case search using non-identity field without
+     * facing any permission problem.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public function test_get_users_using_using_non_identity() {
+        $this->resetAfterTest();
+        $context = context_system::instance();
+        $requester = $this->getDataGenerator()->create_user();
+        $role = $this->getDataGenerator()->create_role();
+        role_assign($role, $requester->id, $context);
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context);
+        $this->setUser($requester);
+
+        $this->getDataGenerator()->create_user([
+            'firstname' => 'First Student'
+        ]);
+        $student2 = $this->getDataGenerator()->create_user([
+            'firstname' => 'Second Student'
+        ]);
+
+        $results = external::get_users('Second');
+        $this->assertCount(1, $results);
+        $this->assertEquals((object)[
+            'id' => $student2->id,
+            'fullname' => fullname($student2),
+            'extrafields' => []
+        ], $results[$student2->id]);
+    }
+
+    /**
+     * Test for external::get_users(), case search using identity field but
+     * don't have "moodle/site:viewuseridentity" permission.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public function test_get_users_using_identity_without_permission() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->showuseridentity = 'institution';
+
+        // Create requester user and assign correct capability.
+        $context = context_system::instance();
+        $requester = $this->getDataGenerator()->create_user();
+        $role = $this->getDataGenerator()->create_role();
+        role_assign($role, $requester->id, $context);
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context);
+        $this->setUser($requester);
+
+        $this->getDataGenerator()->create_user([
+            'institution' => 'University1'
+        ]);
+
+        $results = external::get_users('University1');
+        $this->assertEmpty($results);
+    }
+
+    /**
+     * Test for external::get_users(), case search using disabled identity field
+     * even they have "moodle/site:viewuseridentity" permission.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public function test_get_users_using_field_not_in_identity() {
+        $this->resetAfterTest();
+
+        $context = context_system::instance();
+        $requester = $this->getDataGenerator()->create_user();
+        $role = $this->getDataGenerator()->create_role();
+        role_assign($role, $requester->id, $context);
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context);
+        assign_capability('moodle/site:viewuseridentity', CAP_ALLOW, $role, $context);
+        $this->setUser($requester);
+
+        $this->getDataGenerator()->create_user([
+            'institution' => 'University1'
+        ]);
+
+        $results = external::get_users('University1');
+        $this->assertEmpty($results);
+    }
+
+    /**
+     * Test for external::get_users(), case search using enabled identity field
+     * with "moodle/site:viewuseridentity" permission.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public function test_get_users() {
+        global $CFG;
+        $this->resetAfterTest();
+        $CFG->showuseridentity = 'institution';
+        $context = context_system::instance();
+        $requester = $this->getDataGenerator()->create_user();
+        $role = $this->getDataGenerator()->create_role();
+        role_assign($role, $requester->id, $context);
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context);
+        assign_capability('moodle/site:viewuseridentity', CAP_ALLOW, $role, $context);
+        $this->setUser($requester);
+
+        $student1 = $this->getDataGenerator()->create_user([
+            'institution' => 'University1'
+        ]);
+        $this->getDataGenerator()->create_user([
+            'institution' => 'University2'
+        ]);
+
+        $results = external::get_users('University1');
+        $this->assertCount(1, $results);
+        $this->assertEquals((object)[
+            'id' => $student1->id,
+            'fullname' => fullname($student1),
+            'extrafields' => [
+                0 => (object)[
+                    'name' => 'institution',
+                    'value' => 'University1'
+                ]
+            ]
+        ], $results[$student1->id]);
+    }
 }
index a56cc8a..411f0ac 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018120300;
+$plugin->version   = 2019011500;
 $plugin->requires  = 2018112800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index 5cd88b9..4a0d576 100644 (file)
@@ -169,10 +169,7 @@ abstract class base extends \core_analytics\calculable {
 
             if (!is_null($calculatedvalue)) {
                 $notnulls[$sampleid] = $sampleid;
-                if ($calculatedvalue > self::MAX_VALUE || $calculatedvalue < self::MIN_VALUE) {
-                    throw new \coding_exception('Calculated values should be higher than ' . self::MIN_VALUE .
-                        ' and lower than ' . self::MAX_VALUE . ' ' . $calculatedvalue . ' received');
-                }
+                $this->validate_calculated_value($calculatedvalue);
             }
 
             $calculations[$sampleid] = $calculatedvalue;
@@ -182,4 +179,19 @@ abstract class base extends \core_analytics\calculable {
 
         return array($features, $newcalculations, $notnulls);
     }
+
+    /**
+     * Validates the calculated value.
+     *
+     * @throws \coding_exception
+     * @param float $calculatedvalue
+     * @return true
+     */
+    protected function validate_calculated_value($calculatedvalue) {
+        if ($calculatedvalue > self::MAX_VALUE || $calculatedvalue < self::MIN_VALUE) {
+            throw new \coding_exception('Calculated values should be higher than ' . self::MIN_VALUE .
+                ' and lower than ' . self::MAX_VALUE . ' ' . $calculatedvalue . ' received');
+        }
+        return true;
+    }
 }
index a730996..2e4d4a9 100644 (file)
@@ -41,9 +41,7 @@ abstract class binary extends discrete {
      * @return array
      */
     public static final function get_classes() {
-        // It does not really matter, all \core_analytics\local\indicator\discrete get_classes calls have been overwriten as we
-        // only need 1 column here.
-        return array(0);
+        return [-1, 1];
     }
 
     /**
index 8a8aa66..b04b4c6 100644 (file)
@@ -52,8 +52,7 @@ abstract class discrete extends base {
     public static function get_feature_headers() {
         $fullclassname = '\\' . get_called_class();
 
-        $headers = array($fullclassname);
-        foreach (self::get_classes() as $class) {
+        foreach (static::get_classes() as $class) {
             $headers[] = $fullclassname . '/' . $class;
         }
 
@@ -116,26 +115,45 @@ abstract class discrete extends base {
      */
     protected function to_features($calculatedvalues) {
 
-        $classes = self::get_classes();
+        $classes = static::get_classes();
 
         foreach ($calculatedvalues as $sampleid => $calculatedvalue) {
 
-            $classindex = array_search($calculatedvalue, $classes, true);
+            // Using intval as it may come as a float from the db.
+            $classindex = array_search(intval($calculatedvalue), $classes, true);
 
-            if (!$classindex) {
-                throw new \coding_exception(get_class($this) . ' calculated "' . $calculatedvalue .
-                    '" which is not one of its defined classes (' . json_encode($classes) . ')');
+            if ($classindex === false && !is_null($calculatedvalue)) {
+                throw new \coding_exception(get_class($this) . ' calculated value "' . $calculatedvalue .
+                    '" is not one of its defined classes (' . json_encode($classes) . ')');
             }
 
             // We transform the calculated value into multiple features, one for each of the possible classes.
             $features = array_fill(0, count($classes), 0);
 
             // 1 to the selected value.
-            $features[$classindex] = 1;
+            if (!is_null($calculatedvalue)) {
+                $features[$classindex] = 1;
+            }
 
             $calculatedvalues[$sampleid] = $features;
         }
 
         return $calculatedvalues;
     }
+
+    /**
+     * Validates the calculated value.
+     *
+     * @param float $calculatedvalue
+     * @return true
+     */
+    protected function validate_calculated_value($calculatedvalue) {
+
+        // Using intval as it may come as a float from the db.
+        if (!in_array(intval($calculatedvalue), static::get_classes())) {
+            throw new \coding_exception(get_class($this) . ' calculated value "' . $calculatedvalue .
+                '" is not one of its defined classes (' . json_encode(static::get_classes()) . ')');
+        }
+        return true;
+    }
 }
diff --git a/analytics/tests/fixtures/test_indicator_discrete.php b/analytics/tests/fixtures/test_indicator_discrete.php
new file mode 100644 (file)
index 0000000..983b655
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Test indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_indicator_discrete extends \core_analytics\local\indicator\discrete {
+
+    /**
+     * Returns the name.
+     *
+     * If there is a corresponding '_help' string this will be shown as well.
+     *
+     * @return \lang_string
+     */
+    public static function get_name() : \lang_string {
+        // Using a string that exists and contains a corresponding '_help' string.
+        return new \lang_string('allowstealthmodules');
+    }
+
+    /**
+     * The different classes this discrete indicator provides.
+     * @return [type] [description]
+     */
+    protected static function get_classes() {
+        return [0, 1, 2, 3, 4];
+    }
+
+    /**
+     * Just for testing.
+     *
+     * @param  float $value
+     * @param  string $subtype
+     * @return string
+     */
+    public function get_calculation_outcome($value, $subtype = false) {
+        return self::OUTCOME_OK;
+    }
+
+    /**
+     * Custom indicator calculated value display as otherwise we would display meaningless numbers to users.
+     *
+     * @param  float  $value
+     * @param  string $subtype
+     * @return string
+     */
+    public function get_display_value($value, $subtype = false) {
+        return $value;
+    }
+
+    /**
+     * calculate_sample
+     *
+     * @param int $sampleid
+     * @param string $sampleorigin
+     * @param int $starttime
+     * @param int $endtime
+     * @return float
+     */
+    protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
+        return 4;
+    }
+}
index 8b934dd..19106a6 100644 (file)
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class test_indicator_random extends \core_analytics\local\indicator\binary {
+class test_indicator_random extends \core_analytics\local\indicator\linear {
 
     /**
      * Returns a lang_string object representing the name for the indicator.
diff --git a/analytics/tests/indicator_test.php b/analytics/tests/indicator_test.php
new file mode 100644 (file)
index 0000000..add3f3d
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the indicator API.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/test_indicator_max.php');
+require_once(__DIR__ . '/fixtures/test_indicator_discrete.php');
+require_once(__DIR__ . '/fixtures/test_indicator_min.php');
+
+/**
+ * Unit tests for the model.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analytics_indicator_testcase extends advanced_testcase {
+
+    /**
+     * test_validate_calculated_value
+     *
+     * @param string $indicatorclass
+     * @param array $returnedvalue
+     * @dataProvider validate_calculated_value
+     * @return null
+     */
+    public function test_validate_calculated_value($indicatorclass, $returnedvalue) {
+        $indicator = new $indicatorclass();
+        list($values, $unused) = $indicator->calculate([1], 'notrelevanthere');
+        $this->assertEquals($returnedvalue, $values[0]);
+    }
+
+    /**
+     * Data provider for test_validate_calculated_value
+     *
+     * @return array
+     */
+    public function validate_calculated_value() {
+        return [
+            'max' => ['test_indicator_max', [1]],
+            'min' => ['test_indicator_min', [-1]],
+            'discrete' => ['test_indicator_discrete', [0, 0, 0, 0, 1]],
+        ];
+    }
+
+    /**
+     * test_validate_calculated_value_exceptions
+     *
+     * @param string $indicatorclass
+     * @param string $willreturn
+     * @dataProvider validate_calculated_value_exceptions
+     * @expectedException \coding_exception
+     * @return null
+     */
+    public function test_validate_calculated_value_exceptions($indicatorclass, $willreturn) {
+
+        $indicator = new $indicatorclass();
+        $indicatormock = $this->getMockBuilder(get_class($indicator))
+            ->setMethods(['calculate_sample'])
+            ->getMock();
+        $indicatormock->method('calculate_sample')->willReturn($willreturn);
+        list($values, $unused) = $indicatormock->calculate([1], 'notrelevanthere');
+
+    }
+
+    /**
+     * Data provider for test_validate_calculated_value_exceptions
+     *
+     * @return array
+     */
+    public function validate_calculated_value_exceptions() {
+        return [
+            'max' => ['test_indicator_max', 2],
+            'min' => ['test_indicator_min', -2],
+            'discrete' => ['test_indicator_discrete', 7],
+        ];
+    }
+}
index 34d82f7..7d74868 100644 (file)
@@ -452,7 +452,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $indicator = $this->getMockBuilder('test_indicator_max')->setMethods(['calculate_sample'])->getMock();
         $indicator->expects($this->never())->method('calculate_sample');
 
-        $existingcalcs = array(111 => 1, 222 => 0.5);
+        $existingcalcs = array(111 => 1, 222 => -1);
         $sampleids = array(111 => 111, 222 => 222);
         list($values, $unused) = $indicator->calculate($sampleids, $sampleorigin, $starttime, $endtime, $existingcalcs);
     }
index 5eb8434..56e5a7d 100644 (file)
@@ -130,21 +130,10 @@ class auth_plugin_cas extends auth_plugin_ldap {
             }
 
             $authCAS = optional_param('authCAS', '', PARAM_RAW);
-            if ($authCAS == 'NOCAS') {
+            if ($authCAS != 'CAS') {
                 return;
             }
-            // Show authentication form for multi-authentication.
-            // Test pgtIou parameter for proxy mode (https connection in background from CAS server to the php server).
-            if ($authCAS != 'CAS' && !isset($_GET['pgtIou'])) {
-                $PAGE->set_url('/login/index.php');
-                $PAGE->navbar->add($CASform);
-                $PAGE->set_title("$site->fullname: $CASform");
-                $PAGE->set_heading($site->fullname);
-                echo $OUTPUT->header();
-                include($CFG->dirroot.'/auth/cas/cas_form.html');
-                echo $OUTPUT->footer();
-                exit();
-            }
+
         }
 
         // Connection to CAS server
@@ -363,4 +352,35 @@ class auth_plugin_cas extends auth_plugin_ldap {
             phpCAS::logoutWithRedirectService($backurl);
         }
     }
+
+    /**
+     * Return a list of identity providers to display on the login page.
+     *
+     * @param string|moodle_url $wantsurl The requested URL.
+     * @return array List of arrays with keys url, iconurl and name.
+     */
+    public function loginpage_idp_list($wantsurl) {
+        if (empty($this->config->hostname)) {
+            // CAS is not configured.
+            return [];
+        }
+
+        $iconurl = moodle_url::make_pluginfile_url(
+            context_system::instance()->id,
+            'auth_cas',
+            'logo',
+            null,
+            '/',
+            $this->config->auth_logo);
+
+        return [
+            [
+                'url' => new moodle_url(get_login_url(), [
+                        'authCAS' => 'CAS',
+                    ]),
+                'iconurl' => $iconurl,
+                'name' => format_string($this->config->auth_name),
+            ],
+        ];
+    }
 }
diff --git a/auth/cas/cas_form.html b/auth/cas/cas_form.html
deleted file mode 100644 (file)
index 52319a3..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="loginbox clearfix">
-<div class="loginpanel">
-<div>
-<a href="<?php echo get_login_url() . '?authCAS=CAS';?>"><?php print_string('accesCAS', 'auth_cas');?></a>
-</div>
-<br/>
-<div>
-<a href="<?php echo get_login_url() . '?authCAS=NOCAS';?>"><?php print_string('accesNOCAS', 'auth_cas');?></a>
-</div>
-</div>
-</div>
index a7c3662..3e465f9 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['accesCAS'] = 'CAS users';
-$string['accesNOCAS'] = 'other users';
+$string['auth_cas_auth_name'] = 'Authentication method name';
+$string['auth_cas_auth_name_description'] = 'Provide a name for the CAS authentication method that is familiar to your users.';
+$string['auth_cas_auth_logo'] = 'Authentication method logo';
+$string['auth_cas_auth_logo_description'] = 'Provide a logo for the CAS authentication method that is familiar to your users.';
 $string['auth_cas_auth_user_create'] = 'Create users externally';
+$string['auth_cas_auth_service'] = 'CAS';
 $string['auth_cas_baseuri'] = 'URI of the server (nothing if no baseUri)<br />For example, if the CAS server responds to host.domaine.fr/CAS/ then<br />cas_baseuri = CAS/';
 $string['auth_cas_baseuri_key'] = 'Base URI';
 $string['auth_cas_broken_password'] = 'You cannot proceed without changing your password, however there is no available page for changing it. Please contact your Moodle Administrator.';
@@ -75,3 +78,7 @@ $string['noldapserver'] = 'No LDAP server configured for CAS! Syncing disabled.'
 $string['pluginname'] = 'CAS server (SSO)';
 $string['synctask'] = 'CAS users sync job';
 $string['privacy:metadata'] = 'The CAS server (SSO) authentication plugin does not store any personal data.';
+
+// Deprecated since Moodle 3.7.
+$string['accesCAS'] = 'CAS users';
+$string['accesNOCAS'] = 'other users';
diff --git a/auth/cas/lang/en/deprecated.txt b/auth/cas/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..6854e8e
--- /dev/null
@@ -0,0 +1,2 @@
+accesCAS,auth_cas
+accesNOCAS,auth_cas
diff --git a/auth/cas/lib.php b/auth/cas/lib.php
new file mode 100644 (file)
index 0000000..7127556
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Authentication Plugin: CAS Authentication.
+ *
+ * Authentication using CAS (Central Authentication Server).
+ *
+ * @package     auth_cas
+ * @copyright   2018 Fabrice Ménard <menard.fabrice@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Serves the logo file settings.
+ *
+ * @param   stdClass $course course object
+ * @param   stdClass $cm course module object
+ * @param   stdClass $context context object
+ * @param   string $filearea file area
+ * @param   array $args extra arguments
+ * @param   bool $forcedownload whether or not force download
+ * @param   array $options additional options affecting the file serving
+ * @return  bool false|void
+ */
+function auth_cas_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []) {
+    if ($context->contextlevel != CONTEXT_SYSTEM) {
+        return false;
+    }
+
+    if ($filearea !== 'logo' ) {
+        return false;
+    }
+
+    // Extract the filename / filepath from the $args array.
+    $filename = array_pop($args);
+    if (!$args) {
+        $filepath = '/';
+    } else {
+        $filepath = '/' . implode('/', $args) . '/';
+    }
+
+    // Retrieve the file from the Files API.
+    $itemid = 0;
+    $fs = get_file_storage();
+    $file = $fs->get_file($context->id, 'auth_cas', $filearea, $itemid, $filepath, $filename);
+    if (!$file) {
+        return false; // The file does not exist.
+    }
+
+    send_stored_file($file, null, 0, $forcedownload, $options);
+}
index 2bd7434..5434984 100644 (file)
@@ -45,6 +45,20 @@ if ($ADMIN->fulltree) {
         $settings->add(new admin_setting_heading('auth_cas/casserversettings',
                 new lang_string('auth_cas_server_settings', 'auth_cas'), ''));
 
+        // Authentication method name.
+        $settings->add(new admin_setting_configtext('auth_cas/auth_name',
+                get_string('auth_cas_auth_name', 'auth_cas'),
+                get_string('auth_cas_auth_name_description', 'auth_cas'),
+                get_string('auth_cas_auth_service', 'auth_cas'),
+                PARAM_RAW_TRIMMED));
+
+        // Authentication method logo.
+        $opts = array('accepted_types' => array('.png', '.jpg', '.gif', '.webp', '.tiff', '.svg'));
+        $settings->add(new admin_setting_configstoredfile('auth_cas/auth_logo',
+                 get_string('auth_cas_auth_logo', 'auth_cas'),
+                 get_string('auth_cas_auth_logo_description', 'auth_cas'), 'logo', 0, $opts));
+
+
         // Hostname.
         $settings->add(new admin_setting_configtext('auth_cas/hostname',
                 get_string('auth_cas_hostname_key', 'auth_cas'),
index f735c62..7c47f6a 100644 (file)
@@ -26,7 +26,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2018121400;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018112800;        // Requires this Moodle version
 $plugin->component = 'auth_cas';        // Full name of the plugin (used for diagnostics)
 
index 6e6c7ff..1052f3a 100644 (file)
@@ -2089,6 +2089,31 @@ class auth_plugin_ldap extends auth_plugin_base {
         return (bool)$user->suspended;
     }
 
+    /**
+     * Test a DN
+     *
+     * @param resource $ldapconn
+     * @param string $dn The DN to check for existence
+     * @param string $message The identifier of a string as in get_string()
+     * @param string|object|array $a An object, string or number that can be used
+     *      within translation strings as in get_string()
+     * @return true or a message in case of error
+     */
+    private function test_dn($ldapconn, $dn, $message, $a = null) {
+        $ldapresult = @ldap_read($ldapconn, $dn, '(objectClass=*)', array());
+        if (!$ldapresult) {
+            if (ldap_errno($ldapconn) == 32) {
+                // No such object.
+                return get_string($message, 'auth_ldap', $a);
+            }
+
+            $a = array('code' => ldap_errno($ldapconn), 'subject' => $a, 'message' => ldap_error($ldapconn));
+            return get_string('diag_genericerror', 'auth_ldap', $a);
+        }
+
+        return true;
+    }
+
     /**
      * Test if settings are correct, print info to output.
      */
@@ -2096,35 +2121,66 @@ class auth_plugin_ldap extends auth_plugin_base {
         global $OUTPUT;
 
         if (!function_exists('ldap_connect')) { // Is php-ldap really there?
-            echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'));
+            echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR);
             return;
         }
 
         // Check to see if this is actually configured.
-        if ((isset($this->config->host_url)) && ($this->config->host_url !== '')) {
+        if (empty($this->config->host_url)) {
+            // LDAP is not even configured.
+            echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR);
+            return;
+        }
+
+        if ($this->config->ldap_version != 3) {
+            echo $OUTPUT->notification(get_string('diag_toooldversion', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING);
+        }
+
+        try {
+            $ldapconn = $this->ldap_connect();
+        } catch (Exception $e) {
+            echo $OUTPUT->notification($e->getMessage(), \core\output\notification::NOTIFY_ERROR);
+            return;
+        }
 
-            try {
-                $ldapconn = $this->ldap_connect();
-                // Try to connect to the LDAP server.  See if the page size setting is supported on this server.
-                $pagedresultssupported = ldap_paged_results_supported($this->config->ldap_version, $ldapconn);
-            } catch (Exception $e) {
+        // Display paged file results.
+        if (!ldap_paged_results_supported($this->config->ldap_version, $ldapconn)) {
+            echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
+        }
 
-                // If we couldn't connect and get the supported options, we can only assume we don't support paged results.
-                $pagedresultssupported = false;
+        // Check contexts.
+        foreach (explode(';', $this->config->contexts) as $context) {
+            $context = trim($context);
+            if (empty($context)) {
+                echo $OUTPUT->notification(get_string('diag_emptycontext', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING);
+                continue;
             }
 
-            // Display paged file results.
-            if ((!$pagedresultssupported)) {
-                echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
-            } else if ($ldapconn) {
-                // We were able to connect successfuly.
-                echo $OUTPUT->notification(get_string('connectingldapsuccess', 'auth_ldap'), \core\output\notification::NOTIFY_SUCCESS);
+            $message = $this->test_dn($ldapconn, $context, 'diag_contextnotfound', $context);
+            if ($message !== true) {
+                echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
             }
+        }
 
-        } else {
-            // LDAP is not even configured.
-            echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
+        // Create system role mapping field for each assignable system role.
+        $roles = get_ldap_assignable_role_names();
+        foreach ($roles as $role) {
+            foreach (explode(';', $this->config->{$role['settingname']}) as $groupdn) {
+                if (empty($groupdn)) {
+                    continue;
+                }
+
+                $role['group'] = $groupdn;
+                $message = $this->test_dn($ldapconn, $groupdn, 'diag_rolegroupnotfound', $role);
+                if ($message !== true) {
+                    echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
+                }
+            }
         }
+
+        $this->ldap_close(true);
+        // We were able to connect successfuly.
+        echo $OUTPUT->notification(get_string('connectingldapsuccess', 'auth_ldap'), \core\output\notification::NOTIFY_SUCCESS);
     }
 
     /**
index 13f87ce..76a4317 100644 (file)
@@ -163,6 +163,12 @@ $string['userentriestoupdate'] = "User entries to be updated: {\$a}\n";
 $string['usernotfound'] = 'User not found in LDAP';
 $string['useracctctrlerror'] = 'Error getting userAccountControl for {$a}';
 
+$string['diag_genericerror'] = 'LDAP error {$a->code} reading {$a->subject}: {$a->message}.';
+$string['diag_toooldversion'] = 'Its is very unlikely a modern LDAP server uses LDAPv2 protocol. Wrong settings can corrupt values in user fields. Check with your LDAP administrator.';
+$string['diag_emptycontext'] = 'Empty context found.';
+$string['diag_contextnotfound'] = 'Context {$a} does not  exists or cannot be read by bind DN.';
+$string['diag_rolegroupnotfound'] = 'Group {$a->group} for role {$a->localname} does not exists or cannot be read by bind DN.';
+
 // Deprecated since Moodle 3.4.
 $string['auth_ldap_creators'] = 'List of groups or contexts whose members are allowed to create new courses. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\'';
 $string['auth_ldap_creators_key'] = 'Creators';
index f2f536d..ccb32cf 100644 (file)
@@ -161,18 +161,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextuser' => CONTEXT_USER,
-            'contextid' => $context->id
-        ];
-
-        $sql = "SELECT ctx.instanceid as userid
-                  FROM {mnet_log} ml
-                  JOIN {context} ctx
-                       ON ctx.instanceid = ml.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {mnet_log}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index d0dfa65..cd1750c 100644 (file)
@@ -99,18 +99,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextuser' => CONTEXT_USER,
-            'contextid' => $context->id
-        ];
-
-        $sql = "SELECT ctx.instanceid as userid
-                  FROM {auth_oauth2_linked_login} ao
-                  JOIN {context} ctx
-                       ON ctx.instanceid = ao.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {auth_oauth2_linked_login}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index 39ca032..c52ad12 100644 (file)
@@ -308,9 +308,9 @@ class auth_plugin_shibboleth extends auth_plugin_base {
 
     /**
      * Sets the standard SAML domain cookie that is also used to preselect
-     * the right entry on the local wayf
+     * the right entry on the local way
      *
-     * @param IdP identifiere
+     * @param string $selectedIDP IDP identifier
      */
     function set_saml_cookie($selectedIDP) {
         if (isset($_COOKIE['_saml_idp']))
@@ -325,41 +325,12 @@ class auth_plugin_shibboleth extends auth_plugin_base {
         setcookie ('_saml_idp', generate_cookie_value($IDPArray), time() + (100*24*3600));
     }
 
-     /**
-     * Prints the option elements for the select element of the drop down list
-     *
-     */
-    function print_idp_list(){
-        $config = get_config('auth_shibboleth');
-
-        $IdPs = get_idp_list($config->organization_selection);
-        if (isset($_COOKIE['_saml_idp'])){
-            $idp_cookie = generate_cookie_array($_COOKIE['_saml_idp']);
-            do {
-                $selectedIdP = array_pop($idp_cookie);
-            } while (!isset($IdPs[$selectedIdP]) && count($idp_cookie) > 0);
-
-        } else {
-            $selectedIdP = '-';
-        }
-
-        foreach($IdPs as $IdP => $data){
-            if ($IdP == $selectedIdP){
-                echo '<option value="'.$IdP.'" selected="selected">'.$data[0].'</option>';
-            } else {
-                echo '<option value="'.$IdP.'">'.$data[0].'</option>';
-            }
-        }
-    }
-
-
-     /**
+    /**
      * Generate array of IdPs from Moodle Shibboleth settings
      *
      * @param string Text containing tuble/triple of IdP entityId, name and (optionally) session initiator
      * @return array Identifier of IdPs and their name/session initiator
      */
-
     function get_idp_list($organization_selection) {
         $idp_list = array();
 
diff --git a/auth/shibboleth/index_form.html b/auth/shibboleth/index_form.html
deleted file mode 100644 (file)
index 9f1e23e..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-$config = get_config('auth_shibboleth');
-
-if ($show_instructions) {
-    $columns = 'twocolumns';
-} else {
-    $columns = 'onecolumn';
-}
-?>
-<div class="loginbox clearfix <?php echo $columns ?>">
-  <div class="loginpanel">
-    <!--<h2><?php print_string("returningtosite") ?></h2>-->
-
-    <h2><?php
-        if (isset($config->login_name) && !empty($config->login_name)){
-            echo $config->login_name;
-        } else {
-            print_string("auth_shibboleth_login_long", "auth_shibboleth");
-        }
-    ?></h2>
-      <div class="subcontent loginsub">
-        <div class="desc">
-        <?php
-          if (!empty($errormsg)) {
-              echo '<div class="loginerrors">';
-              echo $OUTPUT->error_text($errormsg);
-              echo '</div>';
-          }
-
-        ?>
-          <div class="guestsub">
-          <p><label for="idp"><?php print_string("auth_shibboleth_select_organization", "auth_shibboleth"); ?></label></p>
-            <form action="login.php" method="post" id="guestlogin">
-            <select id="idp" name="idp">
-                <option value="-" ><?php print_string("auth_shibboleth_select_member", "auth_shibboleth"); ?></option>
-                <?php
-                    print_idp_list();
-                ?>
-            </select><p><input type="submit" value="<?php print_string("select"); ?>" accesskey="s" /></p>
-            </form>
-            <p>
-            <?php
-                print_string("auth_shib_contact_administrator", "auth_shibboleth", get_admin()->email);
-            ?>
-            </p>
-          </div>
-         </div>
-      </div>
-
-<?php if ($CFG->guestloginbutton) {  ?>
-      <div class="subcontent guestsub">
-        <div class="desc">
-          <?php print_string("someallowguest") ?>
-        </div>
-        <form action="../../login/index.php" method="post" id="guestlogin">
-          <div class="guestform">
-            <input type="hidden" name="logintoken" value="<?php echo s(\core\session\manager::get_login_token()); ?>" />
-            <input type="hidden" name="username" value="guest" />
-            <input type="hidden" name="password" value="guest" />
-            <input type="submit" value="<?php print_string("loginguest") ?>" />
-          </div>
-        </form>
-      </div>
-<?php } ?>
-     </div>
-
-
-<?php if ($show_instructions) { ?>
-    <div class="signuppanel">
-      <h2><?php print_string("firsttime") ?></h2>
-      <div class="subcontent">
-<?php     if (is_enabled_auth('none')) { // instructions override the rest for security reasons
-              print_string("loginstepsnone");
-          } else if ($CFG->registerauth == 'email') {
-              if (!empty($config->auth_instructions)) {
-                  echo format_text($config->auth_instructions);
-              } else {
-                  print_string("loginsteps", "", "signup.php");
-              } ?>
-                 <div class="signupform">
-                   <form action="../../login/signup.php" method="get" id="signup">
-                   <div><input type="submit" value="<?php print_string("startsignup") ?>" /></div>
-                   </form>
-                 </div>
-<?php     } else if (!empty($CFG->registerauth)) {
-              echo format_text($config->auth_instructions); ?>
-              <div class="signupform">
-                <form action="../../login/signup.php" method="get" id="signup">
-                <div><input type="submit" value="<?php print_string("startsignup") ?>" /></div>
-                </form>
-              </div>
-<?php     } else {
-              echo format_text($config->auth_instructions);
-          } ?>
-      </div>
-    </div>
-<?php } ?>
-</div>
index 8ef9ec1..fd8b747 100644 (file)
@@ -41,7 +41,7 @@ $string['auth_shib_convert_data_warning'] = 'The file does not exist or is not r
 $string['auth_shib_changepasswordurl'] = 'Password-change URL';
 $string['auth_shib_idp_list'] = 'Identity providers';
 $string['auth_shib_idp_list_description'] = 'Provide a list of Identity Provider entityIDs to let the user choose from on the login page.<br />On each line there must be a comma-separated tuple for entityID of the IdP (see the Shibboleth metadata file) and Name of IdP as it shall be displayed in the drop-down list.<br />As an optional third parameter you can add the location of a Shibboleth session initiator that shall be used in case your Moodle installation is part of a multi federation setup.';
-$string['auth_shib_instructions'] = 'Use the <a href="{$a}">Shibboleth login</a> to get access via Shibboleth, if your institution supports it.<br />Otherwise, use the normal login form shown here.';
+$string['auth_shib_instructions'] = 'Use the <a href="{$a}">Shibboleth login</a> to get access via Shibboleth, if your institution supports it. Otherwise, use the normal login form shown here.';
 $string['auth_shib_instructions_help'] = 'Here you should provide custom instructions for your users to explain Shibboleth.  It will be shown on the login page in the instructions section. The instructions must include a link to "<b>{$a}</b>" that users click when they want to log in.';
 $string['auth_shib_instructions_key'] = 'Login instructions';
 $string['auth_shib_integrated_wayf'] = 'Moodle WAYF service';
index 6877fcb..d4fc639 100644 (file)
@@ -3,10 +3,9 @@
     require_once("../../config.php");
     require_once($CFG->dirroot."/auth/shibboleth/auth.php");
 
-    //initialize variables
-    $errormsg = '';
+    $idp = optional_param('idp', null, PARAM_RAW);
 
-/// Check for timed out sessions
+    // Check for timed out sessions.
     if (!empty($SESSION->has_timed_out)) {
         $session_has_timed_out = true;
         $SESSION->has_timed_out = false;
@@ -14,8 +13,8 @@
         $session_has_timed_out = false;
     }
 
-
-/// Define variables used in page
+    // Define variables used in page.
+    $isvalid = true;
     $site = get_site();
 
     $loginsite = get_string("loginsite");
 
     $config = get_config('auth_shibboleth');
     if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($config->auth_instructions)) {
-        $show_instructions = true;
+        $showinstructions = true;
     } else {
-        $show_instructions = false;
+        $showinstructions = false;
     }
 
-    $IdPs = get_idp_list($config->organization_selection);
-    if (isset($_POST['idp']) && isset($IdPs[$_POST['idp']])){
-        $selectedIdP = $_POST['idp'];
-        set_saml_cookie($selectedIdP);
+    $idplist = get_idp_list($config->organization_selection);
+    if (isset($idp)) {
+        if (isset($idplist[$idp])) {
+            set_saml_cookie($idp);
 
-        // Redirect to SessionInitiator with entityID as argument
-        if (isset($IdPs[$selectedIdP][1]) && !empty($IdPs[$selectedIdP][1])) {
-            // For Shibbolet 1.x Service Providers
-            header('Location: '.$IdPs[$selectedIdP][1].'?providerId='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            $targeturl = new moodle_url('/auth/shibboleth/index.php');
+            $idpinfo = $idplist[$idp];
 
-            // For Shibbolet 2.x Service Providers
-            // header('Location: '.$IdPs[$selectedIdP][1].'?entityID='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            // Redirect to SessionInitiator with entityID as argument.
+            if (isset($idpinfo[1]) && !empty($idpinfo[1])) {
+                $sso = $idpinfo[1];
+            } else {
+                $sso = '/Shibboleth.sso';
+            }
+            // For Shibboleth 1.x Service Providers.
+            header('Location: ' . $sso . '?providerId=' . urlencode($idp) . '&target=' . urlencode($targeturl->out()));
 
         } else {
-            // For Shibbolet 1.x Service Providers
-            header('Location: /Shibboleth.sso?providerId='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
-
-            // For Shibboleth 2.x Service Providers
-            // header('Location: /Shibboleth.sso/DS?entityID='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            $isvalid = false;
         }
-    } elseif (isset($_POST['idp']) && !isset($IdPs[$_POST['idp']]))  {
-        $errormsg = get_string('auth_shibboleth_errormsg', 'auth_shibboleth');
     }
 
     $loginsite = get_string("loginsite");
@@ -60,6 +57,7 @@
     $PAGE->navbar->add($loginsite);
     $PAGE->set_title("$site->fullname: $loginsite");
     $PAGE->set_heading($site->fullname);
+    $PAGE->set_pagelayout('login');
 
     echo $OUTPUT->header();
 
         echo $OUTPUT->confirm(get_string('alreadyloggedin', 'error', fullname($USER)), $logout, $continue);
         echo $OUTPUT->box_end();
     } else {
-        include("index_form.html");
-    }
+        // Print login page.
+        $selectedidp = '-';
+        if (isset($_COOKIE['_saml_idp'])) {
+            $idpcookie = generate_cookie_array($_COOKIE['_saml_idp']);
+            do {
+                $selectedidp = array_pop($idpcookie);
+            } while (!isset($idplist[$selectedidp]) && count($idpcookie) > 0);
+        }
 
-    echo $OUTPUT->footer();
+        $idps = [];
+        foreach ($idplist as $value => $data) {
+            $name = reset($data);
+            $selected = $value === $selectedidp;
+            $idps[] = (object)[
+                'name' => $name,
+                'value' => $value,
+                'selected' => $selected
+            ];
+        }
 
+        // Whether the user can sign up.
+        $cansignup = !empty($CFG->registerauth);
+        // Default instructions.
+        $instructions = format_text($config->auth_instructions);
+        if (is_enabled_auth('none')) {
+            $instructions = get_string('loginstepsnone');
+        } else if ($cansignup) {
+            if ($CFG->registerauth === 'email' && empty($instructions)) {
+                $instructions = get_string('loginsteps');
+            }
+        }
 
+        // Build the template context data.
+        $templatedata = (object)[
+            'adminemail' => get_admin()->email,
+            'cansignup' => $cansignup,
+            'guestlogin' => $CFG->guestloginbutton,
+            'guestloginurl' => new moodle_url('/login/index.php'),
+            'idps' => $idps,
+            'instructions' => $instructions,
+            'loginname' => $config->login_name ?? null,
+            'logintoken' => \core\session\manager::get_login_token(),
+            'loginurl' => new moodle_url('/auth/shibboleth/login.php'),
+            'showinstructions' => $showinstructions,
+            'signupurl' => new moodle_url('/login/signup.php'),
+            'isvalid' => $isvalid
+        ];
+
+        // Render the login form.
+        echo $OUTPUT->render_from_template('auth_shibboleth/login_form', $templatedata);
+    }
+
+    echo $OUTPUT->footer();
diff --git a/auth/shibboleth/templates/login_form.mustache b/auth/shibboleth/templates/login_form.mustache
new file mode 100644 (file)
index 0000000..230f615
--- /dev/null
@@ -0,0 +1,129 @@
+{{!
+    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/>.
+}}
+{{!
+    @template auth_shibboleth/login_form
+
+    Template for the Shibboleth authentication plugin's login form.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * adminemail String The Administrator's email address.
+    * cansignup Boolean Whether a new user can sign up for an account.
+    * guestlogin Boolean Whether to show the guest login section.
+    * guestloginurl String The URL for guest login.
+    * idps Array The list of identity providers for the Shibboleth authentication plugin in value-name pairs per IDP.
+    * instructions String Signup instructions.
+    * isvalid Boolean Whether form validation passes.
+    * loginname String The custom login name.
+    * logintoken String The login token.
+    * loginurl String The login URL.
+    * showinstructions Boolean Whether to show additional login instructions.
+    * signupurl String The signup URL.
+
+    Example context (json):
+    {
+        "loginurl": "#",
+        "guestloginurl": "#",
+        "guestlogin": true,
+        "idps": [
+            { "value": 1, "name": "IDP 1" },
+            { "value": 2, "name": "IDP 2", "selected": true },
+            { "value": 3, "name": "IDP 3" }
+        ],
+        "showinstructions": true,
+        "logintoken": "abcde",
+        "adminemail": "admin@example.com",
+        "loginname": "Shib auth",
+        "cansignup": true,
+        "signupurl": "#",
+        "instructions": "Sign up here",
+        "isvalid": false
+    }
+}}
+
+<div class="my-1 my-sm-5"></div>
+<div class="container">
+    <div class="card">
+        <h2 class="card-header">
+            {{#loginname}}{{.}}{{/loginname}}
+            {{^loginname}}{{#str}}auth_shibboleth_login_long, auth_shibboleth{{/str}}{{/loginname}}
+        </h2>
+        <div class="card-body">
+            <div class="row justify-content-center m-l-1 m-r-1 m-b-1">
+                <div class="col-md-5">
+                    <form action="{{loginurl}}" method="post" id="login">
+                        <div class="form-group">
+                            <label for="idp">{{#str}}auth_shibboleth_select_organization, auth_shibboleth{{/str}}</label>
+                            <select id="idp" name="idp" class="form-control input-block-level {{^isvalid}}is-invalid{{/isvalid}}">
+                                <option value="-">{{#str}}auth_shibboleth_select_member, auth_shibboleth{{/str}}</option>
+                                {{#idps}}
+                                    <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+                                {{/idps}}
+                            </select>
+                            <div class="invalid-feedback text-danger m-b-1" {{#isvalid}}hidden{{/isvalid}}>
+                                {{#str}}auth_shibboleth_errormsg, auth_shibboleth{{/str}}
+                            </div>
+                        </div>
+                        <button type="submit" class="btn btn-primary btn-block m-b-1" accesskey="s">
+                            {{#str}}select, moodle{{/str}}
+                        </button>
+                        <p class="form-text text-muted m-t-1 m-b-1">
+                            {{#str}}auth_shib_contact_administrator, auth_shibboleth, {{adminemail}}{{/str}}
+                        </p>
+                    </form>
+                </div>
+                {{#guestlogin}}
+                <div class="col-md-5">
+                    <p>
+                        {{#str}}someallowguest, moodle{{/str}}
+                    </p>
+                    <form action="{{guestloginurl}}" method="post" id="guestlogin">
+                        <div class="guestform">
+                            <input type="hidden" name="logintoken" value="{{logintoken}}">
+                            <input type="hidden" name="username" value="guest">
+                            <input type="hidden" name="password" value="guest">
+                            <button type="submit" class="btn btn-secondary btn-block">
+                                {{#str}}loginguest, moodle{{/str}}
+                            </button>
+                        </div>
+                    </form>
+                </div>
+                {{/guestlogin}}
+            </div>
+        </div>
+    </div>
+    {{#showinstructions}}
+    <div class="card m-t-1">
+        <div class="card-body m-l-1 m-r-1 m-b-1">
+            <h2 class="card-title">{{#str}}firsttime, moodle{{/str}}</h2>
+            <p>
+                {{{instructions}}}
+            </p>
+            {{#cansignup}}
+            <form action="{{signupurl}}" method="get" id="signup">
+                <button type="submit" class="btn btn-secondary">{{#str}}startsignup, moodle{{/str}}</button>
+            </form>
+            {{/cansignup}}
+        </div>
+    </div>
+    {{/showinstructions}}
+</div>
index 6366c8f..03e989d 100644 (file)
@@ -168,5 +168,10 @@ class backup_root_task extends backup_task {
         $competencies = new backup_competencies_setting();
         $competencies->set_ui(new backup_setting_ui_checkbox($competencies, get_string('rootsettingcompetencies', 'backup')));
         $this->add_setting($competencies);
+
+        // Define custom fields inclusion setting if custom fields are used.
+        $customfields = new backup_customfield_setting('customfield', base_setting::IS_BOOLEAN, true);
+        $customfields->set_ui(new backup_setting_ui_checkbox($customfields, get_string('rootsettingcustomfield', 'backup')));
+        $this->add_setting($customfields);
     }
 }
index d2e1e5f..9125673 100644 (file)
@@ -74,6 +74,15 @@ class backup_users_setting extends backup_generic_setting {}
 class backup_groups_setting extends backup_generic_setting {
 }
 
+/**
+ * root setting to control if backup will include custom field information
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2018 Daniel Neis Araujo
+ */
+class backup_customfield_setting extends backup_generic_setting {
+}
+
 /**
  * root setting to control if backup will include activities or no.
  * A lot of other settings (_included at activity levels)
index ef99fa6..19e314f 100644 (file)
@@ -391,6 +391,11 @@ class backup_course_structure_step extends backup_structure_step {
         $tag = new backup_nested_element('tag', array('id'), array(
             'name', 'rawname'));
 
+        $customfields = new backup_nested_element('customfields');
+        $customfield = new backup_nested_element('customfield', array('id'), array(
+          'shortname', 'type', 'value', 'valueformat'
+        ));
+
         // attach format plugin structure to $course element, only one allowed
         $this->add_plugin_structure('format', $course, false);
 
@@ -425,6 +430,9 @@ class backup_course_structure_step extends backup_structure_step {
         $course->add_child($tags);
         $tags->add_child($tag);
 
+        $course->add_child($customfields);
+        $customfields->add_child($customfield);
+
         // Set the sources
 
         $courserec = $DB->get_record('course', array('id' => $this->task->get_courseid()));
@@ -457,6 +465,10 @@ class backup_course_structure_step extends backup_structure_step {
                                      backup_helper::is_sqlparam('course'),
                                      backup::VAR_PARENTID));
 
+        $handler = core_course\customfield\course_handler::create();
+        $fieldsforbackup = $handler->get_instance_data_for_backup($this->task->get_courseid());
+        $customfield->set_source_array($fieldsforbackup);
+
         // Some annotations
 
         $course->annotate_ids('grouping', 'defaultgroupingid');
index eb03318..3069a0b 100644 (file)
@@ -286,5 +286,9 @@ class restore_root_task extends restore_task {
         $competencies = new restore_competencies_setting($hascompetencies);
         $competencies->set_ui(new backup_setting_ui_checkbox($competencies, get_string('rootsettingcompetencies', 'backup')));
         $this->add_setting($competencies);
+
+        $customfields = new restore_customfield_setting('customfields', base_setting::IS_BOOLEAN, $defaultvalue);
+        $customfields->set_ui(new backup_setting_ui_checkbox($customfields, get_string('rootsettingcustomfield', 'backup')));
+        $this->add_setting($customfields);
     }
 }
index 4d559e0..daa3fec 100644 (file)
@@ -52,6 +52,15 @@ class restore_users_setting extends restore_generic_setting {}
 class restore_groups_setting extends restore_generic_setting {
 }
 
+/**
+ * root setting to control if restore will include custom field information
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2018 Daniel Neis Araujo
+ */
+class restore_customfield_setting extends restore_generic_setting {
+}
+
 /**
  * root setting to control if restore will create role assignments
  * or no (any level), depends of @restore_users_setting
index 859198e..b3d5322 100644 (file)
@@ -1785,6 +1785,7 @@ class restore_course_structure_step extends restore_structure_step {
         $course = new restore_path_element('course', '/course');
         $category = new restore_path_element('category', '/course/category');
         $tag = new restore_path_element('tag', '/course/tags/tag');
+        $customfield = new restore_path_element('customfield', '/course/customfields/customfield');
         $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
 
         // Apply for 'format' plugins optional paths at course level
@@ -1808,7 +1809,7 @@ class restore_course_structure_step extends restore_structure_step {
         // Apply for admin tool plugins optional paths at course level.
         $this->add_plugin_structure('tool', $course);
 
-        return array($course, $category, $tag, $allowed_module);
+        return array($course, $category, $tag, $customfield, $allowed_module);
     }
 
     /**
@@ -1932,6 +1933,16 @@ class restore_course_structure_step extends restore_structure_step {
                 context_course::instance($this->get_courseid()), $data->rawname);
     }
 
+    /**
+     * Process custom fields
+     *
+     * @param array $data
+     */
+    public function process_customfield($data) {
+        $handler = core_course\customfield\course_handler::create();
+        $handler->restore_instance_data_from_backup($this->task, $data);
+    }
+
     public function process_allowed_module($data) {
         $data = (object)$data;
 
index 8e9ed2e..f547d22 100644 (file)
@@ -188,7 +188,7 @@ class award_criteria_activity extends award_criteria {
      * @return bool Whether criteria is complete
      */
     public function review($userid, $filtered = false) {
-        $completionstates = array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS);
+        $completionstates = array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL);
 
         if ($this->course->startdate > time()) {
             return false;
index 998e091..98d8466 100644 (file)
@@ -351,7 +351,9 @@ class core_badges_renderer extends plugin_renderer_base {
         if (!empty($badge->version)) {
             $dl[get_string('version', 'badges')] = $badge->version;
         }
-        $dl[get_string('language')] = $languages[$badge->language];
+        if (!empty($badge->language)) {
+            $dl[get_string('language')] = $languages[$badge->language];
+        }
         $dl[get_string('description', 'badges')] = $badge->description;
         if (!empty($badge->imageauthorname)) {
             $dl[get_string('imageauthorname', 'badges')] = $badge->imageauthorname;
@@ -417,15 +419,18 @@ class core_badges_renderer extends plugin_renderer_base {
         if (!empty($endorsement)) {
             $output .= self::print_badge_endorsement($badge);
         }
-        $relatedbadges = $badge->get_related_badges();
-        if (!empty($relatedbadges)) {
+
+        $relatedbadges = $badge->get_related_badges(true);
+        $items = array();
+        foreach ($relatedbadges as $related) {
+            $relatedurl = new moodle_url('/badges/overview.php', array('id' => $related->id));
+            $items[] = html_writer::link($relatedurl->out(), $related->name, array('target' => '_blank'));
+        }
+        if (!empty($items)) {
             $output .= $this->heading(get_string('relatedbages', 'badges'), 3);
-            $items = array();
-            foreach ($relatedbadges as $related) {
-                $items[] = $related->name;
-            }
             $output .= html_writer::alist($items, array(), 'ul');
         }
+
         $competencies = $badge->get_alignment();
         if (!empty($competencies)) {
             $output .= $this->heading(get_string('alignment', 'badges'), 3);
diff --git a/badges/tests/behat/criteria_activity.feature b/badges/tests/behat/criteria_activity.feature
new file mode 100644 (file)
index 0000000..b71fcea
--- /dev/null
@@ -0,0 +1,69 @@
+@mod @mod_quiz @core @core_badges @_file_upload @javascript
+Feature: Award badges based on activity completion
+  In order to ensure a student has learned the material before being marked complete
+  As a teacher
+  I need to set a quiz to award a badge when upon completion when the student receives a passing grade, or completed_fail if they use all attempts without passing
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | 1        | student1@example.com |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category | enablecompletion |
+      | Course 1 | C1        | 0        | 1                |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And the following config values are set as admin:
+      | grade_item_advanced | hiddenuntil |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype     | name           | questiontext              |
+      | Test questions   | truefalse | First question | Answer the first question |
+    And the following "activities" exist:
+      | activity   | name           | course | idnumber | attempts | gradepass | completion | completionattemptsexhausted | completionpass | completionusegrade |
+      | quiz       | Test quiz name | C1     | quiz1    | 2        | 5.00      | 2          | 1                           | 1              | 1                  |
+    And quiz "Test quiz name" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And user "student1" has attempted "Test quiz name" with responses:
+      | slot | response |
+      |   1  | False    |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Add a new badge" in current page administration
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge |
+      | Description | Course badge description |
+      | issuername | Tester of course badge |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the field "type" to "Activity completion"
+    And I set the field "Quiz - Test quiz name" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I should see "Recipients (0)"
+    And I log out
+
+  Scenario: Student earns a badge using activity completion, but does not get passing grade
+    When I log in as "student1"
+    And I am on "Course 1" course homepage
+    And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
+    And I follow "Test quiz name"
+    And I press "Re-attempt quiz"
+    And I set the field "False" to "1"
+    And I press "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+    And I log out
+    Then I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Manage badges" in current page administration
+    And I follow "Course Badge"
+    And I should see "Recipients (1)"
index 9ed8f32..dd32b8f 100644 (file)
@@ -105,18 +105,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextid' => $context->id,
-            'contextuser' => CONTEXT_USER,
-        ];
-
-        $sql = "SELECT bc.userid as userid
-                  FROM {block_community} bc
-                  JOIN {context} ctx
-                       ON ctx.instanceid = bc.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {block_community}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index e7a080f..aa32e78 100644 (file)
@@ -98,23 +98,26 @@ class provider implements
      * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
      */
     public static function get_users_in_context(userlist $userlist) {
+        // This block doesn't know who information is stored against unless it
+        // is at the user context.
         $context = $userlist->get_context();
 
-        if (!is_a($context, \context_block::class)) {
+        if (!$context instanceof \context_block) {
             return;
         }
 
+        $sql = "SELECT bpc.instanceid AS userid
+                  FROM {block_instances} bi
+                  JOIN {context} bpc ON bpc.id = bi.parentcontextid
+                 WHERE bi.blockname = 'html'
+                   AND bpc.contextlevel = :contextuser
+                   AND bi.id = :blockinstanceid";
+
         $params = [
-            'contextid'    => $context->id,
             'contextuser' => CONTEXT_USER,
+            'blockinstanceid' => $context->instanceid
         ];
 
-        $sql = "SELECT bpc.instanceid AS userid
-                  FROM {context} c
-                  JOIN {block_instances} bi ON bi.id = c.instanceid AND bi.blockname = 'html'
-                  JOIN {context} bpc ON bpc.id = bi.parentcontextid AND bpc.contextlevel = :contextuser
-                 WHERE c.id = :contextid";
-
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index 73da9c9..056ac20 100644 (file)
@@ -44,14 +44,4 @@ $capabilities = array(
             'user' => CAP_ALLOW
         )
     ),
-
-    // Whether or not a user can see the block.
-    'block/lp:view' => array(
-        'captype' => 'read',
-        'contextlevel' => CONTEXT_SYSTEM,
-        'archetypes' => array(
-            'user' => CAP_ALLOW
-        ),
-    ),
-
 );
index 520f710..6207e9d 100644 (file)
@@ -27,7 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 $string['competenciestoreview'] = 'Competencies to review';
 $string['lp:addinstance'] = 'Add a new learning plans block';
 $string['lp:myaddinstance'] = 'Add a new learning plans block to Dashboard';
-$string['lp:view'] = 'View learning plans block';
 $string['myplans'] = 'My plans';
 $string['noactiveplans'] = 'No active plans at the moment.';
 $string['planstoreview'] = 'Plans to review';
diff --git a/blocks/lp/upgrade.txt b/blocks/lp/upgrade.txt
new file mode 100644 (file)
index 0000000..4d24c96
--- /dev/null
@@ -0,0 +1,5 @@
+This file describes API changes in the lp block code.
+
+=== 3.7 ===
+
+* The 'block/lp:view' capability has been removed. It has never been used in code.
\ No newline at end of file
index 26e7063..8b829a0 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;
+$plugin->version   = 2018121900;
 $plugin->requires  = 2018112800;
 $plugin->component = 'block_lp';
 $plugin->dependencies = array(
index dbce038..99b59c4 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index f9d0e1d..85c81e1 100644 (file)
@@ -64,7 +64,7 @@ function(
         COURSES_CARDS: 'block_myoverview/view-cards',
         COURSES_LIST: 'block_myoverview/view-list',
         COURSES_SUMMARY: 'block_myoverview/view-summary',
-        NOCOURSES: 'block_myoverview/no-courses'
+        NOCOURSES: 'core_course/no-courses'
     };
 
     var NUMCOURSES_PERPAGE = [12, 24, 48];
index 75c38d3..22c49e3 100644 (file)
@@ -54,7 +54,6 @@ $string['lastaccessed'] = 'Last accessed';
 $string['list'] = 'List';
 $string['myoverview:addinstance'] = 'Add a new course overview block';
 $string['myoverview:myaddinstance'] = 'Add a new course overview block to Dashboard';
-$string['nocourses'] = 'No courses';
 $string['past'] = 'Past';
 $string['pluginname'] = 'Course overview';
 $string['privacy:metadata:overviewsortpreference'] = 'The Course overview block sort preference.';
@@ -90,3 +89,6 @@ $string['timeline'] = 'Timeline';
 $string['viewcoursename'] = 'View course {$a}';
 $string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.';
 $string['viewcourse'] = 'View course';
+
+// Deprecated since Moodle 3.7.
+$string['nocourses'] = 'No courses';
\ No newline at end of file
index 7927bba..4e22e76 100644 (file)
@@ -12,4 +12,5 @@ sortbycourses,block_myoverview
 sortbydates,block_myoverview
 timeline,block_myoverview
 viewcoursename,block_myoverview
-privacy:metadata:overviewlasttab,block_myoverview
\ No newline at end of file
+privacy:metadata:overviewlasttab,block_myoverview
+nocourses,block_myoverview
\ No newline at end of file
index d61c984..5bbe7ef 100644 (file)
 <div data-region="loading-placeholder-content" aria-hidden="true">
     {{#cards}}
     <div class="card-deck dashboard-card-deck one-row" style="height: 13rem">
-        <div class="card dashboard-card border-0">
-            <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-            </div>
-            <div class="card-body course-info-container">
-                <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-            </div>
-        </div>
-        <div class="card dashboard-card border-0">
-            <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-            </div>
-            <div class="card-body course-info-container">
-                <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-            </div>
-        </div>
-        <div class="card dashboard-card border-0">
-            <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-            </div>
-            <div class="card-body course-info-container">
-                <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-            </div>
-        </div>
-        <div class="card dashboard-card border-0">
-            <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-            </div>
-            <div class="card-body course-info-container">
-                <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-            </div>
-        </div>
+        {{> core_course/placeholder-course }}
+        {{> core_course/placeholder-course }}
+        {{> core_course/placeholder-course }}
+        {{> core_course/placeholder-course }}
     </div>
     {{/cards}}
     {{#list}}
index 42a4026..203eb03 100644 (file)
@@ -24,7 +24,7 @@
 M.block_private_files = {};
 
 M.block_private_files.init_tree = function(Y, expand_all, htmlid) {
-    Y.use('yui2-treeview', function(Y) {
+    Y.use('yui2-treeview', 'node-event-simulate', function(Y) {
         var tree = new Y.YUI2.widget.TreeView(htmlid);
 
         tree.subscribe("clickEvent", function(node, event) {
@@ -32,6 +32,12 @@ M.block_private_files.init_tree = function(Y, expand_all, htmlid) {
             return false;
         });
 
+        tree.subscribe("enterKeyPressed", function(node) {
+            // We want keyboard activation to trigger a click on the first link.
+            Y.one(node.getContentEl()).one('a').simulate('click');
+            return false;
+        });
+
         if (expand_all) {
             tree.expandAll();
         }
index 73692b5..594bbde 100644 (file)
Binary files a/blocks/recentlyaccessedcourses/amd/build/main.min.js and b/blocks/recentlyaccessedcourses/amd/build/main.min.js differ
index 70e679e..2fdd54c 100644 (file)
@@ -69,13 +69,13 @@ define(
          */
         var renderCourses = function(root, courses) {
             if (courses.length > 0) {
-                return Templates.render('block_recentlyaccessedcourses/view-cards', {
+                return Templates.render('core_course/view-cards', {
                     courses: courses
                 });
             } else {
-                var nocoursesimgurl = root.attr('data-nocoursesimgurl');
+                var nocoursesimgurl = root.attr('data-nocoursesimg');
                 return Templates.render('block_recentlyaccessedcourses/no-courses', {
-                    nocoursesimgurl: nocoursesimgurl
+                    nocoursesimg: nocoursesimgurl
                 });
             }
         };
index 09bbe03..06eb282 100644 (file)
@@ -49,7 +49,7 @@ class main implements renderable, templatable {
 
         return [
             'userid' => $USER->id,
-            'nocoursesimgurl' => $nocoursesurl
+            'nocoursesimg' => $nocoursesurl
         ];
     }
 }
index 9730dcd..cdf71be 100644 (file)
@@ -20,8 +20,8 @@
  * @copyright  2018 Victor Deniz <victor@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-$string['nocourses'] = 'No recent courses';
 $string['pluginname'] = 'Recently accessed courses';
 $string['privacy:metadata'] = 'The Recently accessed courses block does not store any personal data.';
 $string['recentlyaccessedcourses:addinstance'] = 'Add a new Recently accessed courses block';
 $string['recentlyaccessedcourses:myaddinstance'] = 'Add a new recently accessed courses block to Dashboard';
+$string['nocourses'] = 'No recent courses';
\ No newline at end of file
index 22a8648..84e64d5 100644 (file)
@@ -22,7 +22,7 @@
     Example context (json):
     {
         "userid": 2,
-        "nocoursesimgurl": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
+        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
     }
 }}
 
index 27ab207..cf1b8b0 100644 (file)
     You should have received a copy of the GNU General Public License
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
-{{!
-    @template block_recentlyaccessedcourses/no-courses
-
-    This template renders the no courses message.
-
-    Example context (json):
-    {
-        "nocoursesimgurl": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
-    }
-}}
-<div class="text-xs-center text-center m-t-3" data-region="empty-message">
-    <img class="empty-placeholder-image-lg m-t-1"
-         src="{{nocoursesimgurl}}"
-         alt="{{#str}} nocourses, block_recentlyaccessedcourses {{/str}}"
-         role="presentation">
-    <p class="text-muted mt-3">{{#str}} nocourses, block_recentlyaccessedcourses {{/str}}</p>
-</div>
\ No newline at end of file
+{{< core_course/no-courses}}
+    {{$nocoursestring}}
+        {{#str}} nocourses, block_recentlyaccessedcourses {{/str}}
+    {{/nocoursestring}}
+{{/ core_course/no-courses}}
index b277154..ec44dae 100644 (file)
 
     Example context (json):
     {
-        "nocoursesimgurl": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
+        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
     }
 }}
 <div id="recentlyaccessedcourses-view-{{uniqid}}"
      data-region="recentlyaccessedcourses-view"
-     data-nocoursesimgurl="{{nocoursesimgurl}}">
+     data-nocoursesimg="{{nocoursesimg}}">
     <div data-region="recentlyaccessedcourses-view-content">
         <div data-region="recentlyaccessedcourses-loading-placeholder">
             <div class="card-deck dashboard-card-deck one-row" style="height: 11.1rem">
-                {{> block_recentlyaccessedcourses/placeholder-course }}
-                {{> block_recentlyaccessedcourses/placeholder-course }}
-                {{> block_recentlyaccessedcourses/placeholder-course }}
-                {{> block_recentlyaccessedcourses/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
             </div>
         </div>
     </div>
index 214148e..e2b71e6 100644 (file)
@@ -98,18 +98,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextid' => $context->id,
-            'contextuser' => CONTEXT_USER,
-        ];
-
-        $sql = "SELECT brc.userid as userid
-                  FROM {block_rss_client} brc
-                  JOIN {context} ctx
-                       ON ctx.instanceid = brc.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {block_rss_client}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index 8ebbc68..9b9beb9 100644 (file)
Binary files a/blocks/starredcourses/amd/build/main.min.js and b/blocks/starredcourses/amd/build/main.min.js differ
index 81d4130..0c6d9c0 100644 (file)
@@ -53,7 +53,7 @@ function(
      */
     var renderCourses = function(root, courses) {
         if (courses.length > 0) {
-            return Templates.render('block_starredcourses/view-cards', {
+            return Templates.render('core_course/view-cards', {
                 courses: courses
             });
         } else {
index 72040e2..37d51a6 100644 (file)
@@ -22,9 +22,8 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['nocourses'] = 'No starred courses';
 $string['pluginname'] = 'Starred courses';
 $string['privacy:metadata'] = 'The starred courses block does not store any personal data.';
 $string['starredcourses:addinstance'] = 'Add a new starred courses block';
 $string['starredcourses:myaddinstance'] = 'Add a new starred courses block to Dashboard';
-
+$string['nocourses'] = 'No starred courses';
\ No newline at end of file
index a00de4e..7333fc7 100644 (file)
@@ -1,28 +1,21 @@
 {{!
     This file is part of Moodle - http://moodle.org/
-     Moodle is free software: you can redistribute it and/or modify
+
+    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,
+
+    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
+
+    You should have received a copy of the GNU General Public License
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
-{{!
-    @template block_starredcourses/no-courses
-     This template renders the no courses message.
-     Example context (json):
-    {
-        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_recentcourses/1535727318/courses"
-    }
-}}
-<div class="text-xs-center text-center m-t-3" data-region="empty-message">
-    <img class="empty-placeholder-image-lg m-t-1"
-         src="{{nocoursesimg}}"
-         alt="{{#str}} nocourses, block_starredcourses {{/str}}"
-         role="presentation">
-    <p class="text-muted mt-3">{{#str}} nocourses, block_starredcourses {{/str}}</p>
-</div>
+{{< core_course/no-courses}}
+    {{$nocoursestring}}
+        {{#str}} nocourses, block_starredcourses {{/str}}
+    {{/nocoursestring}}
+{{/ core_course/no-courses}}
diff --git a/blocks/starredcourses/templates/placeholder-course.mustache b/blocks/starredcourses/templates/placeholder-course.mustache
deleted file mode 100644 (file)
index 3ce061e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-{{!
-    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/>.
-}}
-{{!
-    @template block_starredcourses/placeholder-course
-     This template renders an course card item loading placeholder for the starred courses block.
-     Example context (json):
-    {}
-}}
-<div class="card dashboard-card border-0">
-    <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-    </div>
-    <div class="card-body recent-course-info-container">
-        <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-    </div>
-</div>
diff --git a/blocks/starredcourses/templates/view-cards.mustache b/blocks/starredcourses/templates/view-cards.mustache
deleted file mode 100644 (file)
index b84d24e..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-{{!
-    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 Licensebllsdsadfasfd
-    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-}}
-{{!
-    @template block_starredcourses/view-cards
-     This template renders the carousel for the starredcourses block.
-     Example context (json):
-    {
-        "courses": [
-            {
-                "name": "Assignment due 1",
-                "viewurl": "https://moodlesite/course/view.php?id=2",
-                "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
-                "fullname": "course 3"
-            }
-        ]
-    }
-}}
-
-{{< core_course/coursecards }}
-    {{$classes}}one-row{{/classes}}
-    {{$coursename}} {{fullname}} {{/coursename}}
-{{/ core_course/coursecards }}
index 7046b96..fd2f548 100644 (file)
@@ -30,9 +30,9 @@
     <div data-region="starred-courses-view-content">
         <div data-region="starred-courses-loading-placeholder">
             <div class="card-deck dashboard-card-deck one-row" style="height: 11.1rem">
-                {{> block_starredcourses/placeholder-course }}
-                {{> block_starredcourses/placeholder-course }}
-                {{> block_starredcourses/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
             </div>
         </div>
     </div>
index 77d7f91..931dec3 100644 (file)
@@ -18,9 +18,11 @@ Feature: Upload users to a cohort
     And I am on "Course 1" course homepage
     And I add "Cohort sync" enrolment method with:
       | Cohort | Cohort 1 |
+    And I should see "Cohort sync (Cohort 1 - Student)"
     And I am on "Course 2" course homepage
     And I add "Cohort sync" enrolment method with:
       | Cohort | Cohort 2 |
+    And I should see "Cohort sync (Cohort 2 - Student)"
     When I navigate to "Users > Accounts > Upload users" in site administration
     And I upload "lib/tests/fixtures/upload_users_cohorts.csv" file to "File" filemanager
     And I press "Upload users"
index ec0633e..dc30c34 100644 (file)
@@ -170,6 +170,7 @@ class comment_manager {
 
         $link = new moodle_url('/comment/index.php', array('action' => 'delete', 'sesskey' => sesskey()));
         foreach ($comments as $c) {
+            $userdata = html_writer::link(new moodle_url('/user/profile.php', ['id' => $c->userid]), $c->fullname);
             $this->setup_plugin($c);
             if (!empty($this->plugintype)) {
                 $context_url = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'url', array($c));
@@ -180,7 +181,7 @@ class comment_manager {
                 $action .= html_writer::empty_tag('br');
                 $action .= html_writer::link($context_url, get_string('commentincontext'), array('target'=>'_blank'));
             }
-            $table->data[] = array($checkbox, $c->fullname, $c->content, $action);
+            $table->data[] = array($checkbox, $userdata, $c->content, $action);
         }
         echo html_writer::table($table);
         echo $OUTPUT->paging_bar($count, $page, $this->perpage, $CFG->wwwroot.'/comment/index.php');
index d92fdaf..1cb3fa3 100644 (file)
@@ -75,6 +75,7 @@ class core_completion_external extends external_api {
 
         $context = context_module::instance($cmid);
         self::validate_context($context);
+        require_capability('moodle/course:togglecompletion', $context);
 
         list($course, $cm) = get_course_and_cm_from_cmid($cmid);
 
@@ -268,7 +269,8 @@ class core_completion_external extends external_api {
                        'state'         => $activitycompletiondata->completionstate,
                        'timecompleted' => $activitycompletiondata->timemodified,
                        'tracking'      => $activity->completion,
-                       'overrideby'    => $activitycompletiondata->overrideby
+                       'overrideby'    => $activitycompletiondata->overrideby,
+                       'valueused'     => core_availability\info::completion_value_used($course, $activity->id)
             );
         }
 
@@ -302,6 +304,8 @@ class core_completion_external extends external_api {
                                                                     0 means none, 1 manual, 2 automatic'),
                             'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the status, or null',
                                 VALUE_OPTIONAL),
+                            'valueused' => new external_value(PARAM_BOOL, 'Whether the completion status affects the availability
+                                    of another activity.', VALUE_OPTIONAL),
                         ), 'Activity'
                     ), 'List of activities status'
                 ),
index a1ec90b..0861a0e 100644 (file)
@@ -161,47 +161,55 @@ class provider implements
         $completioninfo = new \completion_info($course);
         $completion = $completioninfo->is_enabled();
 
-        if ($completion == COMPLETION_ENABLED) {
+        if ($completion != COMPLETION_ENABLED) {
+            return [];
+        }
+
+        $coursecomplete = $completioninfo->is_course_complete($user->id);
 
-            $coursecomplete = $completioninfo->is_course_complete($user->id);
+        if ($coursecomplete) {
+            $status = get_string('complete');
+        } else {
             $criteriacomplete = $completioninfo->count_course_user_data($user->id);
             $ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]);
 
-            $status = ($coursecomplete) ? get_string('complete') : '';
-            $status = (!$criteriacomplete && !$ccompletion->timestarted) ? get_string('notyetstarted', 'completion') :
-                    get_string('inprogress', 'completion');
-
-            $completions = $completioninfo->get_completions($user->id);
-            $overall = get_string('nocriteriaset', 'completion');
-            if (!empty($completions)) {
-                if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
-                    $overall = get_string('criteriarequiredall', 'completion');
-                } else {
-                    $overall = get_string('criteriarequiredany', 'completion');
-                }
+            if (!$criteriacomplete && !$ccompletion->timestarted) {
+                $status = get_string('notyetstarted', 'completion');
+            } else {
+                $status = get_string('inprogress', 'completion');
             }
+        }
 
-            $coursecompletiondata = [
-                'status' => $status,
-                'required' => $overall,
-            ];
-
-            $coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) {
-                $criteria = $completion->get_criteria();
-                $aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype);
-                $required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') :
-                        get_string('any', 'completion');
-                $data = [
-                    'required' => $required,
-                    'completed' => transform::yesno($completion->is_complete()),
-                    'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : ''
-                ];
-                $details = $criteria->get_details($completion);
-                $data = array_merge($data, $details);
-                return $data;
-            }, $completions);
-            return $coursecompletiondata;
+        $completions = $completioninfo->get_completions($user->id);
+        $overall = get_string('nocriteriaset', 'completion');
+        if (!empty($completions)) {
+            if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
+                $overall = get_string('criteriarequiredall', 'completion');
+            } else {
+                $overall = get_string('criteriarequiredany', 'completion');
+            }
         }
+
+        $coursecompletiondata = [
+            'status' => $status,
+            'required' => $overall,
+        ];
+
+        $coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) {
+            $criteria = $completion->get_criteria();
+            $aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype);
+            $required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') :
+                    get_string('any', 'completion');
+            $data = [
+                'required' => $required,
+                'completed' => transform::yesno($completion->is_complete()),
+                'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : ''
+            ];
+            $details = $criteria->get_details($completion);
+            $data = array_merge($data, $details);
+            return $data;
+        }, $completions);
+        return $coursecompletiondata;
     }
 
     /**
index ae06d97..dc0d76e 100644 (file)
@@ -103,12 +103,14 @@ class core_completion_externallib_testcase extends externallib_advanced_testcase
         $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1,
                                                                     'groupmode' => SEPARATEGROUPS,
                                                                     'groupmodeforce' => 1));
+        availability_completion\condition::wipe_static_cache();
 
         $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
                                                              array('completion' => 1));
         $forum = $this->getDataGenerator()->create_module('forum',  array('course' => $course->id),
                                                              array('completion' => 1));
-        $assign = $this->getDataGenerator()->create_module('assign',  array('course' => $course->id));
+        $availability = '{"op":"&","c":[{"type":"completion","cm":' . $forum->cmid .',"e":1}],"showc":[true]}';
+        $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['availability' => $availability]);
         $page = $this->getDataGenerator()->create_module('page',  array('course' => $course->id),
                                                             array('completion' => 1, 'visible' => 0));
 
@@ -146,10 +148,12 @@ class core_completion_externallib_testcase extends externallib_advanced_testcase
                 $activitiesfound++;
                 $this->assertEquals(COMPLETION_COMPLETE, $status['state']);
                 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']);
+                $this->assertTrue($status['valueused']);
             } else if ($status['cmid'] == $data->cmid and $status['modname'] == 'data' and $status['instance'] == $data->id) {
                 $activitiesfound++;
                 $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']);
                 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']);
+                $this->assertFalse($status['valueused']);
             }
         }
         $this->assertEquals(2, $activitiesfound);
index ffff66a..7bab239 100644 (file)
@@ -193,13 +193,31 @@ class core_completion_privacy_test extends \core_privacy\tests\provider_testcase
         $hasno = array_search('No', $coursecompletion1['criteria'], true);
         $this->assertFalse($hasno);
         $coursecompletion2 = \core_completion\privacy\provider::get_course_completion_info($user2, $this->course);
-        $hasyes = array_search('Yes', $coursecompletion1['criteria'], true);
+        $hasyes = array_search('Yes', $coursecompletion2['criteria'], true);
         $this->assertFalse($hasyes);
         $coursecompletion3 = \core_completion\privacy\provider::get_course_completion_info($user3, $this->course);
-        $hasno = array_search('No', $coursecompletion1['criteria'], true);
+        $hasno = array_search('No', $coursecompletion3['criteria'], true);
         $this->assertFalse($hasno);
         $coursecompletion4 = \core_completion\privacy\provider::get_course_completion_info($user4, $this->course);
-        $hasyes = array_search('Yes', $coursecompletion1['criteria'], true);
+        $hasyes = array_search('Yes', $coursecompletion4['criteria'], true);
         $this->assertFalse($hasyes);
     }
+
+    /**
+     * Test getting course completion information with completion disabled.
+     */
+    public function test_get_course_completion_info_completion_disabled() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 0]);
+
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+        $coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $course);
+
+        $this->assertTrue(is_array($coursecompletion));
+        $this->assertEmpty($coursecompletion);
+    }
 }
index 8ade6e1..db2324d 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /completion/* - completion,
 information provided here is intended especially for developers.
 
+=== 3.7 ===
+ * External function core_completion_external::get_activities_completion_status new returns the following additional field:
+   - valueused (indicates whether the completion state affects the availability of other content)
+
 === 2.9 ===
 
 * A completed and failed activity counts as a completed activity for
index c5659ea..006768a 100644 (file)
@@ -6,8 +6,8 @@
     "homepage": "https://moodle.org",
     "require-dev": {
         "phpunit/phpunit": "6.5.*",
-        "phpunit/dbUnit": "3.0.*",
+        "phpunit/dbunit": "3.0.*",
         "moodlehq/behat-extension": "3.37.0",
-        "mikey179/vfsStream": "^1.6"
+        "mikey179/vfsstream": "^1.6"
     }
 }
index 29f9b79..9cd8bba 100644 (file)
@@ -608,6 +608,12 @@ $CFG->admin = 'admin';
 //
 //      $CFG->disablelogintoken = true;
 //
+// Moodle 3.7+ checks that cron is running frequently. If the time between cron runs
+// is greater than this value (in seconds), you get a warning on the admin page. (This
+// setting only controls whether or not the warning appears, it has no other effect.)
+//
+//      $CFG->expectedcronfrequency = 200;
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index 2880675..c365857 100644 (file)
@@ -903,6 +903,18 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         $cache->set_many($values);
     }
 
+    /**
+     * Preloads the custom fields values in bulk
+     *
+     * @param array $records
+     */
+    public static function preload_custom_fields(array &$records) {
+        $customfields = \core_course\customfield\course_handler::create()->get_instances_data(array_keys($records));
+        foreach ($customfields as $courseid => $data) {
+            $records[$courseid]->customfields = $data;
+        }
+    }
+
     /**
      * Verify user enrollments for multiple course-user combinations
      *
@@ -1009,6 +1021,10 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         if (!empty($options['coursecontacts'])) {
             self::preload_course_contacts($list);
         }
+        // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
+        if (!empty($options['customfields'])) {
+            self::preload_custom_fields($list);
+        }
         return $list;
     }
 
@@ -1404,6 +1420,10 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
                 if (!empty($options['coursecontacts'])) {
                     self::preload_course_contacts($records);
                 }
+                // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
+                if (!empty($options['customfields'])) {
+                    self::preload_custom_fields($records);
+                }
                 // If option 'idonly' is specified no further action is needed, just return list of ids.
                 if (!empty($options['idonly'])) {
                     return array_keys($records);
@@ -1493,6 +1513,10 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         if (!empty($preloadcoursecontacts)) {
             self::preload_course_contacts($records);
         }
+        // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
+        if (!empty($options['customfields'])) {
+            self::preload_custom_fields($records);
+        }
         // If option 'idonly' is specified no further action is needed, just return list of ids.
         if (!empty($options['idonly'])) {
             return array_keys($records);
@@ -1606,6 +1630,10 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
                 if (!empty($options['idonly'])) {
                     return array_keys($records);
                 }
+                // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
+                if (!empty($options['customfields'])) {
+                    self::preload_custom_fields($records);
+                }
                 // Prepare the list of core_course_list_element objects.
                 foreach ($ids as $id) {
                     $courses[$id] = new core_course_list_element($records[$id]);
@@ -1645,6 +1673,10 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
             if (!empty($options['coursecontacts'])) {
                 self::preload_course_contacts($list);
             }
+            // Preload custom fields if necessary - saves DB queries later to do it for each course separately.
+            if (!empty($options['customfields'])) {
+                self::preload_custom_fields($list);
+            }
             // If option 'idonly' is specified no further action is needed, just return list of ids.
             if (!empty($options['idonly'])) {
                 return array_keys($list);
diff --git a/course/classes/customfield/course_handler.php b/course/classes/customfield/course_handler.php
new file mode 100644 (file)
index 0000000..d7badc8
--- /dev/null
@@ -0,0 +1,241 @@
+<?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/>.
+
+/**
+ * Course handler for custom fields
+ *
+ * @package   core_course
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\customfield;
+
+defined('MOODLE_INTERNAL') || die;
+
+use core_customfield\api;
+use core_customfield\field_controller;
+
+/**
+ * Course handler for custom fields
+ *
+ * @package core_course
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_handler extends \core_customfield\handler {
+
+    /**
+     * @var course_handler
+     */
+    static protected $singleton;
+
+    /**
+     * @var \context
+     */
+    protected $parentcontext;
+
+    /** @var int Field is displayed in the course listing, visible to everybody */
+    const VISIBLETOALL = 2;
+    /** @var int Field is displayed in the course listing but only for teachers */
+    const VISIBLETOTEACHERS = 1;
+    /** @var int Field is not displayed in the course listing */
+    const NOTVISIBLE = 0;
+
+    /**
+     * Returns a singleton
+     *
+     * @param int $itemid
+     * @return \core_course\customfield\course_handler
+     */
+    public static function create(int $itemid = 0) : \core_customfield\handler {
+        if (static::$singleton === null) {
+            self::$singleton = new static(0);
+        }
+        return self::$singleton;
+    }
+
+    /**
+     * The current user can configure custom fields on this component.
+     *
+     * @return bool true if the current can configure custom fields, false otherwise
+     */
+    public function can_configure() : bool {
+        return has_capability('moodle/course:configurecustomfields', $this->get_configuration_context());
+    }
+
+    /**
+     * The current user can edit custom fields on the given course.
+     *
+     * @param field_controller $field
+     * @param int $instanceid id of the course to test edit permission
+     * @return bool true if the current can edit custom fields, false otherwise
+     */
+    public function can_edit(field_controller $field, int $instanceid = 0) : bool {
+        if ($instanceid) {
+            $context = $this->get_instance_context($instanceid);
+            return (!$field->get_configdata_property('locked') ||
+                    has_capability('moodle/course:changelockedcustomfields', $context));
+        } else {
+            $context = $this->get_parent_context();
+            return (!$field->get_configdata_property('locked') ||
+                guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context));
+        }
+    }
+
+    /**
+     * The current user can view custom fields on the given course.
+     *
+     * @param field_controller $field
+     * @param int $instanceid id of the course to test edit permission
+     * @return bool true if the current can edit custom fields, false otherwise
+     */
+    public function can_view(field_controller $field, int $instanceid) : bool {
+        $visibility = $field->get_configdata_property('visibility');
+        if ($visibility == self::NOTVISIBLE) {
+            return false;
+        } else if ($visibility == self::VISIBLETOTEACHERS) {
+            return has_capability('moodle/course:update', $this->get_instance_context($instanceid));
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Sets parent context for the course
+     *
+     * This may be needed when course is being created, there is no course context but we need to check capabilities
+     *
+     * @param \context $context
+     */
+    public function set_parent_context(\context $context) {
+        $this->parentcontext = $context;
+    }
+
+    /**
+     * Returns the parent context for the course
+     *
+     * @return \context
+     */
+    protected function get_parent_context() : \context {
+        global $PAGE;
+        if ($this->parentcontext) {
+            return $this->parentcontext;
+        } else if ($PAGE->context && $PAGE->context instanceof \context_coursecat) {
+            return $PAGE->context;
+        }
+        return \context_system::instance();
+    }
+
+    /**
+     * Context that should be used for new categories created by this handler
+     *
+     * @return \context the context for configuration
+     */
+    public function get_configuration_context() : \context {
+        return \context_system::instance();
+    }
+
+    /**
+     * URL for configuration of the fields on this handler.
+     *
+     * @return \moodle_url The URL to configure custom fields for this component
+     */
+    public function get_configuration_url() : \moodle_url {
+        return new \moodle_url('/course/customfield.php');
+    }
+
+    /**
+     * Returns the context for the data associated with the given instanceid.
+     *
+     * @param int $instanceid id of the record to get the context for
+     * @return \context the context for the given record
+     */
+    public function get_instance_context(int $instanceid = 0) : \context {
+        if ($instanceid > 0) {
+            return \context_course::instance($instanceid);
+        } else {
+            return \context_system::instance();
+        }
+    }
+
+    /**
+     * Allows to add custom controls to the field configuration form that will be saved in configdata
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public function config_form_definition(\MoodleQuickForm $mform) {
+        $mform->addElement('header', 'course_handler_header', get_string('customfieldsettings', 'core_course'));
+        $mform->setExpanded('course_handler_header', true);
+
+        // If field is locked.
+        $mform->addElement('selectyesno', 'configdata[locked]', get_string('customfield_islocked', 'core_course'));
+        $mform->addHelpButton('configdata[locked]', 'customfield_islocked', 'core_course');
+
+        // Field data visibility.
+        $visibilityoptions = [self::VISIBLETOALL => get_string('customfield_visibletoall', 'core_course'),
+            self::VISIBLETOTEACHERS => get_string('customfield_visibletoteachers', 'core_course'),
+            self::NOTVISIBLE => get_string('customfield_notvisible', 'core_course')];
+        $mform->addElement('select', 'configdata[visibility]', get_string('customfield_visibility', 'core_course'),
+            $visibilityoptions);
+        $mform->addHelpButton('configdata[visibility]', 'customfield_visibility', 'core_course');
+    }
+
+    /**
+     * Creates or updates custom field data.
+     *
+     * @param \restore_task $task
+     * @param array $data
+     */
+    public function restore_instance_data_from_backup(\restore_task $task, array $data) {
+        $courseid = $task->get_courseid();
+        $context = $this->get_instance_context($courseid);
+        $editablefields = $this->get_editable_fields($courseid);
+        $records = api::get_instance_fields_data($editablefields, $courseid);
+        $target = $task->get_target();
+        $override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING);
+
+        foreach ($records as $d) {
+            $field = $d->get_field();
+            if ($field->get('shortname') === $data['shortname'] && $field->get('type') === $data['type']) {
+                if (!$d->get('id') || $override) {
+                    $d->set($d->datafield(), $data['value']);
+                    $d->set('value', $data['value']);
+                    $d->set('valueformat', $data['valueformat']);
+                    $d->set('contextid', $context->id);
+                    $d->save();
+                }
+                return;
+            }
+        }
+    }
+
+    /**
+     * Set up page customfield/edit.php
+     *
+     * @param field_controller $field
+     * @return string page heading
+     */
+    public function setup_edit_page(field_controller $field) : string {
+        global $CFG, $PAGE;
+        require_once($CFG->libdir.'/adminlib.php');
+
+        $title = parent::setup_edit_page($field);
+        admin_externalpage_setup('course_customfield');
+        $PAGE->navbar->add($title);
+        return $title;
+    }
+}
index b9d3010..7d5eab4 100644 (file)
@@ -200,6 +200,28 @@ class core_course_list_element implements IteratorAggregate {
         return $this->coursecontacts;
     }
 
+    /**
+     * Returns custom fields data for this course
+     *
+     * @return \core_customfield\data_controller[]
+     */
+    public function get_custom_fields() : array {
+        if (!isset($this->record->customfields)) {
+            $this->record->customfields = \core_course\customfield\course_handler::create()->get_instance_data($this->id);
+        }
+        return $this->record->customfields;
+    }
+
+    /**
+     * Does this course have custom fields
+     *
+     * @return bool
+     */
+    public function has_custom_fields() : bool {
+        $customfields = $this->get_custom_fields();
+        return !empty($customfields);
+    }
+
     /**
      * Checks if course has any associated overview files
      *
diff --git a/course/classes/search/customfield.php b/course/classes/search/customfield.php
new file mode 100644 (file)
index 0000000..8f5db60
--- /dev/null
@@ -0,0 +1,184 @@
+<?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/>.
+
+/**
+ * Search area for course custom fields.
+ *
+ * @package core_course
+ * @copyright Toni Barbera <toni@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\search;
+
+use core_course\customfield\course_handler;
+use core_customfield\data_controller;
+use core_customfield\field_controller;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for course custom fields.
+ *
+ * @package core_course
+ * @copyright Toni Barbera <toni@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class customfield extends \core_search\base {
+
+    /**
+     * Custom fields are indexed at course context.
+     *
+     * @var array
+     */
+    protected static $levels = [CONTEXT_COURSE];
+
+    /**
+     * Returns recordset containing required data for indexing
+     * course custom fields.
+     *
+     * @param int $modifiedfrom timestamp
+     * @param \context|null $context Restriction context
+     * @return \moodle_recordset|null Recordset or null if no change possible
+     */
+    public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
+        global $DB;
+
+        list ($contextjoin, $contextparams) = $this->get_course_level_context_restriction_sql($context, 'c', SQL_PARAMS_NAMED);
+        if ($contextjoin === null) {
+            return null;
+        }
+
+        $fields = course_handler::create()->get_fields();
+        if (!$fields) {
+            $fields = array();
+        }
+        list($fieldsql, $fieldparam) = $DB->get_in_or_equal(array_keys($fields), SQL_PARAMS_NAMED, 'fld', true, true);
+
+        // Restrict recordset to CONTEXT_COURSE (since we are implementing it to core_course\search).
+        $sql = "SELECT d.*
+                  FROM {customfield_data} d
+                  JOIN {course} c ON c.id = d.instanceid
+                  JOIN {context} cnt ON cnt.instanceid = c.id
+           $contextjoin
+                 WHERE d.timemodified >= :modifiedfrom
+                   AND cnt.contextlevel = :contextlevel
+                   AND d.fieldid $fieldsql
+              ORDER BY d.timemodified ASC";
+        return $DB->get_recordset_sql($sql , array_merge($contextparams,
+            ['modifiedfrom' => $modifiedfrom, 'contextlevel' => CONTEXT_COURSE], $fieldparam));
+    }
+
+    /**
+     * Returns the document associated with this section.
+     *
+     * @param \stdClass $record
+     * @param array $options
+     * @return \core_search\document|bool
+     */
+    public function get_document($record, $options = array()) {
+        global $PAGE;
+
+        try {
+            $context = \context_course::instance($record->instanceid);
+        } catch (\moodle_exception $ex) {
+            // Notify it as we run here as admin, we should see everything.
+            debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' .
+                $ex->getMessage(), DEBUG_DEVELOPER);
+            return false;
+        }
+
+        $handler = course_handler::create();
+        $field = $handler->get_fields()[$record->fieldid];
+        $data = data_controller::create(0, $record, $field);
+
+        // Prepare associative array with data from DB.
+        $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+        $doc->set('title', content_to_text($field->get('name'), false));
+        $doc->set('content', content_to_text($data->export_value(), FORMAT_HTML));
+        $doc->set('contextid', $context->id);
+        $doc->set('courseid', $context->instanceid);
+        $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
+        $doc->set('modified', $record->timemodified);
+
+        // Check if this document should be considered new.
+        if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $record->timecreated)) {
+            // If the document was created after the last index time, it must be new.
+            $doc->set_is_new(true);
+        }
+
+        return $doc;
+    }
+
+    /**
+     * Whether the user can access the document or not.
+     *
+     * @param int $id The course instance id.
+     * @return int
+     */
+    public function check_access($id) {
+        global $DB;
+        $course = $DB->get_record('course', array('id' => $id));
+        if (!$course) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+        if (can_access_course($course)) {
+            return \core_search\manager::ACCESS_GRANTED;
+        }
+        return \core_search\manager::ACCESS_DENIED;
+    }
+
+    /**
+     * Link to the course.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_doc_url(\core_search\document $doc) {
+        return $this->get_context_url($doc);
+    }
+
+    /**
+     * Link to the course.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_context_url(\core_search\document $doc) {
+        return new \moodle_url('/course/view.php', array('id' => $doc->get('courseid')));
+    }
+
+    /**
+     * Returns the moodle component name.
+     *
+     * It might be the plugin name (whole frankenstyle name) or the core subsystem name.
+     *
+     * @return string
+     */
+    public function get_component_name() {
+        return 'course';
+    }
+
+    /**
+     * Returns an icon instance for the document.
+     *
+     * @param \core_search\document $doc
+     * @return \core_search\document_icon
+     */
+    public function get_doc_icon(\core_search\document $doc) : \core_search\document_icon {
+        return new \core_search\document_icon('i/customfield');
+    }
+}
index 9cbdc58..bac0127 100644 (file)
@@ -184,4 +184,13 @@ class mycourse extends \core_search\base {
     public function get_doc_icon(\core_search\document $doc) : \core_search\document_icon {
         return new \core_search\document_icon('i/course');
     }
+
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [\core_search\manager::SEARCH_AREA_CATEGORY_COURSES];
+    }
 }
index 42badb4..6be829d 100644 (file)
@@ -205,4 +205,13 @@ class section extends \core_search\base {
     public function get_doc_icon(\core_search\document $doc) : \core_search\document_icon {
         return new \core_search\document_icon('i/section');
     }
+
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [\core_search\manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
+    }
 }
index 8e74142..a5b5c8e 100644 (file)
@@ -23,7 +23,6 @@ M.core_completion.init = function(Y) {
                 iconkey,
                 button = args.image.get('parentNode');
 
-
             if (current == 1) {
                 altstr = M.util.get_string('completion-alt-manual-y', 'completion', modulename);
                 iconkey = 'i/completion-manual-y';
@@ -33,11 +32,13 @@ M.core_completion.init = function(Y) {
                 iconkey = 'i/completion-manual-n';
                 args.state.set('value', 1);
             }
-            button.set('title', altstr);
 
             require(['core/templates', 'core/notification'], function(Templates, Notification) {
                 Templates.renderPix(iconkey, 'core', altstr).then(function(html) {
-                    Templates.replaceNode(args.image.getDOMNode(), html, '');
+                    var id = button.get('id'),
+                        postFocus = '$(document.getElementById("' + id + '")).focus();';
+
+                    Templates.replaceNode(args.image.getDOMNode(), html, postFocus);
                 }).catch(Notification.exception);
             });
         }
diff --git a/course/customfield.php b/course/customfield.php
new file mode 100644 (file)
index 0000000..2e92ec8
--- /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/>.
+
+/**
+ * Manage course custom fields
+ *
+ * @package core_course
+ * @copyright 2018 Toni Barbera (toni@moodle.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+admin_externalpage_setup('course_customfield');
+
+$output = $PAGE->get_renderer('core_customfield');
+$handler = core_course\customfield\course_handler::create();
+$outputpage = new \core_customfield\output\management($handler);
+
+echo $output->header(),
+     $output->heading(new lang_string('course_customfield', 'admin')),
+     $output->render($outputpage),
+     $output->footer();
index a91cb5f..1f42e6c 100644 (file)
@@ -320,6 +320,11 @@ class course_edit_form extends moodleform {
                     array('itemtype' => 'course', 'component' => 'core'));
         }
 
+        // Add custom fields to the form.
+        $handler = core_course\customfield\course_handler::create();
+        $handler->set_parent_context($categorycontext); // For course handler only.
+        $handler->instance_form_definition($mform, empty($course->id) ? 0 : $course->id);
+
         // When two elements we need a group.
         $buttonarray = array();
         $classarray = array('class' => 'form-submit');
@@ -334,6 +339,8 @@ class course_edit_form extends moodleform {
         $mform->addElement('hidden', 'id', null);
         $mform->setType('id', PARAM_INT);
 
+        // Prepare custom fields data.
+        $handler->instance_form_before_set_data($course);
         // Finally set the current form data
         $this->set_data($course);
     }
@@ -383,6 +390,10 @@ class course_edit_form extends moodleform {
                 $mform->removeElement('newsitems');
             }
         }
+
+        // Tweak the form with values provided by custom fields in use.
+        $handler  = core_course\customfield\course_handler::create();
+        $handler->instance_form_definition_after_data($mform, empty($courseid) ? 0 : $courseid);
     }
 
     /**
@@ -425,7 +436,10 @@ class course_edit_form extends moodleform {
             $errors = array_merge($errors, $formaterrors);
         }
 
+        // Add the custom fields validation.
+        $handler = core_course\customfield\course_handler::create();
+        $errors  = array_merge($errors, $handler->instance_form_validation($data, $files));
+
         return $errors;
     }
 }
-
index 81e21ab..27d2629 100644 (file)
@@ -277,7 +277,8 @@ class core_course_external extends external_api {
                             $module['completiondata'] = array(
                                 'state'         => $completiondata->completionstate,
                                 'timecompleted' => $completiondata->timemodified,
-                                'overrideby'    => $completiondata->overrideby
+                                'overrideby'    => $completiondata->overrideby,
+                                'valueused'     => core_availability\info::completion_value_used($course, $cm->id)
                             );
                         }
 
@@ -440,6 +441,8 @@ class core_course_external extends external_api {
                                             'timecompleted' => new external_value(PARAM_INT, 'Timestamp for completion status.'),
                                             'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the
                                                 status.'),
+                                            'valueused' => new external_value(PARAM_BOOL, 'Whether the completion status affects
+                                                the availability of another activity.', VALUE_OPTIONAL),
                                         ), 'Module completion data.', VALUE_OPTIONAL
                                     ),
                                     'contents' => new external_multiple_structure(
@@ -553,6 +556,19 @@ class core_course_external extends external_api {
                 $courseinfo['numsections'] = $courseformatoptions['numsections'];
             }
 
+            $handler = core_course\customfield\course_handler::create();
+            if ($customfields = $handler->export_instance_data($course->id)) {
+                $courseinfo['customfields'] = [];
+                foreach ($customfields as $data) {
+                    $courseinfo['customfields'][] = [
+                        'type' => $data->get_type(),
+                        'value' => $data->get_value(),
+                        'name' => $data->get_name(),
+                        'shortname' => $data->get_shortname()
+                    ];
+                }
+            }
+
             //some field should be returned only if the user has update permission
             $courseadmin = has_capability('moodle/course:update', $context);
             if ($courseadmin) {
@@ -663,9 +679,16 @@ class core_course_external extends external_api {
                                 new external_single_structure(
                                     array('name' => new external_value(PARAM_ALPHANUMEXT, 'course format option name'),
                                         'value' => new external_value(PARAM_RAW, 'course format option value')
-                                )),
-                                    'additional options for particular course format', VALUE_OPTIONAL
+                                )), 'additional options for particular course format', VALUE_OPTIONAL
                              ),
+                            'customfields' => new external_multiple_structure(
+                                new external_single_structure(
+                                    ['name' => new external_value(PARAM_TEXT, 'The name of the custom field'),
+                                     'shortname' => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'),
+                                     'type'  => new external_value(PARAM_COMPONENT,
+                                         'The type of the custom field - text, checkbox...'),
+                                     'value' => new external_value(PARAM_RAW, 'The value of the custom field')]
+                                ), 'Custom fields and associated values', VALUE_OPTIONAL),
                         ), 'course'
                 )
         );
@@ -739,8 +762,14 @@ class core_course_external extends external_api {
                                         'value' => new external_value(PARAM_RAW, 'course format option value')
                                 )),
                                     'additional options for particular course format', VALUE_OPTIONAL),
-                        )
-                    ), 'courses to create'
+                            'customfields' => new external_multiple_structure(
+                                new external_single_structure(
+                                    array(
+                                        'shortname'  => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'),
+                                        'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
+                                )), 'custom fields for the course', VALUE_OPTIONAL
+                            )
+                    )), 'courses to create'
                 )
             )
         );
@@ -828,6 +857,13 @@ class core_course_external extends external_api {
                 }
             }
 
+            // Custom fields.
+            if (!empty($course['customfields'])) {
+                foreach ($course['customfields'] as $field) {
+                    $course['customfield_'.$field['shortname']] = $field['value'];
+                }
+            }
+
             //Note: create_course() core function check shortname, idnumber, category
             $course['id'] = create_course((object) $course)->id;
 
@@ -909,8 +945,14 @@ class core_course_external extends external_api {
                                 new external_single_structure(
                                     array('name' => new external_value(PARAM_ALPHANUMEXT, 'course format option name'),
                                         'value' => new external_value(PARAM_RAW, 'course format option value')
-                                )),
-                                    'additional options for particular course format', VALUE_OPTIONAL),
+                                )), 'additional options for particular course format', VALUE_OPTIONAL),
+                            'customfields' => new external_multiple_structure(
+                                new external_single_structure(
+                                    [
+                                        'shortname'  => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'),
+                                        'value' => new external_value(PARAM_RAW, 'The value of the custom field')
+                                    ]
+                                ), 'Custom fields', VALUE_OPTIONAL),
                         )
                     ), 'courses to update'
                 )
@@ -1024,6 +1066,13 @@ class core_course_external extends external_api {
                     }
                 }
 
+                // Prepare list of custom fields.
+                if (isset($course['customfields'])) {
+                    foreach ($course['customfields'] as $field) {
+                        $course['customfield_' . $field['shortname']] = $field['value'];
+                    }
+                }
+
                 // Update course if user has all required capabilities.
                 update_course((object) $course);
             } catch (Exception $e) {
@@ -2499,6 +2548,17 @@ class core_course_external extends external_api {
                 new external_value(PARAM_PLUGIN, 'enrollment method'),
                 'enrollment methods list'
             ),
+            'customfields' => new external_multiple_structure(
+                new external_single_structure(
+                    array(
+                        'name' => new external_value(PARAM_RAW, 'The name of the custom field'),
+                        'shortname' => new external_value(PARAM_RAW,
+                            'The shortname of the custom field - to be able to build the field class in the code'),
+                        'type'  => new external_value(PARAM_ALPHANUMEXT,
+                            'The type of the custom field - text field, checkbox...'),
+                        'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
+                    )
+                ), 'Custom fields', VALUE_OPTIONAL),
         );
 
         if (!$onlypublicdata) {
index 0ba58c6..40c880d 100644 (file)
@@ -1111,6 +1111,11 @@ abstract class format_base {
         $DB->delete_records('course_sections', array('id' => $section->id));
         rebuild_course_cache($course->id, true);
 
+        // Delete section summary files.
+        $context = \context_course::instance($course->id);
+        $fs = get_file_storage();
+        $fs->delete_area_files($context->id, 'course', 'section', $section->id);
+
         // Descrease 'numsections' if needed.
         if ($decreasenumsections) {
             $this->update_course_format_options(array('numsections' => $course->numsections - 1));
index 5f13036..36e7bd0 100644 (file)
@@ -2488,6 +2488,15 @@ function create_course($data, $editoroptions = NULL) {
         core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
     }
 
+    // Save custom fields if there are any of them in the form.
+    $handler = core_course\customfield\course_handler::create();
+    // Make sure to set the handler's parent context first.
+    $coursecatcontext = context_coursecat::instance($category->id);
+    $handler->set_parent_context($coursecatcontext);
+    // Save the custom field data.
+    $data->id = $course->id;
+    $handler->instance_form_save($data, true);
+
     return $course;
 }
 
@@ -2572,6 +2581,10 @@ function update_course($data, $editoroptions = NULL) {
         }
     }
 
+    // Update custom fields if there are any of them in the form.
+    $handler = core_course\customfield\course_handler::create();
+    $handler->instance_form_save($data);
+
     // Update with the new data
     $DB->update_record('course', $data);
     // make sure the modinfo cache is reset
@@ -3420,6 +3433,13 @@ function duplicate_module($course, $cm) {
     $rc = new restore_controller($backupid, $course->id,
             backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
 
+    // Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
+    $plan = $rc->get_plan();
+    $groupsetting = $plan->get_setting('groups');
+    if (empty($groupsetting->get_value())) {
+        $groupsetting->set_value(true);
+    }
+
     $cmcontext = context_module::instance($cm->id);
     if (!$rc->execute_precheck()) {
         $precheckresults = $rc->get_precheck_results();
diff --git