Merge branch 'MDL-65134-master' of git://github.com/peterRd/moodle
authorJake Dallimore <jake@moodle.com>
Fri, 3 May 2019 02:05:26 +0000 (10:05 +0800)
committerJake Dallimore <jake@moodle.com>
Fri, 3 May 2019 02:05:26 +0000 (10:05 +0800)
441 files changed:
admin/cli/install.php
admin/settings/analytics.php
admin/settings/users.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/tests/behat/restoredefault.feature
admin/tool/behat/renderer.php
admin/tool/log/classes/local/privacy/helper.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/monitor/classes/privacy/provider.php
admin/tool/uploaduser/locallib.php
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/util/helper/tests/backup_encode_content_test.php
backup/util/settings/setting_dependency.class.php
backup/util/settings/tests/settings_test.php
backup/util/ui/classes/privacy/provider.php
badges/classes/privacy/provider.php
badges/tests/badgeslib_test.php
badges/tests/privacy_test.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/navigation/lang/en/block_navigation.php
calendar/amd/build/modal_event_form.min.js
calendar/amd/build/view_manager.min.js
calendar/amd/src/modal_event_form.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/classes/privacy/provider.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
calendar/tests/behat/calendar.feature
calendar/tests/privacy_test.php
cohort/classes/privacy/provider.php
competency/classes/course_module_competency.php
competency/lib.php
competency/tests/course_module_competency_test.php [new file with mode: 0644]
competency/tests/lib_test.php
completion/classes/privacy/provider.php
completion/criteria/completion_criteria.php
completion/cron.php [deleted file]
completion/tests/behat/behat_completion.php
completion/tests/behat/completion_course_page_checkboxes.feature [new file with mode: 0644]
completion/upgrade.txt
course/classes/management/helper.php
course/publish/metadata.php
course/renderer.php
course/request.php
course/tests/behat/behat_course.php
course/tests/behat/customfields_locked.feature
course/tests/behat/customfields_visibility.feature
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/classes/field_controller.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/classes/privacy/provider.php
enrol/renderer.php
filter/algebra/tests/filter_test.php
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
group/classes/privacy/provider.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/amd/build/event.min.js
lib/amd/src/event.js
lib/badgeslib.php
lib/behat/classes/behat_command.php
lib/behat/classes/partial_named_selector.php
lib/behat/classes/util.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/message/manager.php
lib/classes/message/message.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/string_manager_standard.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.xml
lib/db/messages.php
lib/db/upgrade.php
lib/editor/atto/plugins/emoticon/lib.php
lib/editor/atto/plugins/title/yui/build/moodle-atto_title-button/moodle-atto_title-button-debug.js
lib/editor/atto/plugins/title/yui/build/moodle-atto_title-button/moodle-atto_title-button-min.js
lib/editor/atto/plugins/title/yui/build/moodle-atto_title-button/moodle-atto_title-button.js
lib/editor/atto/plugins/title/yui/src/button/js/button.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/autosave-io.js
lib/editor/tinymce/module.js
lib/editor/tinymce/plugins/moodleemoticon/dialog.php
lib/editor/tinymce/plugins/moodleemoticon/lib.php
lib/editor/tinymce/tiny_mce/3.5.11/tiny_mce_src.js
lib/filelib.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/select.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/messagelib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm/element.php
lib/portfoliolib.php
lib/questionlib.php
lib/setup.php
lib/setuplib.php
lib/templates/permissionmanager_panelcontent.mustache
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/exporter_test.php
lib/tests/filelib_test.php
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/tests/outputcomponents_test.php
lib/tests/string_manager_standard_test.php
lib/tests/time_splittings_test.php
lib/upgrade.txt
lib/weblib.php
lib/yui/build/moodle-core-event/moodle-core-event-debug.js
lib/yui/build/moodle-core-event/moodle-core-event-min.js
lib/yui/build/moodle-core-event/moodle-core-event.js
lib/yui/src/event/js/event.js
message/classes/api.php
message/classes/helper.php
message/classes/privacy/provider.php
message/externallib.php
message/lib.php
message/output/airnotifier/tests/externallib_test.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/version.php
message/output/popup/classes/api.php
message/output/popup/externallib.php
message/output/popup/tests/base.php
message/output/popup/tests/externallib_test.php
message/templates/message_drawer_view_contact_body_content.mustache
message/templates/message_drawer_view_conversation_body.mustache
message/templates/message_drawer_view_conversation_header_content_type_private.mustache
message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
message/templates/message_drawer_view_conversation_header_content_type_public.mustache
message/templates/message_drawer_view_conversation_header_edit_mode.mustache
message/tests/api_test.php
message/tests/behat/behat_message.php
message/tests/behat/favourite_conversations.feature [new file with mode: 0644]
message/tests/behat/message_delete_conversation.feature [new file with mode: 0644]
message/tests/behat/message_drawer_manage_contacts.feature
message/tests/behat/message_manage_preferences.feature [new file with mode: 0644]
message/tests/behat/unread_messages.feature [new file with mode: 0644]
message/tests/externallib_test.php
message/tests/helper_test.php
message/tests/privacy_provider_test.php
message/upgrade.txt
mnet/service/enrol/classes/privacy/provider.php
mod/assign/amd/build/grading_panel.min.js
mod/assign/amd/src/grading_panel.js
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/tests/locallib_test.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/external_test.php
mod/feedback/tests/lib_test.php
mod/feedback/tests/privacy_test.php
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/lock_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/pin_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/repository.min.js
mod/forum/amd/build/selectors.min.js
mod/forum/amd/src/discussion_list.js
mod/forum/amd/src/favourite_toggle.js [new file with mode: 0644]
mod/forum/amd/src/lock_toggle.js [new file with mode: 0644]
mod/forum/amd/src/pin_toggle.js [new file with mode: 0644]
mod/forum/amd/src/repository.js
mod/forum/amd/src/selectors.js
mod/forum/backup/moodle2/backup_forum_stepslib.php
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/data_mappers/legacy/discussion.php
mod/forum/classes/local/entities/discussion.php
mod/forum/classes/local/entities/forum.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/entity.php
mod/forum/classes/local/factories/exporter.php
mod/forum/classes/local/factories/legacy_data_mapper.php
mod/forum/classes/local/factories/renderer.php
mod/forum/classes/local/factories/url.php
mod/forum/classes/local/factories/vault.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/privacy/provider.php
mod/forum/classes/task/send_user_notifications.php
mod/forum/db/access.php
mod/forum/db/install.xml
mod/forum/db/messages.php
mod/forum/db/services.php
mod/forum/db/upgrade.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 [new file with mode: 0644]
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/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/discussion_lock.feature [new file with mode: 0644]
mod/forum/tests/behat/favourite_discussion.feature [new file with mode: 0644]
mod/forum/tests/entities_discussion_summary_test.php
mod/forum/tests/entities_discussion_test.php
mod/forum/tests/entities_forum_test.php
mod/forum/tests/exporters_author_test.php
mod/forum/tests/exporters_discussion_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/generator/lib.php
mod/forum/tests/mail_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/db/messages.php
mod/lesson/essay.php
mod/lesson/lang/en/lesson.php
mod/lesson/mod_form.php
mod/lti/mod_form.php
mod/page/mod_form.php
mod/quiz/accessrule/accessrulebase.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/db/messages.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/mod_form.php
mod/quiz/tests/external_test.php
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/survey/tests/behat/survey_completion.feature
mod/url/mod_form.php
mod/workshop/mod_form.php
pix/e/styleparagraph.png [new file with mode: 0644]
pix/e/styleparagraph.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/interactivecountback/behaviour.php
question/category.php
question/category_class.php
question/edit.php
question/engine/questionattemptstep.php
question/engine/upgrade/tests/helper.php
question/export.php
question/format.php
question/import.php
question/preview.php
question/previewlib.php
question/question.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/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/questiontypebase.php
report/stats/classes/privacy/provider.php
repository/dropbox/lang/en/repository_dropbox.php
repository/filesystem/lang/en/repository_filesystem.php
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/bootstrap/utilities/_position.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/question.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/pix/screenshot.png
theme/classic/style/moodle.css
user/lib.php
user/tests/behat/view_full_profile.feature
user/tests/userlib_test.php
version.php

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 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 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 bf2b7a1..7de834a 100644 (file)
@@ -162,11 +162,11 @@ class tool_behat_renderer extends plugin_renderer_base {
         $html .= $this->output->heading($title);
 
         // Info.
-        $installurl = behat_command::DOCS_URL . '#Installation';
+        $installurl = behat_command::DOCS_URL;
         $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
-        $writetestsurl = behat_command::DOCS_URL . '#Writing_features';
+        $writetestsurl = 'https://docs.moodle.org/dev/Writing acceptance tests';
         $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
-        $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
+        $writestepsurl = 'https://docs.moodle.org/dev/Writing_new_acceptance_test_step_definitions';
         $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
         $infos = array(
             get_string('installinfo', 'tool_behat', $installlink),
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 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';
index d8ca913..2ffbf66 100644 (file)
@@ -114,29 +114,16 @@ class provider implements
     public static function get_users_in_context(userlist $userlist) {
         $context = $userlist->get_context();
 
-        if (!is_a($context, \context_user::class)) {
+        if (!$context instanceof \context_user) {
             return;
         }
 
-        $params = [
-            'contextid' => $context->id,
-            'contextuser' => CONTEXT_USER,
-        ];
-
-        $sql = "SELECT mr.userid
-                  FROM {context} ctx
-                  JOIN {tool_monitor_rules} mr ON ctx.instanceid = mr.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
+        $params = ['userid' => $context->instanceid];
 
+        $sql = "SELECT userid FROM {tool_monitor_rules} WHERE userid = :userid";
         $userlist->add_from_sql('userid', $sql, $params);
 
-        $sql = "SELECT ms.userid
-                  FROM {context} ctx
-             LEFT JOIN {tool_monitor_subscriptions} ms ON ctx.instanceid = ms.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid FROM {tool_monitor_subscriptions} WHERE userid = :userid";
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index ae3dfe3..f6eecce 100644 (file)
@@ -183,6 +183,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 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 149f606..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);
+        }
     }
 }
 
