Merge branch 'MDL-64200-master' of git://github.com/damyon/moodle
authorAdrian Greeve <abgreeve@gmail.com>
Wed, 20 Feb 2019 14:57:55 +0000 (15:57 +0100)
committerAdrian Greeve <abgreeve@gmail.com>
Wed, 20 Feb 2019 14:57:55 +0000 (15:57 +0100)
602 files changed:
.gitignore
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/analytics/classes/output/invalid_analysables.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/templates/invalid_analysables.mustache
admin/tool/behat/cli/util.php
admin/tool/behat/cli/util_single_run.php
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/tests/behat/manipulate_forms.feature
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
admin/tool/filetypes/edit_form.php
admin/tool/langimport/classes/locale.php [new file with mode: 0644]
admin/tool/langimport/index.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/langimport/tests/locale_test.php [new file with mode: 0644]
admin/tool/lp/classes/external.php
admin/tool/lp/classes/external/user_competency_summary_in_course_exporter.php
admin/tool/lp/classes/output/course_competencies_page.php
admin/tool/lp/templates/course_competencies_page.mustache
admin/tool/lp/templates/user_competency_summary_in_course.mustache
admin/tool/lp/tests/behat/plan_crud.feature
admin/tool/uploadcourse/classes/base_form.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploaduser/user_form.php
admin/tool/usertours/classes/local/target/block.php
admin/tool/usertours/classes/local/target/selector.php
admin/tool/usertours/classes/local/target/unattached.php
admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php [new file with mode: 0644]
admin/tool/xmldb/actions/edit_table/edit_table.class.php
admin/tool/xmldb/lang/en/tool_xmldb.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]
auth/tests/behat/behat_auth.php
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/login/block_login.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
cache/classes/loaders.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cohort/tests/behat/upload_cohort_users.feature
comment/locallib.php
competency/classes/api.php
competency/tests/api_test.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/deletecategory_form.php
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/modlib.php
course/moodleform_mod.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/app_courselist.feature [new file with mode: 0644]
course/tests/behat/customfields_locked.feature [new file with mode: 0644]
course/tests/behat/customfields_visibility.feature [new file with mode: 0644]
course/tests/behat/view_subfolders_inline.feature
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/locallib.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
enrol/renderer.php
enrol/tests/course_enrolment_manager_test.php
enrol/upgrade.txt
enrol/yui/otherusersmanager/otherusersmanager.js
grade/edit/tree/calculation.php
group/externallib.php
group/overview.php
group/tests/externallib_test.php
install/lang/el/langconfig.php
install/lang/hr/admin.php
install/lang/ja/install.php
install/lang/pt/admin.php
install/lang/ro/install.php
lang/en/admin.php
lang/en/backup.php
lang/en/competency.php
lang/en/course.php
lang/en/customfield.php [new file with mode: 0644]
lang/en/enrol.php
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/checkbox-toggleall.min.js [new file with mode: 0644]
lib/amd/build/form-autocomplete.min.js
lib/amd/build/icon_system_fontawesome.min.js
lib/amd/build/inplace_editable.min.js
lib/amd/build/storagewrapper.min.js
lib/amd/src/checkbox-toggleall.js [new file with mode: 0644]
lib/amd/src/form-autocomplete.js
lib/amd/src/icon_system_fontawesome.js
lib/amd/src/inplace_editable.js
lib/amd/src/storagewrapper.js
lib/behat/behat_base.php
lib/behat/classes/behat_command.php
lib/behat/classes/behat_config_util.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/messages.php
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/form/amd/build/showadvanced.min.js [new file with mode: 0644]
lib/form/amd/src/showadvanced.js [new file with mode: 0644]
lib/form/modgrade.php
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js [deleted file]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js [deleted file]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js [deleted file]
lib/form/yui/src/showadvanced/build.json [deleted file]
lib/form/yui/src/showadvanced/js/showadvanced.js [deleted file]
lib/form/yui/src/showadvanced/meta/showadvanced.json [deleted file]
lib/formslib.php
lib/gradelib.php
lib/grouplib.php
lib/moodlelib.php
lib/navigationlib.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/loginform.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/app_behat_runtime.js [new file with mode: 0644]
lib/tests/behat/behat_app.php [new file with mode: 0644]
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.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]
login/change_password_form.php
login/forgot_password_form.php
message/amd/build/notification_processor_settings.min.js
message/amd/build/preferences_notifications_list_controller.min.js
message/amd/build/preferences_processor_form.min.js
message/amd/src/notification_processor_settings.js
message/amd/src/preferences_notifications_list_controller.js
message/amd/src/preferences_processor_form.js
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/pendingcontactrequests.php [deleted file]
message/templates/message_drawer_contacts_list.mustache
message/templates/message_drawer_messages_list.mustache
message/templates/message_drawer_non_contacts_list.mustache
message/templates/preferences_processor.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/privacy_provider_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/base.php
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/tests/events_test.php
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/chat/lib.php
mod/chat/tests/lib_test.php
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/behat/app_basic_usage.feature [new file with mode: 0644]
mod/forum/tests/behat/posts_ordering_general.feature
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/amd/build/qbankmanager.min.js [new file with mode: 0644]
question/amd/src/qbankmanager.js [new file with mode: 0644]
question/behaviour/adaptive/tests/walkthrough_test.php
question/behaviour/behaviourbase.php
question/behaviour/interactivecountback/tests/walkthrough_test.php
question/behaviour/manualgraded/tests/walkthrough_test.php
question/category_class.php
question/classes/bank/checkbox_column.php
question/classes/bank/tags_action_column.php
question/classes/bank/view.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/tests/behat/select_questions.feature [new file with mode: 0644]
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/essay/backup/moodle2/restore_qtype_essay_plugin.class.php
question/type/essay/tests/restore_test.php [new file with mode: 0644]
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
question/type/match/tests/walkthrough_test.php
question/type/multianswer/tests/walkthrough_test.php
question/type/numerical/tests/walkthrough_test.php
question/type/randomsamatch/tests/walkthrough_test.php
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js [deleted file]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js [deleted file]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js [deleted file]
question/yui/src/qbankmanager/build.json [deleted file]
question/yui/src/qbankmanager/js/qbankmanager.js [deleted file]
question/yui/src/qbankmanager/meta/qbankmanager.json [deleted file]
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/classes/output/core_renderer.php
theme/boost/config.php
theme/boost/layout/columns2.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/forms.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/form_autocomplete_input.mustache
theme/boost/templates/core/help_icon.mustache
theme/boost/templates/core/loginform.mustache
theme/boost/templates/core/navbar.mustache
theme/boost/templates/core_form/element-password.mustache
theme/boost/templates/flat_navigation.mustache
theme/boost/templates/footer.mustache
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/mod_assign/grading_actions.mustache
theme/boost/templates/navbar-secure.mustache
theme/boost/templates/navbar.mustache
theme/boost/templates/secure.mustache
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/forms.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/editadvanced_form.php
user/editlib.php
user/index.php
user/language_form.php
user/lib.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/behat/behat_user.php
user/tests/behat/input-purpose.feature [new file with mode: 0644]
user/tests/search_test.php
version.php
webservice/classes/privacy/provider.php

