Merge branch 'MDL-63959-master' of git://github.com/tobiasreischmann/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 10 May 2019 03:41:11 +0000 (11:41 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Fri, 10 May 2019 03:41:11 +0000 (11:41 +0800)
617 files changed:
.eslintignore
.stylelintignore
admin/cli/install.php
admin/settings/analytics.php
admin/settings/badges.php
admin/settings/users.php
admin/templates/setting_configduration.mustache
admin/tests/behat/enable_multiple_accounts_use_same_email.feature
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/tests/behat/restoredefault.feature
admin/tool/log/classes/local/privacy/helper.php
admin/tool/lp/amd/build/competencies.min.js
admin/tool/lp/amd/build/course_competency_settings.min.js
admin/tool/lp/amd/build/module_navigation.min.js [new file with mode: 0644]
admin/tool/lp/amd/src/competencies.js
admin/tool/lp/amd/src/course_competency_settings.js
admin/tool/lp/amd/src/module_navigation.js [new file with mode: 0644]
admin/tool/lp/classes/external.php
admin/tool/lp/classes/output/course_competencies_page.php
admin/tool/lp/classes/output/module_navigation.php [new file with mode: 0644]
admin/tool/lp/classes/output/renderer.php
admin/tool/lp/coursecompetencies.php
admin/tool/lp/lang/en/tool_lp.php
admin/tool/lp/templates/course_competencies_page.mustache
admin/tool/lp/templates/module_navigation.mustache [new file with mode: 0644]
admin/tool/lp/templates/user_competency_course_navigation.mustache
admin/tool/lp/tests/behat/course_competencies.feature [new file with mode: 0644]
admin/tool/lp/tests/externallib_test.php
admin/tool/messageinbound/classes/manager.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/uploaduser/classes/local/field_value_validators.php [new file with mode: 0644]
admin/tool/uploaduser/index.php
admin/tool/uploaduser/lang/en/tool_uploaduser.php
admin/tool/uploaduser/locallib.php
admin/tool/uploaduser/tests/behat/upload_users.feature
admin/tool/uploaduser/tests/field_value_validators_test.php [new file with mode: 0644]
admin/tool/usertours/lang/en/tool_usertours.php
analytics/classes/analysis.php
analytics/classes/local/time_splitting/periodic.php
analytics/classes/local/time_splitting/upcoming_periodic.php
analytics/tests/analysis_test.php [new file with mode: 0644]
analytics/tests/fixtures/test_timesplitting_upcoming_seconds.php [new file with mode: 0644]
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/tests/async_backup_test.php
backup/util/helper/async_helper.class.php
backup/util/settings/setting_dependency.class.php
backup/util/settings/tests/settings_test.php
badges/action.php
badges/alignment.php
badges/assertion.php
badges/backpack-add.php [new file with mode: 0644]
badges/backpack.js
badges/backpackemailverify.php
badges/backpacks.php [new file with mode: 0644]
badges/badge.php
badges/badge_json.php
badges/classes/assertion.php
badges/classes/backpack_api.php [new file with mode: 0644]
badges/classes/backpack_api_mapping.php [new file with mode: 0644]
badges/classes/badge.php [new file with mode: 0644]
badges/classes/external.php
badges/classes/external/alignment_exporter.php
badges/classes/external/assertion_exporter.php [new file with mode: 0644]
badges/classes/external/backpack_exporter.php [new file with mode: 0644]
badges/classes/external/badgeclass_exporter.php [new file with mode: 0644]
badges/classes/external/collection_exporter.php [new file with mode: 0644]
badges/classes/external/issuer_exporter.php [new file with mode: 0644]
badges/classes/external/recipient_exporter.php [new file with mode: 0644]
badges/classes/external/user_badge_exporter.php
badges/classes/external/verification_exporter.php [new file with mode: 0644]
badges/classes/form/backpack.php [moved from badges/backpack_form.php with 60% similarity]
badges/classes/form/badge.php [moved from badges/edit_form.php with 71% similarity]
badges/classes/form/collections.php [new file with mode: 0644]
badges/classes/form/external_backpack.php [new file with mode: 0644]
badges/classes/form/message.php [new file with mode: 0644]
badges/classes/observer.php
badges/classes/output/badge_alignments.php [new file with mode: 0644]
badges/classes/output/badge_collection.php [new file with mode: 0644]
badges/classes/output/badge_management.php [new file with mode: 0644]
badges/classes/output/badge_recipients.php [new file with mode: 0644]
badges/classes/output/badge_related.php [new file with mode: 0644]
badges/classes/output/badge_user_collection.php [new file with mode: 0644]
badges/classes/output/external_backpacks_page.php [new file with mode: 0644]
badges/classes/output/external_backpacks_table.php [new file with mode: 0644]
badges/classes/output/external_badge.php [new file with mode: 0644]
badges/classes/output/issued_badge.php [new file with mode: 0644]
badges/classes/privacy/provider.php
badges/edit.php
badges/external.php
badges/index.php
badges/lib/backpacklib.php [deleted file]
badges/mybackpack.php
badges/mybadges.php
badges/newbadge.php
badges/recipients.php
badges/related.php
badges/renderer.php
badges/templates/external_backpacks_page.mustache [new file with mode: 0644]
badges/tests/badgeslib_test.php
badges/tests/behat/add_badge.feature
badges/tests/external_test.php
badges/tests/privacy_test.php
badges/upgrade.txt
badges/view.php
blocks/moodleblock.class.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/view-cards.mustache
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/navigation/lang/en/block_navigation.php
calendar/amd/build/view_manager.min.js
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/event_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/lib.php
calendar/templates/calendar_day.mustache
calendar/templates/day_detailed.mustache
calendar/templates/event_details.mustache [new file with mode: 0644]
calendar/templates/event_item.mustache
calendar/templates/event_list.mustache
calendar/templates/event_summary_body.mustache
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache
calendar/templates/upcoming_mini.mustache
cohort/upload.php
cohort/upload_form.php
competency/classes/course_module_competency.php
competency/tests/course_module_competency_test.php [new file with mode: 0644]
completion/criteria/completion_criteria.php
completion/cron.php [deleted file]
composer.lock
course/classes/external/course_summary_exporter.php
course/classes/management/helper.php
course/externallib.php
course/publish/backup.php
course/publish/metadata.php
course/request.php
course/templates/coursecard.mustache
course/tests/behat/behat_course.php
course/tests/behat/customfields_locked.feature
course/tests/behat/customfields_visibility.feature
course/tests/externallib_test.php
course/upgrade.txt
customfield/field/checkbox/classes/data_controller.php
customfield/field/checkbox/lang/en/customfield_checkbox.php
customfield/field/date/lang/en/customfield_date.php
customfield/field/select/lang/en/customfield_select.php
customfield/field/text/lang/en/customfield_text.php
customfield/field/text/tests/behat/field.feature
customfield/field/textarea/lang/en/customfield_textarea.php
customfield/tests/behat/edit_fields_settings.feature
customfield/tests/behat/required_field.feature
customfield/tests/behat/unique_field.feature
enrol/renderer.php
favourites/classes/local/service/component_favourite_service.php [new file with mode: 0644]
favourites/classes/service_factory.php
favourites/tests/component_favourite_service_test.php [new file with mode: 0644]
favourites/tests/repository_test.php
favourites/tests/user_favourite_service_test.php [moved from favourites/tests/service_test.php with 100% similarity]
filter/mathjaxloader/filter.php
grade/grading/classes/privacy/gradingform_legacy_polyfill.php
grade/grading/tests/privacy_legacy_polyfill_test.php
grade/import/direct/lang/en/gradeimport_direct.php
grade/templates/edit_tree.mustache
group/tests/privacy_provider_test.php
install.php
install/css.php
install/lang/it_wp/langconfig.php [new file with mode: 0644]
install/lang/no_wp/langconfig.php [new file with mode: 0644]
install/lang/pt/install.php
lang/en/access.php
lang/en/admin.php
lang/en/analytics.php
lang/en/backup.php
lang/en/badges.php
lang/en/cache.php
lang/en/course.php
lang/en/customfield.php
lang/en/deprecated.txt
lang/en/message.php
lang/en/moodle.php
lang/en/question.php
lib/badgeslib.php
lib/behat/classes/partial_named_selector.php
lib/behat/lib.php
lib/classes/event/question_base.php [new file with mode: 0644]
lib/classes/event/question_category_base.php [new file with mode: 0644]
lib/classes/event/question_category_created.php
lib/classes/event/question_category_deleted.php [new file with mode: 0644]
lib/classes/event/question_category_moved.php [new file with mode: 0644]
lib/classes/event/question_category_updated.php [new file with mode: 0644]
lib/classes/event/question_category_viewed.php [new file with mode: 0644]
lib/classes/event/question_created.php [new file with mode: 0644]
lib/classes/event/question_deleted.php [new file with mode: 0644]
lib/classes/event/question_moved.php [new file with mode: 0644]
lib/classes/event/question_updated.php [new file with mode: 0644]
lib/classes/event/question_viewed.php [new file with mode: 0644]
lib/classes/event/questions_exported.php [new file with mode: 0644]
lib/classes/event/questions_imported.php [new file with mode: 0644]
lib/classes/external/exporter.php
lib/classes/hub/registration.php
lib/classes/message/inbound/private_files_handler.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/classes/string_manager_standard.php
lib/classes/task/badges_cron_task.php
lib/classes/task/completion_daily_task.php
lib/classes/task/completion_regular_task.php
lib/classes/task/send_failed_login_notifications_task.php
lib/completionlib.php
lib/db/caches.php
lib/db/install.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/editor/atto/plugins/emoticon/lib.php
lib/editor/classes/privacy/provider.php
lib/editor/tests/privacy_provider_test.php
lib/editor/tinymce/module.js
lib/editor/tinymce/plugins/moodleemoticon/dialog.php
lib/editor/tinymce/plugins/moodleemoticon/lib.php
lib/form/cancel.php
lib/form/course.php
lib/form/filemanager.php
lib/form/filepicker.js
lib/form/filepicker.php
lib/form/float.php [new file with mode: 0644]
lib/form/group.php
lib/form/listing.php
lib/form/submit.php
lib/form/templatable_form_element.php
lib/form/templates/element-advcheckbox-inline.mustache
lib/form/templates/element-advcheckbox.mustache
lib/form/templates/element-autocomplete-inline.mustache
lib/form/templates/element-autocomplete.mustache
lib/form/templates/element-button-inline.mustache
lib/form/templates/element-button.mustache
lib/form/templates/element-checkbox-inline.mustache
lib/form/templates/element-checkbox.mustache
lib/form/templates/element-password.mustache
lib/form/templates/element-passwordunmask.mustache
lib/form/templates/element-radio-inline.mustache
lib/form/templates/element-radio.mustache
lib/form/templates/element-select-inline.mustache
lib/form/templates/element-select.mustache
lib/form/templates/element-selectgroups-inline.mustache
lib/form/templates/element-selectgroups.mustache
lib/form/templates/element-selectwithlink.mustache
lib/form/templates/element-submit.mustache
lib/form/templates/element-tags-inline.mustache
lib/form/templates/element-tags.mustache
lib/form/templates/element-template-inline.mustache
lib/form/templates/element-template.mustache
lib/form/templates/element-text-inline.mustache
lib/form/templates/element-text.mustache
lib/form/templates/element-textarea.mustache
lib/form/templates/element-url.mustache
lib/form/tests/float_test.php [new file with mode: 0644]
lib/formslib.php
lib/installlib.php
lib/moodlelib.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm/element.php
lib/php-jwt/LICENSE [new file with mode: 0644]
lib/php-jwt/README.md [new file with mode: 0644]
lib/php-jwt/composer.json [new file with mode: 0644]
lib/php-jwt/readme_moodle.txt [new file with mode: 0644]
lib/php-jwt/src/BeforeValidException.php [new file with mode: 0644]
lib/php-jwt/src/ExpiredException.php [new file with mode: 0644]
lib/php-jwt/src/JWT.php [new file with mode: 0644]
lib/php-jwt/src/SignatureInvalidException.php [new file with mode: 0644]
lib/portfoliolib.php
lib/questionlib.php
lib/setup.php
lib/setuplib.php
lib/templates/block.mustache
lib/templates/dataformat_selector.mustache
lib/templates/permissionmanager_panelcontent.mustache
lib/testing/generator/data_generator.php
lib/tests/behat/behat_data_generators.php
lib/tests/exporter_test.php
lib/tests/fixtures/upload_users_themes.csv [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/other/pdflibtestpage.php
lib/tests/string_manager_standard_test.php
lib/tests/time_splittings_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
message/amd/build/message_drawer_router.min.js
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_renderer.min.js
message/amd/build/message_drawer_view_conversation_state_manager.min.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/build/message_repository.min.js
message/amd/src/message_drawer_router.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_conversation_renderer.js
message/amd/src/message_drawer_view_conversation_state_manager.js
message/amd/src/message_drawer_view_overview_section.js
message/amd/src/message_repository.js
message/classes/api.php
message/classes/helper.php
message/externallib.php
message/output/email/classes/event_observers.php [new file with mode: 0644]
message/output/email/classes/output/email_digest.php
message/output/email/classes/task/send_email_task.php
message/output/email/db/events.php [new file with mode: 0644]
message/output/email/lang/en/message_email.php
message/output/email/templates/email_digest_html.mustache
message/output/email/templates/email_digest_text.mustache
message/output/email/tests/event_observers_test.php [new file with mode: 0644]
message/output/email/tests/send_email_task_test.php
message/output/email/version.php
message/templates/message_drawer_view_conversation_body_confirm_dialogue.mustache
message/tests/api_test.php
message/tests/behat/behat_message.php
message/tests/behat/favourite_conversations.feature
message/tests/behat/group_conversation.feature
message/tests/behat/message_delete_conversation.feature
message/tests/behat/message_drawer_manage_contacts.feature
message/tests/behat/message_manage_preferences.feature
message/tests/behat/message_send_messages.feature
message/tests/behat/self_conversation.feature [new file with mode: 0644]
message/tests/behat/unread_messages.feature
message/tests/externallib_test.php
message/tests/helper_test.php
message/tests/privacy_provider_test.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/annotationstamp.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/lang/en/assign.php
mod/book/tool/print/classes/output/print_book_page.php
mod/book/tool/print/classes/output/renderer.php
mod/chat/lang/en/chat.php
mod/choice/lang/en/choice.php
mod/choice/mod_form.php
mod/data/lang/en/data.php
mod/data/mod_form.php
mod/feedback/classes/completion.php
mod/feedback/classes/structure.php
mod/feedback/item/feedback_item_form_class.php
mod/feedback/item/multichoice/multichoice_form.php
mod/feedback/item/multichoicerated/multichoicerated_form.php
mod/feedback/lib.php
mod/feedback/tests/behat/question_types.feature
mod/feedback/tests/behat/question_types_non_anon.feature
mod/feedback/tests/lib_test.php
mod/feedback/tests/privacy_test.php
mod/forum/amd/build/discussion.min.js
mod/forum/amd/build/discussion_list.min.js
mod/forum/amd/build/favourite_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/inpage_reply.min.js
mod/forum/amd/build/pin_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/posts_list.min.js
mod/forum/amd/build/repository.min.js
mod/forum/amd/build/selectors.min.js
mod/forum/amd/src/discussion.js
mod/forum/amd/src/discussion_list.js
mod/forum/amd/src/favourite_toggle.js [new file with mode: 0644]
mod/forum/amd/src/inpage_reply.js
mod/forum/amd/src/pin_toggle.js [new file with mode: 0644]
mod/forum/amd/src/posts_list.js
mod/forum/amd/src/repository.js
mod/forum/amd/src/selectors.js
mod/forum/classes/local/builders/exported_discussion.php [new file with mode: 0644]
mod/forum/classes/local/builders/exported_discussion_summaries.php
mod/forum/classes/local/builders/exported_posts.php
mod/forum/classes/local/container.php
mod/forum/classes/local/entities/discussion.php
mod/forum/classes/local/exporters/author.php
mod/forum/classes/local/exporters/discussion.php
mod/forum/classes/local/exporters/discussion_summaries.php
mod/forum/classes/local/exporters/discussion_summary.php
mod/forum/classes/local/exporters/forum.php
mod/forum/classes/local/exporters/post.php
mod/forum/classes/local/exporters/posts.php
mod/forum/classes/local/factories/builder.php
mod/forum/classes/local/factories/exporter.php
mod/forum/classes/local/factories/renderer.php
mod/forum/classes/local/factories/url.php
mod/forum/classes/local/managers/capability.php
mod/forum/classes/local/renderers/discussion.php
mod/forum/classes/local/renderers/discussion_list.php
mod/forum/classes/local/vaults/author.php
mod/forum/classes/local/vaults/db_table_vault.php
mod/forum/classes/local/vaults/discussion.php
mod/forum/classes/local/vaults/discussion_list.php
mod/forum/classes/local/vaults/forum.php
mod/forum/classes/local/vaults/post.php
mod/forum/classes/local/vaults/post_read_receipt_collection.php
mod/forum/classes/post_form.php
mod/forum/classes/privacy/provider.php
mod/forum/db/access.php
mod/forum/db/services.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/mod_form.php
mod/forum/pix/t/star.png [new file with mode: 0644]
mod/forum/pix/t/star.svg [new file with mode: 0644]
mod/forum/post.php
mod/forum/styles.css
mod/forum/templates/discussion_favourite_toggle.mustache [new file with mode: 0644]
mod/forum/templates/discussion_list.mustache
mod/forum/templates/discussion_lock_toggle.mustache
mod/forum/templates/discussion_pin_toggle.mustache [new file with mode: 0644]
mod/forum/templates/discussion_subscription_toggle.mustache
mod/forum/templates/forum_action_menu.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion.mustache
mod/forum/templates/forum_discussion_favourite_toggle.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion_post.mustache
mod/forum/templates/inpage_reply.mustache
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/discussion_lock.feature
mod/forum/tests/behat/favourite_discussion.feature [new file with mode: 0644]
mod/forum/tests/behat/inpage_reply.feature
mod/forum/tests/exporters_author_test.php
mod/forum/tests/exporters_forum_test.php
mod/forum/tests/exporters_post_test.php
mod/forum/tests/externallib_test.php
mod/forum/tests/vaults_author_test.php
mod/forum/tests/vaults_discussion_list_test.php
mod/forum/tests/vaults_post_test.php
mod/forum/upgrade.txt
mod/forum/version.php
mod/forum/view.php
mod/glossary/edit_form.php
mod/glossary/mod_form.php
mod/lesson/continue.php
mod/lesson/lang/en/lesson.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/essay.php
mod/lti/OAuthBody.php
mod/lti/amd/build/tool_card_controller.min.js
mod/lti/amd/src/tool_card_controller.js
mod/lti/auth.php [new file with mode: 0644]
mod/lti/backup/moodle2/backup_lti_stepslib.php
mod/lti/certs.php [new file with mode: 0644]
mod/lti/classes/external.php
mod/lti/classes/local/ltiservice/resource_base.php
mod/lti/classes/local/ltiservice/response.php
mod/lti/classes/local/ltiservice/service_base.php
mod/lti/classes/output/tool_configure_page.php
mod/lti/classes/task/clean_access_tokens.php [new file with mode: 0644]
mod/lti/contentitem.php
mod/lti/contentitem_return.php
mod/lti/db/install.php [new file with mode: 0644]
mod/lti/db/install.xml
mod/lti/db/services.php
mod/lti/db/tasks.php [new file with mode: 0644]
mod/lti/db/upgrade.php
mod/lti/edit_form.php
mod/lti/instructor_edit_tool_type.php
mod/lti/lang/en/lti.php
mod/lti/launch.php
mod/lti/locallib.php
mod/lti/mod_form.php
mod/lti/service.php
mod/lti/service/basicoutcomes/classes/local/resources/basicoutcomes.php [new file with mode: 0644]
mod/lti/service/basicoutcomes/classes/local/service/basicoutcomes.php [new file with mode: 0644]
mod/lti/service/basicoutcomes/classes/privacy/provider.php [new file with mode: 0644]
mod/lti/service/basicoutcomes/lang/en/ltiservice_basicoutcomes.php [new file with mode: 0644]
mod/lti/service/basicoutcomes/version.php [new file with mode: 0644]
mod/lti/service/gradebookservices/classes/local/resources/lineitem.php
mod/lti/service/gradebookservices/classes/local/resources/lineitems.php
mod/lti/service/gradebookservices/classes/local/resources/results.php
mod/lti/service/gradebookservices/classes/local/resources/scores.php
mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php
mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php
mod/lti/service/memberships/classes/local/resources/contextmemberships.php
mod/lti/service/memberships/classes/local/resources/linkmemberships.php
mod/lti/service/memberships/classes/local/service/memberships.php
mod/lti/service/memberships/lang/en/ltiservice_memberships.php
mod/lti/service/profile/classes/local/resources/profile.php
mod/lti/service/toolproxy/classes/local/resources/toolproxy.php
mod/lti/service/toolsettings/classes/local/resources/contextsettings.php
mod/lti/service/toolsettings/classes/local/resources/linksettings.php
mod/lti/service/toolsettings/classes/local/resources/systemsettings.php
mod/lti/service/toolsettings/classes/local/service/toolsettings.php
mod/lti/service/toolsettings/lang/en/ltiservice_toolsettings.php
mod/lti/servicelib.php
mod/lti/services.php
mod/lti/templates/tool_card.mustache
mod/lti/templates/tool_config_modal_body.mustache [new file with mode: 0644]
mod/lti/templates/tool_config_modal_footer.mustache [new file with mode: 0644]
mod/lti/tests/locallib_test.php
mod/lti/tests/task_clean_access_tokens_test.php [new file with mode: 0644]
mod/lti/token.php [new file with mode: 0644]
mod/lti/typessettings.php
mod/lti/upgrade.txt
mod/lti/version.php
mod/lti/view.php
mod/page/mod_form.php
mod/quiz/amd/build/modal_quiz_question_bank.min.js
mod/quiz/amd/src/modal_quiz_question_bank.js
mod/quiz/classes/output/edit_renderer.php
mod/quiz/classes/structure.php
mod/quiz/lang/en/quiz.php
mod/quiz/mod_form.php
mod/quiz/styles.css
mod/quiz/tests/behat/editing_add_from_question_bank.feature
mod/quiz/tests/behat/editing_remove_multiple_questions.feature
mod/quiz/tests/structure_test.php
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/quiz/yui/src/toolboxes/js/toolbox.js
mod/resource/mod_form.php
mod/scorm/mod_form.php
mod/scorm/report/interactions/classes/report.php
mod/scorm/report/objectives/classes/report.php
mod/url/mod_form.php
mod/workshop/mod_form.php
pix/moodlelogo-med-white.gif [deleted file]
pix/moodlelogo-med.gif [deleted file]
pix/moodlelogo-med.png [deleted file]
pix/moodlelogo.gif [deleted file]
pix/moodlelogo.png
pix/moodlelogo.svg
pix/moodlelogo_grayhat.png [new file with mode: 0644]
pix/moodlelogo_grayhat.svg [new file with mode: 0644]
pix/t/emptystar.png [new file with mode: 0644]
pix/t/emptystar.svg [new file with mode: 0644]
question/behaviour/behaviourbase.php
question/category.php
question/category_class.php
question/edit.php
question/engine/questionattempt.php
question/engine/tests/questionattempt_test.php
question/export.php
question/format.php
question/import.php
question/preview.php
question/previewlib.php
question/tests/events_test.php
question/type/calculated/datasetitems_form.php
question/type/calculated/questiontype.php
question/type/calculatedsimple/edit_calculatedsimple_form.php
question/type/calculatedsimple/questiontype.php
question/type/calculatedsimple/tests/questiontype_test.php
question/type/ddwtos/amd/build/ddwtos.min.js
question/type/ddwtos/amd/src/ddwtos.js
question/type/edit_question_form.php
question/type/multichoice/amd/build/clearchoice.min.js [new file with mode: 0644]
question/type/multichoice/amd/src/clearchoice.js [new file with mode: 0644]
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/question.php
question/type/multichoice/renderer.php
question/type/multichoice/tests/behat/preview.feature
question/type/multichoice/tests/question_multi_test.php
question/type/multichoice/tests/question_single_test.php
question/type/multichoice/tests/walkthrough_test.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/questiontype.php
question/type/numerical/tests/behat/add.feature [new file with mode: 0644]
question/type/numerical/tests/behat/backup_and_restore.feature [new file with mode: 0644]
question/type/numerical/tests/behat/edit.feature [new file with mode: 0644]
question/type/numerical/tests/behat/export.feature [new file with mode: 0644]
question/type/numerical/tests/behat/import.feature [new file with mode: 0644]
question/type/numerical/tests/behat/preview.feature [new file with mode: 0644]
question/type/numerical/tests/fixtures/testquestion.moodle.xml [new file with mode: 0644]
question/type/questionbase.php
question/type/questiontypebase.php
report/competency/amd/build/grading_popup.min.js
report/competency/amd/build/user_course_navigation.min.js
report/competency/amd/src/grading_popup.js
report/competency/amd/src/user_course_navigation.js
report/competency/classes/external.php
report/competency/classes/output/report.php
report/competency/classes/output/user_course_navigation.php
report/competency/index.php
report/competency/lang/en/report_competency.php
report/competency/lib.php
report/competency/templates/report.mustache
report/competency/templates/user_course_navigation.mustache
report/competency/tests/behat/breakdown_by_activity.feature [new file with mode: 0644]
repository/dropbox/lang/en/repository_dropbox.php
repository/filesystem/lang/en/repository_filesystem.php
repository/url/locallib.php
repository/user/pix/icon.png
repository/user/pix/icon.svg
tag/classes/external/tag_area_exporter.php
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/pending.min.js [new file with mode: 0644]
theme/boost/amd/src/loader.js
theme/boost/amd/src/pending.js [new file with mode: 0644]
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/core_form/element-float-inline.mustache [new file with mode: 0644]
theme/boost/templates/core_form/element-float.mustache [new file with mode: 0644]
theme/classic/lang/en/theme_classic.php
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
user/edit_form.php
user/editadvanced_form.php
user/emailupdate.php
user/tests/behat/addnewuser.feature
user/tests/behat/view_full_profile.feature
version.php

index 14e635f..a262404 100644 (file)
@@ -64,6 +64,7 @@ lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
 lib/geopattern-php/
+lib/php-jwt/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 128c038..6bcaefe 100644 (file)
@@ -65,6 +65,7 @@ lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
 lib/geopattern-php/
+lib/php-jwt/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 1e5837f..e9203ec 100644 (file)
@@ -710,7 +710,7 @@ if ($interactive) {
     cli_separator();
     cli_heading(get_string('cliadminemail', 'install'));
     $prompt = get_string('clitypevaluedefault', 'admin', $options['adminemail']);
-    $options['adminemail'] = cli_input($prompt);
+    $options['adminemail'] = cli_input($prompt, $options['adminemail']);
 }
 
 // Validate that the address provided was an e-mail address.
index dfbb1d6..5270745 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
+
+    $settings = new admin_settingpage('analyticssite', new lang_string('analyticssiteinfo', 'analytics'));
+    $ADMIN->add('analytics', $settings);
+
+    if ($ADMIN->fulltree) {
+        $modeinstructions = [
+            'facetoface' => get_string('modeinstructionfacetoface', 'analytics'),
+            'blendedhybrid' => get_string('modeinstructionblendedhybrid', 'analytics'),
+            'fullyonline' => get_string('modeinstructionfullyonline', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/modeinstruction', get_string('modeinstruction', 'analytics'),
+            '', [], $modeinstructions));
+
+        $settings->add(new admin_setting_configtext_with_maxlength('analytics/percentonline',
+            get_string('percentonline', 'analytics'),
+            get_string('percentonline_help', 'analytics'), '', PARAM_INT, 3, 3));
+
+        $typeinstitutions = [
+            'typeinstitutionacademic' => get_string('typeinstitutionacademic', 'analytics'),
+            'typeinstitutiontraining' => get_string('typeinstitutiontraining', 'analytics'),
+            'typeinstitutionngo' => get_string('typeinstitutionngo', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/typeinstitution', get_string('typeinstitution', 'analytics'),
+            '', [], $typeinstitutions));
+
+        $levelinstitutions = [
+            'levelinstitutionisced0' => get_string('levelinstitutionisced0', 'analytics'),
+            'levelinstitutionisced1' => get_string('levelinstitutionisced1', 'analytics'),
+            'levelinstitutionisced2' => get_string('levelinstitutionisced2', 'analytics'),
+            'levelinstitutionisced3' => get_string('levelinstitutionisced3', 'analytics'),
+            'levelinstitutionisced4' => get_string('levelinstitutionisced4', 'analytics'),
+            'levelinstitutionisced5' => get_string('levelinstitutionisced5', 'analytics'),
+            'levelinstitutionisced6' => get_string('levelinstitutionisced6', 'analytics'),
+            'levelinstitutionisced7' => get_string('levelinstitutionisced7', 'analytics'),
+            'levelinstitutionisced8' => get_string('levelinstitutionisced8', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/levelinstitution',
+            get_string('levelinstitution', 'analytics'), '', [], $levelinstitutions));
+    }
+
     $settings = new admin_settingpage('analyticssettings', new lang_string('analyticssettings', 'analytics'));
     $ADMIN->add('analytics', $settings);
 
     if ($ADMIN->fulltree) {
+
+
         // Select the site prediction's processor.
         $predictionprocessors = \core_analytics\manager::get_all_prediction_processors();
         $predictors = array();
index 276582c..0ea5193 100644 (file)
@@ -57,9 +57,11 @@ if (($hassiteconfig || has_any_capability(array(
             new lang_string('badgesalt_desc', 'badges'),
             'badges' . $SITE->timecreated, PARAM_ALPHANUM));
 
-    $globalsettings->add(new admin_setting_configcheckbox('badges_allowexternalbackpack',
-            new lang_string('allowexternalbackpack', 'badges'),
-            new lang_string('allowexternalbackpack_desc', 'badges'), 1));
+    $backpacks = badges_get_site_backpacks();
+    $choices = array();
+    foreach ($backpacks as $backpack) {
+        $choices[$backpack->id] = $backpack->backpackweburl;
+    }
 
     $globalsettings->add(new admin_setting_configcheckbox('badges_allowcoursebadges',
             new lang_string('allowcoursebadges', 'badges'),
@@ -91,4 +93,32 @@ if (($hassiteconfig || has_any_capability(array(
             array('moodle/badges:createbadge'), empty($CFG->enablebadges)
         )
     );
+    $backpacksettings = new admin_settingpage('backpacksettings', new lang_string('backpacksettings', 'badges'),
+            array('moodle/badges:manageglobalsettings'), empty($CFG->enablebadges));
+
+    $backpacksettings->add(new admin_setting_configcheckbox('badges_allowexternalbackpack',
+            new lang_string('allowexternalbackpack', 'badges'),
+            new lang_string('allowexternalbackpack_desc', 'badges'), 1));
+
+    $backpacksettings->add(new admin_setting_configselect('badges_site_backpack',
+            new lang_string('sitebackpack', 'badges'),
+            new lang_string('sitebackpack_help', 'badges'),
+            1, $choices));
+
+    $warning = badges_verify_site_backpack();
+    if (!empty($warning)) {
+        $backpacksettings->add(new admin_setting_description('badges_site_backpack_verify',
+            new lang_string('sitebackpackverify', 'badges'),
+            $warning));
+    }
+
+    $ADMIN->add('badges', $backpacksettings);
+
+    $ADMIN->add('badges',
+        new admin_externalpage('managebackpacks',
+            new lang_string('managebackpacks', 'badges'),
+            new moodle_url('/badges/backpacks.php'),
+            array('moodle/badges:manageglobalsettings'), empty($CFG->enablebadges) || empty($CFG->badges_allowexternalbackpack)
+        )
+    );
 }
index 3adbf0b..901a38a 100644 (file)
@@ -174,12 +174,11 @@ if ($hassiteconfig
         // Options include fields from the user table that might be helpful to
         // distinguish when adding or listing users ('I want to add the John
         // Smith from Science faculty').
-        // Username is not included as an option because in some sites, it might
-        // be a security problem to reveal usernames even to trusted staff.
         // Custom user profile fields are not currently supported.
         $temp->add(new admin_setting_configmulticheckbox('showuseridentity',
                 new lang_string('showuseridentity', 'admin'),
                 new lang_string('showuseridentity_desc', 'admin'), array('email' => 1), array(
+                    'username'    => new lang_string('username'),
                     'idnumber'    => new lang_string('idnumber'),
                     'email'       => new lang_string('email'),
                     'phone1'      => new lang_string('phone1'),
@@ -255,4 +254,4 @@ if ($hassiteconfig) {
         new lang_string('sitepolicyguest_help', 'core_admin'), (isset($CFG->sitepolicy) ? $CFG->sitepolicy : ''), PARAM_RAW));
 
     $ADMIN->add('privacy', $temp);
-}
\ No newline at end of file
+}
index d2faa4a..d20dabc 100644 (file)
@@ -40,7 +40,7 @@
     <div class="form-inline">
         <input type="text" size="5" id="{{id}}v" name="{{name}}[v]" value="{{value}}" class="form-control text-ltr">
         <label class="sr-only" for="{{id}}u">{{#str}}durationunits, admin{{/str}}</label>
-        <select id="{{id}}u" name="{{name}}[u]" class="form-control">
+        <select id="{{id}}u" name="{{name}}[u]" class="form-control custom-select">
             {{#options}}
                 <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
             {{/options}}
index 65dffa3..29eeb4b 100644 (file)
@@ -1,57 +1,74 @@
-@core @core_admin
-Feature: Enable multiple accounts to have the same email address
-  In order to have multiple accounts registerd on the system with the same email address
+@core @core_admin @core_user
+Feature: Allowing multiple accounts to have the same email address
+  In order to manage user accounts
   As an admin
-  I need to enable multiple accounts to be registered with the same email address and verify it is applied
+  I need to be able to set whether to allow multiple accounts with the same email or not
 
-  Background:
-    Given I log in as "admin"
-
-  Scenario: Enable registration of multiple accounts with the same email address
+  Scenario Outline: Create a user with the same email as an existing user
     Given the following config values are set as admin:
-      | allowaccountssameemail | 1 |
-    When I navigate to "Users > Accounts > Add a new user" in site administration
-    And I set the following fields to these values:
-      | Username                        | testmultiemailuser1             |
-      | Choose an authentication method | Manual accounts                 |
-      | New password                    | test@User1                      |
-      | First name                      | Test                            |
-      | Surname                         | Multi1                          |
-      | Email address                   | testmultiemailuser@example.com  |
-    And I press "Create user"
-    And I should see "Test Multi1"
-    And I press "Add a new user"
+      | allowaccountssameemail | <allowsameemail> |
+    And the following "users" exist:
+      | username  | firstname | lastname | email           |
+      | s1        | John      | Doe      | s1@example.com  |
+    When I log in as "admin"
+    And I navigate to "Users > Accounts > Add a new user" in site administration
     And I set the following fields to these values:
-      | Username                        | testmultiemailuser2             |
-      | Choose an authentication method | Manual accounts                 |
-      | New password                    | test@User2                      |
-      | First name                      | Test                            |
-      | Surname                         | Multi2                          |
-      | Email address                   | testmultiemailuser@example.com  |
+      | Username      | s2      |
+      | First name    | Jane    |
+      | Surname       | Doe     |
+      | Email address | <email> |
+      | New password  | test    |
     And I press "Create user"
-    Then I should see "Test Multi2"
-    And I should not see "This email address is already registered"
+    Then I should <expect> "This email address is already registered."
 
-  Scenario: Disable registration of multiple accounts with the same email address
+    Examples:
+      | allowsameemail | email          | expect  |
+      | 0              | s1@example.com | see     |
+      | 0              | S1@EXAMPLE.COM | see     |
+      | 1              | s1@example.com | not see |
+      | 1              | S1@EXAMPLE.COM | not see |
+
+  Scenario Outline: Update a user with the same email as an existing user
     Given the following config values are set as admin:
-      | allowaccountssameemail | 0 |
-    When I navigate to "Users > Accounts > Add a new user" in site administration
-    And I set the following fields to these values:
-      | Username                        | testmultiemailuser1             |
-      | Choose an authentication method | Manual accounts                 |
-      | New password                    | test@User1                      |
-      | First name                      | Test                            |
-      | Surname                         | Multi1                          |
-      | Email address                   | testmultiemailuser@example.com  |
-    And I press "Create user"
-    And I should see "Test Multi1"
-    And I press "Add a new user"
-    And I set the following fields to these values:
-      | Username                        | testmultiemailuser2             |
-      | Choose an authentication method | Manual accounts                 |
-      | New password                    | test@User2                      |
-      | First name                      | Test                            |
-      | Surname                         | Multi2                          |
-      | Email address                   | testmultiemailuser@example.com  |
-    And I press "Create user"
-    Then I should see "This email address is already registered"
\ No newline at end of file
+      | allowaccountssameemail | <allowsameemail> |
+    And the following "users" exist:
+      | username  | firstname | lastname | email           |
+      | s1        | John      | Doe      | s1@example.com  |
+      | s2        | Jane      | Doe      | s2@example.com  |
+    When I log in as "admin"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
+    And I click on "Edit" "link" in the "Jane Doe" "table_row"
+    And I set the field "Email address" to "<email>"
+    And I press "Update profile"
+    Then I should <expect> "This email address is already registered."
+
+    Examples:
+      | allowsameemail | email          | expect  |
+      | 0              | s1@example.com | see     |
+      | 0              | S1@EXAMPLE.COM | see     |
+      | 1              | s1@example.com | not see |
+      | 1              | S1@EXAMPLE.COM | not see |
+      | 0              | S2@EXAMPLE.COM | not see |
+      | 1              | S2@EXAMPLE.COM | not see |
+
+  Scenario Outline: Update own user profile with the same email as an existing user
+    Given the following config values are set as admin:
+      | allowaccountssameemail | <allowsameemail> |
+    And the following "users" exist:
+      | username  | firstname | lastname | email           |
+      | s1        | John      | Doe      | s1@example.com  |
+      | s2        | Jane      | Doe      | s2@example.com  |
+    When I log in as "s2"
+    And I open my profile in edit mode
+    And I set the field "Email address" to "<email>"
+    And I press "Update profile"
+    Then I should <expect> "This email address is already registered."
+
+    Examples:
+      | allowsameemail | email          | expect  |
+      | 0              | s1@example.com | see     |
+      | 0              | S1@EXAMPLE.COM | see     |
+      | 1              | s1@example.com | not see |
+      | 1              | S1@EXAMPLE.COM | not see |
+      | 0              | S2@EXAMPLE.COM | not see |
+      | 1              | S2@EXAMPLE.COM | not see |
index 7e4d208..9e7f30d 100644 (file)
@@ -43,7 +43,7 @@ $string['componentselectnone'] = 'Unselect all';
 $string['createmodel'] = 'Create model';
 $string['currenttimesplitting'] = 'Current time-splitting method';
 $string['delete'] = 'Delete';
-$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes can not be reverted.';
+$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes cannot be reverted.';
 $string['disabled'] = 'Disabled';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
@@ -98,7 +98,7 @@ $string['invalidindicatorsremoved'] = 'A new model has been added. Indicators th
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
-$string['missingmoodleversion'] = 'Imported file does not define a moodle version number';
+$string['missingmoodleversion'] = 'Imported file doesn\'t define a version number';
 $string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelname'] = 'Model name';
@@ -120,7 +120,7 @@ $string['previouspage'] = 'Previous page';
 $string['restoredefault'] = 'Restore default models';
 $string['restoredefaultempty'] = 'Please select models to be restored.';
 $string['restoredefaultinfo'] = 'These default models are missing or have changed since being installed. You can restore selected default models.';
-$string['restoredefaultnone'] = 'All default models provided by the Moodle core and installed plugins have been already created. No new models were found, there is nothing to restore.';
+$string['restoredefaultnone'] = 'All default models provided by core and installed plugins have been created. No new models were found; there is nothing to restore.';
 $string['restoredefaultsome'] = 'Succesfully re-created {$a->count} new model(s).';
 $string['restoredefaultsubmit'] = 'Restore selected';
 $string['samestartdate'] = 'Current start date is good';
@@ -135,7 +135,7 @@ $string['trainandpredictmodel'] = 'Training model and calculating predictions';
 $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
 $string['trainmodels'] = 'Train models';
-$string['versionnotsame'] = 'Imported file was from a different moodle version ({$a->importedversion}) than the current one ({$a->version})';
+$string['versionnotsame'] = 'Imported file was from a different version ({$a->importedversion}) than the current one ({$a->version})';
 $string['viewlog'] = 'Evaluation log';
 $string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
 $string['weeksenddatedefault'] = 'End date automatically calculated from the course start date.';
index 74ff239..f819e5f 100644 (file)
@@ -65,7 +65,7 @@ Feature: Restoring default models
     And I should see "Analytics models"
     And I should see "No teaching"
     When I click on "Restore default models" "link"
-    Then I should see "All default models provided by the Moodle core and installed plugins have been already created. No new models were found, there is nothing to restore."
+    Then I should see "All default models provided by core and installed plugins have been created. No new models were found; there is nothing to restore."
     And I click on "Back" "link"
     And I should see "Analytics models"
 
index 95c09e9..a90f1fc 100644 (file)
@@ -85,7 +85,7 @@ class helper {
         } else {
             $name = $record->eventname;
             $description = "Unknown event ({$name})";
-            $other = unserialize($record->other);
+            $other = \tool_log\helper\reader::decode_other($record->other);
         }
 
         $realuserid = $record->realuserid;
index 1f5ddbe..6d24fd8 100644 (file)
Binary files a/admin/tool/lp/amd/build/competencies.min.js and b/admin/tool/lp/amd/build/competencies.min.js differ
index 5fe246e..fe53dc6 100644 (file)
Binary files a/admin/tool/lp/amd/build/course_competency_settings.min.js and b/admin/tool/lp/amd/build/course_competency_settings.min.js differ
diff --git a/admin/tool/lp/amd/build/module_navigation.min.js b/admin/tool/lp/amd/build/module_navigation.min.js
new file mode 100644 (file)
index 0000000..db9d241
Binary files /dev/null and b/admin/tool/lp/amd/build/module_navigation.min.js differ
index 1b82116..e3a5176 100644 (file)
@@ -144,7 +144,7 @@ define(['jquery',
                     });
                     requests.push({
                         methodname: 'tool_lp_data_for_course_competencies_page',
-                        args: {courseid: self.itemid}
+                        args: {courseid: self.itemid, moduleid: 0}
                     });
 
                     pagerender = 'tool_lp/course_competencies_page';
@@ -212,7 +212,7 @@ define(['jquery',
                 {methodname: 'core_competency_remove_competency_from_course',
                     args: {courseid: localthis.itemid, competencyid: deleteid}},
                 {methodname: 'tool_lp_data_for_course_competencies_page',
-                    args: {courseid: localthis.itemid}}
+                    args: {courseid: localthis.itemid, moduleid: 0}}
             ]);
             pagerender = 'tool_lp/course_competencies_page';
             pageregion = 'coursecompetenciespage';
@@ -311,7 +311,7 @@ define(['jquery',
                     {methodname: 'core_competency_set_course_competency_ruleoutcome',
                       args: {coursecompetencyid: coursecompetencyid, ruleoutcome: ruleoutcome}},
                     {methodname: 'tool_lp_data_for_course_competencies_page',
-                      args: {courseid: localthis.itemid}}
+                      args: {courseid: localthis.itemid, moduleid: 0}}
                 ]);
 
                 requests[1].done(function(context) {
index 06af055..ff21ade 100644 (file)
@@ -134,7 +134,7 @@ define(['jquery',
 
         ajax.call([
             {methodname: 'tool_lp_data_for_course_competencies_page',
-              args: {courseid: courseId}}
+              args: {courseid: courseId, moduleid: 0}}
         ])[0].done(function(context) {
             templates.render('tool_lp/course_competencies_page', context).done(function(html, js) {
                 $('[data-region="coursecompetenciespage"]').replaceWith(html);
diff --git a/admin/tool/lp/amd/src/module_navigation.js b/admin/tool/lp/amd/src/module_navigation.js
new file mode 100644 (file)
index 0000000..d08a550
--- /dev/null
@@ -0,0 +1,62 @@
+// 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/>.
+
+/**
+ * Module to navigation between users in a course.
+ *
+ * @package    tool_lp
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery'], function($) {
+
+    /**
+     * ModuleNavigation
+     *
+     * @param {String} moduleSelector The selector of the module element.
+     * @param {String} baseUrl The base url for the page (no params).
+     * @param {Number} courseId The course id
+     * @param {Number} moduleId The activity module (filter)
+     */
+    var ModuleNavigation = function(moduleSelector, baseUrl, courseId, moduleId) {
+        this._baseUrl = baseUrl;
+        this._moduleId = moduleId;
+        this._courseId = courseId;
+
+        $(moduleSelector).on('change', this._moduleChanged.bind(this));
+    };
+
+    /**
+     * The module was changed in the select list.
+     *
+     * @method _moduleChanged
+     * @param {Event} e the event
+     */
+    ModuleNavigation.prototype._moduleChanged = function(e) {
+        var newModuleId = $(e.target).val();
+        var queryStr = '?mod=' + newModuleId + '&courseid=' + this._courseId;
+        document.location = this._baseUrl + queryStr;
+    };
+
+    /** @type {Number} The id of the course. */
+    ModuleNavigation.prototype._courseId = null;
+    /** @type {Number} The id of the module. */
+    ModuleNavigation.prototype._moduleId = null;
+    /** @type {String} Plugin base url. */
+    ModuleNavigation.prototype._baseUrl = null;
+
+    return /** @alias module:tool_lp/module_navigation */ ModuleNavigation;
+});
index d79317c..ada3e88 100644 (file)
@@ -363,7 +363,13 @@ class external extends external_api {
             'The course id',
             VALUE_REQUIRED
         );
-        $params = array('courseid' => $courseid);
+        $moduleid = new external_value(
+            PARAM_INT,
+            'The module id',
+            VALUE_DEFAULT,
+            0
+        );
+        $params = array('courseid' => $courseid, 'moduleid' => $moduleid);
         return new external_function_parameters($params);
     }
 
@@ -371,16 +377,18 @@ class external extends external_api {
      * Loads the data required to render the course_competencies_page template.
      *
      * @param int $courseid The course id to check.
+     * @param int $moduleid The module id to check (0 for no filter).
      * @return boolean
      */
-    public static function data_for_course_competencies_page($courseid) {
+    public static function data_for_course_competencies_page($courseid, $moduleid) {
         global $PAGE;
         $params = self::validate_parameters(self::data_for_course_competencies_page_parameters(), array(
             'courseid' => $courseid,
+            'moduleid' => $moduleid,
         ));
         self::validate_context(context_course::instance($params['courseid']));
 
-        $renderable = new output\course_competencies_page($params['courseid']);
+        $renderable = new output\course_competencies_page($params['courseid'], $params['moduleid']);
         $renderer = $PAGE->get_renderer('tool_lp');
 
         $data = $renderable->export_for_template($renderer);
@@ -425,6 +433,7 @@ class external extends external_api {
                 ),
             ))),
             'manageurl' => new external_value(PARAM_LOCALURL, 'Url to the manage competencies page.'),
+            'pluginbaseurl' => new external_value(PARAM_LOCALURL, 'Url to the course competencies page.'),
         ));
 
     }
index 218d8a5..830cf8a 100644 (file)
@@ -57,6 +57,9 @@ class course_competencies_page implements renderable, templatable {
     /** @var int $courseid Course id for this page. */
     protected $courseid = null;
 
+    /** @var int $moduleid Module id for this page. */
+    protected $moduleid = null;
+
     /** @var context $context The context for this page. */
     protected $context = null;
 
@@ -76,10 +79,31 @@ class course_competencies_page implements renderable, templatable {
      * Construct this renderable.
      * @param int $courseid The course record for this page.
      */
-    public function __construct($courseid) {
+    public function __construct($courseid, $moduleid) {
         $this->context = context_course::instance($courseid);
         $this->courseid = $courseid;
+        $this->moduleid = $moduleid;
         $this->coursecompetencylist = api::list_course_competencies($courseid);
+
+        if ($this->moduleid > 0) {
+            $modulecompetencies = api::list_course_module_competencies_in_course_module($this->moduleid);
+            foreach ($this->coursecompetencylist as $ccid => $coursecompetency) {
+                $coursecompetency = $coursecompetency['coursecompetency'];
+                $found = false;
+                foreach ($modulecompetencies as $mcid => $modulecompetency) {
+                    if ($modulecompetency->get('competencyid') == $coursecompetency->get('competencyid')) {
+                        $found = true;
+                        break;
+                    }
+                }
+
+                if (!$found) {
+                    // We need to filter out this competency.
+                    unset($this->coursecompetencylist[$ccid]);
+                }
+            }
+        }
+
         $this->canmanagecoursecompetencies = has_capability('moodle/competency:coursecompetencymanage', $this->context);
         $this->canconfigurecoursecompetencies = has_capability('moodle/competency:coursecompetencyconfigure', $this->context);
         $this->cangradecompetencies = has_capability('moodle/competency:competencygrade', $this->context);
@@ -112,6 +136,7 @@ class course_competencies_page implements renderable, templatable {
 
         $data = new stdClass();
         $data->courseid = $this->courseid;
+        $data->moduleid = $this->moduleid;
         $data->pagecontextid = $this->context->id;
         $data->competencies = array();
         $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
@@ -120,6 +145,24 @@ class course_competencies_page implements renderable, templatable {
         if ($gradable) {
             $usercompetencycourses = api::list_user_competencies_in_course($this->courseid, $USER->id);
             $data->gradableuserid = $USER->id;
+
+            if ($this->moduleid > 0) {
+                $modulecompetencies = api::list_course_module_competencies_in_course_module($this->moduleid);
+                foreach ($usercompetencycourses as $ucid => $usercoursecompetency) {
+                    $found = false;
+                    foreach ($modulecompetencies as $mcid => $modulecompetency) {
+                        if ($modulecompetency->get('competencyid') == $usercoursecompetency->get('competencyid')) {
+                            $found = true;
+                            break;
+                        }
+                    }
+
+                    if (!$found) {
+                        // We need to filter out this competency.
+                        unset($usercompetencycourses[$ucid]);
+                    }
+                }
+            }
         }
 
         $ruleoutcomelist = course_competency::get_ruleoutcome_list();
diff --git a/admin/tool/lp/classes/output/module_navigation.php b/admin/tool/lp/classes/output/module_navigation.php
new file mode 100644 (file)
index 0000000..68ff9b0
--- /dev/null
@@ -0,0 +1,103 @@
+<?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/>.
+
+/**
+ * User navigation class.
+ *
+ * @package    tool_lp
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_lp\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+use context_course;
+use core_course\external\course_module_summary_exporter;
+use stdClass;
+
+/**
+ * User course navigation class.
+ *
+ * @package    tool_lp
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class module_navigation implements renderable, templatable {
+
+    /** @var courseid */
+    protected $courseid;
+
+    /** @var moduleid */
+    protected $moduleid;
+
+    /** @var baseurl */
+    protected $baseurl;
+
+    /**
+     * Construct.
+     *
+     * @param int $courseid
+     * @param int $moduleid
+     * @param string $baseurl
+     */
+    public function __construct($courseid, $moduleid, $baseurl) {
+        $this->courseid = $courseid;
+        $this->moduleid = $moduleid;
+        $this->baseurl = $baseurl;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+
+        $context = context_course::instance($this->courseid);
+
+        $data = new stdClass();
+        $data->courseid = $this->courseid;
+        $data->moduleid = $this->moduleid;
+        $data->baseurl = $this->baseurl;
+        $data->hasmodules = false;
+        $data->modules = array();
+
+        $data->hasmodules = true;
+        $data->modules = array();
+        $empty = (object)['id' => 0, 'name' => get_string('nofiltersapplied')];
+        $data->modules[] = $empty;
+
+        $modinfo = get_fast_modinfo($this->courseid);
+        foreach ($modinfo->get_cms() as $cm) {
+            if ($cm->uservisible) {
+                $exporter = new course_module_summary_exporter(null, ['cm' => $cm]);
+                $module = $exporter->export($output);
+                if ($module->id == $this->moduleid) {
+                    $module->selected = true;
+                }
+                $data->modules[] = $module;
+                $data->hasmodules = true;
+            }
+        }
+
+        return $data;
+    }
+}
index ca45e29..3907215 100644 (file)
@@ -263,4 +263,16 @@ class renderer extends plugin_renderer_base {
         $n = new \core\output\notification($message, \core\output\notification::NOTIFY_SUCCESS);
         return $this->render($n);
     }
+
+    /**
+     * Defer to template.
+     *
+     * @param module_navigation $nav
+     * @return string
+     */
+    public function render_module_navigation(module_navigation $nav) {
+        $data = $nav->export_for_template($this);
+        return parent::render_from_template('tool_lp/module_navigation', $data);
+    }
+
 }
index e700a17..895b96a 100644 (file)
 require_once(__DIR__ . '/../../../config.php');
 
 $id = required_param('courseid', PARAM_INT);
+$currentmodule = optional_param('mod', 0, PARAM_INT);
+if ($currentmodule > 0) {
+    $cm = get_coursemodule_from_id('', $currentmodule, 0, false, MUST_EXIST);
+}
 
 $params = array('id' => $id);
 $course = $DB->get_record('course', $params, '*', MUST_EXIST);
@@ -33,16 +37,22 @@ require_login($course);
 \core_competency\api::require_enabled();
 
 $context = context_course::instance($course->id);
-$urlparams = array('courseid' => $id);
+$urlparams = array('courseid' => $id, 'mod' => $currentmodule);
 
 $url = new moodle_url('/admin/tool/lp/coursecompetencies.php', $urlparams);
 
 list($title, $subtitle) = \tool_lp\page_helper::setup_for_course($url, $course);
+if ($currentmodule > 0) {
+    $title = get_string('filtermodule', 'report_competency', format_string($cm->name));
+}
 
 $output = $PAGE->get_renderer('tool_lp');
-$page = new \tool_lp\output\course_competencies_page($course->id);
+$page = new \tool_lp\output\course_competencies_page($course->id, $currentmodule);
 
 echo $output->header();
+$baseurl = new moodle_url('/admin/tool/lp/coursecompetencies.php');
+$nav = new \tool_lp\output\module_navigation($course->id, $currentmodule, $baseurl);
+echo $output->render($nav);
 echo $output->heading($title);
 
 echo $output->render($page);
index 156299c..c4db7ba 100644 (file)
@@ -107,6 +107,7 @@ $string['editthisuserevidence'] = 'Edit this evidence';
 $string['edituserevidence'] = 'Edit evidence';
 $string['evidence'] = 'Evidence';
 $string['findcourses'] = 'Find courses';
+$string['filterbyactivity'] = 'Filter competencies by resource or activity';
 $string['frameworkcannotbedeleted'] = 'The competency framework \'{$a}\' cannot be deleted';
 $string['hidden'] = 'Hidden';
 $string['hiddenhint'] = '(hidden)';
@@ -145,6 +146,7 @@ $string['nfiles'] = '{$a} file(s)';
 $string['noactivities'] = 'No activities';
 $string['nocompetencies'] = 'No competencies have been created in this framework.';
 $string['nocompetenciesincourse'] = 'No competencies have been linked to this course.';
+$string['nocompetenciesinactivity'] = 'No competencies have been linked to this activity or resource.';
 $string['nocompetenciesinevidence'] = 'No competencies have been linked to this evidence.';
 $string['nocompetenciesinlearningplan'] = 'No competencies have been linked to this learning plan.';
 $string['nocompetenciesintemplate'] = 'No competencies have been linked to this learning plan template.';
index b41af9b..8de78a0 100644 (file)
 </table>
 {{^competencies}}
 <p class="alert alert-info">
-    {{#str}}nocompetenciesincourse, tool_lp{{/str}}
+    {{#moduleid}}
+        {{#str}}nocompetenciesinactivity, tool_lp{{/str}}
+    {{/moduleid}}
+    {{^moduleid}}
+        {{#str}}nocompetenciesincourse, tool_lp{{/str}}
+    {{/moduleid}}
 </p>
 {{/competencies}}
 </div>
diff --git a/admin/tool/lp/templates/module_navigation.mustache b/admin/tool/lp/templates/module_navigation.mustache
new file mode 100644 (file)
index 0000000..aa10239
--- /dev/null
@@ -0,0 +1,52 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_lp/module_navigation
+
+    Show an auto-complete for filtering by competencies linked to a module.
+
+    Context variables required for this template:
+    * hasmodules
+    * modules - array
+      * id
+      * selected
+      * name
+
+    // No example context because the JS is connected to webservices
+}}
+<div class="float-right card p-2">
+<form class="user-competency-course-navigation">
+{{#hasmodules}}
+<span>
+<label for="module-nav-{{uniqid}}" class="accesshide">{{#str}}filterbyactivity, tool_lp{{/str}}</label>
+<select id="module-nav-{{uniqid}}">
+{{#modules}}
+<option value="{{id}}" {{#selected}}selected="selected"{{/selected}}>{{name}}</option>
+{{/modules}}
+</select>
+</span>
+{{/hasmodules}}
+</form>
+</div>
+{{#js}}
+require(['core/form-autocomplete', 'tool_lp/module_navigation'], function(autocomplete, nav) {
+    (new nav('#module-nav-{{uniqid}}', '{{baseurl}}', {{courseid}}, {{moduleid}}));
+{{#hasmodules}}
+    autocomplete.enhance('#module-nav-{{uniqid}}', false, false, {{# quote }}{{# str }}filterbyactivity, tool_lp{{/ str }}{{/ quote }});
+{{/hasmodules}}
+});
+{{/js}}
index 9ffc29a..331c3e1 100644 (file)
@@ -35,7 +35,7 @@
 
     // No example context because the JS is connected to webservices
 }}
-<div class="float-sm-right card card-block">
+<div class="float-sm-right card card-block p-x-1 p-b-1">
 <p>{{{groupselector}}}</p>
 <form class="user-competency-course-navigation">
 {{#hasusers}}
diff --git a/admin/tool/lp/tests/behat/course_competencies.feature b/admin/tool/lp/tests/behat/course_competencies.feature
new file mode 100644 (file)
index 0000000..f765213
--- /dev/null
@@ -0,0 +1,63 @@
+@report @javascript @tool_lp
+Feature: See the competencies for an activity on the course competencies page.
+  As a student
+  In order to see only the competencies for an activity in the course competencies page.
+
+  Background:
+    Given the following lp "frameworks" exist:
+      | shortname | idnumber |
+      | Test-Framework | ID-FW1 |
+    And the following lp "competencies" exist:
+      | shortname | framework |
+      | Test-Comp1 | ID-FW1 |
+      | Test-Comp2 | ID-FW1 |
+    Given the following "courses" exist:
+      | shortname | fullname   |
+      | C1        | Course 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | name       | intro      | course | idnumber |
+      | page     | PageName1  | PageDesc1  | C1     | PAGE1    |
+      | page     | PageName2  | PageDesc2  | C1     | PAGE2    |
+    And I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "Competencies"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "Test-Comp1" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "Test-Comp2" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I am on "Course 1" course homepage
+    And I follow "PageName1"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Expand all"
+    And I set the field "Course competencies" to "Test-Comp1"
+    And I press "Save and return to course"
+    And I log out
+
+  @javascript
+  Scenario: Go to the competency course competencies page.
+    When I log in as "student1"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "Competencies"
+    Then I should see "Test-Comp1"
+    And I should see "Test-Comp2"
+    And I set the field "Filter competencies by resource or activity" to "PageName1"
+    And I press key "13" in the field "Filter competencies by resource or activity"
+    And I should see "Test-Comp1"
+    And I should not see "Test-Comp2"
+    And I set the field "Filter competencies by resource or activity" to "PageName2"
+    And I press key "13" in the field "Filter competencies by resource or activity"
+    And I should not see "Test-Comp1"
+    And I should not see "Test-Comp2"
+    And I should see "No competencies have been linked to this activity or resource."
index 44c9521..450570a 100644 (file)
@@ -461,4 +461,33 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('A', $summary->evidence[1]->gradename);
     }
 
+    public function test_data_for_course_competency_page() {
+        $this->setAdminUser();
+
+        $dg = $this->getDataGenerator();
+        $lpg = $dg->get_plugin_generator('core_competency');
+        $f1 = $lpg->create_framework();
+        $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
+        $course1 = $dg->create_course(array('category' => $this->category->id));
+        $cc = api::add_competency_to_course($course1->id, $c1->get('id'));
+
+        $evidence = \core_competency\external::grade_competency($this->user->id, $c1->get('id'), 1, true);
+        $evidence = \core_competency\external::grade_competency($this->user->id, $c1->get('id'), 2, true);
+
+        $pagegenerator = $this->getDataGenerator()->get_plugin_generator('mod_page');
+        $page = $pagegenerator->create_instance(array('course' => $course1->id));
+        $page2 = $pagegenerator->create_instance(array('course' => $course1->id));
+
+        $cm = get_coursemodule_from_instance('page', $page->id);
+        $cm2 = get_coursemodule_from_instance('page', $page2->id);
+        // Add the competency to the course module.
+        $ccm = api::add_competency_to_course_module($cm, $c1->get('id'));
+        $summary = external::data_for_course_competencies_page($course1->id, 0);
+        $summary2 = external::data_for_course_competencies_page($course1->id, $cm->id);
+        $summary3 = external::data_for_course_competencies_page($course1->id, $cm2->id);
+
+        $this->assertEquals(count($summary->competencies), 1);
+        $this->assertEquals(count($summary->competencies), count($summary2->competencies));
+        $this->assertEquals(count($summary3->competencies), 0);
+    }
 }
index 6596aad..e440f87 100644 (file)
@@ -672,7 +672,7 @@ class manager {
         $attachment->charset        = $partdata->getCharset();
         $attachment->description    = $partdata->getDescription();
         $attachment->contentid      = $partdata->getContentId();
-        $attachment->filesize       = $messagedata->getBodyPartSize($part);
+        $attachment->filesize       = $partdata->getBytes();
 
         if (!empty($CFG->antiviruses)) {
             mtrace("--> Attempting virus scan of '{$attachment->filename}'");
index 1e1bd2b..02027bd 100644 (file)
@@ -99,7 +99,7 @@ class api {
                     $lang = array();
                     if (!empty($addoninfo['lang'])) {
                         $stringmanager = get_string_manager();
-                        $langs = $stringmanager->get_list_of_translations();
+                        $langs = $stringmanager->get_list_of_translations(true);
                         foreach ($langs as $langid => $langname) {
                             foreach ($addoninfo['lang'] as $stringinfo) {
                                 $lang[$langid][$stringinfo[0]] =
@@ -443,7 +443,7 @@ class api {
                 'NoDelegate_CoreCoursesDownload' => new lang_string('downloadcourses', 'tool_mobile'),
             ),
             "$user" => array(
-                'CoreCourseOptionsDelegate_AddonBlog' => new lang_string('blog', 'blog'),
+                'CoreUserDelegate_AddonBlog:blogs' => new lang_string('blog', 'blog'),
                 '$mmUserDelegate_mmaBadges' => new lang_string('badges', 'badges'),
                 '$mmUserDelegate_mmaCompetency:learningPlan' => new lang_string('competencies', 'competency'),
                 '$mmUserDelegate_mmaCourseCompletion:viewCompletion' => new lang_string('coursecompletion', 'completion'),
index 1ce5887..0b6ba81 100644 (file)
@@ -87,7 +87,7 @@ $string['mobilesettings'] = 'Mobile settings';
 $string['offlineuse'] = 'Offline use';
 $string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
-$string['readingthisemailgettheapp'] = 'Reading this in your e-mail? <a href="{$a}">Download the mobile app and receive notifications on your mobile devices</a>.';
+$string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
 $string['setuplink'] = 'App download page';
diff --git a/admin/tool/uploaduser/classes/local/field_value_validators.php b/admin/tool/uploaduser/classes/local/field_value_validators.php
new file mode 100644 (file)
index 0000000..dd14677
--- /dev/null
@@ -0,0 +1,79 @@
+<?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/>.
+
+/**
+ * File containing the field_value_validators class.
+ *
+ * @package    tool_uploaduser
+ * @copyright  2019 Mathew May
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_uploaduser\local;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Field validator class.
+ *
+ * @package    tool_uploaduser
+ * @copyright  2019 Mathew May
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class field_value_validators {
+
+    /**
+     * List of valid and compatible themes.
+     *
+     * @return array
+     */
+    protected static $themescache;
+
+    /**
+     * Validates the value provided for the theme field.
+     *
+     * @param string $value The value for the theme field.
+     * @return array Contains the validation status and message.
+     */
+    public static function validate_theme($value) {
+        global $CFG;
+
+        $status = 'normal';
+        $message = '';
+
+        // Validate if user themes are allowed.
+        if (!$CFG->allowuserthemes) {
+            $status = 'warning';
+            $message = get_string('userthemesnotallowed', 'tool_uploaduser');
+        } else {
+            // Cache list of themes if not yet set.
+            if (!isset(self::$themescache)) {
+                self::$themescache = get_list_of_themes();
+            }
+
+            // Check if we have a valid theme.
+            if (empty($value)) {
+                $status = 'warning';
+                $message = get_string('notheme', 'tool_uploaduser');
+            } else if (!isset(self::$themescache[$value])) {
+                $status = 'warning';
+                $message = get_string('invalidtheme', 'tool_uploaduser', s($value));
+            }
+        }
+
+        return [$status, $message];
+    }
+}
index 9a60af3..019da51 100644 (file)
@@ -32,6 +32,8 @@ require_once($CFG->dirroot.'/group/lib.php');
 require_once($CFG->dirroot.'/cohort/lib.php');
 require_once('locallib.php');
 require_once('user_form.php');
+require_once('classes/local/field_value_validators.php');
+use tool_uploaduser\local\field_value_validators;
 
 $iid         = optional_param('iid', '', PARAM_INT);
 $previewrows = optional_param('previewrows', 10, PARAM_INT);
@@ -93,6 +95,7 @@ $STD_FIELDS = array('id', 'username', 'email',
         'auth',        // watch out when changing auth type or using external auth plugins!
         'oldusername', // use when renaming users - this is the original username
         'suspended',   // 1 means suspend user account, 0 means activate user account, nothing means keep as is for existing users
+        'theme',       // Define a theme for user when 'allowuserthemes' is enabled.
         'deleted',     // 1 means delete user
         'mnethostid',  // Can not be used for adding, updating or deleting of users - only for enrolments, groups, cohorts and suspending.
         'interests',
@@ -101,7 +104,6 @@ $STD_FIELDS = array('id', 'username', 'email',
 $STD_FIELDS = array_merge($STD_FIELDS, get_all_user_name_fields());
 
 $PRF_FIELDS = array();
-
 if ($proffields = $DB->get_records('user_info_field')) {
     foreach ($proffields as $key => $proffield) {
         $profilefieldname = 'profile_field_'.$proffield->shortname;
@@ -355,6 +357,16 @@ if ($formdata = $mform2->is_cancelled()) {
             $upt->track('username', s($user->username), 'normal', false);
         }
 
+        // Verify if the theme is valid and allowed to be set.
+        if (isset($user->theme)) {
+            list($status, $message) = field_value_validators::validate_theme($user->theme);
+            if ($status !== 'normal' && !empty($message)) {
+                $upt->track('status', $message, $status);
+                // Unset the theme when validation fails.
+                unset($user->theme);
+            }
+        }
+
         // add default values for remaining fields
         $formdefaults = array();
         if (!$existinguser || ($updatetype != UU_UPDATE_FILEOVERRIDE && $updatetype != UU_UPDATE_NOCHANGES)) {
@@ -1211,6 +1223,14 @@ while ($linenum <= $previewrows and $fields = $cir->next()) {
     if (isset($rowcols['city'])) {
         $rowcols['city'] = $rowcols['city'];
     }
+
+    if (isset($rowcols['theme'])) {
+        list($status, $message) = field_value_validators::validate_theme($rowcols['theme']);
+        if ($status !== 'normal' && !empty($message)) {
+            $rowcols['status'][] = $message;
+        }
+    }
+
     // Check if rowcols have custom profile field with correct data and update error state.
     $noerror = uu_check_custom_profile_data($rowcols) && $noerror;
     $rowcols['status'] = implode('<br />', $rowcols['status']);
@@ -1243,4 +1263,3 @@ if ($noerror) {
 }
 echo $OUTPUT->footer();
 die;
-
index 3823bec..e94ba5e 100644 (file)
@@ -35,12 +35,15 @@ $string['errormnetadd'] = 'Can not add remote users';
 $string['errors'] = 'Errors';
 $string['invalidupdatetype'] = 'This option cannot be selected with the chosen upload type.';
 $string['invaliduserdata'] = 'Invalid data detected for user {$a} and it has been automatically cleaned.';
+$string['invalidtheme'] = 'Theme "{$a}" is not installed and will be ignored.';
 $string['nochanges'] = 'No changes';
+$string['notheme'] = 'No theme is defined for this user.';
 $string['pluginname'] = 'User upload';
 $string['renameerrors'] = 'Rename errors';
 $string['requiredtemplate'] = 'Required. You may use template syntax here (%l = lastname, %f = firstname, %u = username). See help for details and examples.';
 $string['rowpreviewnum'] = 'Preview rows';
 $string['unassignedsysrole'] = 'Unassigned system role {$a}';
+$string['userthemesnotallowed'] = 'User themes are not enabled, so any included in the upload users file will be ignored.';
 $string['uploadpicture_baduserfield'] = 'The user attribute specified is not valid. Please, try again.';
 $string['uploadpicture_cannotmovezip'] = 'Cannot move zip file to temporary directory.';
 $string['uploadpicture_cannotprocessdir'] = 'Cannot process unzipped files.';
index ae3dfe3..e988e77 100644 (file)
@@ -56,7 +56,13 @@ define('UU_PWRESET_ALL', 2);
  */
 class uu_progress_tracker {
     private $_row;
-    public $columns = array('status', 'line', 'id', 'username', 'firstname', 'lastname', 'email', 'password', 'auth', 'enrolments', 'suspended', 'deleted');
+
+    /**
+     * The columns shown on the table.
+     * @var array
+     */
+    public $columns = array('status', 'line', 'id', 'username', 'firstname', 'lastname', 'email',
+                            'password', 'auth', 'enrolments', 'suspended', 'theme', 'deleted');
 
     /**
      * Print table header.
@@ -77,6 +83,7 @@ class uu_progress_tracker {
         echo '<th class="header c'.$ci++.'" scope="col">'.get_string('authentication').'</th>';
         echo '<th class="header c'.$ci++.'" scope="col">'.get_string('enrolments', 'enrol').'</th>';
         echo '<th class="header c'.$ci++.'" scope="col">'.get_string('suspended', 'auth').'</th>';
+        echo '<th class="header c'.$ci++.'" scope="col">'.get_string('theme').'</th>';
         echo '<th class="header c'.$ci++.'" scope="col">'.get_string('delete').'</th>';
         echo '</tr>';
         $this->_row = null;
@@ -183,6 +190,7 @@ function uu_validate_user_upload_columns(csv_import_reader $cir, $stdfields, $pr
     $processed = array();
     foreach ($columns as $key=>$unused) {
         $field = $columns[$key];
+        $field = trim($field);
         $lcfield = core_text::strtolower($field);
         if (in_array($field, $stdfields) or in_array($lcfield, $stdfields)) {
             // standard fields are only lowercase
index e7d88a3..ffd9140 100644 (file)
@@ -84,3 +84,64 @@ Feature: Upload users
     And I should see "Super field"
     And I should see "The big guy"
     And I log out
+
+  @javascript
+  Scenario: Upload users setting their user theme
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Maths    | math102   | 0        |
+    # We need to do a bit of setup here.
+    And I change window size to "large"
+    And I log in as "admin"
+    And I navigate to "Security > Site security settings" in site administration
+    And I click on "Password policy" "checkbox"
+    And I click on "Save changes" "button"
+    And I navigate to "Appearance > Themes > Theme settings" in site administration
+    And I click on "Allow user themes" "checkbox"
+    And I click on "Save changes" "button"
+    # Upload the users.
+    And I navigate to "Users > Accounts > Upload users" in site administration
+    When I upload "lib/tests/fixtures/upload_users_themes.csv" file to "File" filemanager
+    And I press "Upload users"
+    Then I should see "Upload users preview"
+    And I should see "boost"
+    And I should see "classic"
+    And I should see "No theme is defined for this user."
+    And I should see "Theme \"somefaketheme\" is not installed and will be ignored."
+    And I press "Upload users"
+    And I should see "Users created: 4"
+    And I press "Continue"
+    And I log out
+    # Boost check.
+    And I log in as "jonest"
+    And I am on "Maths" course homepage
+    And "Turn editing on" "button" should not exist
+    And I log out
+    # Classic check.
+    And I log in as "reznor"
+    And I am on "Maths" course homepage
+    And "Turn editing on" "button" should exist
+
+  @javascript
+  Scenario: Upload users setting their user theme when allowuserthemes is false
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Maths    | math102   | 0        |
+    # We need to do a bit of setup here.
+    And I change window size to "large"
+    And I log in as "admin"
+    And I navigate to "Security > Site security settings" in site administration
+    And I click on "Password policy" "checkbox"
+    And I click on "Save changes" "button"
+    # Upload the users.
+    And I navigate to "Users > Accounts > Upload users" in site administration
+    When I upload "lib/tests/fixtures/upload_users_themes.csv" file to "File" filemanager
+    And I press "Upload users"
+    Then I should see "Upload users preview"
+    And I should see "boost"
+    And I should see "classic"
+    And I press "Upload users"
+    And I should see "User themes are not enabled, so any included in the upload users file will be ignored."
+    And I should see "Users created: 4"
+    And I press "Continue"
+    And I log out
diff --git a/admin/tool/uploaduser/tests/field_value_validators_test.php b/admin/tool/uploaduser/tests/field_value_validators_test.php
new file mode 100644 (file)
index 0000000..e74dbe2
--- /dev/null
@@ -0,0 +1,82 @@
+<?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 field value validators of tool_uploaduser.
+ *
+ * @package    tool_uploaduser
+ * @copyright  2019 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_uploaduser\local\field_value_validators;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Tests for field value validators of tool_uploaduser.
+ *
+ * @package    tool_uploaduser
+ * @copyright  2019 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class field_value_validators_testcase extends advanced_testcase {
+
+    /**
+     * Data provider for \field_value_validators_testcase::test_validate_theme().
+     */
+    public function themes_provider() {
+        return [
+            'User themes disabled' => [
+                false, 'boost', 'warning', get_string('userthemesnotallowed', 'tool_uploaduser')
+            ],
+            'User themes enabled, empty theme' => [
+                true, '', 'warning', get_string('notheme', 'tool_uploaduser')
+            ],
+            'User themes enabled, invalid theme' => [
+                true, 'badtheme', 'warning', get_string('invalidtheme', 'tool_uploaduser', 'badtheme')
+            ],
+            'User themes enabled, valid theme' => [
+                true, 'boost', 'normal', ''
+            ],
+        ];
+    }
+
+    /**
+     * Unit test for \tool_uploaduser\local\field_value_validators::validate_theme()
+     *
+     * @dataProvider themes_provider
+     * @param boolean $userthemesallowed Whether to allow user themes.
+     * @param string $themename The theme name to be tested.
+     * @param string $expectedstatus The expected status.
+     * @param string $expectedmessage The expected validation message.
+     */
+    public function test_validate_theme($userthemesallowed, $themename, $expectedstatus, $expectedmessage) {
+        $this->resetAfterTest();
+
+        // Set value for $CFG->allowuserthemes.
+        set_config('allowuserthemes', $userthemesallowed);
+
+        // Validate the theme.
+        list($status, $message) = field_value_validators::validate_theme($themename);
+
+        // Check the status and validation message.
+        $this->assertEquals($expectedstatus, $status);
+        $this->assertEquals($expectedmessage, $message);
+    }
+}
index 8ebce53..f61d13c 100644 (file)
@@ -208,7 +208,7 @@ You can also choose to display the courses in a list, with summary information,
 
 // 3.6 Messaging tour.
 $string['tour4_title_messaging'] = 'New messaging interface';
-$string['tour4_content_messaging'] = 'Moodle 3.6 provides a new interface to messaging, ability for group messaging within a course, along with better control over who can message you.';
+$string['tour4_content_messaging'] = 'New messaging features include group messaging within a course and better control over who can message you.';
 $string['tour4_title_icon'] = 'Messaging';
 $string['tour4_content_icon'] = 'You can access your messages from any page using this icon.
 
index bcb621c..270a9f6 100644 (file)
@@ -67,6 +67,9 @@ class analysis {
         $this->analyser = $analyser;
         $this->includetarget = $includetarget;
         $this->result = $result;
+
+        // We cache the first time analysables were analysed because time-splitting methods can depend on these info.
+        self::fill_firstanalyses_cache($this->analyser->get_modelid());
     }
 
     /**
@@ -81,10 +84,6 @@ class analysis {
         // Time limit control.
         $modeltimelimit = intval(get_config('analytics', 'modeltimelimit'));
 
-        $filesbytimesplitting = array();
-
-        $alreadyprocessedanalysables = $this->get_processed_analysables();
-
         if ($this->includetarget) {
             $action = 'training';
         } else {
@@ -92,6 +91,8 @@ class analysis {
         }
         $analysables = $this->analyser->get_analysables_iterator($action);
 
+        $processedanalysables = $this->get_processed_analysables();
+
         $inittime = microtime(true);
         foreach ($analysables as $analysable) {
             $processed = false;
@@ -121,13 +122,16 @@ class analysis {
                 }
             }
 
-            // Updated regardless of how well the analysis went.
-            if ($this->analyser->get_target()->always_update_analysis_time() || $processed) {
-                $this->update_analysable_analysed_time($alreadyprocessedanalysables, $analysable->get_id());
-            }
-
-            // Apply time limit.
             if (!$options['evaluation']) {
+
+                if (empty($processedanalysables[$analysable->get_id()]) ||
+                        $this->analyser->get_target()->always_update_analysis_time() || $processed) {
+                    // We store the list of processed analysables even if the target does not always_update_analysis_time(),
+                    // what always_update_analysis_time controls is the update of the data.
+                    $this->update_analysable_analysed_time($processedanalysables, $analysable->get_id());
+                }
+
+                // Apply time limit.
                 $timespent = microtime(true) - $inittime;
                 if ($modeltimelimit <= $timespent) {
                     break;
@@ -150,7 +154,7 @@ class analysis {
 
         // Weird select fields ordering for performance (analysableid key matching, analysableid is also unique by modelid).
         return $DB->get_records_select('analytics_used_analysables', $select,
-            $params, 'timeanalysed DESC', 'analysableid, modelid, action, timeanalysed, id AS primarykey');
+            $params, 'timeanalysed DESC', 'analysableid, modelid, action, firstanalysis, timeanalysed, id AS primarykey');
     }
 
     /**
@@ -590,13 +594,16 @@ class analysis {
     protected function update_analysable_analysed_time(array $processedanalysables, int $analysableid) {
         global $DB;
 
+        $now = time();
+
         if (!empty($processedanalysables[$analysableid])) {
             $obj = $processedanalysables[$analysableid];
 
             $obj->id = $obj->primarykey;
             unset($obj->primarykey);
 
-            $obj->timeanalysed = time();
+            $obj->timeanalysed = $now;
+
             $DB->update_record('analytics_used_analysables', $obj);
 
         } else {
@@ -605,10 +612,54 @@ class analysis {
             $obj->modelid = $this->analyser->get_modelid();
             $obj->action = ($this->includetarget) ? 'training' : 'prediction';
             $obj->analysableid = $analysableid;
-            $obj->timeanalysed = time();
+            $obj->firstanalysis = $now;
+            $obj->timeanalysed = $now;
+
+            $obj->primarykey = $DB->insert_record('analytics_used_analysables', $obj);
+
+            // Update the cache just in case it is used in the same request.
+            $key = $this->analyser->get_modelid() . '_' . $analysableid;
+            $cache = \cache::make('core', 'modelfirstanalyses');
+            $cache->set($key, $now);
+        }
+    }
+
+    /**
+     * Fills a cache containing the first time each analysable in the provided model was analysed.
+     *
+     * @param int $modelid
+     * @param int|null $analysableid
+     * @return null
+     */
+    public static function fill_firstanalyses_cache(int $modelid, ?int $analysableid = null) {
+        global $DB;
 
-            $DB->insert_record('analytics_used_analysables', $obj);
+        // Using composed keys instead of cache $identifiers because of MDL-65358.
+        $primarykey = $DB->sql_concat($modelid, "'_'", 'analysableid');
+        $sql = "SELECT $primarykey AS id, MIN(firstanalysis) AS firstanalysis
+                  FROM {analytics_used_analysables} aua
+                 WHERE modelid = :modelid";
+        $params = ['modelid' => $modelid];
+
+        if ($analysableid) {
+            $sql .= " AND analysableid = :analysableid";
+            $params['analysableid'] = $analysableid;
         }
+
+        $sql .= " GROUP BY modelid, analysableid ORDER BY analysableid";
+
+        $firstanalyses = $DB->get_records_sql($sql, $params);
+        if ($firstanalyses) {
+            $cache = \cache::make('core', 'modelfirstanalyses');
+
+            $firstanalyses = array_map(function($record) {
+                return $record->firstanalysis;
+            }, $firstanalyses);
+
+            $cache->set_many($firstanalyses);
+        }
+
+        return $firstanalyses;
     }
 
     /**
index 4a52370..891820e 100644 (file)
@@ -64,12 +64,12 @@ abstract class periodic extends base {
 
         $periodicity = $this->periodicity();
 
-        $now = new \DateTimeImmutable('now', \core_date::get_server_timezone_object());
-
         if ($this->analysable->get_end()) {
             $end = (new \DateTimeImmutable())->setTimestamp($this->analysable->get_end());
         }
-        $next = (new \DateTimeImmutable())->setTimestamp($this->analysable->get_start());
+        $next = (new \DateTimeImmutable())->setTimestamp($this->get_first_start());
+
+        $now = new \DateTimeImmutable('now', \core_date::get_server_timezone_object());
 
         $ranges = [];
         while ($next < $now &&
@@ -140,4 +140,13 @@ abstract class periodic extends base {
             'time' => $end
         ];
     }
+
+    /**
+     * Get the start of the first time range.
+     *
+     * @return int A timestamp.
+     */
+    protected function get_first_start() {
+        return $this->analysable->get_start();
+    }
 }
index 7cc4054..4960c73 100644 (file)
@@ -68,4 +68,26 @@ abstract class upcoming_periodic extends periodic {
     public function valid_for_evaluation(): bool {
         return false;
     }
+
+    /**
+     * Get the start of the first time range.
+     *
+     * Overwriten to start generating predictions about upcoming stuff from time().
+     *
+     * @return int A timestamp.
+     */
+    protected function get_first_start() {
+        global $DB;
+
+        $cache = \cache::make('core', 'modelfirstanalyses');
+
+        $key = $this->modelid . '_' . $this->analysable->get_id();
+        $firstanalysis = $cache->get($key);
+        if (!empty($firstanalysis)) {
+            return $firstanalysis;
+        }
+
+        // This analysable has not yet been analysed, the start is therefore now (-1 so ready_to_predict can be executed).
+        return time() - 1;
+    }
 }
diff --git a/analytics/tests/analysis_test.php b/analytics/tests/analysis_test.php
new file mode 100644 (file)
index 0000000..9d763cd
--- /dev/null
@@ -0,0 +1,70 @@
+<?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 analysis class.
+ *
+ * @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();
+
+/**
+ * Unit tests for the analysis class.
+ *
+ * @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
+ */
+class analytics_analysis_testcase extends advanced_testcase {
+
+    /**
+     * Test fill_firstanalyses_cache.
+     * @return null
+     */
+    public function test_fill_firstanalyses_cache() {
+        $this->resetAfterTest();
+
+        $this->insert_used(1, 1, 'training', 123);
+        $this->insert_used(1, 2, 'training', 124);
+        $this->insert_used(1, 1, 'prediction', 125);
+
+        $firstanalyses = \core_analytics\analysis::fill_firstanalyses_cache(1);
+        $this->assertCount(2, $firstanalyses);
+        $this->assertEquals(123, $firstanalyses['1_1']);
+        $this->assertEquals(124, $firstanalyses['1_2']);
+
+        // The cached elements gets refreshed.
+        $this->insert_used(1, 1, 'prediction', 122);
+        $firstanalyses = \core_analytics\analysis::fill_firstanalyses_cache(1, 1);
+        $this->assertCount(1, $firstanalyses);
+        $this->assertEquals(122, $firstanalyses['1_1']);
+    }
+
+    private function insert_used($modelid, $analysableid, $action, $timestamp) {
+        global $DB;
+
+        $obj = new \stdClass();
+        $obj->modelid = $modelid;
+        $obj->action = $action;
+        $obj->analysableid = $analysableid;
+        $obj->firstanalysis = $timestamp;
+        $obj->timeanalysed = $timestamp;
+        $obj->id = $DB->insert_record('analytics_used_analysables', $obj);
+    }
+}
diff --git a/analytics/tests/fixtures/test_timesplitting_upcoming_seconds.php b/analytics/tests/fixtures/test_timesplitting_upcoming_seconds.php
new file mode 100644 (file)
index 0000000..9309f75
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Test time splitting.
+ *
+ * @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();
+
+/**
+ * Test time splitting.
+ *
+ * @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
+ */
+class test_timesplitting_upcoming_seconds extends \core_analytics\local\time_splitting\upcoming_periodic {
+
+    /**
+     * Every second.
+     * @return \DateInterval
+     */
+    public function periodicity() {
+        return new \DateInterval('PT1S');
+    }
+
+    /**
+     * Just to comply with the interface.
+     *
+     * @return \lang_string
+     */
+    public static function get_name() : \lang_string {
+        return new \lang_string('error');
+    }
+}
index 5f638ec..11dd4ce 100644 (file)
@@ -806,12 +806,25 @@ class backup_badges_structure_step extends backup_structure_step {
         $badge = new backup_nested_element('badge', array('id'), array('name', 'description',
                 'timecreated', 'timemodified', 'usercreated', 'usermodified', 'issuername',
                 'issuerurl', 'issuercontact', 'expiredate', 'expireperiod', 'type', 'courseid',
-                'message', 'messagesubject', 'attachment', 'notification', 'status', 'nextcron'));
+                'message', 'messagesubject', 'attachment', 'notification', 'status', 'nextcron',
+                'version', 'language', 'imageauthorname', 'imageauthoremail', 'imageauthorurl',
+                'imagecaption'));
 
         $criteria = new backup_nested_element('criteria');
         $criterion = new backup_nested_element('criterion', array('id'), array('badgeid',
                 'criteriatype', 'method', 'description', 'descriptionformat'));
 
+        $endorsement = new backup_nested_element('endorsement', array('id'), array('badgeid',
+                'issuername', 'issuerurl', 'issueremail', 'claimid', 'claimcomment', 'dateissued'));
+
+        $alignments = new backup_nested_element('alignments');
+        $alignment = new backup_nested_element('alignment', array('id'), array('badgeid',
+                'targetname', 'targeturl', 'targetdescription', 'targetframework', 'targetcode'));
+
+        $relatedbadges = new backup_nested_element('relatedbadges');
+        $relatedbadge = new backup_nested_element('relatedbadge', array('id'), array('badgeid',
+                'relatedbadgeid'));
+
         $parameters = new backup_nested_element('parameters');
         $parameter = new backup_nested_element('parameter', array('id'), array('critid',
                 'name', 'value', 'criteriatype'));
@@ -827,6 +840,11 @@ class backup_badges_structure_step extends backup_structure_step {
         $criteria->add_child($criterion);
         $criterion->add_child($parameters);
         $parameters->add_child($parameter);
+        $badge->add_child($endorsement);
+        $badge->add_child($alignments);
+        $alignments->add_child($alignment);
+        $badge->add_child($relatedbadges);
+        $relatedbadges->add_child($relatedbadge);
         $badge->add_child($manual_awards);
         $manual_awards->add_child($manual_award);
 
@@ -834,6 +852,10 @@ class backup_badges_structure_step extends backup_structure_step {
 
         $badge->set_source_table('badge', array('courseid' => backup::VAR_COURSEID));
         $criterion->set_source_table('badge_criteria', array('badgeid' => backup::VAR_PARENTID));
+        $endorsement->set_source_table('badge_endorsement', array('badgeid' => backup::VAR_PARENTID));
+
+        $alignment->set_source_table('badge_alignment', array('badgeid' => backup::VAR_PARENTID));
+        $relatedbadge->set_source_table('badge_related', array('badgeid' => backup::VAR_PARENTID));
 
         $parametersql = 'SELECT cp.*, c.criteriatype
                              FROM {badge_criteria_param} cp JOIN {badge_criteria} c
@@ -850,6 +872,10 @@ class backup_badges_structure_step extends backup_structure_step {
         $badge->annotate_ids('user', 'usermodified');
         $criterion->annotate_ids('badge', 'badgeid');
         $parameter->annotate_ids('criterion', 'critid');
+        $endorsement->annotate_ids('badge', 'badgeid');
+        $alignment->annotate_ids('badge', 'badgeid');
+        $relatedbadge->annotate_ids('badge', 'badgeid');
+        $relatedbadge->annotate_ids('badge', 'relatedbadgeid');
         $badge->annotate_files('badges', 'badgeimage', 'id');
         $manual_award->annotate_ids('badge', 'badgeid');
         $manual_award->annotate_ids('user', 'recipientid');
index ff9e937..fe699b3 100644 (file)
@@ -2517,6 +2517,9 @@ class restore_badges_structure_step extends restore_structure_step {
         $paths[] = new restore_path_element('badge', '/badges/badge');
         $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
         $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
+        $paths[] = new restore_path_element('endorsement', '/badges/badge/endorsement');
+        $paths[] = new restore_path_element('alignment', '/badges/badge/alignments/alignment');
+        $paths[] = new restore_path_element('relatedbadge', '/badges/badge/relatedbadges/relatedbadge');
         $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
 
         return $paths;
@@ -2561,13 +2564,87 @@ class restore_badges_structure_step extends restore_structure_step {
                 'attachment'     => $data->attachment,
                 'notification'   => $data->notification,
                 'status'         => BADGE_STATUS_INACTIVE,
-                'nextcron'       => $data->nextcron
+                'nextcron'       => $data->nextcron,
+                'version'        => $data->version,
+                'language'       => $data->language,
+                'imageauthorname' => $data->imageauthorname,
+                'imageauthoremail' => $data->imageauthoremail,
+                'imageauthorurl' => $data->imageauthorurl,
+                'imagecaption'   => $data->imagecaption
         );
 
         $newid = $DB->insert_record('badge', $params);
         $this->set_mapping('badge', $data->id, $newid, $restorefiles);
     }
 
+    /**
+     * Create an endorsement for a badge.
+     *
+     * @param mixed $data
+     * @return void
+     */
+    public function process_endorsement($data) {
+        global $DB;
+
+        $data = (object)$data;
+
+        $params = [
+            'badgeid' => $this->get_new_parentid('badge'),
+            'issuername' => $data->issuername,
+            'issuerurl' => $data->issuerurl,
+            'issueremail' => $data->issueremail,
+            'claimid' => $data->claimid,
+            'claimcomment' => $data->claimcomment,
+            'dateissued' => $this->apply_date_offset($data->dateissued)
+        ];
+        $newid = $DB->insert_record('badge_endorsement', $params);
+        $this->set_mapping('endorsement', $data->id, $newid);
+    }
+
+    /**
+     * Link to related badges for a badge. This relies on post processing in after_execute().
+     *
+     * @param mixed $data
+     * @return void
+     */
+    public function process_relatedbadge($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $relatedbadgeid = $data->relatedbadgeid;
+
+        if ($relatedbadgeid) {
+            // Only backup and restore related badges if they are contained in the backup file.
+            $params = array(
+                    'badgeid'           => $this->get_new_parentid('badge'),
+                    'relatedbadgeid'    => $relatedbadgeid
+            );
+            $newid = $DB->insert_record('badge_related', $params);
+        }
+    }
+
+    /**
+     * Link to an alignment for a badge.
+     *
+     * @param mixed $data
+     * @return void
+     */
+    public function process_alignment($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $params = array(
+                'badgeid'           => $this->get_new_parentid('badge'),
+                'targetname'        => $data->targetname,
+                'targeturl'         => $data->targeturl,
+                'targetdescription' => $data->targetdescription,
+                'targetframework'   => $data->targetframework,
+                'targetcode'        => $data->targetcode
+        );
+        $newid = $DB->insert_record('badge_alignment', $params);
+        $this->set_mapping('alignment', $data->id, $newid);
+    }
+
     public function process_criterion($data) {
         global $DB;
 
@@ -2580,6 +2657,7 @@ class restore_badges_structure_step extends restore_structure_step {
                 'description'       => isset($data->description) ? $data->description : '',
                 'descriptionformat' => isset($data->descriptionformat) ? $data->descriptionformat : 0,
         );
+
         $newid = $DB->insert_record('badge_criteria', $params);
         $this->set_mapping('criterion', $data->id, $newid);
     }
@@ -2613,6 +2691,14 @@ class restore_badges_structure_step extends restore_structure_step {
             } else {
                 return;
             }
+        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) {
+            $competencyid = $this->get_mappingid('competency', $data->value);
+            if (!empty($competencyid)) {
+                $params['name'] = 'competency_' . $competencyid;
+                $params['value'] = $competencyid;
+            } else {
+                return;
+            }
         }
 
         if (!$DB->record_exists('badge_criteria_param', $params)) {
@@ -2645,8 +2731,38 @@ class restore_badges_structure_step extends restore_structure_step {
     }
 
     protected function after_execute() {
+        global $DB;
         // Add related files.
         $this->add_related_files('badges', 'badgeimage', 'badge');
+
+        $badgeid = $this->get_new_parentid('badge');
+        // Remap any related badges.
+        // We do this in the DB directly because this is backup/restore it is not valid to call into
+        // the component API.
+        $params = array('badgeid' => $badgeid);
+        $query = "SELECT DISTINCT br.id, br.badgeid, br.relatedbadgeid
+                    FROM {badge_related} br
+                   WHERE (br.badgeid = :badgeid)";
+        $relatedbadges = $DB->get_records_sql($query, $params);
+        $newrelatedids = [];
+        foreach ($relatedbadges as $relatedbadge) {
+            $relatedid = $this->get_mappingid('badge', $relatedbadge->relatedbadgeid);
+            $params['relatedbadgeid'] = $relatedbadge->relatedbadgeid;
+            $DB->delete_records_select('badge_related', '(badgeid = :badgeid AND relatedbadgeid = :relatedbadgeid)', $params);
+            if ($relatedid) {
+                $newrelatedids[] = $relatedid;
+            }
+        }
+        if (!empty($newrelatedids)) {
+            $relatedbadges = [];
+            foreach ($newrelatedids as $relatedid) {
+                $relatedbadge = new stdClass();
+                $relatedbadge->badgeid = $badgeid;
+                $relatedbadge->relatedbadgeid = $relatedid;
+                $relatedbadges[] = $relatedbadge;
+            }
+            $DB->insert_records('badge_related', $relatedbadges);
+        }
     }
 }
 
index f982a6d..509c83c 100644 (file)
@@ -86,6 +86,7 @@ class core_backup_async_backup_testcase extends \core_privacy\tests\provider_tes
                 backup::INTERACTIVE_YES, backup::MODE_ASYNC, $USER->id);
         $bc->finish_ui();
         $backupid = $bc->get_backupid();
+        $bc->destroy();
 
         $prebackuprec = $DB->get_record('backup_controllers', array('backupid' => $backupid));
 
index c6ac5bb..b36e515 100644 (file)
@@ -314,6 +314,7 @@ class async_helper  {
             $filename = $bc->get_plan()->get_setting('filename')->get_value();
             $timecreated = $backup->timecreated;
             $status = $renderer->get_status_display($bc->get_status(), $bc->get_backupid());
+            $bc->destroy();
 
             $tablerow = array($filename, userdate($timecreated), '-', '-', '-', $status);
             $tabledata[] = $tablerow;
index 079537f..22ac0ed 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -82,7 +81,7 @@ abstract class setting_dependency {
      * Destroy all circular references. It helps PHP 5.2 a lot!
      */
     public function destroy() {
-        // No need to destroy anything recursively here, direct reset
+        // No need to destroy anything recursively here, direct reset.
         $this->setting = null;
         $this->dependentsetting = null;
     }
@@ -94,16 +93,19 @@ abstract class setting_dependency {
      * @return bool
      */
     final public function process_change($changetype, $oldvalue) {
-        // Check the type of change requested
+        // Check the type of change requested.
         switch ($changetype) {
-            // Process a status change
-            case base_setting::CHANGED_STATUS: return $this->process_status_change($oldvalue);
-            // Process a visibility change
-            case base_setting::CHANGED_VISIBILITY: return $this->process_visibility_change($oldvalue);
-            // Process a value change
-            case base_setting::CHANGED_VALUE: return $this->process_value_change($oldvalue);
+            // Process a status change.
+            case base_setting::CHANGED_STATUS:
+                return $this->process_status_change($oldvalue);
+            // Process a visibility change.
+            case base_setting::CHANGED_VISIBILITY:
+                return $this->process_visibility_change($oldvalue);
+            // Process a value change.
+            case base_setting::CHANGED_VALUE:
+                return $this->process_value_change($oldvalue);
         }
-        // Throw an exception if we get this far
+        // Throw an exception if we get this far.
         throw new backup_ui_exception('unknownchangetype');
     }
     /**
@@ -112,11 +114,11 @@ abstract class setting_dependency {
      * @return bool
      */
     protected function process_visibility_change($oldvisibility) {
-        // Store the current dependent settings visibility for comparison
+        // Store the current dependent settings visibility for comparison.
         $prevalue = $this->dependentsetting->get_visibility();
-        // Set it regardless of whether we need to
+        // Set it regardless of whether we need to.
         $this->dependentsetting->set_visibility($this->setting->get_visibility());
-        // Return true if it changed
+        // Return true if it changed.
         return ($prevalue != $this->dependentsetting->get_visibility());
     }
     /**
@@ -182,15 +184,16 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      */
     public function __construct(base_setting $setting, base_setting $dependentsetting, $value, $defaultvalue = false) {
         parent::__construct($setting, $dependentsetting, $defaultvalue);
-        $this->value = ($value)?(string)$value:0;
+        $this->value = ($value) ? (string)$value : 0;
     }
     /**
      * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || $this->setting->get_value() == $this->value) {
+        // If the setting is locked or the dependent setting should be locked then return true.
+        if ($this->setting->get_status() !== base_setting::NOT_LOCKED ||
+                $this->evaluate_disabled_condition($this->setting->get_value())) {
             return true;
         }
         // Else the dependent setting is not locked by this setting_dependency.
@@ -208,17 +211,25 @@ class setting_dependency_disabledif_equals extends setting_dependency {
             return false;
         }
         $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        if ($this->setting->get_value() == $this->value) {
+        // If the setting is the desired value enact the dependency.
+        $settingvalue = $this->setting->get_value();
+        if ($this->evaluate_disabled_condition($settingvalue)) {
             // The dependent setting needs to be locked by hierachy and set to the
             // default value.
             $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            $this->dependentsetting->set_value($this->defaultvalue);
+
+            // For checkboxes the default value is false, but when the setting is
+            // locked, the value should inherit from the parent setting.
+            if ($this->defaultvalue === false) {
+                $this->dependentsetting->set_value($settingvalue);
+            } else {
+                $this->dependentsetting->set_value($this->defaultvalue);
+            }
         } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
+            // We can unlock the dependent setting.
             $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
         }
-        // Return true if the value has changed for the dependent setting
+        // Return true if the value has changed for the dependent setting.
         return ($prevalue != $this->dependentsetting->get_value());
     }
     /**
@@ -227,17 +238,18 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      * @return bool
      */
     protected function process_status_change($oldstatus) {
-        // Store the dependent status
+        // Store the dependent status.
         $prevalue = $this->dependentsetting->get_status();
-        // Store the current status
+        // Store the current status.
         $currentstatus = $this->setting->get_status();
         if ($currentstatus == base_setting::NOT_LOCKED) {
-            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY && $this->setting->get_value() != $this->value) {
-                // Dependency has changes, is not fine, unlock the dependent setting
+            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY &&
+                    !$this->evaluate_disabled_condition($this->setting->get_value())) {
+                // Dependency has changes, is not fine, unlock the dependent setting.
                 $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
             }
         } else {
-            // Make sure the dependent setting is also locked, in this case by hierarchy
+            // Make sure the dependent setting is also locked, in this case by hierarchy.
             $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
         }
         // Return true if the dependent setting has changed.
@@ -248,17 +260,17 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      * @return bool True if there were changes
      */
     public function enforce() {
-        // This will be set to true if ANYTHING changes
+        // This will be set to true if ANYTHING changes.
         $changes = false;
-        // First process any value changes
+        // First process any value changes.
         if ($this->process_value_change($this->setting->get_value())) {
             $changes = true;
         }
-        // Second process any status changes
+        // Second process any status changes.
         if ($this->process_status_change($this->setting->get_status())) {
             $changes = true;
         }
-        // Finally process visibility changes
+        // Finally process visibility changes.
         if ($this->process_visibility_change($this->setting->get_visibility())) {
             $changes = true;
         }
@@ -271,152 +283,72 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'eq',
-            'value'=>$this->value
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'eq',
+            'value' => $this->value
         );
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * This function should be abstract, but there will probably be existing sub-classes so we must provide a default
+     * implementation.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return $value == $this->value;
+    }
 }
 
 /**
-* A dependency that disables the secondary setting if the primary setting is
-* not equal to the provided value
-*
-* @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com>
-* @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
-*/
+ * A dependency that disables the secondary setting if the primary setting is
+ * not equal to the provided value
+ *
+ * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class setting_dependency_disabledif_not_equals extends setting_dependency_disabledif_equals {
+
     /**
-    * Enforces the dependency if required.
-    * @return bool True if there were changes
-    */
-    public function enforce() {
-        // This will be set to true if ANYTHING changes
-        $changes = false;
-        // First process any value changes
-        if (!$this->process_value_change($this->setting->get_value())) {
-            $changes = true;
-        }
-        // Second process any status changes
-        if ($this->process_status_change($this->setting->get_status())) {
-            $changes = true;
-        }
-        // Finally process visibility changes
-        if ($this->process_visibility_change($this->setting->get_visibility())) {
-            $changes = true;
-        }
-        return $changes;
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return $value != $this->value;
     }
+
     /**
-    * Returns an array of properties suitable to be used to define a moodleforms
-    * disabled command
-    * @return array
-    */
+     * Returns an array of properties suitable to be used to define a moodleforms
+     * disabled command
+     * @return array
+     */
     public function get_moodleform_properties() {
         return array(
-                'setting'=>$this->dependentsetting->get_ui_name(),
-                'dependenton'=>$this->setting->get_ui_name(),
-                'condition'=>'notequal',
-                'value'=>$this->value
+                'setting' => $this->dependentsetting->get_ui_name(),
+                'dependenton' => $this->setting->get_ui_name(),
+                'condition' => 'notequal',
+                'value' => $this->value
         );
     }
 }
 
-//with array
-class setting_dependency_disabledif_equals2 extends setting_dependency {
-    /**
-     * The value to compare to
-     * @var mixed
-     */
-    protected $value;
-    /**
-     * Creates the dependency
-     *
-     * @param base_setting $setting
-     * @param base_setting $dependentsetting
-     * @param mixed $value
-     * @param mixed $defaultvalue
-     */
-    public function __construct(base_setting $setting, base_setting $dependentsetting, array $value, $defaultvalue = false) {
-        parent::__construct($setting, $dependentsetting, $defaultvalue);
-        $this->value = $value;
-    }
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || in_array($this->setting->get_value(), $this->value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        if (in_array($this->setting->get_value(), $this->value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            $this->dependentsetting->set_value($this->defaultvalue);
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
+/**
+ * Disable if a value is in a list.
+ */
+class setting_dependency_disabledif_in_array extends setting_dependency_disabledif_equals {
+
     /**
-     * Processes a status change in the primary setting
-     * @param mixed $oldstatus
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
      * @return bool
      */
-    protected function process_status_change($oldstatus) {
-        // Store the dependent status
-        $prevalue = $this->dependentsetting->get_status();
-        // Store the current status
-        $currentstatus = $this->setting->get_status();
-        if ($currentstatus == base_setting::NOT_LOCKED) {
-            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY && !in_array($this->setting->get_value(), $this->value)) {
-                // Dependency has changes, is not fine, unlock the dependent setting
-                $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-            }
-        } else {
-            // Make sure the dependent setting is also locked, in this case by hierarchy
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-        }
-        // Return true if the dependent setting has changed.
-        return ($prevalue != $this->dependentsetting->get_status());
-    }
-    /**
-     * Enforces the dependency if required.
-     * @return bool True if there were changes
-     */
-    public function enforce() {
-        // This will be set to true if ANYTHING changes
-        $changes = false;
-        // First process any value changes
-        if ($this->process_value_change($this->setting->get_value())) {
-            $changes = true;
-        }
-        // Second process any status changes
-        if ($this->process_status_change($this->setting->get_status())) {
-            $changes = true;
-        }
-        // Finally process visibility changes
-        if ($this->process_visibility_change($this->setting->get_visibility())) {
-            $changes = true;
-        }
-        return $changes;
+    protected function evaluate_disabled_condition($value) {
+        return in_array($value, $this->value);
     }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -424,14 +356,19 @@ class setting_dependency_disabledif_equals2 extends setting_dependency {
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'eq',
-            'value'=>$this->value
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'eq',
+            'value' => $this->value
         );
     }
 }
 
+/**
+ * This class is here for backwards compatibility (terrible name).
+ */
+class setting_dependency_disabledif_equals2 extends setting_dependency_disabledif_in_array {
+}
 
 /**
  * A dependency that disables the secondary element if the primary element is
@@ -452,9 +389,9 @@ class setting_dependency_disabledif_checked extends setting_dependency_disabledi
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'checked'
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'checked'
         );
     }
 }
@@ -478,9 +415,9 @@ class setting_dependency_disabledif_not_checked extends setting_dependency_disab
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notchecked'
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notchecked'
         );
     }
 }
@@ -497,6 +434,16 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
         parent::__construct($setting, $dependentsetting, false, $defaultvalue);
         $this->value = false;
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return !empty($value);
+    }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -504,50 +451,12 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notequal',
-            'value'=>''
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notequal',
+            'value' => ''
         );
     }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        $value = $this->setting->get_value();
-        if (!empty($value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            if ($this->defaultvalue === false) {
-                $this->dependentsetting->set_value($value);
-            } else {
-                $this->dependentsetting->set_value($this->defaultvalue);
-            }
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
-
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || !empty($value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
 }
 
 /**
@@ -562,6 +471,16 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
         parent::__construct($setting, $dependentsetting, false, $defaultvalue);
         $this->value = false;
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return empty($value);
+    }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -569,47 +488,10 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notequal',
-            'value'=>''
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notequal',
+            'value' => ''
         );
     }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        $value = $this->setting->get_value();
-        if (empty($value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            if ($this->defaultvalue === false) {
-                $this->dependentsetting->set_value($value);
-            } else {
-                $this->dependentsetting->set_value($this->defaultvalue);
-            }
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || empty($value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
 }
index 45bc4ec..63999fe 100644 (file)
@@ -45,7 +45,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test base_setting class
      */
-    function test_base_setting() {
+    public function test_base_setting() {
         // Instantiate base_setting and check everything
         $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN);
         $this->assertTrue($bs instanceof base_setting);
@@ -290,10 +290,54 @@ class backp_settings_testcase extends basic_testcase {
         $this->assertEquals($ubs3->get_status(), $ubs1->get_status());
     }
 
+    /**
+     * Test that locked and unlocked states on dependent backup settings at the same level
+     * correctly do not flow from the parent to the child setting when the setting is locked by permissions.
+     */
+    public function test_dependency_empty_locked_by_permission_child_is_not_unlocked() {
+        // Check dependencies are working ok.
+        $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, 2);
+        $bs1->set_level(1);
+        $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, 2);
+        $bs2->set_level(1); // Same level *must* work.
+        $bs1->add_dependency($bs2, setting_dependency::DISABLED_EMPTY);
+
+        $bs1->set_status(base_setting::LOCKED_BY_PERMISSION);
+        $this->assertEquals(base_setting::LOCKED_BY_HIERARCHY, $bs2->get_status());
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs1->get_status());
+        $bs2->set_status(base_setting::LOCKED_BY_PERMISSION);
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs1->get_status());
+
+        // Unlocking the parent should NOT unlock the child.
+        $bs1->set_status(base_setting::NOT_LOCKED);
+
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs2->get_status());
+    }
+
+    /**
+     * Test that locked and unlocked states on dependent backup settings at the same level
+     * correctly do flow from the parent to the child setting when the setting is locked by config.
+     */
+    public function test_dependency_not_empty_locked_by_config_parent_is_unlocked() {
+        $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, 0);
+        $bs1->set_level(1);
+        $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, 0);
+        $bs2->set_level(1); // Same level *must* work.
+        $bs1->add_dependency($bs2, setting_dependency::DISABLED_NOT_EMPTY);
+
+        $bs1->set_status(base_setting::LOCKED_BY_CONFIG);
+        $this->assertEquals(base_setting::LOCKED_BY_HIERARCHY, $bs2->get_status());
+        $this->assertEquals(base_setting::LOCKED_BY_CONFIG, $bs1->get_status());
+
+        // Unlocking the parent should unlock the child.
+        $bs1->set_status(base_setting::NOT_LOCKED);
+        $this->assertEquals(base_setting::NOT_LOCKED, $bs2->get_status());
+    }
+
     /**
      * test backup_setting class
      */
-    function test_backup_setting() {
+    public function test_backup_setting() {
         // Instantiate backup_setting class and set level
         $bs = new mock_backup_setting('test', base_setting::IS_INTEGER, null);
         $bs->set_level(1);
@@ -340,7 +384,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test activity_backup_setting class
      */
-    function test_activity_backup_setting() {
+    public function test_activity_backup_setting() {
         $bs = new mock_activity_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::ACTIVITY_LEVEL);
 
@@ -355,7 +399,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test section_backup_setting class
      */
-    function test_section_backup_setting() {
+    public function test_section_backup_setting() {
         $bs = new mock_section_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::SECTION_LEVEL);
 
@@ -370,7 +414,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test course_backup_setting class
      */
-    function test_course_backup_setting() {
+    public function test_course_backup_setting() {
         $bs = new mock_course_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::COURSE_LEVEL);
 
index 05dafdd..a3bdb56 100644 (file)
@@ -67,7 +67,7 @@ if ($copy) {
     $cloneid = $badge->make_clone();
     // If a user can edit badge details, they will be redirected to the edit page.
     if (has_capability('moodle/badges:configuredetails', $context)) {
-        redirect(new moodle_url('/badges/edit.php', array('id' => $cloneid, 'action' => 'details')));
+        redirect(new moodle_url('/badges/edit.php', array('id' => $cloneid, 'action' => 'badge')));
     }
     redirect(new moodle_url('/badges/overview.php', array('id' => $cloneid)));
 }
index f67a304..cf5b433 100644 (file)
@@ -99,7 +99,7 @@ if ($alignmentid || $action == 'add' || $action == 'edit') {
     }
     $alignments = $badge->get_alignments();
     if (count($alignments) > 0) {
-        $renderrelated = new badge_alignments($alignments, $badgeid);
+        $renderrelated = new \core_badges\output\badge_alignments($alignments, $badgeid);
         echo $output->render($renderrelated);
     } else {
         echo $output->notification(get_string('noalignment', 'badges'));
index 67b4144..ec08d0f 100644 (file)
@@ -28,6 +28,7 @@ define('AJAX_SCRIPT', true);
 define('NO_MOODLE_COOKIES', true); // No need for a session here.
 
 require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
 
 if (empty($CFG->enablebadges)) {
     print_error('badgesdisabled', 'badges');
diff --git a/badges/backpack-add.php b/badges/backpack-add.php
new file mode 100644 (file)
index 0000000..9c57520
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Optionally award a badge and redirect to the my badges page.
+ *
+ * @package    core_badges
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+if (badges_open_badges_backpack_api() != OPEN_BADGES_V2) {
+    throw new coding_exception('No backpacks support Open Badges V2.');
+}
+
+require_login();
+
+$id = required_param('hash', PARAM_ALPHANUM);
+
+$PAGE->set_url('/badges/backpack-add.php', array('hash' => $id));
+$PAGE->set_context(context_system::instance());
+$output = $PAGE->get_renderer('core', 'badges');
+
+$issuedbadge = new \core_badges\output\issued_badge($id);
+if (!empty($issuedbadge->recipient->id)) {
+    // The flow for issuing a badge is:
+    // * Create issuer
+    // * Create badge
+    // * Create assertion (Award the badge!)
+
+    // Get the backpack.
+    $badgeid = $issuedbadge->badgeid;
+    $badge = new badge($badgeid);
+    $backpack = $DB->get_record('badge_backpack', array('userid' => $USER->id));
+    $sitebackpack = badges_get_site_backpack($backpack->externalbackpackid);
+    $assertion = new core_badges_assertion($id, $sitebackpack->apiversion);
+    $api = new \core_badges\backpack_api($sitebackpack);
+    $api->authenticate();
+
+    // Create issuer.
+    $issuer = $assertion->get_issuer();
+    if (!($issuerentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email']))) {
+        $response = $api->put_issuer($issuer);
+        if (!$response) {
+            throw new moodle_exception('invalidrequest', 'error');
+        }
+        $issuerentityid = $response->id;
+        badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email'], $issuerentityid);
+    }
+    // Create badge.
+    $badge = $assertion->get_badge_class(false);
+    $badgeid = $assertion->get_badge_id();
+    if (!($badgeentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid))) {
+        $response = $api->put_badgeclass($issuerentityid, $badge);
+        if (!$response) {
+            throw new moodle_exception('invalidrequest', 'error');
+        }
+        $badgeentityid = $response->id;
+        badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid, $badgeentityid);
+    }
+
+    // Create assertion (Award the badge!).
+    $assertiondata = $assertion->get_badge_assertion(false, false);
+
+    $assertionid = $assertion->get_assertion_hash();
+
+    if (!($assertionentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid))) {
+        $response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
+        if (!$response) {
+            throw new moodle_exception('invalidrequest', 'error');
+        }
+        $assertionentityid = $response->id;
+        badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid, $assertionentityid);
+        $response = ['success' => 'addedtobackpack'];
+    } else {
+        $response = ['warning' => 'existsinbackpack'];
+    }
+    redirect(new moodle_url('/badges/mybadges.php', $response));
+} else {
+    redirect(new moodle_url('/badges/mybadges.php'));
+}
index 2282907..3ecdf77 100644 (file)
@@ -1,5 +1,6 @@
 /**
  * Push badges to backpack.
+ * @deprecated since 3.7
  */
 function addtobackpack(event, args) {
     var badgetable = Y.one('#issued-badge-table');
@@ -20,6 +21,7 @@ function addtobackpack(event, args) {
 
 /**
  * Check if website is externally accessible from the backpack.
+ * @deprecated since 3.7
  */
 function check_site_access() {
     var add = Y.one('#check_connection');
index d9dfda2..9b563ff 100644 (file)
  */
 require_once(__DIR__ . '/../config.php');
 require_once($CFG->libdir . '/badgeslib.php');
-require_once(__DIR__ . '/lib/backpacklib.php');
 
 $data = optional_param('data', '', PARAM_RAW);
 require_login();
-$PAGE->set_url('/badges/openbackpackemailverify.php');
+$PAGE->set_url('/badges/backpackemailverify.php');
 $PAGE->set_context(context_user::instance($USER->id));
 $redirect = '/badges/mybackpack.php';
 
@@ -37,17 +36,20 @@ $storedsecret = get_user_preferences('badges_email_verify_secret');
 if (!is_null($storedsecret)) {
     if ($data === $storedsecret) {
         $storedemail = get_user_preferences('badges_email_verify_address');
+        $backpackid = get_user_preferences('badges_email_verify_backpackid');
+        $password = get_user_preferences('badges_email_verify_password');
+
+        $backpack = badges_get_site_backpack($backpackid);
 
         $data = new stdClass();
-        $data->backpackurl = BADGE_BACKPACKURL;
         $data->email = $storedemail;
-        $bp = new OpenBadgesBackpackHandler($data);
+        $data->password = $password;
+        $data->externalbackpackid = $backpackid;
+        $bp = new \core_badges\backpack_api($backpack, $data);
 
         // Make sure we have all the required information before trying to save the connection.
-        $backpackuser = $bp->curl_request('user');
-        if (isset($backpackuser->status) && $backpackuser->status === 'okay' && isset($backpackuser->userId)) {
-            $backpackuid = $backpackuser->userId;
-        } else {
+        $backpackuid = $bp->authenticate();
+        if (empty($backpackuid) || !empty($backpackuid->error)) {
             redirect(new moodle_url($redirect), get_string('backpackconnectionunexpectedresult', 'badges'),
                 null, \core\output\notification::NOTIFY_ERROR);
         }
@@ -55,10 +57,11 @@ if (!is_null($storedsecret)) {
         $obj = new stdClass();
         $obj->userid = $USER->id;
         $obj->email = $data->email;
-        $obj->backpackurl = $data->backpackurl;
+        $obj->externalbackpackid = $backpackid;
         $obj->backpackuid = $backpackuid;
         $obj->autosync = 0;
-        $obj->password = '';
+        $obj->password = $password;
+
         $DB->insert_record('badge_backpack', $obj);
 
         // Remove the verification vars and redirect to the mypackpack page.
diff --git a/badges/backpacks.php b/badges/backpacks.php
new file mode 100644 (file)
index 0000000..341d511
--- /dev/null
@@ -0,0 +1,78 @@
+<?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/>.
+
+/**
+ * Display a list of badge backpacks for the site.
+ *
+ * @package    core_badges
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+$context = context_system::instance();
+$PAGE->set_context($context);
+
+require_login(0, false);
+require_capability('moodle/badges:manageglobalsettings', $context);
+// There should be an admin setting to completely turn off badges.
+$output = $PAGE->get_renderer('core', 'badges');
+
+$id = optional_param('id', 0, PARAM_INT);
+$action = optional_param('action', '', PARAM_ALPHA);
+
+$PAGE->set_pagelayout('admin');
+$url = new moodle_url('/badges/backpacks.php');
+
+if (empty($CFG->badges_allowexternalbackpack)) {
+    redirect($CFG->wwwroot);
+}
+
+$PAGE->set_url($url);
+$PAGE->set_title(get_string('managebackpacks', 'badges'));
+$PAGE->set_heading($SITE->fullname);
+if ($action == 'edit') {
+    $backpack = null;
+    if (!empty($id)) {
+        $backpack = badges_get_site_backpack($id);
+    }
+    $form = new \core_badges\form\external_backpack(null, ['externalbackpack' => $backpack]);
+    if ($form->is_cancelled()) {
+        redirect($url);
+    } else if ($data = $form->get_data()) {
+        require_sesskey();
+        if (!empty($data->id)) {
+            badges_update_site_backpack($data->id, $data);
+        } else {
+            badges_create_site_backpack($data);
+        }
+        redirect($url);
+    }
+
+    echo $OUTPUT->header();
+    echo $output->heading(get_string('managebackpacks', 'badges'));
+
+    $form->display();
+} else {
+    echo $OUTPUT->header();
+    echo $output->heading(get_string('managebackpacks', 'badges'));
+
+    $page = new \core_badges\output\external_backpacks_page($url);
+    echo $output->render($page);
+}
+
+echo $OUTPUT->footer();
index 560bb88..23a59a6 100644 (file)
@@ -38,7 +38,7 @@ $PAGE->set_url('/badges/badge.php', array('hash' => $id));
 $PAGE->set_pagelayout('base');
 $PAGE->set_title(get_string('issuedbadge', 'badges'));
 
-$badge = new issued_badge($id);
+$badge = new \core_badges\output\issued_badge($id);
 if (!empty($badge->recipient->id)) {
     if ($bake && ($badge->recipient->id == $USER->id)) {
         $name = str_replace(' ', '_', $badge->badgeclass['name']) . '.png';
index 2362bad..0f09064 100644 (file)
@@ -108,7 +108,7 @@ if ($badge->status != BADGE_STATUS_INACTIVE) {
                 if ($item->targetcode) {
                     $alignment['targetCode'] = $item->targetcode;
                 }
-                $json['alignment'][] = $alignment;
+                $json['alignments'][] = $alignment;
             }
         }
     } else if ($action == 0) {
index e1e0451..9525019 100644 (file)
@@ -87,34 +87,72 @@ class core_badges_assertion {
         $this->_obversion = $obversion;
     }
 
+    /**
+     * Get the local id for this badge.
+     *
+     * @return int
+     */
+    public function get_badge_id() {
+        $badgeid = 0;
+        if ($this->_data) {
+            $badgeid = $this->_data->id;
+        }
+        return $badgeid;
+    }
+
+    /**
+     * Get the local id for this badge assertion.
+     *
+     * @return string
+     */
+    public function get_assertion_hash() {
+        $hash = '';
+        if ($this->_data) {
+            $hash = $this->_data->uniquehash;
+        }
+        return $hash;
+    }
+
     /**
      * Get badge assertion.
      *
+     * @param boolean $issued Include the nested badge issued information.
+     * @param boolean $usesalt Hash the identity and include the salt information for the hash.
      * @return array Badge assertion.
      */
-    public function get_badge_assertion() {
+    public function get_badge_assertion($issued = true, $usesalt = true) {
         global $CFG;
         $assertion = array();
         if ($this->_data) {
             $hash = $this->_data->uniquehash;
             $email = empty($this->_data->backpackemail) ? $this->_data->email : $this->_data->backpackemail;
-            $assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash));
+            $assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'obversion' => $this->_obversion));
             $classurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'action' => 1));
 
             // Required.
             $assertion['uid'] = $hash;
             $assertion['recipient'] = array();
-            $assertion['recipient']['identity'] = 'sha256$' . hash('sha256', $email . $CFG->badges_badgesalt);
+            if ($usesalt) {
+                $assertion['recipient']['identity'] = 'sha256$' . hash('sha256', $email . $CFG->badges_badgesalt);
+            } else {
+                $assertion['recipient']['identity'] = $email;
+            }
             $assertion['recipient']['type'] = 'email'; // Currently the only supported type.
             $assertion['recipient']['hashed'] = true; // We are always hashing recipient.
-            $assertion['recipient']['salt'] = $CFG->badges_badgesalt;
-            $assertion['badge'] = $classurl->out(false);
+            if ($usesalt) {
+                $assertion['recipient']['salt'] = $CFG->badges_badgesalt;
+            }
+            if ($issued) {
+                $assertion['badge'] = $classurl->out(false);
+            }
             $assertion['verify'] = array();
             $assertion['verify']['type'] = 'hosted'; // 'Signed' is not implemented yet.
             $assertion['verify']['url'] = $assertionurl->out(false);
             $assertion['issuedOn'] = $this->_data->dateissued;
+            if ($issued) {
+                $assertion['evidence'] = $this->_url->out(false); // Currently issued badge URL.
+            }
             // Optional.
-            $assertion['evidence'] = $this->_url->out(false); // Currently issued badge URL.
             if (!empty($this->_data->dateexpire)) {
                 $assertion['expires'] = $this->_data->dateexpire;
             }
@@ -126,25 +164,42 @@ class core_badges_assertion {
     /**
      * Get badge class information.
      *
+     * @param boolean $issued Include the nested badge issuer information.
      * @return array Badge Class information.
      */
-    public function get_badge_class() {
-        $class = array();
+    public function get_badge_class($issued = true) {
+        $class = [];
         if ($this->_data) {
             if (empty($this->_data->courseid)) {
                 $context = context_system::instance();
             } else {
                 $context = context_course::instance($this->_data->courseid);
             }
-            $issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0));
-
             // Required.
             $class['name'] = $this->_data->name;
             $class['description'] = $this->_data->description;
-            $class['image'] = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f1')->out(false);
+            $storage = get_file_storage();
+            $imagefile = $storage->get_file($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f3.png');
+            if ($imagefile) {
+                $imagedata = base64_encode($imagefile->get_content());
+            } else {
+                if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
+                    // Unit tests the file might not exist yet.
+                    $imagedata = '';
+                } else {
+                    throw new coding_exception('Image file does not exist.');
+                }
+            }
+            $class['image'] = 'data:image/png;base64,' . $imagedata;
             $class['criteria'] = $this->_url->out(false); // Currently issued badge URL.
-            $class['issuer'] = $issuerurl->out(false);
+            if ($issued) {
+                $issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0));
+                $class['issuer'] = $issuerurl->out(false);
+            }
             $this->embed_data_badge_version2($class, OPEN_BADGES_V2_TYPE_BADGE);
+            if (!$issued) {
+                unset($class['issuer']);
+            }
         }
         return $class;
     }
@@ -155,14 +210,21 @@ class core_badges_assertion {
      * @return array Issuer information.
      */
     public function get_issuer() {
+        global $CFG;
         $issuer = array();
         if ($this->_data) {
             // Required.
-            $issuer['name'] = $this->_data->issuername;
-            $issuer['url'] = $this->_data->issuerurl;
-            // Optional.
-            if (!empty($this->_data->issuercontact)) {
-                $issuer['email'] = $this->_data->issuercontact;
+            if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+                $issuer['name'] = $this->_data->issuername;
+                $issuer['url'] = $this->_data->issuerurl;
+                // Optional.
+                if (!empty($this->_data->issuercontact)) {
+                    $issuer['email'] = $this->_data->issuercontact;
+                } else {
+                    $issuer['email'] = $CFG->badges_defaultissuercontact;
+                }
+            } else {
+                $issuer = badges_get_default_issuer();
             }
         }
         $this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER);
@@ -284,7 +346,6 @@ class core_badges_assertion {
                 }
                 unset($json['uid']);
             }
-
             // For Badge.
             if ($type == OPEN_BADGES_V2_TYPE_BADGE) {
                 $json['@context'] = OPEN_BADGES_V2_CONTEXT;
@@ -302,23 +363,25 @@ class core_badges_assertion {
                     $json['endorsement'] = $endorsementurl->out(false);
                 }
                 if ($alignments = $this->get_alignments()) {
-                    $json['alignment'] = $alignments;
+                    $json['alignments'] = $alignments;
                 }
                 if ($this->_data->imageauthorname ||
                         $this->_data->imageauthoremail ||
                         $this->_data->imageauthorurl ||
                         $this->_data->imagecaption) {
-                    $urlimage = moodle_url::make_pluginfile_url($context->id,
-                        'badges', 'badgeimage', $this->_data->id, '/', 'f1')->out(false);
-                    $json['image'] = array();
-                    $json['image']['id'] = $urlimage;
-                    if ($this->_data->imageauthorname || $this->_data->imageauthoremail || $this->_data->imageauthorurl) {
-                        $authorimage = new moodle_url('/badges/image_author_json.php', array('id' => $this->_data->id));
-                        $json['image']['author'] = $authorimage->out(false);
-                    }
-                    if ($this->_data->imagecaption) {
-                        $json['image']['caption'] = $this->_data->imagecaption;
+                    $storage = get_file_storage();
+                    $imagefile = $storage->get_file($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f1.png');
+                    if ($imagefile) {
+                        $imagedata = base64_encode($imagefile->get_content());
+                    } else {
+                        // The file might not exist in unit tests.
+                        if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
+                            $imagedata = '';
+                        } else {
+                            throw new coding_exception('Image file does not exist.');
+                        }
                     }
+                    $json['image'] = 'data:image/png;base64,' . $imagedata;
                 }
             }
 
diff --git a/badges/classes/backpack_api.php b/badges/classes/backpack_api.php
new file mode 100644 (file)
index 0000000..e96dd02
--- /dev/null
@@ -0,0 +1,673 @@
+<?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/>.
+
+/**
+ * Communicate with backpacks.
+ *
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+namespace core_badges;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/filelib.php');
+
+use cache;
+use coding_exception;
+use core_badges\external\assertion_exporter;
+use core_badges\external\collection_exporter;
+use core_badges\external\issuer_exporter;
+use core_badges\external\badgeclass_exporter;
+use curl;
+use stdClass;
+use context_system;
+
+define('BADGE_ACCESS_TOKEN', 'access');
+define('BADGE_USER_ID_TOKEN', 'user_id');
+define('BADGE_BACKPACK_ID_TOKEN', 'backpack_id');
+define('BADGE_REFRESH_TOKEN', 'refresh');
+define('BADGE_EXPIRES_TOKEN', 'expires');
+
+/**
+ * Class for communicating with backpacks.
+ *
+ * @package   core_badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backpack_api {
+
+    /** @var string The email address of the issuer or the backpack owner. */
+    private $email;
+
+    /** @var string The base url used for api requests to this backpack. */
+    private $backpackapiurl;
+
+    /** @var integer The backpack api version to use. */
+    private $backpackapiversion;
+
+    /** @var string The password to authenticate requests. */
+    private $password;
+
+    /** @var boolean User or site api requests. */
+    private $isuserbackpack;
+
+    /** @var integer The id of the backpack we are talking to. */
+    private $backpackid;
+
+    /** @var \backpack_api_mapping[] List of apis for the user or site using api version 1 or 2. */
+    private $mappings = [];
+
+    /**
+     * Create a wrapper to communicate with the backpack.
+     *
+     * The resulting class can only do either site backpack communication or
+     * user backpack communication.
+     *
+     * @param stdClass $sitebackpack The site backpack record
+     * @param mixed $userbackpack Optional - if passed it represents the users backpack.
+     */
+    public function __construct($sitebackpack, $userbackpack = false) {
+        global $CFG;
+        $admin = get_admin();
+
+        $this->backpackapiurl = $sitebackpack->backpackapiurl;
+        $this->backpackapiurl = $sitebackpack->backpackapiurl;
+        $this->backpackapiversion = $sitebackpack->apiversion;
+        $this->password = $sitebackpack->password;
+        $this->email = !empty($CFG->badges_defaultissuercontact) ? $CFG->badges_defaultissuercontact : $admin->email;
+        $this->isuserbackpack = false;
+        $this->backpackid = $sitebackpack->id;
+        if (!empty($userbackpack)) {
+            if ($userbackpack->externalbackpackid != $sitebackpack->id) {
+                throw new coding_exception('Incorrect backpack');
+            }
+            $this->isuserbackpack = true;
+            $this->password = $userbackpack->password;
+            $this->email = $userbackpack->email;
+        }
+
+        $this->define_mappings();
+        // Clear the last authentication error.
+        backpack_api_mapping::set_authentication_error('');
+    }
+
+    /**
+     * Define the mappings supported by this usage and api version.
+     */
+    private function define_mappings() {
+        if ($this->backpackapiversion == OPEN_BADGES_V2) {
+            if ($this->isuserbackpack) {
+                $mapping = [];
+                $mapping[] = [
+                    'collections',                              // Action.
+                    '[URL]/backpack/collections',               // URL
+                    [],                                         // Post params.
+                    '',                                         // Request exporter.
+                    'core_badges\external\collection_exporter', // Response exporter.
+                    true,                                       // Multiple.
+                    'get',                                      // Method.
+                    true,                                       // JSON Encoded.
+                    true                                  &nbs