@@ -5269,7 +5385,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     }
 
     /**
-     * This method does the acutal work for process_question_usage or
+     * This method does the actual work for process_question_usage or
      * process_{nameprefix}_question_usage.
      * @param array $data the data from the XML file.
      * @param string $nameprefix the element name prefix.
@@ -5304,7 +5420,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     abstract protected function inform_new_usage_id($newusageid);
 
     /**
-     * This method does the acutal work for process_question_attempt or
+     * This method does the actual work for process_question_attempt or
      * process_{nameprefix}_question_attempt.
      * @param array $data the data from the XML file.
      * @param string $nameprefix the element name prefix.
@@ -5334,7 +5450,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     }
 
     /**
-     * This method does the acutal work for process_question_attempt_step or
+     * This method does the actual work for process_question_attempt_step or
      * process_{nameprefix}_question_attempt_step.
      * @param array $data the data from the XML file.
      * @param string $nameprefix the element name prefix.
index 63554d3..5406712 100644 (file)
@@ -33,7 +33,7 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_course_task.class.php');
 /**
  * Tests for encoding content links in backup_course_task.
  *
- * The code that this tests is acutally in backup/moodle2/backup_course_task.class.php,
+ * The code that this tests is actually in backup/moodle2/backup_course_task.class.php,
  * but there is no place for unit tests near there, and perhaps one day it will
  * be refactored so it becomes more generic.
  */
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 8c36b45..e4893e6 100644 (file)
@@ -142,18 +142,11 @@ class provider implements
         $context = $userlist->get_context();
 
         if ($context instanceof \context_course) {
-            $params = [
-                'contextcourse' => CONTEXT_COURSE,
-                'contextid' => $context->id,
-
-            ];
+            $params = ['courseid' => $context->instanceid];
 
             $sql = "SELECT bc.userid
                       FROM {backup_controllers} bc
-                      JOIN {context} ctx
-                           ON ctx.instanceid = bc.itemid
-                           AND ctx.contextlevel = :contextcourse
-                     WHERE ctx.id = :contextid
+                     WHERE bc.itemid = :courseid
                            AND bc.type = :typecourse";
 
             $courseparams = ['typecourse' => 'course'] + $params;
@@ -164,10 +157,7 @@ class provider implements
                       FROM {backup_controllers} bc
                       JOIN {course_sections} c
                            ON bc.itemid = c.id
-                      JOIN {context} ctx
-                           ON ctx.instanceid = c.course
-                           AND ctx.contextlevel = :contextcourse
-                     WHERE ctx.id = :contextid
+                     WHERE c.course = :courseid
                            AND bc.type = :typesection";
 
             $sectionparams = ['typesection' => 'section'] + $params;
@@ -177,17 +167,13 @@ class provider implements
 
         if ($context instanceof \context_module) {
             $params = [
-                'contextmodule' => CONTEXT_MODULE,
-                'contextid' => $context->id,
+                'cmid' => $context->instanceid,
                 'typeactivity' => 'activity'
             ];
 
             $sql = "SELECT bc.userid
                       FROM {backup_controllers} bc
-                      JOIN {context} ctx
-                           ON ctx.instanceid = bc.itemid
-                           AND ctx.contextlevel = :contextmodule
-                     WHERE ctx.id = :contextid
+                     WHERE bc.itemid = :cmid
                            AND bc.type = :typeactivity";
 
             $userlist->add_from_sql('userid', $sql, $params);
index ab3c83d..a0bafb4 100644 (file)
@@ -197,20 +197,21 @@ class provider implements
 
         if ($context->contextlevel == CONTEXT_COURSE || $context->contextlevel == CONTEXT_SYSTEM) {
             // Find the modifications we made on badges (course & system).
-            $params = [
-                'courselevel' => CONTEXT_COURSE,
-                'syscontextid' => SYSCONTEXTID,
-                'typecourse' => BADGE_TYPE_COURSE,
-                'typesite' => BADGE_TYPE_SITE,
-                'contextid' => $context->id,
-            ];
+            if ($context->contextlevel == CONTEXT_COURSE) {
+                $extrawhere = 'AND b.courseid = :courseid';
+                $params = [
+                    'badgetype' => BADGE_TYPE_COURSE,
+                    'courseid'  => $context->instanceid
+                ];
+            } else {
+                $extrawhere = '';
+                $params = ['badgetype' => BADGE_TYPE_SITE];
+            }
 
             $sql = "SELECT b.usermodified, b.usercreated
                       FROM {badge} b
-                      JOIN {context} ctx
-                           ON (b.type = :typecourse AND b.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel)
-                           OR (b.type = :typesite AND ctx.id = :syscontextid)
-                     WHERE ctx.id = :contextid";
+                     WHERE b.type = :badgetype
+                           $extrawhere";
 
             $userlist->add_from_sql('usermodified', $sql, $params);
             $userlist->add_from_sql('usercreated', $sql, $params);
@@ -376,18 +377,40 @@ class provider implements
 
             // Export the badges.
             $uniqueid = $DB->sql_concat_join("'-'", ['b.id', 'COALESCE(bc.id, 0)', 'COALESCE(bi.id, 0)',
-                'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)']);
+                'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)', 'COALESCE(brb.id, 0)', 'COALESCE(ba.id, 0)']);
             $sql = "
                 SELECT $uniqueid AS uniqueid, b.id,
                        bi.id AS biid, bi.dateissued, bi.dateexpire, bi.uniquehash,
                        bma.id AS bmaid, bma.datemet, bma.issuerid,
                        bcm.id AS bcmid,
                        c.fullname AS coursename,
+                       be.id AS beid,
+                       be.issuername AS beissuername,
+                       be.issuerurl AS beissuerurl,
+                       be.issueremail AS beissueremail,
+                       be.claimid AS beclaimid,
+                       be.claimcomment AS beclaimcomment,
+                       be.dateissued AS bedateissued,
+                       brb.id as rbid,
+                       brb.badgeid as rbbadgeid,
+                       brb.relatedbadgeid as rbrelatedbadgeid,
+                       ba.id as baid,
+                       ba.targetname as batargetname,
+                       ba.targeturl as batargeturl,
+                       ba.targetdescription as batargetdescription,
+                       ba.targetframework as batargetframework,
+                       ba.targetcode as batargetcode,
                        $ctxfields
                   FROM {badge} b
              LEFT JOIN {badge_issued} bi
                     ON bi.badgeid = b.id
                    AND bi.userid = :userid1
+            LEFT JOIN {badge_related} brb
+                    ON ( b.id = brb.badgeid OR b.id = brb.relatedbadgeid )
+             LEFT JOIN {badge_alignment} ba
+                    ON ( b.id = ba.badgeid )
+             LEFT JOIN {badge_endorsement} be
+                    ON be.badgeid = b.id
              LEFT JOIN {badge_manual_award} bma
                     ON bma.badgeid = b.id
                    AND bma.recipientid = :userid2
@@ -421,9 +444,16 @@ class provider implements
                 if ($carry === null) {
                     $carry = [
                         'name' => $badge->name,
+                        'version' => $badge->version,
+                        'language' => $badge->language,
+                        'imageauthorname' => $badge->imageauthorname,
+                        'imageauthoremail' => $badge->imageauthoremail,
+                        'imageauthorurl' => $badge->imageauthorurl,
+                        'imagecaption' => $badge->imagecaption,
                         'issued' => null,
                         'manual_award' => null,
-                        'criteria_met' => []
+                        'criteria_met' => [],
+                        'endorsement' => null,
                     ];
 
                     if ($badge->type == BADGE_TYPE_COURSE) {
@@ -431,6 +461,17 @@ class provider implements
                         $carry['course'] = format_string($record->coursename, true, ['context' => $badge->get_context()]);
                     }
 
+                    if (!empty($record->beid)) {
+                        $carry['endorsement'] = [
+                            'issuername' => $record->beissuername,
+                            'issuerurl' => $record->beissuerurl,
+                            'issueremail' => $record->beissueremail,
+                            'claimid' => $record->beclaimid,
+                            'claimcomment' => $record->beclaimcomment,
+                            'dateissued' => $record->bedateissued ? transform::datetime($record->bedateissued) : null
+                        ];
+                    }
+
                     if (!empty($record->biid)) {
                         $carry['issued'] = [
                             'issued_on' => transform::datetime($record->dateissued),
@@ -446,6 +487,52 @@ class provider implements
                         ];
                     }
                 }
+                if (!empty($record->rbid)) {
+                    if (empty($carry['related_badge'])) {
+                        $carry['related_badge'] = [];
+                    }
+                    $rbid = $record->rbbadgeid;
+                    if ($rbid == $record->id) {
+                        $rbid = $record->rbrelatedbadgeid;
+                    }
+                    $exists = false;
+                    foreach ($carry['related_badge'] as $related) {
+                        if ($related['badgeid'] == $rbid) {
+                            $exists = true;
+                            break;
+                        }
+                    }
+                    if (!$exists) {
+                        $relatedbadge = new badge($rbid);
+                        $carry['related_badge'][] = [
+                            'badgeid' => $rbid,
+                            'badgename' => $relatedbadge->name
+                        ];
+                    }
+                }
+
+                if (!empty($record->baid)) {
+                    if (empty($carry['alignment'])) {
+                        $carry['alignment'] = [];
+                    }
+                    $exists = false;
+                    $newalignment = [
+                        'targetname' => $record->batargetname,
+                        'targeturl' => $record->batargeturl,
+                        'targetdescription' => $record->batargetdescription,
+                        'targetframework' => $record->batargetframework,
+                        'targetcode' => $record->batargetcode,
+                    ];
+                    foreach ($carry['alignment'] as $alignment) {
+                        if ($alignment == $newalignment) {
+                            $exists = true;
+                            break;
+                        }
+                    }
+                    if (!$exists) {
+                        $carry['alignment'][] = $newalignment;
+                    }
+                }
 
                 // Export the details of the criteria met.
                 // We only do that once, when we find that a least one criteria was met.
index d37e417..4d9604e 100644 (file)
@@ -290,13 +290,29 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
     }
 
     public function test_badge_awards() {
+        global $DB;
         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
         $badge = new badge($this->badgeid);
         $user1 = $this->getDataGenerator()->create_user();
 
-        $badge->issue($user1->id, true);
+        $sink = $this->redirectMessages();
+
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_badgerecipientnotice_loggedoff', 'email', $user1);
+
+        $badge->issue($user1->id, false);
+        $this->assertDebuggingCalled(); // Expect debugging while baking a badge via phpunit.
         $this->assertTrue($badge->is_issued($user1->id));
 
+        $messages = $sink->get_messages();
+        $sink->close();
+        $this->assertCount(1, $messages);
+        $message = array_pop($messages);
+        // Check we have the expected data.
+        $customdata = json_decode($message->customdata);
+        $this->assertObjectHasAttribute('notificationiconurl', $customdata);
+        $this->assertObjectHasAttribute('hash', $customdata);
+
         $user2 = $this->getDataGenerator()->create_user();
         $badge->issue($user2->id, true);
         $this->assertTrue($badge->is_issued($user2->id));
index 1bfb93a..fb75770 100644 (file)
@@ -306,7 +306,11 @@ class core_badges_privacy_testcase extends provider_testcase {
         $u2ctx = context_user::instance($u2->id);
 
         $b1 = $this->create_badge(['usercreated' => $u3->id]);
+        $this->endorse_badge(['badgeid' => $b1->id]);
+        $this->align_badge(['badgeid' => $b1->id], ' (1)');
+        $this->align_badge(['badgeid' => $b1->id], ' (2)');
         $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id, 'usermodified' => $u3->id]);
+        $this->relate_badge($b1->id, $b2->id);
         $b3 = $this->create_badge();
         $b3crit = $this->create_criteria_manual($b3->id);
         $b4 = $this->create_badge();
@@ -333,6 +337,12 @@ class core_badges_privacy_testcase extends provider_testcase {
         $path = [get_string('badges', 'core_badges'), "{$b1->name} ({$b1->id})"];
         $data = writer::with_context($u1ctx)->get_data($path);
         $this->assertEquals($b1->name, $data->name);
+        $this->assertEquals($b1->version, $data->version);
+        $this->assertEquals($b1->language, $data->language);
+        $this->assertEquals($b1->imageauthorname, $data->imageauthorname);
+        $this->assertEquals($b1->imageauthoremail, $data->imageauthoremail);
+        $this->assertEquals($b1->imageauthorurl, $data->imageauthorurl);
+        $this->assertEquals($b1->imagecaption, $data->imagecaption);
         $this->assertNotEmpty($data->issued);
         $this->assertEmpty($data->manual_award);
         $this->assertEmpty($data->criteria_met);
@@ -340,6 +350,33 @@ class core_badges_privacy_testcase extends provider_testcase {
         $this->assertEquals('yoohoo', $data->issued['unique_hash']);
         $this->assertNull($data->issued['expires_on']);
 
+        $this->assertNotEmpty($data->endorsement);
+        $this->assertNotEmpty($data->endorsement['issuername']);
+        $this->assertNotEmpty($data->endorsement['issuerurl']);
+        $this->assertNotEmpty($data->endorsement['issueremail']);
+        $this->assertNotEmpty($data->endorsement['claimid']);
+        $this->assertNotEmpty($data->endorsement['claimcomment']);
+        $this->assertNotEmpty($data->endorsement['dateissued']);
+
+        $this->assertNotEmpty($data->related_badge);
+        $this->assertNotEmpty($data->related_badge[0]);
+        $this->assertEquals($data->related_badge[0]['badgeid'], $b2->id);
+        $this->assertEquals($data->related_badge[0]['badgename'], $b2->name);
+
+        $this->assertNotEmpty($data->alignment);
+        $this->assertNotEmpty($data->alignment[0]);
+        $this->assertNotEmpty($data->alignment[0]['targetname']);
+        $this->assertNotEmpty($data->alignment[0]['targeturl']);
+        $this->assertNotEmpty($data->alignment[0]['targetdescription']);
+        $this->assertNotEmpty($data->alignment[0]['targetframework']);
+        $this->assertNotEmpty($data->alignment[0]['targetcode']);
+        $this->assertNotEmpty($data->alignment[1]);
+        $this->assertNotEmpty($data->alignment[1]['targetname']);
+        $this->assertNotEmpty($data->alignment[1]['targeturl']);
+        $this->assertNotEmpty($data->alignment[1]['targetdescription']);
+        $this->assertNotEmpty($data->alignment[1]['targetframework']);
+        $this->assertNotEmpty($data->alignment[1]['targetcode']);
+
         $path = [get_string('badges', 'core_badges'), "{$b2->name} ({$b2->id})"];
         $data = writer::with_context($u1ctx)->get_data($path);
         $this->assertEquals($b2->name, $data->name);
@@ -598,12 +635,79 @@ class core_badges_privacy_testcase extends provider_testcase {
             'attachment' => 1,
             'notification' => 0,
             'status' => BADGE_STATUS_ACTIVE,
+            'version' => OPEN_BADGES_V2,
+            'language' => 'en',
+            'imageauthorname' => 'Image author',
+            'imageauthoremail' => 'author@example.com',
+            'imageauthorurl' => 'http://image.example.com/',
+            'imagecaption' => 'Image caption'
         ], $params);
         $record->id = $DB->insert_record('badge', $record);
 
         return $record;
     }
 