index 1dab0f7..b96359f 100644 (file)
@@ -9,10 +9,18 @@
 #
 # See gitignore(5) man page for more details
 #
+
+# Swap files (vim)
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+# Temporary files including undo
+*~
+#
 /config.php
 /lib/editor/tinymce/extra/tools/temp/
-*~
-*.swp
 /tags
 /TAGS
 /cscope.*
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 a5bda09..0bb3902 100644 (file)
@@ -153,6 +153,12 @@ class invalid_analysables implements \renderable, \templatable {
             $data->analysables[] = $obj;
         }
 
+        if (empty($data->analysables)) {
+            $data->noanalysables = [
+                'message' => get_string('noinvalidanalysables', 'tool_analytics'),
+                'announce' => true,
+            ];
+        }
         return $data;
     }
 }
index cbd0a60..83e2755 100644 (file)
@@ -247,7 +247,7 @@ class models_list implements \renderable, \templatable {
             }
 
             // Clear model.
-            if (!empty($predictioncontexts)) {
+            if (!empty($predictioncontexts) || $model->is_trained()) {
                 $actionid = 'clear-' . $model->get_id();
                 $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'clear']);
                 $urlparams['action'] = 'clear';
index 8df2cc3..1c3bba2 100644 (file)
@@ -75,6 +75,7 @@ $string['nextpage'] = 'Next page';
 $string['nodatatoevaluate'] = 'There is no data to evaluate the model';
 $string['nodatatopredict'] = 'No new elements to get predictions for';
 $string['nodatatotrain'] = 'There is no new data that can be used for training';
+$string['noinvalidanalysables'] = 'This site does not contain any invalid analysable element.';
 $string['notdefined'] = 'Not yet defined';
 $string['pluginname'] = 'Analytic models';
 $string['predictionresults'] = 'Prediction results';
index c97dd6b..2b3ddd6 100644 (file)
 <div class="box">
     <h3>{{#str}}modelinvalidanalysables, tool_analytics, {{modelname}}{{/str}}</h3>
     <div>{{#str}}invalidanalysablesinfo, tool_analytics{{/str}}</div>
-    <div class="m-t-2 m-b-1">
-        <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
-        <span>{{#next}}{{> core/single_button}}{{/next}}</span>
-    </div>
-    <table class="generaltable fullwidth">
-        <caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
-        <thead>
-            <tr>
-                <th scope="col">{{#str}}name{{/str}}</th>
-                <th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
-                <th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
-            </tr>
-        </thead>
-        <tbody>
-        {{#analysables}}
-            <tr>
-                <td>{{{url}}}</td>
-                <td>{{validtraining}}</td>
-                <td>{{validprediction}}</td>
-            </tr>
-        {{/analysables}}
-        </tbody>
-    </table>
-    <div class="m-t-1 m-b-2">
-        <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
-        <span>{{#next}}{{> core/single_button}}{{/next}}</span>
-    </div>
+    {{#noanalysables}}
+        <div class="m-t-2 m-b-1">
+            {{> core/notification_info}}
+        </div>
+    {{/noanalysables}}
+    {{^noanalysables}}
+        <div class="m-t-2 m-b-1">
+            <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+            <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+        </div>
+        <table class="generaltable fullwidth">
+            <caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
+            <thead>
+                <tr>
+                    <th scope="col">{{#str}}name{{/str}}</th>
+                    <th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
+                    <th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
+                </tr>
+            </thead>
+            <tbody>
+            {{#analysables}}
+                <tr>
+                    <td>{{{url}}}</td>
+                    <td>{{validtraining}}</td>
+                    <td>{{validprediction}}</td>
+                </tr>
+            {{/analysables}}
+            </tbody>
+        </table>
+        <div class="m-t-1 m-b-2">
+            <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+            <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+        </div>
+    {{/noanalysables}}
 </div>
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 f2cd279..88e3365 100644 (file)
@@ -24,6 +24,7 @@
 
 $string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
 $string['allavailablesteps'] = 'All available step definitions';
+$string['errorapproot'] = '$CFG->behat_ionic_dirroot is not pointing to a valid Moodle Mobile developer install.';
 $string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
 $string['errorcomposer'] = 'Composer dependencies are not installed.';
 $string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
index 85f52de..8e4f776 100644 (file)
@@ -28,6 +28,6 @@ Feature: Forms manipulation
     When I expand all fieldsets
     Then I should see "Close the quiz"
     And I should see "Group mode"
-    And I should see "Grouping"
+    And I should see "ID number"
     And I should not see "Show more..." in the "region-main" "region"
     And I should see "Show less..."
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 f2c60aa..ed94462 100644 (file)
@@ -65,12 +65,12 @@ class tool_filetypes_form extends moodleform {
         $mform->addElement('text', 'description',  get_string('description', 'tool_filetypes'));
         $mform->setType('description', PARAM_TEXT);
         $mform->addHelpButton('description', 'description', 'tool_filetypes');
-        $mform->disabledIf('description', 'descriptiontype', 'ne', 'custom');
+        $mform->hideIf('description', 'descriptiontype', 'ne', 'custom');
 
         $mform->addElement('text', 'corestring',  get_string('corestring', 'tool_filetypes'));
         $mform->setType('corestring', PARAM_ALPHANUMEXT);
         $mform->addHelpButton('corestring', 'corestring', 'tool_filetypes');
-        $mform->disabledIf('corestring', 'descriptiontype', 'ne', 'lang');
+        $mform->hideIf('corestring', 'descriptiontype', 'ne', 'lang');
 
         $mform->addElement('checkbox', 'defaulticon',  get_string('defaulticon', 'tool_filetypes'));
         $mform->addHelpButton('defaulticon', 'defaulticon', 'tool_filetypes');
diff --git a/admin/tool/langimport/classes/locale.php b/admin/tool/langimport/classes/locale.php
new file mode 100644 (file)
index 0000000..69c43b6
--- /dev/null
@@ -0,0 +1,84 @@
+<?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/>.
+
+/**
+ * Helper class for the language import tool.
+ *
+ * @package    tool_langimport
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport;
+
+use coding_exception;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Helper class for the language import tool.
+ *
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class locale {
+    /**
+     * Checks availability of locale on current operating system.
+     *
+     * @param string $langpackcode E.g.: en, es, fr, de.
+     * @return bool TRUE if the locale is available on OS.
+     * @throws coding_exception when $langpackcode parameter is a non-empty string.
+     */
+    public function check_locale_availability(string $langpackcode) : bool {
+        global $CFG;
+
+        if (empty($langpackcode)) {
+            throw new coding_exception('Invalid language pack code in \\'.__METHOD__.'() call, only non-empty string is allowed');
+        }
+
+        // Fetch the correct locale based on ostype.
+        if ($CFG->ostype === 'WINDOWS') {
+            $stringtofetch = 'localewin';
+        } else {
+            $stringtofetch = 'locale';
+        }
+
+        // Store current locale.
+        $currentlocale = $this->set_locale(LC_ALL, 0);
+
+        $locale = get_string_manager()->get_string($stringtofetch, 'langconfig', $a = null, $langpackcode);
+
+        // Try to set new locale.
+        $return = $this->set_locale(LC_ALL, $locale);
+
+        // Restore current locale.
+        $this->set_locale(LC_ALL, $currentlocale);
+
+        // If $return is not equal to false, it means that setlocale() succeed to change locale.
+        return $return !== false;
+    }
+
+    /**
+     * Wrap for the native PHP function setlocale().
+     *
+     * @param int $category Specifying the category of the functions affected by the locale setting.
+     * @param string $locale E.g.: en_AU.utf8, en_GB.utf8, es_ES.utf8, fr_FR.utf8, de_DE.utf8.
+     * @return string|false Returns the new current locale, or FALSE on error.
+     */
+    protected function set_locale(int $category = LC_ALL, string $locale = '0') {
+        return setlocale($category, $locale);
+    }
+}
index 92a09da..1180abc 100644 (file)
@@ -109,9 +109,16 @@ echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('langimport', 'tool_langimport'));
 
 $installedlangs = get_string_manager()->get_list_of_translations(true);
+$locale = new \tool_langimport\locale();
 
+$missinglocales = '';
 $missingparents = array();
-foreach ($installedlangs as $installedlang => $unused) {
+foreach ($installedlangs as $installedlang => $langpackname) {
+    // Check locale availability.
+    if (!$locale->check_locale_availability($installedlang)) {
+        $missinglocales .= '<li>'.$langpackname.'</li>';
+    }
+
     $parent = get_parent_language($installedlang);
     if (empty($parent)) {
         continue;
@@ -121,6 +128,14 @@ foreach ($installedlangs as $installedlang => $unused) {
     }
 }
 
+if (!empty($missinglocales)) {
+    // There is at least one missing locale.
+    $a = new stdClass();
+    $a->globallocale = moodle_getlocale();
+    $a->missinglocales = $missinglocales;
+    $controller->errors[] = get_string('langunsupported', 'tool_langimport', $a);
+}
+
 if ($availablelangs = $controller->availablelangs) {
     $remote = true;
 } else {
index faeb02c..27739ed 100644 (file)
@@ -37,6 +37,7 @@ $string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
 $string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
 $string['langpackupdated'] = 'Language pack \'{$a}\' was successfully updated';
 $string['langpackupdatedevent'] = 'Language pack updated';
+$string['langunsupported'] = '<p>Your server does not seem to fully support the following languages:</p><ul>{$a->missinglocales}</ul><p>Instead, the global locale ({$a->globallocale}) will be used to format certain strings such as dates or numbers.</p>';
 $string['langupdatecomplete'] = 'Language pack update completed';
 $string['missingcfglangotherroot'] = 'Missing configuration value $CFG->langotherroot';
 $string['missinglangparent'] = 'Missing parent language <em>{$a->parent}</em> of <em>{$a->lang}</em>.';
diff --git a/admin/tool/langimport/tests/locale_test.php b/admin/tool/langimport/tests/locale_test.php
new file mode 100644 (file)
index 0000000..4d1ffef
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Tests for \tool_langimport\locale class.
+ *
+ * @package    tool_langimport
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for \tool_langimport\locale class.
+ *
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class locale_testcase extends \advanced_testcase {
+    /**
+     * Test that \tool_langimport\locale::check_locale_availability() works as expected.
+     *
+     * @return void
+     */
+    public function test_check_locale_availability() {
+        // Create a mock of set_locale() method to simulate :
+        // - first setlocale() call which backup current locale
+        // - second setlocale() call which try to set new 'es' locale
+        // - third setlocale() call which restore locale.
+        $mock = $this->getMockBuilder(\tool_langimport\locale::class)
+            ->setMethods(['set_locale'])
+            ->getMock();
+        $mock->method('set_locale')->will($this->onConsecutiveCalls('en', 'es', 'en'));
+
+        // Test what happen when locale is available on system.
+        $result = $mock->check_locale_availability('en');
+        $this->assertTrue($result);
+
+        // Create a mock of set_locale() method to simulate :
+        // - first setlocale() call which backup current locale
+        // - second setlocale() call which fail to set new locale
+        // - third setlocale() call which restore locale.
+        $mock = $this->getMockBuilder(\tool_langimport\locale::class)
+            ->setMethods(['set_locale'])
+            ->getMock();
+        $mock->method('set_locale')->will($this->onConsecutiveCalls('en', false, 'en'));
+
+        // Test what happen when locale is not available on system.
+        $result = $mock->check_locale_availability('en');
+        $this->assertFalse($result);
+
+        // Test an invalid parameter.
+        $locale = new \tool_langimport\locale();
+        $this->expectException(coding_exception::class);
+        $locale->check_locale_availability('');
+    }
+}
index 77cf893..f798e7d 100644 (file)
@@ -420,6 +420,9 @@ class external extends external_api {
                     ))
                 ),
                 'comppath' => competency_path_exporter::get_read_structure(),
+                'plans' => new external_multiple_structure(
+                    plan_exporter::get_read_structure()
+                ),
             ))),
             'manageurl' => new external_value(PARAM_LOCALURL, 'Url to the manage competencies page.'),
         ));
index d1b7b7f..78ea24a 100644 (file)
@@ -26,11 +26,13 @@ defined('MOODLE_INTERNAL') || die();
 
 use core_competency\api;
 use core_competency\user_competency;
+use core_competency\external\plan_exporter;
 use core_course\external\course_module_summary_exporter;
 use core_course\external\course_summary_exporter;
 use context_course;
 use renderer_base;
 use stdClass;
+use moodle_url;
 
 /**
  * Class for exporting user competency data with additional related data in a plan.
@@ -62,7 +64,14 @@ class user_competency_summary_in_course_exporter extends \core\external\exporter
             'coursemodules' => array(
                 'type' => course_module_summary_exporter::read_properties_definition(),
                 'multiple' => true
-            )
+            ),
+            'plans' => array(
+                'type' => plan_exporter::read_properties_definition(),
+                'multiple' => true
+            ),
+            'pluginbaseurl' => [
+                'type' => PARAM_URL
+            ],
         );
     }
 
@@ -95,6 +104,16 @@ class user_competency_summary_in_course_exporter extends \core\external\exporter
         }
         $result->coursemodules = $exportedmodules;
 
+        // User learning plans.
+        $plans = api::list_plans_with_competency($this->related['user']->id, $this->related['competency']);
+        $exportedplans = array();
+        foreach ($plans as $plan) {
+            $planexporter = new plan_exporter($plan, array('template' => $plan->get_template()));
+            $exportedplans[] = $planexporter->export($output);
+        }
+        $result->plans = $exportedplans;
+        $result->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
+
         return (array) $result;
     }
 }
index eaf7d34..218d8a5 100644 (file)
@@ -41,6 +41,7 @@ use core_competency\external\course_competency_exporter;
 use core_competency\external\course_competency_settings_exporter;
 use core_competency\external\user_competency_course_exporter;
 use core_competency\external\user_competency_exporter;
+use core_competency\external\plan_exporter;
 use tool_lp\external\competency_path_exporter;
 use tool_lp\external\course_competency_statistics_exporter;
 use core_course\external\course_module_summary_exporter;
@@ -113,6 +114,7 @@ class course_competencies_page implements renderable, templatable {
         $data->courseid = $this->courseid;
         $data->pagecontextid = $this->context->id;
         $data->competencies = array();
+        $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
 
         $gradable = is_enrolled($this->context, $USER, 'moodle/competency:coursecompetencygradable');
         if ($gradable) {
@@ -154,12 +156,21 @@ class course_competencies_page implements renderable, templatable {
                 'context' => $context
             ]);
 
+            // User learning plans.
+            $plans = api::list_plans_with_competency($USER->id, $competency);
+            $exportedplans = array();
+            foreach ($plans as $plan) {
+                $planexporter = new plan_exporter($plan, array('template' => $plan->get_template()));
+                $exportedplans[] = $planexporter->export($output);
+            }
+
             $onerow = array(
                 'competency' => $compexporter->export($output),
                 'coursecompetency' => $ccexporter->export($output),
                 'ruleoutcomeoptions' => $ccoutcomeoptions,
                 'coursemodules' => $exportedmodules,
-                'comppath' => $pathexporter->export($output)
+                'comppath' => $pathexporter->export($output),
+                'plans' => $exportedplans
             );
             if ($gradable) {
                 $foundusercompetencycourse = false;
index eaa0f43..6d89b20 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template tool_lp/course_competencies_page
+
     Course competencies template.
 
     For a full list of the context for this template see the course_competencies_page renderable.
+
+    This template includes ajax functionality, so it cannot be shown in the template library.
 }}
 <div data-region="coursecompetenciespage">
     <div data-region="actions" class="clearfix">
@@ -66,7 +70,7 @@
         <div class="clearfix"></div>
         {{/canmanagecoursecompetencies}}
         {{#competency}}
-            <a href="{{pluginbaseurl}}user_competency_in_course.php?courseid={{courseid}}&competencyid={{competency.id}}&userid={{gradableuserid}}"
+            <a href="{{pluginbaseurl}}/user_competency_in_course.php?courseid={{courseid}}&competencyid={{competency.id}}&userid={{gradableuserid}}"
                    id="competency-info-link-{{competency.id}}"
                    title="{{#str}}viewdetails, tool_lp{{/str}}">
                 <p><strong>{{{competency.shortname}}} <em>{{competency.idnumber}}</em></strong></p>
         {{/canmanagecoursecompetencies}}
         <div data-region="coursecompetencyactivities">
         <p>
-        <ul class="inline list-inline">
+        <strong>{{#str}}activities{{/str}}</strong>
+        <ul class="inline list-inline p-2">
         {{#coursemodules}}
             <li class="list-inline-item"><a href="{{url}}"><img src="{{iconurl}}"> {{name}} </a></li>
         {{/coursemodules}}
         {{^coursemodules}}
-            <li class="list-inline-item"><span class="alert">{{#str}}noactivities, tool_lp{{/str}}</span></li>
+            <li class="list-inline-item">{{#str}}noactivities, tool_lp{{/str}}</li>
         {{/coursemodules}}
         </ul>
         </p>
         </div>
+        <div data-region="learningplans">
+        <p>
+        <strong>{{#str}}userplans, core_competency{{/str}}</strong>
+        <ul class="inline list-inline p-2">
+        {{#plans}}
+            <li class="list-inline-item"><a href="{{pluginbaseurl}}/plan.php?id={{id}}">{{{name}}}</a></li>
+        {{/plans}}
+        {{^plans}}
+            <li class="list-inline-item">{{#str}}nouserplanswithcompetency, core_competency{{/str}}</li>
+        {{/plans}}
+        </ul>
+        </p>
+        </div>
     </td>
     </tr>
 {{/competencies}}
index 22cf96d..ce80bbb 100644 (file)
         </dd>
         {{/user}}
         {{/displayuser}}
+        <dt>{{#str}}userplans, competency{{/str}}</dt>
+        <dd>
+        <p>
+        <ul class="inline list-inline">
+        {{#plans}}
+            <li class="list-inline-item"><a href="{{pluginbaseurl}}/plan.php?id={{id}}">{{{name}}}</a></li>
+        {{/plans}}
+        {{^plans}}
+            <li>{{#str}}nouserplanswithcompetency, competency{{/str}}</li>
+        {{/plans}}
+        </ul>
+        </p>
+        </dd>
         {{#usercompetencycourse}}
         <dt>{{#str}}proficient, tool_lp{{/str}}</dt>
         <dd>
index d2ca869..0fbbfd1 100644 (file)
@@ -130,3 +130,35 @@ Feature: Manage plearning plan
     When I click on "Delete" "button" in the "Confirm" "dialogue"
     And I wait until the page is ready
     Then I should not see "Science plan Year-4"
+
+  Scenario: See a learning plan from a course
+    Given the following lp "plans" exist:
+      | name | user | description |
+      | Science plan Year-manage | admin | science plan description |
+    And the following lp "frameworks" exist:
+      | shortname | idnumber |
+      | Framework 1 | sc-y-2 |
+    And the following lp "competencies" exist:
+      | shortname | framework |
+      | comp1 | sc-y-2 |
+      | comp2 | sc-y-2 |
+    And I follow "Learning plans"
+    And I should see "Science plan Year-manage"
+    And I follow "Science plan Year-manage"
+    And I should see "Add competency"
+    And I press "Add competency"
+    And "Competency picker" "dialogue" should be visible
+    And I select "comp1" of the competency tree
+    When I click on "Add" "button" in the "Competency picker" "dialogue"
+    Then "comp1" "table_row" should exist
+    And I create a course with:
+      | Course full name | New course fullname |
+      | Course short name | New course shortname |
+    And I follow "New course fullname"
+    And I follow "Competencies"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "comp1" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I should see "Learning plans"
+    And I should see "Science plan Year-manage"
index 868fe6a..38aa6be 100644 (file)
@@ -72,26 +72,26 @@ class tool_uploadcourse_base_form extends moodleform {
         );
         $mform->addElement('select', 'options[updatemode]', get_string('updatemode', 'tool_uploadcourse'), $choices);
         $mform->setDefault('options[updatemode]', tool_uploadcourse_processor::UPDATE_NOTHING);
-        $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->addHelpButton('options[updatemode]', 'updatemode', 'tool_uploadcourse');
 
         $mform->addElement('selectyesno', 'options[allowdeletes]', get_string('allowdeletes', 'tool_uploadcourse'));
         $mform->setDefault('options[allowdeletes]', 0);
-        $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->addHelpButton('options[allowdeletes]', 'allowdeletes', 'tool_uploadcourse');
 
         $mform->addElement('selectyesno', 'options[allowrenames]', get_string('allowrenames', 'tool_uploadcourse'));
         $mform->setDefault('options[allowrenames]', 0);
-        $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->addHelpButton('options[allowrenames]', 'allowrenames', 'tool_uploadcourse');
 
         $mform->addElement('selectyesno', 'options[allowresets]', get_string('allowresets', 'tool_uploadcourse'));
         $mform->setDefault('options[allowresets]', 0);
-        $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->addHelpButton('options[allowresets]', 'allowresets', 'tool_uploadcourse');
     }
 
index 89ee15d..58c39d4 100644 (file)
@@ -57,8 +57,8 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             'maxlength="100" size="20"');
         $mform->setType('options[shortnametemplate]', PARAM_RAW);
         $mform->addHelpButton('options[shortnametemplate]', 'shortnametemplate', 'tool_uploadcourse');
-        $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE);
-        $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY);
+        $mform->hideIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE);
+        $mform->hideIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY);
 
         // Restore file is not in the array options on purpose, because formslib can't handle it!
         $contextid = $this->_customdata['contextid'];
@@ -73,8 +73,8 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
 
         $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse'));
         $mform->setDefault('options[reset]', 0);
-        $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->disabledIf('options[reset]', 'options[allowresets]', 'eq', 0);
         $mform->addHelpButton('options[reset]', 'reset', 'tool_uploadcourse');
 
index c0eb764..558f324 100644 (file)
@@ -95,7 +95,7 @@ class admin_uploaduser_form2 extends moodleform {
         $choices = array(0 => get_string('infilefield', 'auth'), 1 => get_string('createpasswordifneeded', 'auth'));
         $mform->addElement('select', 'uupasswordnew', get_string('uupasswordnew', 'tool_uploaduser'), $choices);
         $mform->setDefault('uupasswordnew', 1);
-        $mform->disabledIf('uupasswordnew', 'uutype', 'eq', UU_USER_UPDATE);
+        $mform->hideIf('uupasswordnew', 'uutype', 'eq', UU_USER_UPDATE);
 
         $choices = array(UU_UPDATE_NOCHANGES    => get_string('nochanges', 'tool_uploaduser'),
                          UU_UPDATE_FILEOVERRIDE => get_string('uuupdatefromfile', 'tool_uploaduser'),
@@ -103,16 +103,16 @@ class admin_uploaduser_form2 extends moodleform {
                          UU_UPDATE_MISSING      => get_string('uuupdatemissing', 'tool_uploaduser'));
         $mform->addElement('select', 'uuupdatetype', get_string('uuupdatetype', 'tool_uploaduser'), $choices);
         $mform->setDefault('uuupdatetype', UU_UPDATE_NOCHANGES);
-        $mform->disabledIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDINC);
 
         $choices = array(0 => get_string('nochanges', 'tool_uploaduser'), 1 => get_string('update'));
         $mform->addElement('select', 'uupasswordold', get_string('uupasswordold', 'tool_uploaduser'), $choices);
         $mform->setDefault('uupasswordold', 0);
-        $mform->disabledIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDINC);
-        $mform->disabledIf('uupasswordold', 'uuupdatetype', 'eq', 0);
-        $mform->disabledIf('uupasswordold', 'uuupdatetype', 'eq', 3);
+        $mform->hideIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uupasswordold', 'uuupdatetype', 'eq', 0);
+        $mform->hideIf('uupasswordold', 'uuupdatetype', 'eq', 3);
 
         $choices = array(UU_PWRESET_WEAK => get_string('usersweakpassword', 'tool_uploaduser'),
                          UU_PWRESET_NONE => get_string('none'),
@@ -125,18 +125,18 @@ class admin_uploaduser_form2 extends moodleform {
 
         $mform->addElement('selectyesno', 'uuallowrenames', get_string('allowrenames', 'tool_uploaduser'));
         $mform->setDefault('uuallowrenames', 0);
-        $mform->disabledIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDINC);
 
         $mform->addElement('selectyesno', 'uuallowdeletes', get_string('allowdeletes', 'tool_uploaduser'));
         $mform->setDefault('uuallowdeletes', 0);
-        $mform->disabledIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDINC);
 
         $mform->addElement('selectyesno', 'uuallowsuspends', get_string('allowsuspends', 'tool_uploaduser'));
         $mform->setDefault('uuallowsuspends', 1);
-        $mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDINC);
 
         if (!empty($CFG->allowaccountssameemail)) {
             $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploaduser'));
@@ -209,14 +209,14 @@ class admin_uploaduser_form2 extends moodleform {
         $mform->addElement('text', 'username', get_string('uuusernametemplate', 'tool_uploaduser'), 'size="20"');
         $mform->setType('username', PARAM_RAW); // No cleaning here. The process verifies it later.
         $mform->addRule('username', get_string('requiredtemplate', 'tool_uploaduser'), 'required', null, 'client');
-        $mform->disabledIf('username', 'uutype', 'eq', UU_USER_ADD_UPDATE);
-        $mform->disabledIf('username', 'uutype', 'eq', UU_USER_UPDATE);
+        $mform->hideIf('username', 'uutype', 'eq', UU_USER_ADD_UPDATE);
+        $mform->hideIf('username', 'uutype', 'eq', UU_USER_UPDATE);
         $mform->setForceLtr('username');
 
         $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"');
         $mform->setType('email', PARAM_RAW); // No cleaning here. The process verifies it later.
-        $mform->disabledIf('email', 'uutype', 'eq', UU_USER_ADD_UPDATE);
-        $mform->disabledIf('email', 'uutype', 'eq', UU_USER_UPDATE);
+        $mform->hideIf('email', 'uutype', 'eq', UU_USER_ADD_UPDATE);
+        $mform->hideIf('email', 'uutype', 'eq', UU_USER_UPDATE);
         $mform->setForceLtr('email');
 
         // only enabled and known to work plugins
index ffbd787..3f38f0a 100644 (file)
@@ -101,7 +101,7 @@ class block extends base {
      * @param   MoodleQuickForm $mform      The form to add configuration to.
      */
     public static function add_disabled_constraints_to_form(\MoodleQuickForm $mform) {
-        $mform->disabledIf('targetvalue_block', 'targettype', 'noteq',
+        $mform->hideIf('targetvalue_block', 'targettype', 'noteq',
                 \tool_usertours\target::get_target_constant_for_class(get_class()));
     }
 
index ae6f2fc..3e3fd58 100644 (file)
@@ -91,7 +91,7 @@ class selector extends base {
      * @param   MoodleQuickForm $mform      The form to add configuration to.
      */
     public static function add_disabled_constraints_to_form(\MoodleQuickForm $mform) {
-        $mform->disabledIf('targetvalue_selector', 'targettype', 'noteq',
+        $mform->hideIf('targetvalue_selector', 'targettype', 'noteq',
                 \tool_usertours\target::get_target_constant_for_class(get_class()));
     }
 
index 6a4b078..696efe5 100644 (file)
@@ -84,7 +84,7 @@ class unattached extends base {
         $myvalue = \tool_usertours\target::get_target_constant_for_class(get_class());
 
         foreach (array_keys(self::$forcedsettings) as $settingname) {
-            $mform->disabledIf($settingname, 'targettype', 'eq', $myvalue);
+            $mform->hideIf($settingname, 'targettype', 'eq', $myvalue);
         }
     }
 
diff --git a/admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php b/admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php
new file mode 100644 (file)
index 0000000..a323c1e
--- /dev/null
@@ -0,0 +1,154 @@
+<?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/>.
+
+/**
+ * @package    tool_xmldb
+ * @copyright  2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Add the mandatory fields for persistent to the table.
+ *
+ * @package    tool_xmldb
+ * @copyright  2019 Michael Aherne
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class add_persistent_mandatory extends XMLDBAction {
+
+    function init() {
+
+        parent::init();
+
+        // Get needed strings.
+        $this->loadStrings(array(
+            'addpersistent' => 'tool_xmldb',
+            'persistentfieldsconfirm' => 'tool_xmldb',
+            'persistentfieldscomplete' => 'tool_xmldb',
+            'persistentfieldsexist' => 'tool_xmldb',
+            'back' => 'core'
+        ));
+
+    }
+
+    function getTitle() {
+        return $this->str['addpersistent'];
+    }
+
+    function invoke() {
+
+        parent::invoke();
+
+        $this->does_generate = ACTION_GENERATE_HTML;
+
+        global $CFG, $XMLDB, $OUTPUT;
+
+        $dir = required_param('dir', PARAM_PATH);
+        $dirpath = $CFG->dirroot . $dir;
+
+        if (empty($XMLDB->dbdirs)) {
+            return false;
+        }
+
+        if (!empty($XMLDB->editeddirs)) {
+            $editeddir = $XMLDB->editeddirs[$dirpath];
+            $structure = $editeddir->xml_file->getStructure();
+        }
+
+        $tableparam = required_param('table', PARAM_ALPHANUMEXT);
+
+        /** @var xmldb_table $table */
+        $table = $structure->getTable($tableparam);
+
+        $result = true;
+        // Launch postaction if exists (leave this here!)
+        if ($this->getPostAction() && $result) {
+            return $this->launch($this->getPostAction());
+        }
+
+        $confirm = optional_param('confirm', false, PARAM_BOOL);
+
+        $fields = ['usermodified', 'timecreated', 'timemodified'];
+        $existing = [];
+        foreach ($fields as $field) {
+            if ($table->getField($field)) {
+                $existing[] = $field;
+            }
+        }
+
+        $returnurl = new \moodle_url('/admin/tool/xmldb/index.php', [
+            'table' => $tableparam,
+            'dir' => $dir,
+            'action' => 'edit_table'
+        ]);
+
+        $backbutton = html_writer::link($returnurl, '[' . $this->str['back'] . ']');
+        $actionbuttons = html_writer::tag('p', $backbutton, ['class' => 'centerpara buttons']);
+
+        if (!$confirm) {
+
+            if (!empty($existing)) {
+
+                $message = html_writer::span($this->str['persistentfieldsexist']);
+                $message .= html_writer::alist($existing);
+                $this->output .= $OUTPUT->notification($message);
+
+                if (count($existing) == count($fields)) {
+                    $this->output .= $actionbuttons;
+                    return true;
+                }
+            }
+
+            $confirmurl = new \moodle_url('/admin/tool/xmldb/index.php', [
+                'table' => $tableparam,
+                'dir' => $dir,
+                'action' => 'add_persistent_mandatory',
+                'sesskey' => sesskey(),
+                'confirm' => '1'
+            ]);
+
+            $message = html_writer::span($this->str['persistentfieldsconfirm']);
+            $message .= html_writer::alist(array_diff($fields, $existing));
+            $this->output .= $OUTPUT->confirm($message, $confirmurl, $returnurl);
+
+        } else {
+
+            $fieldsadded = [];
+            foreach ($fields as $field) {
+                if (!in_array($field, $existing)) {
+                    $fieldsadded[] = $field;
+                    $table->add_field($field, XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, 0);
+                }
+            }
+
+            if (!$table->getKey('usermodified')) {
+                $table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
+            }
+
+            $structure->setVersion(userdate(time(), '%Y%m%d', 99, false));
+            $structure->setChanged(true);
+
+            $message = html_writer::span($this->str['persistentfieldscomplete']);
+            $message .= html_writer::alist(array_diff($fields, $existing));
+            $this->output .= $OUTPUT->notification($message, 'success');
+
+            $this->output .= $actionbuttons;
+        }
+
+        return $result;
+    }
+
+}
index 5cd4bcd..aa82f99 100644 (file)
@@ -44,6 +44,7 @@ class edit_table extends XMLDBAction {
 
         // Get needed strings
         $this->loadStrings(array(
+            'addpersistent' => 'tool_xmldb',
             'change' => 'tool_xmldb',
             'vieworiginal' => 'tool_xmldb',
             'viewedited' => 'tool_xmldb',
@@ -177,6 +178,15 @@ class edit_table extends XMLDBAction {
         $b .= '<a href="index.php?action=view_table_sql&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' .$this->str['viewsqlcode'] . ']</a>';
         // The view php code button
         $b .= '&nbsp;<a href="index.php?action=view_table_php&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' . $this->str['viewphpcode'] . ']</a>';
+        // The add persistent fields button.
+        $url = new \moodle_url('/admin/tool/xmldb/index.php', [
+            'action' => 'add_persistent_mandatory',
+            'sesskey' => sesskey(),
+            'table' => $tableparam,
+            'dir'=> str_replace($CFG->dirroot, '', $dirpath)
+        ]);
+        $b .= '&nbsp;' . \html_writer::link($url, '[' . $this->str['addpersistent'] . ']');
+
         // The save button (if possible)
         if ($cansavenow) {
             $b .= '&nbsp;<a href="index.php?action=save_xml_file&amp;sesskey=' . sesskey() . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '&amp;time=' . time() . '&amp;unload=false&amp;postaction=edit_table&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' . $this->str['save'] . ']</a>';
index d531a86..d2efeab 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 $string['actual'] = 'Actual';
+$string['addpersistent'] = 'Add mandatory persistent fields';
 $string['aftertable'] = 'After table:';
 $string['back'] = 'Back';
 $string['backtomainview'] = 'Back to main';
@@ -169,6 +170,9 @@ $string['numberincorrectwholepart'] = 'Too big whole number part for number fiel
 $string['pendingchanges'] = 'Note: You have performed changes to this file. They can be saved at any moment.';
 $string['pendingchangescannotbesaved'] = 'There are changes in this file but they cannot be saved! Please verify that both the directory and the "install.xml" within it have write permissions for the web server.';
 $string['pendingchangescannotbesavedreload'] = 'There are changes in this file but they cannot be saved! Please verify that both the directory and the "install.xml" within it have write permissions for the web server. Then reload this page and you should be able to save those changes.';
+$string['persistentfieldsconfirm'] = 'Do you want to add the following fields: ';
+$string['persistentfieldscomplete'] = 'The following fields have been added: ';
+$string['persistentfieldsexist'] = 'The following fields already exist: ';
 $string['pluginname'] = 'XMLDB editor';
 $string['primarykeyonlyallownotnullfields'] = 'Primary keys cannot be null';
 $string['reserved'] = 'Reserved';
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..c774de1 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);
     }
 
@@ -273,13 +265,15 @@ class provider implements
             return;
         }
 
+        $userid = $contextlist->get_user()->id;
         foreach ($contextlist->get_contexts() as $context) {
             if ($context->contextlevel != CONTEXT_USER) {
-                return;
+                continue;
+            }
+            if ($context->instanceid == $userid) {
+                // Because we only use user contexts the instance ID is the user ID.
+                $DB->delete_records('mnet_log', ['userid' => $context->instanceid]);
             }
-
-            // Because we only use user contexts the instance ID is the user ID.
-            $DB->delete_records('mnet_log', ['userid' => $context->instanceid]);
         }
     }
 }
index d0dfa65..f5130ae 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);
     }
 
@@ -178,12 +170,15 @@ class provider implements
         if (empty($contextlist->count())) {
             return;
         }
+        $userid = $contextlist->get_user()->id;
         foreach ($contextlist->get_contexts() as $context) {
             if ($context->contextlevel != CONTEXT_USER) {
-                return;
+                continue;
+            }
+            if ($context->instanceid == $userid) {
+                // Because we only use user contexts the instance ID is the user ID.
+                static::delete_user_data($context->instanceid);
             }
-            // Because we only use user contexts the instance ID is the user ID.
-            static::delete_user_data($context->instanceid);
         }
     }
 
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 27ebd41..30d1691 100644 (file)
@@ -44,6 +44,12 @@ class behat_auth extends behat_base {
      * @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)"$/
      */
     public function i_log_in_as($username) {
+        // In the mobile app the required tasks are different.
+        if ($this->is_in_app()) {
+            $this->execute('behat_app::login', [$username]);
+            return;
+        }
+
         // Visit login page.
         $this->getSession()->visit($this->locate_path('login/index.php'));
 
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)"