+    /**
+     * Relate a badge.
+     *
+     * @param int $badgeid The badge ID.
+     * @param int $relatedbadgeid The related badge ID.
+     * @return object
+     */
+    protected function relate_badge(int $badgeid, int $relatedbadgeid) {
+        global $DB;
+        $record = (object) [
+            'badgeid' => $badgeid,
+            'relatedbadgeid' => $relatedbadgeid
+        ];
+        $record->id = $DB->insert_record('badge_related', $record);
+
+        return $record;
+    }
+
+    /**
+     * Align a badge.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function align_badge(array $params = [], $suffix = '') {
+        global $DB;
+        $record = (object) array_merge([
+            'badgeid' => null,
+            'targetname' => "Alignment name" . $suffix,
+            'targeturl' => "http://issuer-url.domain.co.nz",
+            'targetdescription' => "Description" . $suffix,
+            'targetframework' => "Framework" . $suffix,
+            'targetcode' => "Code . $suffix"
+        ], $params);
+        $record->id = $DB->insert_record('badge_alignment', $record);
+
+        return $record;
+    }
+
+    /**
+     * Endorse a badge.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function endorse_badge(array $params = []) {
+        global $DB;
+        $record = (object) array_merge([
+            'badgeid' => null,
+            'issuername' => "External issuer name",
+            'issuerurl' => "http://issuer-url.domain.co.nz",
+            'issueremail' => "issuer@example.com",
+            'claimid' => "Claim ID",
+            'claimcomment' => "Claim comment",
+            'dateissued' => time()
+        ], $params);
+        $record->id = $DB->insert_record('badge_endorsement', $record);
+
+        return $record;
+    }
+
     /**
      * Create a backpack.
      *
index b6f51b8..79a0b78 100644 (file)
@@ -49,8 +49,8 @@ $string['card'] = 'Card';
 $string['cards'] = 'Cards';
 $string['courseprogress'] = 'Course progress:';
 $string['completepercent'] = '{$a}% complete';
-$string['displaycategories'] = 'Display Categories';
-$string['displaycategories_help'] = 'Display the Course Category on dashboard course items including cards, list items and summary items';
+$string['displaycategories'] = 'Display categories';
+$string['displaycategories_help'] = 'Display the course category on dashboard course items including cards, list items and summary items.';
 $string['favourites'] = 'Starred';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
index 89c95e6..94e10ac 100644 (file)
@@ -27,7 +27,7 @@
 $string['everything'] = 'Everything';
 $string['courses'] = 'Categories and courses';
 $string['coursestructures'] = 'Categories, courses, and course structures';
-$string['courseactivities'] = 'Categories, courses, and course Activities';
+$string['courseactivities'] = 'Categories, courses, and course activities';
 $string['enabledockdesc'] = 'Allow the user to dock this block';
 $string['expansionlimit'] = 'Generate navigation for the following';
 $string['linkcategoriesdesc'] = 'Display categories as links';
index 79035b5..2befc26 100644 (file)
Binary files a/calendar/amd/build/modal_event_form.min.js and b/calendar/amd/build/modal_event_form.min.js differ
index dd946d4..e615028 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index 2401ac5..be7f718 100644 (file)
@@ -407,7 +407,17 @@ define([
      * @return {object} A promise
      */
     ModalEventForm.prototype.save = function() {
-        var loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
+        var invalid,
+            loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
+
+        // Now the change events have run, see if there are any "invalid" form fields.
+        invalid = this.getForm().find('[aria-invalid="true"]');
+
+        // If we found invalid fields, focus on the first one and do not submit via ajax.
+        if (invalid.length) {
+            invalid.first().focus();
+            return;
+        }
 
         loadingContainer.removeClass('hidden');
         this.disableButtons();
@@ -472,6 +482,8 @@ define([
         // Catch the submit event before it is actually processed by the browser and
         // prevent the submission. We'll take it from here.
         this.getModal().on('submit', function(e) {
+            Event.notifyFormSubmitAjax(this.getForm()[0]);
+
             this.save();
 
             // Stop the form from actually submitting and prevent it's
index 2c59a72..7043f75 100644 (file)
@@ -339,28 +339,6 @@ define([
                 .fail(Notification.exception);
         };
 
-        /**
-         * Convert the given event type into one of either user, site,
-         * group, category, or course.
-         *
-         * @param {String} eventType The calendar event type
-         * @return {String}
-         */
-        var normaliseEventType = function(eventType) {
-            switch (eventType) {
-                case 'user':
-                    return 'user';
-                case 'site':
-                    return 'site';
-                case 'group':
-                    return 'group';
-                case 'category':
-                    return 'category';
-                default:
-                    return 'course';
-            }
-        };
-
         /**
          * Get the CSS class to apply for the given event type.
          *
@@ -368,7 +346,7 @@ define([
          * @return {String}
          */
         var getEventTypeClassFromType = function(eventType) {
-            return 'calendar_event_' + normaliseEventType(eventType);
+            return 'calendar_event_' + eventType;
         };
 
         /**
@@ -385,12 +363,9 @@ define([
                     throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
                 }
                 var eventData = getEventResponse.event;
-                typeClass = getEventTypeClassFromType(eventData.eventtype);
+                typeClass = getEventTypeClassFromType(eventData.normalisedeventtype);
 
-                return getEventType(eventData.eventtype).then(function(eventType) {
-                    eventData.eventtype = eventType;
-                    return eventData;
-                });
+                return eventData;
             }).then(function(eventData) {
                 // Build the modal parameters from the event data.
                 var modalParams = {
@@ -422,19 +397,6 @@ define([
             }).fail(Notification.exception);
         };
 
-        /**
-         * Get the event type lang string.
-         *
-         * @param {String} eventType The event type.
-         * @return {promise} The lang string promise.
-         */
-        var getEventType = function(eventType) {
-            var lang = 'type' + normaliseEventType(eventType);
-            return Str.get_string(lang, 'core_calendar').then(function(langStr) {
-                return langStr;
-            });
-        };
-
         return {
             init: function(root) {
                 registerEventListeners(root);
index f4b629f..ea32de3 100644 (file)
@@ -52,9 +52,6 @@ class calendar_event_exporter extends event_exporter_base {
             'type' => PARAM_BOOL,
             'default' => false,
         ];
-        $values['calendareventtype'] = [
-            'type' => PARAM_TEXT,
-        ];
         $values['popupname'] = [
             'type' => PARAM_RAW,
         ];
@@ -172,8 +169,6 @@ class calendar_event_exporter extends event_exporter_base {
             $values['popupname'] = get_string('eventnameandcourse', 'calendar', $eventnameparams);
         }
 
-        $values['calendareventtype'] = $this->get_calendar_event_type();
-
         if ($event->get_course_module()) {
             $values = array_merge($values, $this->get_module_timestamp_limits($event));
         } else if ($hascourse && $course->id != SITEID && empty($event->get_group())) {
index 51ee3e4..015d71a 100644 (file)
@@ -55,7 +55,6 @@ class event_exporter extends event_exporter_base {
             'type' => event_action_exporter::read_properties_definition(),
             'optional' => true,
         ];
-
         return $values;
     }
 
index d20cfb3..08d14ac 100644 (file)
@@ -125,7 +125,7 @@ class event_exporter_base extends exporter {
                 'null' => NULL_ALLOWED
             ],
             'location' => [
-                'type' => PARAM_RAW_TRIMMED,
+                'type' => PARAM_RAW,
                 'optional' => true,
                 'default' => null,
                 'null' => NULL_ALLOWED
@@ -236,6 +236,12 @@ class event_exporter_base extends exporter {
                 'default' => null,
                 'null' => NULL_ALLOWED
             ],
+            'normalisedeventtype' => [
+                'type' => PARAM_TEXT
+            ],
+            'normalisedeventtypetext' => [
+                'type' => PARAM_TEXT
+            ],
         ];
     }
 
@@ -254,11 +260,14 @@ class event_exporter_base extends exporter {
         $values['isactionevent'] = false;
         $values['iscourseevent'] = false;
         $values['iscategoryevent'] = false;
+        $values['normalisedeventtype'] = $event->get_type();
         if ($moduleproxy = $event->get_course_module()) {
             // We need a separate property to flag if an event is action event.
             // That's required because canedit return true but action action events cannot be edited on the calendar UI.
             // But they are considered editable because you can drag and drop the event on the month view.
             $values['isactionevent'] = true;
+            // Activity events are normalised to "look" like course events.
+            $values['normalisedeventtype'] = 'course';
         } else if ($event->get_type() == 'course') {
             $values['iscourseevent'] = true;
         } else if ($event->get_type() == 'category') {
@@ -266,6 +275,7 @@ class event_exporter_base extends exporter {
         }
         $timesort = $event->get_times()->get_sort_time()->getTimestamp();
         $iconexporter = new event_icon_exporter($event, ['context' => $context]);
+        $values['normalisedeventtypetext'] = get_string('type' . $values['normalisedeventtype'], 'calendar');
 
         $values['icon'] = $iconexporter->export($output);
 
index 8ed10c2..369b601 100644 (file)
@@ -160,93 +160,61 @@ class provider implements
      * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
      */
     public static function get_users_in_context(userlist $userlist) {
+        global $DB;
+
         $context = $userlist->get_context();
 
-        $allowedcontexts = [
-            CONTEXT_SYSTEM,
-            CONTEXT_COURSECAT,
-            CONTEXT_COURSE,
-            CONTEXT_MODULE,
-            CONTEXT_USER
-        ];
+        // Calendar Events can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
+        // Course and Course Group (CONTEXT_COURSE), User (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts.
+        if ($context->contextlevel == CONTEXT_MODULE) {
+            $params = ['cmid' => $context->instanceid];
+
+            $sql = "SELECT e.userid
+                      FROM {course_modules} cm
+                      JOIN {modules} m ON m.id = cm.module
+                      JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
+                     WHERE cm.id = :cmid";
+
+            $userlist->add_from_sql('userid', $sql, $params);
+        } else if ($context->contextlevel == CONTEXT_SYSTEM) {
+            // Get contexts of Calendar Events for the owner.
+            $sql = "SELECT userid FROM {event} WHERE eventtype = 'site'";
+            $userlist->add_from_sql('userid', $sql, []);
+
+            // Get contexts for Calendar Subscriptions for the owner.
+            $sql = "SELECT userid FROM {event_subscriptions} WHERE eventtype = 'site'";
+            $userlist->add_from_sql('userid', $sql, []);
+        } else if (in_array($context->contextlevel, [CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER])) {
+            $eventfields = [
+                CONTEXT_COURSECAT   => 'categoryid',
+                CONTEXT_COURSE      => 'courseid',
+                CONTEXT_USER        => 'userid'
+            ];
+            $eventfield = $eventfields[$context->contextlevel];
 
-        if (!in_array($context->contextlevel, $allowedcontexts)) {
-            return;
+            $eventtypes = [
+                CONTEXT_COURSECAT   => 'category',
+                CONTEXT_COURSE      => ['course' , 'group'],
+                CONTEXT_USER        => 'user'
+            ];
+            list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);
+
+            $params = $eventtypeparams + ['instanceid' => $context->instanceid];
+
+            // Get contexts of Calendar Events for the owner.
+            $sql = "SELECT userid
+                      FROM {event}
+                     WHERE eventtype $eventtypesql
+                           AND $eventfield = :instanceid";
+            $userlist->add_from_sql('userid', $sql, $params);
+
+            // Get contexts for Calendar Subscriptions for the owner.
+            $sql = "SELECT userid
+                      FROM {event_subscriptions}
+                     WHERE eventtype $eventtypesql
+                           AND $eventfield = :instanceid";
+            $userlist->add_from_sql('userid', $sql, $params);
         }
-
-        $params = [
-            'modulecontext'      => CONTEXT_MODULE,
-            'contextid'          => $context->id,
-        ];
-
-        $sql = "SELECT e.userid
-                  FROM {course_modules} cm
-                  JOIN {modules} m
-                       ON m.id = cm.module
-                  JOIN {event} e
-                       ON e.modulename = m.name
-                          AND e.courseid = cm.course
-                          AND e.instance = cm.instance
-                  JOIN {context} ctx
-                       ON ctx.instanceid = cm.id
-                          AND ctx.contextlevel = :modulecontext
-                 WHERE ctx.id = :contextid";
-
-        $userlist->add_from_sql('userid', $sql, $params);
-
-        // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
-        $params = [
-            'sitecontext'        => CONTEXT_SYSTEM,
-            'categorycontext'    => CONTEXT_COURSECAT,
-            'coursecontext'      => CONTEXT_COURSE,
-            'groupcontext'       => CONTEXT_COURSE,
-            'usercontext'        => CONTEXT_USER,
-            'contextid'          => $context->id
-        ];
-
-        // Get contexts of Calendar Events for the owner.
-        $sql = "SELECT e.userid
-                  FROM {event} e
-                  JOIN {context} ctx
-                       ON (ctx.contextlevel = :sitecontext
-                          AND e.eventtype = 'site')
-                       OR (ctx.contextlevel = :categorycontext
-                          AND ctx.instanceid = e.categoryid
-                          AND e.eventtype = 'category')
-                       OR (ctx.contextlevel = :coursecontext
-                          AND ctx.instanceid = e.courseid
-                          AND e.eventtype = 'course')
-                       OR (ctx.contextlevel = :groupcontext
-                          AND ctx.instanceid = e.courseid
-                          AND e.eventtype = 'group')
-                       OR (ctx.contextlevel = :usercontext
-                          AND ctx.instanceid = e.userid
-                          AND e.eventtype = 'user')
-                 WHERE ctx.id = :contextid";
-
-        $userlist->add_from_sql('userid', $sql, $params);
-
-        // Get contexts for Calendar Subscriptions for the owner.
-        $sql = "SELECT s.userid
-                  FROM {event_subscriptions} s
-                  JOIN {context} ctx
-                       ON (ctx.contextlevel = :sitecontext
-                          AND s.eventtype = 'site')
-                       OR (ctx.instanceid = s.categoryid
-                          AND ctx.contextlevel = :categorycontext
-                          AND s.eventtype = 'category')
-                       OR (ctx.instanceid = s.courseid
-                          AND ctx.contextlevel = :coursecontext
-                          AND s.eventtype = 'course')
-                       OR (ctx.instanceid = s.courseid
-                          AND ctx.contextlevel = :groupcontext
-                          AND s.eventtype = 'group')
-                       OR (ctx.instanceid = s.userid
-                          AND ctx.contextlevel = :usercontext
-                          AND s.eventtype = 'user')
-                 WHERE ctx.id = :contextid";
-
-        $userlist->add_from_sql('userid', $sql, $params);
     }
 
     /**
@@ -481,6 +449,12 @@ class provider implements
     protected static function get_calendar_event_ids_by_context(\context $context, $userids = array()) {
         global $DB;
 
+        // Calendar Events can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
+        // Course and Course Group (CONTEXT_COURSE), User (CONTEXT_USER), or Course Modules (CONTEXT_MODULE) contexts.
+        if (!in_array($context->contextlevel, [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER, CONTEXT_MODULE])) {
+            return [];
+        }
+
         $whereusersql = '';
         $userparams = array();
         if (!empty($userids)) {
@@ -488,59 +462,47 @@ class provider implements
             $whereusersql = "AND e.userid {$usersql}";
         }
 
-        // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
-        if ($context->contextlevel == CONTEXT_MODULE) { // Course Module Contexts.
-            $params = [
-                'modulecontext'     => $context->contextlevel,
-                'contextid'         => $context->id
-            ];
+        if ($context->contextlevel == CONTEXT_MODULE) { // Course Module events.
+            $params = ['cmid' => $context->instanceid];
 
             // Get Calendar Events for the specified Course Module context.
-            $sql = "SELECT DISTINCT
-                           e.id AS eventid
-                      FROM {context} ctx
-                INNER JOIN {course_modules} cm
-                           ON cm.id = ctx.instanceid
-                              AND ctx.contextlevel = :modulecontext
-                INNER JOIN {modules} m
-                           ON m.id = cm.module
-                INNER JOIN {event} e
-                           ON e.modulename = m.name
-                              AND e.courseid = cm.course
-                              AND e.instance = cm.instance
-                     WHERE ctx.id = :contextid
-                           {$whereusersql}";
-        } else {                                        // Other Moodle Contexts.
-            $params = [
-                'sitecontext'       => CONTEXT_SYSTEM,
-                'categorycontext'   => CONTEXT_COURSECAT,
-                'coursecontext'     => CONTEXT_COURSE,
-                'groupcontext'      => CONTEXT_COURSE,
-                'usercontext'       => CONTEXT_USER,
-                'contextid'         => $context->id
+            $sql = "SELECT DISTINCT e.id AS eventid
+                      FROM {course_modules} cm
+                      JOIN {modules} m ON m.id = cm.module
+                      JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
+                     WHERE cm.id = :cmid
+                           $whereusersql";
+        } else if ($context->contextlevel == CONTEXT_SYSTEM) { // Site events.
+            $params = [];
+            $sql = "SELECT DISTINCT e.id AS eventid
+                      FROM {event} e
+                     WHERE e.eventtype = 'site'
+                           $whereusersql";
+        } else { // The rest.
+            $eventfields = [
+                CONTEXT_COURSECAT   => 'categoryid',
+                CONTEXT_COURSE      => 'courseid',
+                CONTEXT_USER        => 'userid'
             ];
+            $eventfield = $eventfields[$context->contextlevel];
+
+            $eventtypes = [
+                CONTEXT_COURSECAT   => 'category',
+                CONTEXT_COURSE      => ['course' , 'group'],
+                CONTEXT_USER        => 'user'
+            ];
+            list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);
+
+            $params = $eventtypeparams + ['instanceid' => $context->instanceid];
 
             // Get Calendar Events for the specified Moodle context.
-            $sql = "SELECT DISTINCT
-                           e.id AS eventid
-                      FROM {context} ctx
-                INNER JOIN {event} e
-                           ON (e.eventtype = 'site'
-                              AND ctx.contextlevel = :sitecontext)
-                           OR (e.categoryid = ctx.instanceid
-                              AND e.eventtype = 'category'
-                              AND ctx.contextlevel = :categorycontext)
-                           OR (e.courseid = ctx.instanceid
-                              AND (e.eventtype = 'course'
-                                  OR e.eventtype = 'group'
-                                  OR e.modulename != '0')
-                              AND ctx.contextlevel = :coursecontext)
-                           OR (e.userid = ctx.instanceid
-                              AND e.eventtype = 'user'
-                              AND ctx.contextlevel = :usercontext)
-                     WHERE ctx.id = :contextid
-                           {$whereusersql}";
+            $sql = "SELECT DISTINCT e.id AS eventid
+                      FROM {event} e
+                     WHERE e.eventtype $eventtypesql
+                           AND e.{$eventfield} = :instanceid
+                           $whereusersql";
         }
+
         $params += $userparams;
 
         return $DB->get_records_sql($sql, $params);
@@ -558,15 +520,11 @@ class provider implements
     protected static function get_calendar_subscription_ids_by_context(\context $context, $userids = array()) {
         global $DB;
 
-        // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
-        $params = [
-            'sitecontext'       => CONTEXT_SYSTEM,
-            'categorycontext'   => CONTEXT_COURSECAT,
-            'coursecontext'     => CONTEXT_COURSE,
-            'groupcontext'      => CONTEXT_COURSE,
-            'usercontext'       => CONTEXT_USER,
-            'contextid'         => $context->id
-        ];
+        // Calendar Subscriptions can exist at Site (CONTEXT_SYSTEM), Course Category (CONTEXT_COURSECAT),
+        // Course and Course Group (CONTEXT_COURSE), or User (CONTEXT_USER) contexts.
+        if (!in_array($context->contextlevel, [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_USER])) {
+            return [];
+        }
 
         $whereusersql = '';
         $userparams = array();
@@ -575,27 +533,38 @@ class provider implements
             $whereusersql = "AND s.userid {$usersql}";
         }
 
-        // Get Calendar Subscriptions for the specified context.
-        $sql = "SELECT DISTINCT
-                       s.id AS subscriptionid
-                  FROM {context} ctx
-            INNER JOIN {event_subscriptions} s
-                       ON (s.eventtype = 'site'
-                          AND ctx.contextlevel = :sitecontext)
-                       OR (s.categoryid = ctx.instanceid
-                          AND s.eventtype = 'category'
-                          AND ctx.contextlevel = :categorycontext)
-                       OR (s.courseid = ctx.instanceid
-                          AND s.eventtype = 'course'
-                          AND ctx.contextlevel = :coursecontext)
-                       OR (s.courseid = ctx.instanceid
-                          AND s.eventtype = 'group'
-                          AND ctx.contextlevel = :groupcontext)
-                       OR (s.userid = ctx.instanceid
-                          AND s.eventtype = 'user'
-                          AND ctx.contextlevel = :usercontext)
-                 WHERE ctx.id = :contextid
-                       {$whereusersql}";
+        if ($context->contextlevel == CONTEXT_SYSTEM) {
+            $params = [];
+
+            // Get Calendar Subscriptions for the system context.
+            $sql = "SELECT DISTINCT s.id AS subscriptionid
+                      FROM {event_subscriptions} s
+                     WHERE s.eventtype = 'site'
+                           $whereusersql";
+        } else {
+            $eventfields = [
+                CONTEXT_COURSECAT   => 'categoryid',
+                CONTEXT_COURSE      => 'courseid',
+                CONTEXT_USER        => 'userid'
+            ];
+            $eventfield = $eventfields[$context->contextlevel];
+
+            $eventtypes = [
+                CONTEXT_COURSECAT   => 'category',
+                CONTEXT_COURSE      => ['course' , 'group'],
+                CONTEXT_USER        => 'user'
+            ];
+            list($eventtypesql, $eventtypeparams) = $DB->get_in_or_equal($eventtypes[$context->contextlevel], SQL_PARAMS_NAMED);
+
+            $params = $eventtypeparams + ['instanceid' => $context->instanceid];
+
+            // Get Calendar Subscriptions for the specified context.
+            $sql = "SELECT DISTINCT s.id AS subscriptionid
+                      FROM {event_subscriptions} s
+                     WHERE s.eventtype $eventtypesql
+                           AND s.{$eventfield} = :instanceid
+                           $whereusersql";
+        }
 
         $params += $userparams;
 
index 12bc80b..02c2d97 100644 (file)
@@ -608,49 +608,44 @@ class calendar_event {
             $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall));
 
             if ($updaterepeated) {
-                // Update all.
+
+                $sqlset = 'name = ?,
+                           description = ?,
+                           timeduration = ?,
+                           timemodified = ?,
+                           groupid = ?,
+                           courseid = ?';
+
+                // Note: Group and course id may not be set. If not, keep their current values.
+                $params = [
+                    $this->properties->name,
+                    $this->properties->description,
+                    $this->properties->timeduration,
+                    time(),
+                    isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid,
+                    isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid,
+                ];
+
+                // Note: Only update start date, if it was changed by the user.
                 if ($this->properties->timestart != $event->timestart) {
                     $timestartoffset = $this->properties->timestart - $event->timestart;
-                    $sql = "UPDATE {event}
-                               SET name = ?,
-                                   description = ?,
-                                   timestart = timestart + ?,
-                                   timeduration = ?,
-                                   timemodified = ?,
-                                   groupid = ?,
-                                   courseid = ?
-                             WHERE repeatid = ?";
-                    // Note: Group and course id may not be set. If not, keep their current values.
-                    $params = [
-                        $this->properties->name,
-                        $this->properties->description,
-                        $timestartoffset,
-                        $this->properties->timeduration,
-                        time(),
-                        isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid,
-                        isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid,
-                        $event->repeatid
-                    ];
-                } else {
-                    $sql = "UPDATE {event}
-                               SET name = ?,
-                                   description = ?,
-                                   timeduration = ?,
-                                   timemodified = ?,
-                                   groupid = ?,
-                                   courseid = ?
-                            WHERE repeatid = ?";
-                    // Note: Group and course id may not be set. If not, keep their current values.
-                    $params = [
-                        $this->properties->name,
-                        $this->properties->description,
-                        $this->properties->timeduration,
-                        time(),
-                        isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid,
-                        isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid,
-                        $event->repeatid
-                    ];
+                    $sqlset .= ', timestart = timestart + ?';
+                    $params[] = $timestartoffset;
+                }
+
+                // Note: Only update location, if it was changed by the user.
+                $updatelocation = (!empty($this->properties->location) && $this->properties->location !== $event->location);
+                if ($updatelocation) {
+                    $sqlset .= ', location = ?';
+                    $params[] = $this->properties->location;
                 }
+
+                // Update all.
+                $sql = "UPDATE {event}
+                           SET $sqlset
+                         WHERE repeatid = ?";
+
+                $params[] = $event->repeatid;
                 $DB->execute($sql, $params);
 
                 // Trigger an update event for each of the calendar event.
@@ -2854,7 +2849,7 @@ function calendar_add_icalendar_event($event, $unused = null, $subscriptionid, $
     }
 
     $eventrecord->location = empty($event->properties['LOCATION'][0]->value) ? '' :
-            str_replace('\\', '', $event->properties['LOCATION'][0]->value);
+            trim(str_replace('\\', '', $event->properties['LOCATION'][0]->value));
     $eventrecord->uuid = $event->properties['UID'][0]->value;
     $eventrecord->timemodified = time();
 
index 7d75b57..e82e42a 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/calendar_day
+    @template core_calendar/calendar_day
 
     Calendar day view.
 
index 74d48bb..fde6b2f 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/day_detailed
+    @template core_calendar/day_detailed
 
     Calendar day view.
 
diff --git a/calendar/templates/event_details.mustache b/calendar/templates/event_details.mustache
new file mode 100644 (file)
index 0000000..86fcda4
--- /dev/null
@@ -0,0 +1,120 @@
+{{!
+    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 core_calendar/event_details
+
+    Calendar event details.
+
+    The purpose of this template is to render the event details.
+
+    This template is used in the summary modal, day and upcoming views to output event information consistently
+    across the calendar.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+        "formattedtime": "Wednesday, 17 April, 9:27 AM",
+        "normalisedeventtype": "Group",
+        "description": "An random event description",
+        "location": "Moodle HQ",
+        "isactionevent": "true",
+        "course": {
+            "viewurl": "http://mymoodlesite/course/view.php?id=1",
+            "fullname": "Course name"
+        },
+        "source": "Ical imported",
+        "groupname": "Group 1",
+        "iscategoryevent": "true",
+        "category": {
+            "nestedname": "Category name"
+        },
+        "iscourseevent": "true",
+        "groupname": "Group name",
+        "subscription": "true",
+        "displayeventsource": "true",
+        "subscriptionname": "Subscription name",
+        "subscriptionurl": "http://subscriptionurl"
+    }
+}}
+<div class="row">
+    <div class="col-xs-1">{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}</div>
+    <div class="col-xs-11">{{{formattedtime}}}</div>
+</div>
+<div class="row mt-1">
+    <div class="col-xs-1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
+    <div class="col-xs-11">{{normalisedeventtypetext}}</div>
+</div>
+{{#description}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
+        <div class="description-content col-xs-11">{{{.}}}</div>
+    </div>
+{{/description}}
+{{#location}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/location, core, {{#str}} location {{/str}} {{/pix}}</div>
+        <div class="location-content col-xs-11">{{{.}}}</div>
+    </div>
+{{/location}}
+{{#isactionevent}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+        <div class="col-xs-11"><a href="{{course.viewurl}}">{{{course.fullname}}}</a></div>
+    </div>
+{{/isactionevent}}
+{{#iscategoryevent}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}</div>
+        <div class="col-xs-11">{{{category.nestedname}}}</div>
+    </div>
+{{/iscategoryevent}}
+{{#iscourseevent}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+        <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
+    </div>
+{{/iscourseevent}}
+{{#groupname}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
+        <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
+    </div>
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/groupevent, core, {{#str}} group {{/str}} {{/pix}}</div>
+        <div class="col-xs-11">{{{groupname}}}</div>
+    </div>
+{{/groupname}}
+{{#subscription}}
+    {{#displayeventsource}}
+        <div class="row mt-1">
+            <div class="col-xs-1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
+            <div class="col-xs-11">
+                {{#subscriptionurl}}
+                    <p><a href="{{subscriptionurl}}">{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</a></p>
+                {{/subscriptionurl}}
+                {{^subscriptionurl}}
+                    <p>{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</p>
+                {{/subscriptionurl}}
+            </div>
+        </div>
+    {{/displayeventsource}}
+{{/subscription}}
index 98431ea..ccbf773 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/event_item
+    @template core_calendar/event_item
 
     Calendar event item.
 
 
     Example context (json):
     {
+        "id": 1,
+        "name": "Sample event name",
+        "normalisedeventtype": "course",
+        "course": {
+            "id": 1
+        },
+        "canedit": true,
+        "candelete": true,
+        "isactionevent": true,
+        "icon": {
+            "key": "i/courseevent",
+            "component": "core",
+            "alttext": "Some course event"
+        },
+        "editurl": "#",
+        "url": "#"
     }
 }}
 <div{{!
     }} data-type="event"{{!
     }} data-course-id="{{course.id}}"{{!
     }} data-event-id="{{id}}"{{!
-    }} class="event"{{!
-    }} data-eventtype-{{calendareventtype}}="1"{{!
+    }} class="event m-t-1"{{!
+    }} data-eventtype-{{normalisedeventtype}}="1"{{!
     }} data-event-title="{{name}}"{{!
     }} data-event-count="{{eventcount}}"{{!
     }}>
-    <div class="card">
-        <div class="box card-header clearfix p-y-1">
+    <div class="card rounded">
+        <div class="box card-header clearfix calendar_event_{{normalisedeventtype}}">
             <div class="commands float-sm-right">
                 {{#canedit}}
                     {{#candelete}}
             {{#icon}}<div class="d-inline-block mt-1 align-top">{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}</div>{{/icon}}
             <div class="d-inline-block">
                 <h3 class="name d-inline-block">{{{name}}}</h3>
-                <span class="date float-sm-right mr-1">{{{formattedtime}}}</span>
-                <div class="location">{{#location}}{{{location}}}{{/location}}</div>
             </div>
         </div>
-        <div class="description card-block calendar_event_{{eventtype}}">
-            <p>{{{description}}}</p>
-            {{#iscourseevent}}
-                <div><a href="{{url}}">{{course.fullname}}</a></div>
-            {{/iscourseevent}}
-            {{> core_calendar/event_subscription}}
-            {{#isactionevent}}
-                <a href="{{url}}">{{#str}} gotoactivity, core_calendar {{/str}}</a>
-            {{/isactionevent}}
-            {{#groupname}}
-                <div><a href="{{url}}">{{{course.fullname}}}</a></div>
-                <div>{{{groupname}}}</div>
-            {{/groupname}}
+        <div class="description card-body">
+            {{> core_calendar/event_details }}
         </div>
+        {{#isactionevent}}
+            <div class="card-footer text-right bg-transparent">
+                <a href="{{url}}" class="card-link">{{#str}} gotoactivity, core_calendar {{/str}}</a>
+            </div>
+        {{/isactionevent}}
     </div>
 </div>
index f75157a..ae276ae 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/event_list
+    @template core_calendar/event_list
 
     Calendar event list.
 
index 22d8d27..2d333b4 100644 (file)
     }} data-edit-url="{{editurl}}"{{!
     }}>
     <div class="container-fluid">
-        <div class="row">
-            <div class="col-xs-1">{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{{formattedtime}}}</div>
-        </div>
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{eventtype}}</div>
-        </div>
-        {{#description}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
-            <div class="description-content col-xs-11">{{{.}}}</div>
-        </div>
-        {{/description}}
-        {{#location}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/location, core, {{#str}} location {{/str}} {{/pix}}</div>
-            <div class="location-content col-xs-11">{{{.}}}</div>
-        </div>
-        {{/location}}
-        {{#isactionevent}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
-            <div class="col-xs-11"><a href="{{course.viewurl}}">{{{course.fullname}}}</a></div>
-        </div>
-        {{/isactionevent}}
-        {{#iscategoryevent}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/categoryevent, core, {{#str}} category {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{{category.nestedname}}}</div>
-        </div>
-        {{/iscategoryevent}}
-        {{#iscourseevent}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
-            <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
-        </div>
-        {{/iscourseevent}}
-        {{#groupname}}
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/courseevent, core, {{#str}} course {{/str}} {{/pix}}</div>
-            <div class="col-xs-11"><a href="{{url}}">{{{course.fullname}}}</a></div>
-        </div>
-        <div class="row mt-1">
-            <div class="col-xs-1">{{#pix}} i/groupevent, core, {{#str}} group {{/str}} {{/pix}}</div>
-            <div class="col-xs-11">{{{groupname}}}</div>
-        </div>
-        {{/groupname}}
-        {{#subscription}}
-            {{#displayeventsource}}
-                <div class="row mt-1">
-                    <div class="col-xs-1">{{#pix}} i/rss, core, {{#str}} eventsource, core_calendar {{/str}} {{/pix}}</div>
-                    <div class="col-xs-11">
-                        {{#subscriptionurl}}
-                            <p><a href="{{subscriptionurl}}">{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</a></p>
-                        {{/subscriptionurl}}
-                        {{^subscriptionurl}}
-                            <p>{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</p>
-                        {{/subscriptionurl}}
-                    </div>
-                </div>
-            {{/displayeventsource}}
-        {{/subscription}}
+        {{> core_calendar/event_details }}
     </div>
 </div>
index 5041ca2..84ce421 100644 (file)
@@ -87,7 +87,7 @@
                                         {{/underway}}
                                         {{^underway}}
                                             <li data-region="event-item"
-                                                data-eventtype-{{calendareventtype}}="1"
+                                                data-eventtype-{{normalisedeventtype}}="1"
                                                 {{#draggable}}
                                                     draggable="true"
                                                     data-drag-type="move"
                                                 {{/draggable}}>
 
                                                 <a data-action="view-event" data-event-id="{{id}}" href="{{url}}" title="{{name}}">
-                                                    <span class="badge badge-circle calendar_event_{{calendareventtype}}">
+                                                    <span class="badge badge-circle calendar_event_{{normalisedeventtype}}">
                                                         &nbsp;
                                                     </span>
                                                     {{> core_calendar/event_icon}}
index 2a34335..b1aa06f 100644 (file)
                                 {{$nocontent}}{{#str}}eventnone, calendar{{/str}}{{/nocontent}}
                                 {{$content}}
                                     {{#events}}
-                                        <div data-popover-eventtype-{{calendareventtype}}="1">
+                                        <div data-popover-eventtype-{{normalisedeventtype}}="1">
                                             {{#modulename}}
                                                 {{#pix}} icon, {{modulename}} {{/pix}}
                                             {{/modulename}}
index 082d5bc..c952877 100644 (file)
@@ -41,7 +41,7 @@
     {{#events}}
         <div{{!
             }} class="event"{{!
-            }} data-eventtype-{{calendareventtype}}="1"{{!
+            }} data-eventtype-{{normalisedeventtype}}="1"{{!
             }} data-region="event-item"{{!
         }}>
             <span>{{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}</span>
index 22aed84..cb681d0 100644 (file)
@@ -169,14 +169,11 @@ Feature: Perform basic calendar functionality
     And I follow "This month"
     And I click on "New event" "button"
     When I click on "Save" "button"
-    Then I should see "Required"
+    Then I should see "Required" in the "Event title" "form_row"
     And I am on homepage
     And I follow "This month"
     And I click on "New event" "button"
-    And I set the field "Type of event" to "Course"
-    When I click on "Save" "button"
-    Then I should see "Required"
-    And I should see "Select a course"
     And I set the field "Event title" to "Really awesome event!"
+    And I set the field "Type of event" to "Course"
     When I click on "Save" "button"
-    Then I should see "Select a course"
+    And I should see "Select a course" in the "Course" "form_row"
index 0921296..2b7157d 100644 (file)
@@ -365,12 +365,23 @@ class core_calendar_privacy_testcase extends provider_testcase {
         // Delete all Calendar Events for all Users by Context for Course 2.
         provider::delete_data_for_all_users_in_context($course2context);
 
-        // Verify all Calendar Events for Course 2 were deleted.
-        $events = $DB->get_records('event', array('courseid' => $course2->id));
+        // Verify all Calendar Events for Course 2 context were deleted.
+        $events = $DB->get_records('event', array('courseid' => $course2->id, 'modulename' => '0'));
         $this->assertCount(0, $events);
         // Verify all Calendar Subscriptions for Course 2 were deleted.
         $subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course2->id));
         $this->assertCount(0, $subscriptions);
+
+        // Verify all Calendar Events for the assignment exists still.
+        $events = $DB->get_records('event', array('modulename' => 'assign'));
+        $this->assertCount(2, $events);
+
+        // Delete all Calendar Events for all Users by Context for the assignment.
+        provider::delete_data_for_all_users_in_context($modulecontext);
+
+        // Verify all Calendar Events for the assignment context were deleted.
+        $events = $DB->get_records('event', array('modulename' => 'assign'));
+        $this->assertCount(0, $events);
     }
 
     /**
index 29377ed..9788554 100644 (file)
@@ -95,21 +95,12 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextid' => $context->id,
-            'contextsystem' => CONTEXT_SYSTEM,
-            'contextcoursecat' => CONTEXT_COURSECAT,
-        ];
+        $params = ['contextid' => $context->id];
 
-        $sql = "SELECT cm.userid as userid
+        $sql = "SELECT cm.userid
                   FROM {cohort_members} cm
-                  JOIN {cohort} c
-                       ON cm.cohortid = c.id
-                  JOIN {context} ctx
-                       ON c.contextid = ctx.id
-                       AND (ctx.contextlevel = :contextsystem
-                            OR ctx.contextlevel = :contextcoursecat)
-                 WHERE ctx.id = :contextid";
+                  JOIN {cohort} c ON cm.cohortid = c.id
+                 WHERE c.contextid = :contextid";
 
         $userlist->add_from_sql('userid', $sql, $params);
     }
index 405b495..026ac44 100644 (file)
@@ -192,8 +192,8 @@ class course_module_competency extends persistent {
         $sql = 'SELECT COUNT(comp.id)
                   FROM {' . self::TABLE . '} coursemodulecomp
                   JOIN {' . competency::TABLE . '} comp
-                    ON coursecomp.competencyid = comp.id
-                 WHERE coursecomp.cmid = ? ';
+                    ON coursemodulecomp.competencyid = comp.id
+                 WHERE coursemodulecomp.cmid = ? ';
         $params = array($cmid);
 
         $results = $DB->count_records_sql($sql, $params);
index 1e5bdfd..2c61be4 100644 (file)
@@ -38,7 +38,7 @@ use core_competency\user_evidence;
  * @return array
  */
 function core_competency_comment_add($comment, $params) {
-    global $USER;
+    global $USER, $PAGE;
 
     if (!get_config('core_competency', 'enabled')) {
         return;
@@ -132,10 +132,16 @@ function core_competency_comment_add($comment, $params) {
         $message->contexturl = $url->out(false);
         $message->contexturlname = $urlname;
 
+        $userpicture = new \user_picture($user);
         // Message each recipient.
         foreach ($recipients as $recipient) {
             $msgcopy = clone($message);
             $msgcopy->userto = $recipient;
+            // Generate an out-of-session token for the user receiving the message.
+            $userpicture->includetoken = $recipient;
+            $msgcopy->customdata = [
+                'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
+            ];
             message_send($msgcopy);
         }
 
@@ -201,10 +207,16 @@ function core_competency_comment_add($comment, $params) {
         $message->contexturl = $url->out(false);
         $message->contexturlname = $urlname;
 
+        $userpicture = new \user_picture($user);
         // Message each recipient.
         foreach ($recipients as $recipient) {
             $msgcopy = clone($message);
             $msgcopy->userto = $recipient;
+            // Generate an out-of-session token for the user receiving the message.
+            $userpicture->includetoken = $recipient;
+            $msgcopy->customdata = [
+                'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
+            ];
             message_send($msgcopy);
         }
     }
diff --git a/competency/tests/course_module_competency_test.php b/competency/tests/course_module_competency_test.php
new file mode 100644 (file)
index 0000000..8d951dc
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course module competency persistent class tests.
+ *
+ * @package    core_competency
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+use core_competency\course_module_competency;
+
+/**
+ * Course module competency persistent testcase.
+ *
+ * @package    core_competency
+ * @copyright  2019 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_competency_course_module_competency_testcase extends advanced_testcase {
+
+    public function test_count_competencies() {
+        global $CFG, $DB;
+
+        $this->resetAfterTest(true);
+        $dg = $this->getDataGenerator();
+        $lpg = $dg->get_plugin_generator('core_competency');
+
+        $c1 = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+
+        $framework = $lpg->create_framework();
+        $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));   // In C1, and C2.
+        $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));   // In C2.
+        $lpg->create_course_competency(array('competencyid' => $comp1->get('id'), 'courseid' => $c1->id));
+        $lpg->create_course_competency(array('competencyid' => $comp2->get('id'), 'courseid' => $c1->id));
+
+        $assign1a = $dg->create_module('assign', ['course' => $c1]);
+        $assign1b = $dg->create_module('assign', ['course' => $c1]);
+        $cmc1a = $lpg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign1a->cmid]);
+        $cmc1b = $lpg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign1b->cmid]);
+        $cmc2b = $lpg->create_course_module_competency(['competencyid' => $comp2->get('id'), 'cmid' => $assign1b->cmid]);
+
+        // Enrol the user 1 in C1.
+        $dg->enrol_user($u1->id, $c1->id);
+
+        $all = course_module_competency::list_course_module_competencies($assign1a->cmid);
+        $this->assertEquals(course_module_competency::count_competencies($assign1a->cmid), count($all));
+
+        $all = course_module_competency::list_course_module_competencies($assign1b->cmid);
+        $this->assertEquals(course_module_competency::count_competencies($assign1b->cmid), count($all));
+    }
+
+}
index 9ed2e92..dcfe80e 100644 (file)
@@ -40,12 +40,12 @@ global $CFG;
 class core_competency_lib_testcase extends advanced_testcase {
 
     public function test_comment_add_user_competency() {
-        global $DB;
+        global $DB, $PAGE;
         $this->resetAfterTest();
         $dg = $this->getDataGenerator();
         $lpg = $dg->get_plugin_generator('core_competency');
 
-        $u1 = $dg->create_user();
+        $u1 = $dg->create_user(['picture' => 1]);
         $u2 = $dg->create_user();
         $u3 = $dg->create_user();
         $reviewerroleid = $dg->create_role();
@@ -96,6 +96,13 @@ class core_competency_lib_testcase extends advanced_testcase {
         $this->assertEquals(FORMAT_MOODLE, $message->fullmessageformat);
         $this->assertEquals($expectedurl->out(false), $message->contexturl);
         $this->assertEquals($expectedurlname, $message->contexturlname);
+        // Test customdata.
+        $customdata = json_decode($message->customdata);
+        $this->assertObjectHasAttribute('notificationiconurl', $customdata);
+        $this->assertContains('tokenpluginfile.php', $customdata->notificationiconurl);
+        $userpicture = new \user_picture($u1);
+        $userpicture->includetoken = $u2->id;
+        $this->assertEquals($userpicture->get_url($PAGE)->out(false), $customdata->notificationiconurl);
 
         // Reviewer posts a comment for the user competency being in two plans. Owner is messaged.
         $this->setUser($u2);
@@ -218,6 +225,9 @@ class core_competency_lib_testcase extends advanced_testcase {
         $message = array_pop($messages);
         $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
         $this->assertEquals($u1->id, $message->useridto);
+        // Test customdata.
+        $customdata = json_decode($message->customdata);
+        $this->assertObjectHasAttribute('notificationiconurl', $customdata);
 
         // Post a comment in a plan with reviewer. The reviewer is messaged.
         $p1->set('reviewerid', $u2->id);
index 0861a0e..fb3a4f9 100644 (file)
@@ -110,28 +110,27 @@ class provider implements
      * @param userlist $userlist The userlist to add to.
      */
     public static function add_course_completion_users_to_userlist(userlist $userlist) {
-        $params = [
-            'contextid' => $userlist->get_context()->id,
-            'contextcourse' => CONTEXT_COURSE,
-        ];
+        $context = $userlist->get_context();
+
+        if (!$context instanceof \context_course) {
+            return;
+        }
+
+        $params = ['courseid' => $context->instanceid];
 
         $sql = "SELECT cmc.userid
-                 FROM {context} ctx
-                 JOIN {course} c ON ctx.instanceid = c.id
+                 FROM {course} c
                  JOIN {course_completion_criteria} ccc ON ccc.course = c.id
                  JOIN {course_modules_completion} cmc ON cmc.coursemoduleid = ccc.moduleinstance
-                WHERE ctx.id = :contextid
-                  AND ctx.contextlevel = :contextcourse";
+                WHERE c.id = :courseid";
 
         $userlist->add_from_sql('userid', $sql, $params);
 
         $sql = "SELECT ccc_compl.userid
-                 FROM {context} ctx
-                 JOIN {course} c ON ctx.instanceid = c.id
+                 FROM {course} c
                  JOIN {course_completion_criteria} ccc ON ccc.course = c.id
                  JOIN {course_completion_crit_compl} ccc_compl ON ccc_compl.criteriaid = ccc.id
-                WHERE ctx.id = :contextid
-                  AND ctx.contextlevel = :contextcourse";
+                WHERE c.id = :courseid";
 
         $userlist->add_from_sql('userid', $sql, $params);
     }
index bec45a2..baa3977 100644 (file)
@@ -77,7 +77,9 @@ define('COMPLETION_CRITERIA_TYPE_ROLE',         7);
 define('COMPLETION_CRITERIA_TYPE_COURSE',       8);
 
 /**
- * Criteria type constant to class name mapping
+ * Criteria type constant to class name mapping.
+ *
+ * This global variable would be improved if it was implemented as a cache.
  */
 global $COMPLETION_CRITERIA_TYPES;
 $COMPLETION_CRITERIA_TYPES = array(
diff --git a/completion/cron.php b/completion/cron.php
deleted file mode 100644 (file)
index f56e5dd..0000000
+++ /dev/null
@@ -1,393 +0,0 @@
-<?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/>.
-
-/**
- * Code used by scheduled tasks for reviewing and aggregating course completion criteria.
- *
- * @package core_completion
- * @category completion
- * @copyright 2009 Catalyst IT Ltd
- * @author Aaron Barnes <aaronb@catalyst.net.nz>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-require_once($CFG->libdir.'/completionlib.php');
-
-/**
- * Mark users as started if the config option is set
- *
- * @return void
- */
-function completion_cron_mark_started() {
-    global $CFG, $DB;
-
-    if (debugging()) {
-        mtrace('Marking users as started');
-    }
-
-    if (!empty($CFG->gradebookroles)) {
-        $roles = ' AND ra.roleid IN ('.$CFG->gradebookroles.')';
-    } else {
-        // This causes it to default to everyone (if there is no student role)
-        $roles = '';
-    }
-
-    /**
-     * A quick explaination of this horrible looking query
-     *
-     * It's purpose is to locate all the active participants
-     * of a course with course completion enabled.
-     *
-     * We also only want the users with no course_completions
-     * record as this functions job is to create the missing
-     * ones :)
-     *
-     * We want to record the user's enrolment start time for the
-     * course. This gets tricky because there can be multiple
-     * enrolment plugins active in a course, hence the possibility
-     * of multiple records for each couse/user in the results
-     */
-    $sql = "
-        SELECT
-            c.id AS course,
-            u.id AS userid,
-            crc.id AS completionid,
-            ue.timestart AS timeenrolled,
-            ue.timecreated
-        FROM
-            {user} u
-        INNER JOIN
-            {user_enrolments} ue
-         ON ue.userid = u.id
-        INNER JOIN
-            {enrol} e
-         ON e.id = ue.enrolid
-        INNER JOIN
-            {course} c
-         ON c.id = e.courseid
-        INNER JOIN
-            {role_assignments} ra
-         ON ra.userid = u.id
-        LEFT JOIN
-            {course_completions} crc
-         ON crc.course = c.id
-        AND crc.userid = u.id
-        WHERE
-            c.enablecompletion = 1
-        AND crc.timeenrolled IS NULL
-        AND ue.status = 0
-        AND e.status = 0
-        AND u.deleted = 0
-        AND ue.timestart < ?
-        AND (ue.timeend > ? OR ue.timeend = 0)
-            $roles
-        ORDER BY
-            course,
-            userid
-    ";
-
-    $now = time();
-    $rs = $DB->get_recordset_sql($sql, array($now, $now, $now, $now));
-
-    // Check if result is empty
-    if (!$rs->valid()) {
-        $rs->close(); // Not going to iterate (but exit), close rs
-        return;
-    }
-
-    /**
-     * An explaination of the following loop
-     *
-     * We are essentially doing a group by in the code here (as I can't find
-     * a decent way of doing it in the sql).
-     *
-     * Since there can be multiple enrolment plugins for each course, we can have
-     * multiple rows for each particpant in the query result. This isn't really
-     * a problem until you combine it with the fact that the enrolment plugins
-     * can save the enrol start time in either timestart or timeenrolled.
-     *
-     * The purpose of this loop is to find the earliest enrolment start time for
-     * each participant in each course.
-     */
-    $prev = null;
-    while ($rs->valid() || $prev) {
-
-        $current = $rs->current();
-
-        if (!isset($current->course)) {
-            $current = false;
-        }
-        else {
-            // Not all enrol plugins fill out timestart correctly, so use whichever
-            // is non-zero
-            $current->timeenrolled = max($current->timecreated, $current->timeenrolled);
-        }
-
-        // If we are at the last record,
-        // or we aren't at the first and the record is for a diff user/course
-        if ($prev &&
-            (!$rs->valid() ||
-            ($current->course != $prev->course || $current->userid != $prev->userid))) {
-
-            $completion = new completion_completion();
-            $completion->userid = $prev->userid;
-            $completion->course = $prev->course;
-            $completion->timeenrolled = (string) $prev->timeenrolled;
-            $completion->timestarted = 0;
-            $completion->reaggregate = time();
-
-            if ($prev->completionid) {
-                $completion->id = $prev->completionid;
-            }
-
-            $completion->mark_enrolled();
-
-            if (debugging()) {
-                mtrace('Marked started user '.$prev->userid.' in course '.$prev->course);
-            }
-        }
-        // Else, if this record is for the same user/course
-        elseif ($prev && $current) {
-            // Use oldest timeenrolled
-            $current->timeenrolled = min($current->timeenrolled, $prev->timeenrolled);
-        }
-
-        // Move current record to previous
-        $prev = $current;
-
-        // Move to next record
-        $rs->next();
-    }
-
-    $rs->close();
-}
-
-/**
- * Run installed criteria's data aggregation methods
- *
- * Loop through each installed criteria and run the
- * cron() method if it exists
- *
- * @return void
- */
-function completion_cron_criteria() {
-
-    // Process each criteria type
-    global $CFG, $COMPLETION_CRITERIA_TYPES;
-
-    foreach ($COMPLETION_CRITERIA_TYPES as $type) {
-
-        $object = 'completion_criteria_'.$type;
-        require_once $CFG->dirroot.'/completion/criteria/'.$object.'.php';
-
-        $class = new $object();
-
-        // Run the criteria type's cron method, if it has one
-        if (method_exists($class, 'cron')) {
-
-            if (debugging()) {
-                mtrace('Running '.$object.'->cron()');
-            }
-            $class->cron();
-        }
-    }
-}
-
-/**
- * Aggregate each user's criteria completions
- */
-function completion_cron_completions() {
-    global $DB;
-
-    if (debugging()) {
-        mtrace('Aggregating completions');
-    }
-
-    // Save time started
-    $timestarted = time();
-
-    // Grab all criteria and their associated criteria completions
-    $sql = '
-        SELECT DISTINCT
-            c.id AS course,
-            cr.id AS criteriaid,
-            crc.userid AS userid,
-            cr.criteriatype AS criteriatype,
-            cc.timecompleted AS timecompleted
-        FROM
-            {course_completion_criteria} cr
-        INNER JOIN
-            {course} c
-         ON cr.course = c.id
-        INNER JOIN
-            {course_completions} crc
-         ON crc.course = c.id
-        LEFT JOIN
-            {course_completion_crit_compl} cc
-         ON cc.criteriaid = cr.id
-        AND crc.userid = cc.userid
-        WHERE
-            c.enablecompletion = 1
-        AND crc.timecompleted IS NULL
-        AND crc.reaggregate > 0
-        AND crc.reaggregate < :timestarted
-        ORDER BY
-            course,
-            userid
-    ';
-
-    $rs = $DB->get_recordset_sql($sql, array('timestarted' => $timestarted));
-
-    // Check if result is empty
-    if (!$rs->valid()) {
-        $rs->close(); // Not going to iterate (but exit), close rs
-        return;
-    }
-
-    $current_user = null;
-    $current_course = null;
-    $completions = array();
-
-    while (1) {
-
-        // Grab records for current user/course
-        foreach ($rs as $record) {
-            // If we are still grabbing the same users completions
-            if ($record->userid === $current_user && $record->course === $current_course) {
-                $completions[$record->criteriaid] = $record;
-            } else {
-                break;
-            }
-        }
-
-        // Aggregate
-        if (!empty($completions)) {
-
-            if (debugging()) {
-                mtrace('Aggregating completions for user '.$current_user.' in course '.$current_course);
-            }
-
-            // Get course info object
-            $info = new completion_info((object)array('id' => $current_course));
-
-            // Setup aggregation
-            $overall = $info->get_aggregation_method();
-            $activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY);
-            $prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE);
-            $role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE);
-
-            $overall_status = null;
-            $activity_status = null;
-            $prerequisite_status = null;
-            $role_status = null;
-
-            // Get latest timecompleted
-            $timecompleted = null;
-
-            // Check each of the criteria
-            foreach ($completions as $params) {
-                $timecompleted = max($timecompleted, $params->timecompleted);
-
-                $completion = new completion_criteria_completion((array)$params, false);
-
-                // Handle aggregation special cases
-                if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
-                    completion_cron_aggregate($activity, $completion->is_complete(), $activity_status);
-                } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) {
-                    completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisite_status);
-                } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) {
-                    completion_cron_aggregate($role, $completion->is_complete(), $role_status);
-                } else {
-                    completion_cron_aggregate($overall, $completion->is_complete(), $overall_status);
-                }
-            }
-
-            // Include role criteria aggregation in overall aggregation
-            if ($role_status !== null) {
-                completion_cron_aggregate($overall, $role_status, $overall_status);
-            }
-
-            // Include activity criteria aggregation in overall aggregation
-            if ($activity_status !== null) {
-                completion_cron_aggregate($overall, $activity_status, $overall_status);
-            }
-
-            // Include prerequisite criteria aggregation in overall aggregation
-            if ($prerequisite_status !== null) {
-                completion_cron_aggregate($overall, $prerequisite_status, $overall_status);
-            }
-
-            // If aggregation status is true, mark course complete for user
-            if ($overall_status) {
-                if (debugging()) {
-                    mtrace('Marking complete');
-                }
-
-                $ccompletion = new completion_completion(array('course' => $params->course, 'userid' => $params->userid));
-                $ccompletion->mark_complete($timecompleted);
-            }
-        }
-
-        // If this is the end of the recordset, break the loop
-        if (!$rs->valid()) {
-            $rs->close();
-            break;
-        }
-
-        // New/next user, update user details, reset completions
-        $current_user = $record->userid;
-        $current_course = $record->course;
-        $completions = array();
-        $completions[$record->criteriaid] = $record;
-    }
-
-    // Mark all users as aggregated
-    $sql = "
-        UPDATE
-            {course_completions}
-        SET
-            reaggregate = 0
-        WHERE
-            reaggregate < :timestarted
-        AND reaggregate > 0
-    ";
-
-    $DB->execute($sql, array('timestarted' => $timestarted));
-}
-
-/**
- * Aggregate criteria status's as per configured aggregation method
- *
- * @param int $method COMPLETION_AGGREGATION_* constant
- * @param bool $data Criteria completion status
- * @param bool|null $state Aggregation state
- */
-function completion_cron_aggregate($method, $data, &$state) {
-    if ($method == COMPLETION_AGGREGATION_ALL) {
-        if ($data && $state !== false) {
-            $state = true;
-        } else {
-            $state = false;
-        }
-    } elseif ($method == COMPLETION_AGGREGATION_ANY) {
-        if ($data) {
-            $state = true;
-        } else if (!$data && $state === null) {
-            $state = false;
-        }
-    }
-}
index 296f92e..3956959 100644 (file)
@@ -152,4 +152,72 @@ class behat_completion extends behat_base {
             array($imgalttext, "icon", $activityxpath, "xpath_element")
         );
     }
+
+    /**
+     * Checks if the activity with specified name shows a information completion checkbox (i.e. showing the completion tracking
+     * configuration).
+     *
+     * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion shows a configuration completion checkbox/
+     * @param string $activityname The activity name.
+     * @param string $activitytype The activity type.
+     * @param string $completiontype The completion type.
+     */
+    public function activity_has_configuration_completion_checkbox($activityname, $activitytype, $completiontype) {
+        if ($completiontype == "manual") {
+            $imgname = 'i/completion-manual-enabled';
+        } else {
+            $imgname = 'i/completion-auto-enabled';
+        }
+        $iconxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]";
+        $iconxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]";
+        $iconxpath .= "/descendant::span[@class='actions']/descendant::img[contains(@src, 'i/completion-')]";
+
+        $this->execute("behat_general::the_attribute_of_should_contain",
+            array("src", $iconxpath, "xpath_element", $imgname)
+        );
+    }
+
+    /**
+     * Checks if the activity with specified name shows a tracking completion checkbox (i.e. showing my completion tracking status)
+     *
+     * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion shows a status completion checkbox/
+     * @param string $activityname The activity name.
+     * @param string $activitytype The activity type.
+     * @param string $completiontype The completion type.
+     */
+    public function activity_has_status_completion_checkbox($activityname, $activitytype, $completiontype) {
+        if ($completiontype == "manual") {
+            $imgname = 'i/completion-manual-';
+        } else {
+            $imgname = 'i/completion-auto-';
+        }
+        $iconxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]";
+        $iconxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]";
+        $iconxpath .= "/descendant::span[@class='actions']/descendant::img[contains(@src, 'i/completion-')]";
+
+        $this->execute("behat_general::the_attribute_of_should_contain",
+            array("src", $iconxpath, "xpath_element", $imgname)
+        );
+
+        $this->execute("behat_general::the_attribute_of_should_not_contain",
+            array("src", $iconxpath, "xpath_element", '-enabled')
+        );
+    }
+
+    /**
+     * Checks if the activity with specified name does not show any completion checkbox.
+     *
+     * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity does not show any completion checkbox/
+     * @param string $activityname The activity name.
+     * @param string $activitytype The activity type.
+     */
+    public function activity_has_not_any_completion_checkbox($activityname, $activitytype) {
+        $iconxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]";
+        $iconxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]";
+        $iconxpath .= "/descendant::img[contains(@src, 'i/completion-')]";
+
+        $this->execute("behat_general::should_not_exist",
+            array($iconxpath, "xpath_element")
+        );
+    }
 }
diff --git a/completion/tests/behat/completion_course_page_checkboxes.feature b/completion/tests/behat/completion_course_page_checkboxes.feature
new file mode 100644 (file)
index 0000000..48005ee
--- /dev/null
@@ -0,0 +1,65 @@
+@core @core_completion
+Feature: Show activity completion status or activity completion configuration on the course page
+  In order to understand the configuration or status of an activity's completion
+  As a user
+  I want to see an appropriate checkbox icon besides the activity
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher   | First    | teacher1@example.com |
+      | teacher2 | Teacher   | Second   | teacher2@example.com |
+      | student1 | Student   | First    | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | teacher2 | C1 | teacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | Enable completion tracking | Yes |
+    And I press "Save and display"
+    And the following "activities" exist:
+      | activity | course | idnumber | name            | intro                  | completion | completionview | completionexpected |
+      | forum    | C1     | forum1   | Test forum name | Test forum description | 1          | 0              | 0                  |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                 | intro                       | completion | completionview | completionexpected |
+      | assign   | C1     | assign1  | Test assignment name | Test assignment description | 2          | 1              | 0                  |
+    And the following "activities" exist:
+      | activity | course | idnumber | name           | intro                 | completion | completionview | completionexpected |
+      | quiz     | C1     | quiz1    | Test quiz name | Test quiz description | 0          | 0              | 0                  |
+    And I log out
+
+  Scenario: Show completion status to students
+    Given I log in as "student1"
+    And I am on "Course 1" course homepage
+    Then I should see "Your progress"
+    And the "Test forum name" "Forum" activity with "manual" completion shows a status completion checkbox
+    And the "Test assignment name" "Assign" activity with "auto" completion shows a status completion checkbox
+    And the "Test quiz name" "Quiz" activity does not show any completion checkbox
+
+  Scenario: Show completion configuration to editing teachers
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    Then I should not see "Your progress"
+    And the "Test forum name" "Forum" activity with "manual" completion shows a configuration completion checkbox
+    And the "Test assignment name" "Assign" activity with "auto" completion shows a configuration completion checkbox
+    And the "Test quiz name" "Quiz" activity does not show any completion checkbox
+    And I am on "Course 1" course homepage with editing mode on
+    And I should not see "Your progress"
+    And the "Test forum name" "Forum" activity with "manual" completion shows a configuration completion checkbox
+    And the "Test assignment name" "Assign" activity with "auto" completion shows a configuration completion checkbox
+    And the "Test quiz name" "Quiz" activity does not show any completion checkbox
+
+  Scenario: Show completion configuration to non-editing teachers
+    Given I log in as "teacher2"
+    And I am on "Course 1" course homepage
+    Then I should not see "Your progress"
+    And the "Test forum name" "Forum" activity with "manual" completion shows a configuration completion checkbox
+    And the "Test assignment name" "Assign" activity with "auto" completion shows a configuration completion checkbox
+    And the "Test quiz name" "Quiz" activity does not show any completion checkbox
index db2324d..dc0dbed 100644 (file)
@@ -4,6 +4,10 @@ information provided here is intended especially for developers.
 === 3.7 ===
  * External function core_completion_external::get_activities_completion_status new returns the following additional field:
    - valueused (indicates whether the completion state affects the availability of other content)
+ * On the course page, only users with the capability 'moodle/course:isincompletionreports' (students, by default) can now tick the
+   completion checkboxes. Teachers no longer get working checkboxes; tey see slightly different icons that indicate whether
+   completion is enabled for the activity. These are the same icons which have always been shown to teachers before when the
+   enabled the course editing mode.
 
 === 2.9 ===
 
index 39c2c5e..1a96fbf 100644 (file)
@@ -764,7 +764,7 @@ class helper {
      */
     public static function get_management_viewmodes() {
         return array(
-            'combined' => new \lang_string('categoriesandcoures'),
+            'combined' => new \lang_string('categoriesandcourses'),
             'categories' => new \lang_string('categories'),
             'courses' => new \lang_string('courses')
         );
index dbe7ce2..3042864 100644 (file)
@@ -46,7 +46,7 @@ $PAGE->set_pagelayout('incourse');
 $PAGE->set_title(get_string('course') . ': ' . $course->fullname);
 $PAGE->set_heading($course->fullname);
 
-$context = context_course::instance($courseid);
+$context = context_course::instance($course->id);
 if (empty($CFG->enablecoursepublishing) || !has_capability('moodle/course:publish', $context)) {
     throw new moodle_exception('nopermission');
 }
index 55f5ebe..9d1439c 100644 (file)
@@ -403,8 +403,12 @@ class core_course_renderer extends plugin_renderer_base {
      * @return string
      */
     public function course_section_cm_completion($course, &$completioninfo, cm_info $mod, $displayoptions = array()) {
-        global $CFG, $DB;
+        global $CFG, $DB, $USER;
         $output = '';
+
+        $istrackeduser = $completioninfo->is_tracked_user($USER->id);
+        $isediting = $this->page->user_is_editing();
+
         if (!empty($displayoptions['hidecompletion']) || !isloggedin() || isguestuser() || !$mod->uservisible) {
             return $output;
         }
@@ -412,49 +416,52 @@ class core_course_renderer extends plugin_renderer_base {
             $completioninfo = new completion_info($course);
         }
         $completion = $completioninfo->is_enabled($mod);
+
         if ($completion == COMPLETION_TRACKING_NONE) {
-            if ($this->page->user_is_editing()) {
+            if ($isediting) {
                 $output .= html_writer::span('&nbsp;', 'filler');
             }
             return $output;
         }
 
-        $completiondata = $completioninfo->get_data($mod, true);
         $completionicon = '';
 
-        if ($this->page->user_is_editing()) {
+        if ($isediting || !$istrackeduser) {
             switch ($completion) {
                 case COMPLETION_TRACKING_MANUAL :
                     $completionicon = 'manual-enabled'; break;
                 case COMPLETION_TRACKING_AUTOMATIC :
                     $completionicon = 'auto-enabled'; break;
             }
-        } else if ($completion == COMPLETION_TRACKING_MANUAL) {
-            switch($completiondata->completionstate) {
-                case COMPLETION_INCOMPLETE:
-                    $completionicon = 'manual-n' . ($completiondata->overrideby ? '-override' : '');
-                    break;
-                case COMPLETION_COMPLETE:
-                    $completionicon = 'manual-y' . ($completiondata->overrideby ? '-override' : '');
-                    break;
-            }
-        } else { // Automatic
-            switch($completiondata->completionstate) {
-                case COMPLETION_INCOMPLETE:
-                    $completionicon = 'auto-n' . ($completiondata->overrideby ? '-override' : '');
-                    break;
-                case COMPLETION_COMPLETE:
-                    $completionicon = 'auto-y' . ($completiondata->overrideby ? '-override' : '');
-                    break;
-                case COMPLETION_COMPLETE_PASS:
-                    $completionicon = 'auto-pass'; break;
-                case COMPLETION_COMPLETE_FAIL:
-                    $completionicon = 'auto-fail'; break;
+        } else {
+            $completiondata = $completioninfo->get_data($mod, true);
+            if ($completion == COMPLETION_TRACKING_MANUAL) {
+                switch($completiondata->completionstate) {
+                    case COMPLETION_INCOMPLETE:
+                        $completionicon = 'manual-n' . ($completiondata->overrideby ? '-override' : '');
+                        break;
+                    case COMPLETION_COMPLETE:
+                        $completionicon = 'manual-y' . ($completiondata->overrideby ? '-override' : '');
+                        break;
+                }
+            } else { // Automatic
+                switch($completiondata->completionstate) {
+                    case COMPLETION_INCOMPLETE:
+                        $completionicon = 'auto-n' . ($completiondata->overrideby ? '-override' : '');
+                        break;
+                    case COMPLETION_COMPLETE:
+                        $completionicon = 'auto-y' . ($completiondata->overrideby ? '-override' : '');
+                        break;
+                    case COMPLETION_COMPLETE_PASS:
+                        $completionicon = 'auto-pass'; break;
+                    case COMPLETION_COMPLETE_FAIL:
+                        $completionicon = 'auto-fail'; break;
+                }
             }
         }
         if ($completionicon) {
             $formattedname = html_entity_decode($mod->get_formatted_name(), ENT_QUOTES, 'UTF-8');
-            if ($completiondata->overrideby) {
+            if (!$isediting && $istrackeduser && $completiondata->overrideby) {
                 $args = new stdClass();
                 $args->modname = $formattedname;
                 $overridebyuser = \core_user::get_user($completiondata->overrideby, '*', MUST_EXIST);
@@ -464,7 +471,7 @@ class core_course_renderer extends plugin_renderer_base {
                 $imgalt = get_string('completion-alt-' . $completionicon, 'completion', $formattedname);
             }
 
-            if ($this->page->user_is_editing() || !has_capability('moodle/course:togglecompletion', $mod->context)) {
+            if ($isediting || !$istrackeduser || !has_capability('moodle/course:togglecompletion', $mod->context)) {
                 // When editing, the icon is just an image.
                 $completionpixicon = new pix_icon('i/completion-'.$completionicon, $imgalt, '',
                         array('title' => $imgalt, 'class' => 'iconsmall'));
index 34d02d3..e330e1d 100644 (file)
@@ -53,7 +53,7 @@ require_capability('moodle/course:request', $context);
 
 // Set up the form.
 $data = course_request::prepare();
-$requestform = new course_request_form($url, compact('editoroptions'));
+$requestform = new course_request_form($url);
 $requestform->set_data($data);
 
 $strtitle = get_string('courserequest');
index 7f2b057..3546584 100644 (file)
@@ -83,7 +83,7 @@ class behat_course extends behat_base {
 
         // Select Miscellaneous category.
         $this->i_click_on_category_in_the_management_interface(get_string('miscellaneous'));
-        $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcoures'));
+        $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcourses'));
 
         // Click create new course.
         $this->execute('behat_general::i_click_on_in_the',
index a3c73b6..45d55dd 100644 (file)
@@ -23,7 +23,7 @@ Feature: Fields locked control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
index e8650a3..802f6fa 100644 (file)
@@ -23,7 +23,7 @@ Feature: The visibility of fields control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -43,7 +43,7 @@ Feature: The visibility of fields control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+