Merge branch 'MDL-70897-master' of git://github.com/kabalin/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 29 Apr 2021 02:18:01 +0000 (10:18 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 29 Apr 2021 02:18:01 +0000 (10:18 +0800)
390 files changed:
.grunt/tasks/ignorefiles.js
admin/auth_config.php
admin/cli/fix_orphaned_calendar_events.php [new file with mode: 0644]
admin/cli/upgrade.php
admin/message.php
admin/settings/courses.php
admin/settings/users.php
admin/tool/behat/cli/run.php
admin/tool/monitor/tests/behat/subscription.feature
admin/tool/moodlenet/classes/profile_manager.php
admin/tool/oauth2/classes/form/issuer.php
admin/tool/oauth2/classes/output/renderer.php
admin/tool/oauth2/issuers.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/oauth2/pix/notconfigured.svg [new file with mode: 0644]
admin/tool/oauth2/tests/behat/basic_settings.feature
admin/tool/uploaduser/classes/process.php
admin/tool/uploaduser/locallib.php
admin/tool/uploaduser/tests/behat/upload_users.feature
admin/tool/uploaduser/tests/cli_test.php
admin/tool/uploaduser/user_form.php
admin/user/user_bulk_download.php
analytics/classes/prediction.php
analytics/tests/prediction_actions_test.php
analytics/upgrade.txt
auth/db/tests/db_test.php
auth/email/tests/external_test.php
auth/mnet/classes/privacy/provider.php
auth/mnet/lang/en/auth_mnet.php
auth/oauth2/classes/api.php
auth/oauth2/classes/auth.php
auth/oauth2/classes/output/renderer.php
auth/oauth2/linkedlogins.php
auth/oauth2/login.php
availability/condition/profile/classes/frontend.php
availability/condition/profile/tests/behat/availability_profile.feature
availability/condition/profile/tests/condition_test.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
backup/tests/automated_backup_test.php
backup/tests/backup_cleanup_task_test.php
backup/tests/backup_restore_base_testcase.php [new file with mode: 0644]
backup/tests/backup_restore_permission_test.php [new file with mode: 0644]
backup/upgrade.txt
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_controller_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/helper/backup_anonymizer_helper.class.php
backup/util/settings/tests/settings_test.php
backup/util/ui/tests/behat/import_course.feature
backup/util/ui/tests/behat/restore_moodle2_courses.feature
badges/criteria/award_criteria_profile.php
badges/tests/badgeslib_test.php
blocks/myprofile/classes/output/myprofile.php
blocks/myprofile/edit_form.php
blocks/myprofile/lang/en/block_myprofile.php
blocks/myprofile/templates/myprofile.mustache
blocks/myprofile/tests/behat/block_myprofile.feature
blog/edit_form.php
calendar/tests/calendartype_test.php
calendar/tests/helpers.php
completion/classes/activity_custom_completion.php
completion/classes/cm_completion_details.php
completion/criteria/completion_criteria_grade.php
completion/tests/behat/behat_completion.php
completion/tests/behat/custom_completion_display_conditions.feature
completion/upgrade.txt
course/classes/output/activity_information.php
course/classes/output/section_format/cmitem.php
course/edit_form.php
course/lib.php
course/moodleform_mod.php
course/templates/activity_info.mustache
course/tests/externallib_test.php
customfield/field/text/classes/field_controller.php
enrol/externallib.php
enrol/manual/manage.php
enrol/self/tests/behat/self_enrolment.feature
enrol/tests/enrollib_test.php
grade/classes/external/create_gradecategories.php [new file with mode: 0644]
grade/grading/tests/behat/behat_grading.php
grade/import/csv/classes/load_data.php
grade/lib.php
grade/report/grader/classes/privacy/provider.php
grade/report/grader/lib.php
grade/report/grader/tests/privacy_test.php
grade/report/overview/lib.php
grade/report/user/lib.php
grade/tests/external/create_gradecategories_test.php [new file with mode: 0644]
h5p/h5plib/v124/joubel/editor/h5peditor.class.php
h5p/h5plib/v124/joubel/editor/readme_moodle.txt
install/lang/da_wp/langconfig.php [new file with mode: 0644]
install/lang/el/install.php
install/lang/id/error.php [new file with mode: 0644]
install/lang/vi/moodle.php
lang/en/admin.php
lang/en/backup.php
lang/en/completion.php
lang/en/deprecated.txt
lang/en/moodle.php
lib/authlib.php
lib/behat/classes/behat_config_util.php
lib/behat/classes/behat_generator_base.php
lib/behat/classes/behat_session_trait.php
lib/classes/grades_external.php
lib/classes/oauth2/api.php
lib/classes/oauth2/discovery/openidconnect.php
lib/classes/oauth2/issuer.php
lib/classes/oauth2/refresh_system_tokens_task.php
lib/classes/oauth2/service/facebook.php
lib/classes/oauth2/service/google.php
lib/classes/oauth2/service/microsoft.php
lib/classes/plugin_manager.php
lib/classes/task/calendar_fix_orphaned_events.php [new file with mode: 0644]
lib/classes/task/course_backup_task.php
lib/classes/user.php
lib/completionlib.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/editor/classes/privacy/provider.php
lib/editor/tests/privacy_provider_test.php
lib/enrollib.php
lib/form/classes/privacy/provider.php
lib/form/filemanager.js
lib/form/tests/privacy_provider_test.php
lib/grade/grade_item.php
lib/modinfolib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/phpunit/tests/basic_test.php
lib/plagiarismlib.php
lib/setup.php
lib/setuplib.php
lib/tcpdf/composer.json
lib/tcpdf/fonts/freefont-20120503/CREDITS
lib/tcpdf/include/barcodes/pdf417.php
lib/tcpdf/include/barcodes/qrcode.php
lib/tcpdf/include/tcpdf_colors.php
lib/tcpdf/include/tcpdf_fonts.php
lib/tcpdf/include/tcpdf_images.php
lib/tcpdf/include/tcpdf_static.php
lib/tcpdf/readme_moodle.txt
lib/tcpdf/tcpdf.php
lib/tcpdf/tcpdf_barcodes_1d.php
lib/testing/generator/data_generator.php
lib/tests/behat/behat_permissions.php
lib/tests/behat/datetime_any.feature
lib/tests/behat/showuseridentity.feature
lib/tests/completion_daily_task_test.php
lib/tests/datalib_test.php
lib/tests/encryption_test.php
lib/tests/event_profile_field_test.php
lib/tests/event_test.php
lib/tests/moodlelib_test.php
lib/tests/myprofilelib_test.php
lib/tests/oauth2_test.php
lib/tests/scheduled_task_test.php
lib/tests/task_database_logger_test.php
lib/tests/upgradelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
message/templates/message_drawer_view_conversation_body.mustache
message/templates/message_drawer_view_conversation_footer.mustache
message/templates/message_drawer_view_conversation_header.mustache
message/templates/message_preferences.mustache
message/templates/message_preferences_notification_processor.mustache
message/templates/notification_preferences.mustache
message/templates/notification_preferences_component_notification.mustache
mod/assign/classes/cache/overrides.php [new file with mode: 0644]
mod/assign/classes/dates.php [new file with mode: 0644]
mod/assign/db/caches.php [new file with mode: 0644]
mod/assign/deprecatedlib.php [new file with mode: 0644]
mod/assign/externallib.php
mod/assign/feedback/offline/importgradeslib.php
mod/assign/feedback/offline/lang/en/assignfeedback_offline.php
mod/assign/feedback/offline/locallib.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/overrideedit.php
mod/assign/renderer.php
mod/assign/tests/behat/assign_activity_completion.feature [new file with mode: 0644]
mod/assign/tests/dates_test.php [new file with mode: 0644]
mod/assign/tests/generator/lib.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_participants_test.php
mod/assign/tests/locallib_test.php
mod/assign/version.php
mod/chat/classes/dates.php [new file with mode: 0644]
mod/chat/lang/en/chat.php
mod/chat/lang/en/deprecated.txt [new file with mode: 0644]
mod/chat/lib.php
mod/chat/tests/behat/behat_mod_chat.php [new file with mode: 0644]
mod/chat/tests/dates_test.php [new file with mode: 0644]
mod/chat/view.php
mod/choice/deprecatedlib.php [new file with mode: 0644]
mod/choice/lib.php
mod/data/classes/dates.php [new file with mode: 0644]
mod/data/deprecatedlib.php [new file with mode: 0644]
mod/data/lib.php
mod/data/tests/dates_test.php [new file with mode: 0644]
mod/feedback/classes/dates.php [new file with mode: 0644]
mod/feedback/deprecatedlib.php [new file with mode: 0644]
mod/feedback/lib.php
mod/feedback/tests/dates_test.php [new file with mode: 0644]
mod/folder/backup/moodle2/backup_folder_stepslib.php
mod/folder/lib.php
mod/forum/classes/dates.php [new file with mode: 0644]
mod/forum/classes/task/send_user_notifications.php
mod/forum/deprecatedlib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/tests/dates_test.php [new file with mode: 0644]
mod/forum/tests/mail_test.php
mod/glossary/deprecatedlib.php [new file with mode: 0644]
mod/glossary/lib.php
mod/h5pactivity/classes/external/get_user_attempts.php
mod/h5pactivity/classes/local/manager.php
mod/h5pactivity/classes/local/report/participants.php
mod/h5pactivity/tests/behat/locking.feature [new file with mode: 0644]
mod/h5pactivity/tests/external/get_attempts_test.php
mod/h5pactivity/tests/external/get_user_attempts_test.php
mod/h5pactivity/tests/local/manager_test.php
mod/h5pactivity/upgrade.txt [new file with mode: 0644]
mod/label/classes/completion/custom_completion.php [new file with mode: 0644]
mod/label/tests/behat/label_activity_completion.feature [new file with mode: 0644]
mod/label/view.php
mod/lesson/classes/cache/overrides.php [new file with mode: 0644]
mod/lesson/classes/dates.php [new file with mode: 0644]
mod/lesson/db/caches.php [new file with mode: 0644]
mod/lesson/deprecatedlib.php [new file with mode: 0644]
mod/lesson/index.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/overrideedit.php
mod/lesson/settings.php
mod/lesson/tests/dates_test.php [new file with mode: 0644]
mod/lesson/tests/generator/lib.php
mod/lesson/version.php
mod/lti/OAuth.php
mod/lti/amd/build/tool_configure_controller.min.js
mod/lti/amd/build/tool_configure_controller.min.js.map
mod/lti/amd/src/tool_configure_controller.js
mod/lti/classes/local/ltiopenid/registration_helper.php
mod/lti/classes/output/registration_upgrade_choice_page.php [new file with mode: 0644]
mod/lti/classes/output/renderer.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/openid-configuration.php
mod/lti/openid-registration.php
mod/lti/startltiadvregistration.php
mod/lti/templates/registration_upgrade_choice_page.mustache [new file with mode: 0644]
mod/lti/tests/behat/lti_activity_completion.feature
mod/lti/tests/openidregistration_test.php
mod/page/tests/behat/page_activity_completion.feature
mod/quiz/accessrule/seb/classes/property_list.php
mod/quiz/attemptlib.php
mod/quiz/classes/cache/overrides.php [new file with mode: 0644]
mod/quiz/classes/completion/custom_completion.php
mod/quiz/classes/dates.php [new file with mode: 0644]
mod/quiz/classes/question/bank/custom_view.php
mod/quiz/db/caches.php [new file with mode: 0644]
mod/quiz/deprecatedlib.php [new file with mode: 0644]
mod/quiz/index.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/overrideedit.php
mod/quiz/renderer.php
mod/quiz/report/overview/classes/privacy/provider.php
mod/quiz/report/overview/report.php
mod/quiz/report/overview/tests/privacy_provider_test.php
mod/quiz/review.php
mod/quiz/reviewquestion.php
mod/quiz/tests/behat/attempt_redo_questions.feature
mod/quiz/tests/behat/info_page.feature [new file with mode: 0644]
mod/quiz/tests/dates_test.php [new file with mode: 0644]
mod/quiz/tests/lib_test.php
mod/quiz/version.php
mod/quiz/view.php
mod/resource/classes/completion/custom_completion.php [new file with mode: 0644]
mod/resource/lib.php
mod/resource/tests/behat/resource_activity_completion.feature [new file with mode: 0644]
mod/scorm/classes/dates.php [new file with mode: 0644]
mod/scorm/deprecatedlib.php [new file with mode: 0644]
mod/scorm/lib.php
mod/scorm/mod_form.php
mod/scorm/tests/dates_test.php [new file with mode: 0644]
mod/survey/deprecatedlib.php [new file with mode: 0644]
mod/survey/lib.php
mod/url/classes/completion/custom_completion.php [new file with mode: 0644]
mod/url/lib.php
mod/url/locallib.php
mod/url/tests/behat/url_activity_completion.feature
mod/workshop/classes/dates.php [new file with mode: 0644]
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/tests/dates_test.php [new file with mode: 0644]
question/engine/lib.php
question/engine/questionattemptstep.php
question/engine/questionusage.php
question/engine/renderer.php
question/engine/tests/questionattemptstep_test.php
question/engine/tests/questionusagebyactivity_test.php
question/engine/tests/walkthrough_test.php
question/type/calculatedmulti/edit_calculatedmulti_form.php
question/type/ddimageortext/edit_ddimageortext_form.php
question/type/ddimageortext/questiontype.php
question/type/ddimageortext/tests/behat/add.feature
question/type/ddmarker/edit_ddmarker_form.php
question/type/ddmarker/questiontype.php
question/type/ddmarker/tests/behat/add.feature
question/type/edit_question_form.php
question/type/essay/edit_essay_form.php
question/type/essay/questiontype.php
question/type/essay/tests/behat/add.feature
question/type/match/edit_match_form.php
question/type/match/questiontype.php
question/type/match/tests/behat/add.feature
question/type/multianswer/edit_multianswer_form.php
question/type/multichoice/edit_multichoice_form.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/lang/en/qtype_numerical.php
question/type/numerical/questiontype.php
question/type/numerical/tests/behat/add.feature
report/insights/classes/output/insight.php
report/log/user.php
report/participation/amd/build/participants.min.js
report/participation/amd/build/participants.min.js.map
report/participation/amd/src/participants.js
report/participation/tests/behat/message_participants.feature
repository/googledocs/tests/generator/lib.php
search/classes/external.php
search/engine/solr/classes/engine.php
search/tests/external_test.php
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/user.scss
theme/boost/style/moodle.css
theme/boost/thirdpartylibs.xml
theme/classic/style/moodle.css
user/amd/build/edit_profile_fields.min.js [new file with mode: 0644]
user/amd/build/edit_profile_fields.min.js.map [new file with mode: 0644]
user/amd/src/edit_profile_fields.js [new file with mode: 0644]
user/classes/analytics/indicator/user_profile_set.php
user/classes/fields.php
user/classes/form/profile_category_form.php [new file with mode: 0644]
user/classes/form/profile_field_form.php [new file with mode: 0644]
user/classes/privacy/provider.php
user/editlib.php
user/externallib.php
user/filters/profilefield.php
user/lib.php
user/profile/definelib.php
user/profile/field/checkbox/tests/privacy_test.php
user/profile/field/datetime/tests/privacy_test.php
user/profile/field/menu/tests/privacy_test.php
user/profile/field/social/classes/helper.php [new file with mode: 0644]
user/profile/field/social/classes/privacy/provider.php [new file with mode: 0644]
user/profile/field/social/define.class.php [new file with mode: 0644]
user/profile/field/social/field.class.php [new file with mode: 0644]
user/profile/field/social/lang/en/profilefield_social.php [new file with mode: 0644]
user/profile/field/social/tests/behat/social_profile_field.feature [new file with mode: 0644]
user/profile/field/social/tests/privacy_test.php [new file with mode: 0644]
user/profile/field/social/upgradelib.php [new file with mode: 0644]
user/profile/field/social/version.php [new file with mode: 0644]
user/profile/field/text/tests/privacy_test.php
user/profile/field/textarea/tests/privacy_test.php
user/profile/index.php
user/profile/index_category_form.php [deleted file]
user/profile/index_field_form.php [deleted file]
user/profile/lib.php
user/templates/edit_profile_fields.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/filterrow.mustache
user/tests/behat/custom_profile_fields.feature
user/tests/externallib_test.php
user/tests/privacy_test.php
user/tests/profilelib_test.php
user/tests/userlib_test.php
version.php

index d8b9ec1..9cc2f6c 100644 (file)
@@ -41,7 +41,7 @@ module.exports = grunt => {
             '*/**/yui/src/*/meta/',
             '*/**/build/',
         ].concat(thirdPartyPaths);
-        grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
+        grunt.file.write('.eslintignore', eslintIgnores.join('\n') + '\n');
 
         // Generate .stylelintignore.
         const stylelintIgnores = [
@@ -50,7 +50,7 @@ module.exports = grunt => {
             'theme/boost/style/moodle.css',
             'theme/classic/style/moodle.css',
         ].concat(thirdPartyPaths);
-        grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
+        grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n');
     };
 
     grunt.registerTask('ignorefiles', 'Generate ignore files for linters', handler);
index 3ee8ebc..503dd91 100644 (file)
@@ -171,8 +171,6 @@ function print_auth_lock_options($auth, $user_fields, $helptext, $retrieveopts,
                 // limit for the setting name is 100.
                 continue;
             }
-        } elseif ($fieldname == 'url') {
-            $fieldname = get_string('webpage');
         } else {
             $fieldname = get_string($fieldname);
         }
diff --git a/admin/cli/fix_orphaned_calendar_events.php b/admin/cli/fix_orphaned_calendar_events.php
new file mode 100644 (file)
index 0000000..304b276
--- /dev/null
@@ -0,0 +1,138 @@
+<?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/>.
+
+/**
+ * Fix orphaned calendar events that were broken by MDL-67494.
+ *
+ * This script will look for all the calendar events which userids
+ * where broken by a wrong upgrade step, affecting to Moodle 3.9.5
+ * and up.
+ *
+ * It performs checks to both:
+ *    a) Detect if the site was affected (ran the wrong upgrade step).
+ *    b) Look for orphaned calendar events, categorising them as:
+ *       - standard: site / category / course / group / user events
+ *       - subscription: events created via subscriptions.
+ *       - action: normal action events, created to show common important dates.
+ *       - override: user and group override events, particular, that some activities support.
+ *       - custom: other events, not being any of the above, common or particular.
+ * By specifying it (--fix) try to recover as many broken events (missing userid) as
+ * possible. Standard, subscription, action, override events in core are fully supported but
+ * override or custom events should be fixed by each plugin as far as there isn't any standard
+ * API (plugin-wise) to launch a rebuild of the calendar events.
+ *
+ * @package core
+ * @copyright 2021 onwards Simey Lameze <simey@moodle.com>
+ * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require_once(__DIR__ . '/../../config.php');
+require_once($CFG->libdir . "/clilib.php");
+require_once($CFG->libdir . '/db/upgradelib.php');
+
+// Supported options.
+$long = ['fix'  => false, 'help' => false];
+$short = ['f' => 'fix', 'h' => 'help'];
+
+// CLI options.
+[$options, $unrecognized] = cli_get_params($long, $short);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help']) {
+    $help = <<<EOT
+Fix orphaned calendar events.
+
+  This script detects calendar events that have had their
+  userid lost. By default it will perform various checks
+  and report them, showing the site status in an easy way.
+
+  Also, optionally (--fix), it wil try to recover as many
+  lost userids as possible from different sources. Note that
+  this script aims to process well-know events in core,
+  leaving custom events in 3rd part plugins mostly unmodified
+  because there isn't any consistent way to regenerate them.
+
+  For more details:  https://tracker.moodle.org/browse/MDL-71156
+
+Options:
+  -h, --help    Print out this help.
+  -f, --fix     Fix the orphaned calendar events in the DB.
+                If not specified only check and report problems to STDERR.
+
+Usage:
+  - Only report:    \$ sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_calendar_events.php
+  - Report and fix: \$ sudo -u www-data /usr/bin/php admin/cli/fix_orphaned_calendar_events.php -f
+EOT;
+
+    cli_writeln($help);
+    die;
+}
+
+// Check various usual pre-requisites.
+if (empty($CFG->version)) {
+    cli_error('Database is not yet installed.');
+}
+
+$admin = get_admin();
+if (!$admin) {
+    cli_error('Error: No admin account was found.');
+}
+
+if (moodle_needs_upgrading()) {
+    cli_error('Moodle upgrade pending, script execution suspended.');
+}
+
+// Do everything as admin by default.
+\core\session\manager::set_user($admin);
+
+// Report current site status.
+cli_heading('Checking the site status');
+$needsfix = upgrade_calendar_site_status();
+
+// Report current calendar events status.
+cli_heading('Checking the calendar events status');
+$info = upgrade_calendar_events_status();
+$hasbadevents = $info['total']->bad > 0 || $info['total']->bad != $info['other']->bad;
+$needsfix = $needsfix || $hasbadevents;
+
+// If, selected, fix as many calendar events as possible.
+if ($options['fix']) {
+
+    // If the report has told us that the fix was not needed... ask for confirmation!
+    if (!$needsfix) {
+        cli_writeln("This site DOES NOT NEED to run the calendar events fix.");
+        $input = cli_input('Are you completely sure that you want to run the fix? (y/N)', 'N', ['y', 'Y', 'n', 'N']);
+        if (strtolower($input) != 'y') {
+            exit(0);
+        }
+        cli_writeln("");
+    }
+    cli_heading('Fixing as many as possible calendar events');
+    upgrade_calendar_events_fix_remaining($info);
+    // Report current (after fix) calendar events status.
+    cli_heading('Checking the calendar events status (after fix)');
+    upgrade_calendar_events_status();
+} else if ($needsfix) {
+    // Fix option was not provided but problem events have been found. Notify the user and provide info how to fix these events.
+    cli_writeln("This site NEEDS to run the calendar events fix!");
+    cli_writeln("To fix the calendar events, re-run this script with the --fix option.");
+}
index 12904e5..cbfa2be 100644 (file)
@@ -135,6 +135,15 @@ if (!$envstatus) {
     exit(1);
 }
 
+// Make sure there are no files left over from previous versions.
+if (upgrade_stale_php_files_present()) {
+    cli_problem(get_string('upgradestalefiles', 'admin'));
+
+    // Stale file info contains HTML elements which aren't suitable for CLI.
+    $upgradestalefilesinfo = get_string('upgradestalefilesinfo', 'admin', get_docs_url('Upgrading'));
+    cli_error(strip_tags($upgradestalefilesinfo));
+}
+
 // Test plugin dependencies.
 $failed = array();
 if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
index b5c31d7..aa632b1 100644 (file)
@@ -119,10 +119,19 @@ if (($form = data_submitted()) && confirm_sesskey()) {
     // Save processors enabled/disabled status.
     foreach ($allprocessors as $processor) {
         $enabled = isset($form->{$processor->name});
+        if ($enabled != $processor->enabled) {
+            add_to_config_log($processor->name, $processor->enabled, $enabled, 'core');
+        }
         \core_message\api::update_processor_status($processor, $enabled);
     }
 
     foreach ($newpreferences as $name => $value) {
+        $old = isset($preferences->$name) ? $preferences->$name : '';
+
+        if ($old != $value) {
+            add_to_config_log($name, $old, $value, 'core');
+        }
+
         set_config($name, $value, 'message');
     }
     $transaction->allow_commit();
index 86d198e..2037a63 100644 (file)
@@ -343,6 +343,11 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
 
     // Import defaults section.
     $temp->add(new admin_setting_heading('importsettings', new lang_string('importsettings', 'backup'), ''));
+    $temp->add(new admin_setting_configcheckbox_with_lock(
+            'backup/backup_import_permissions',
+            new lang_string('generalpermissions', 'backup'),
+            new lang_string('configgeneralpermissions', 'backup'),
+            array('value' => 0, 'locked' => 0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_activities', new lang_string('generalactivities','backup'), new lang_string('configgeneralactivities','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_blocks', new lang_string('generalblocks','backup'), new lang_string('configgeneralblocks','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_filters', new lang_string('generalfilters','backup'), new lang_string('configgeneralfilters','backup'), array('value'=>1, 'locked'=>0)));
@@ -517,6 +522,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_role_assignments',
         new lang_string('generalroleassignments', 'backup'),
         new lang_string('configrestoreroleassignments', 'backup'), array('value' => 1, 'locked' => 0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_permissions',
+        new lang_string('generalpermissions', 'backup'),
+        new lang_string('configrestorepermissions', 'backup'), array('value' => 1, 'locked' => 0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_activities',
         new lang_string('generalactivities', 'backup'),
         new lang_string('configrestoreactivities', 'backup'), array('value' => 1, 'locked' => 0)));
index 6e58d4c..aa60229 100644 (file)
@@ -196,12 +196,6 @@ if ($hassiteconfig
                              'country' => new lang_string('country'),
                              'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
                              'timezone' => new lang_string('timezone'),
-                             'webpage' => new lang_string('webpage'),
-                             'icqnumber' => new lang_string('icqnumber'),
-                             'skypeid' => new lang_string('skypeid'),
-                             'yahooid' => new lang_string('yahooid'),
-                             'aimid' => new lang_string('aimid'),
-                             'msnid' => new lang_string('msnid'),
                              'firstaccess' => new lang_string('firstaccess'),
                              'lastaccess' => new lang_string('lastaccess'),
                              'lastip' => new lang_string('lastip'),
@@ -219,7 +213,8 @@ if ($hassiteconfig
                 new lang_string('showuseridentity', 'admin'),
                 new lang_string('showuseridentity_desc', 'admin'), ['email' => 1],
                 function() {
-                    global $DB;
+                    global $CFG;
+                    require_once($CFG->dirroot.'/user/profile/lib.php');
 
                     // Basic fields available in user table.
                     $fields = [
@@ -235,10 +230,10 @@ if ($hassiteconfig
                     ];
 
                     // Custom profile fields.
-                    $profilefields = $DB->get_records('user_info_field', ['datatype' => 'text'], 'sortorder ASC');
-                    foreach ($profilefields as $key => $field) {
-                        // Only reasonable-length fields can be used as identity fields.
-                        if ($field->param2 > 255) {
+                    $profilefields = profile_get_custom_fields();
+                    foreach ($profilefields as $field) {
+                        // Only reasonable-length text fields can be used as identity fields.
+                        if ($field->param2 > 255 || $field->datatype != 'text') {
                             continue;
                         }
                         $fields['profile_field_' . $field->shortname] = $field->name . ' *';
index 98375c4..a5dfa1d 100644 (file)
@@ -45,7 +45,7 @@ list($options, $unrecognised) = cli_get_params(
     array(
         'stop-on-failure' => 0,
         'verbose'  => false,
-        'replace'  => false,
+        'replace'  => '',
         'help'     => false,
         'tags'     => '',
         'profile'  => '',
@@ -70,7 +70,7 @@ $help = "
 Behat utilities to run behat tests in parallel
 
 Usage:
-  php run.php [--BEHAT_OPTION=\"value\"] [--feature=\"value\"] [--replace] [--fromrun=value --torun=value] [--help]
+  php run.php [--BEHAT_OPTION=\"value\"] [--feature=\"value\"] [--replace=\"{run}\"] [--fromrun=value --torun=value] [--help]
 
 Options:
 --BEHAT_OPTION     Any combination of behat option specified in http://behat.readthedocs.org/en/v2.5/guides/6.cli.html
@@ -144,9 +144,10 @@ $extraopts = $unrecognised;
 if ($options['profile']) {
     $profile = $options['profile'];
 
-    // If profile passed is not set, then exit.
+    // If profile passed is not set, then exit (note we skip if the 'replace' option is found within the 'profile' value).
     if (!isset($CFG->behat_config[$profile]) && !isset($CFG->behat_profiles[$profile]) &&
-        !(isset($options['replace']) && (strpos($options['profile'], $options['replace']) >= 0 ))) {
+            !($options['replace'] && (strpos($profile, (string) $options['replace']) !== false))) {
+
         echo "Invalid profile passed: " . $profile . PHP_EOL;
         exit(1);
     }
index 9b74a68..c91d45a 100644 (file)
@@ -102,7 +102,7 @@ Feature: tool_monitor_subscriptions
     Given I log in as "admin"
     And I follow "Preferences" in the user menu
     And I click on "Notification preferences" "link" in the "#page-content" "css_element"
-    And I click on "//td[@data-processor-name='popup']//label[@class='preference-state']" "xpath_element" in the "Notifications of rule subscriptions" "table_row"
+    And I click on "//td[@data-processor-name='popup']//label[@data-state='loggedin']" "xpath_element" in the "Notifications of rule subscriptions" "table_row"
     And I wait until the page is ready
     And I follow "Preferences" in the user menu
     And I follow "Event monitoring"
@@ -123,7 +123,7 @@ Feature: tool_monitor_subscriptions
     Given I log in as "teacher1"
     And I follow "Preferences" in the user menu
     And I click on "Notification preferences" "link" in the "#page-content" "css_element"
-    And I click on "//td[@data-processor-name='popup']//label[@class='preference-state']" "xpath_element" in the "Notifications of rule subscriptions" "table_row"
+    And I click on "//td[@data-processor-name='popup']//label[@data-state='loggedin']" "xpath_element" in the "Notifications of rule subscriptions" "table_row"
     And I wait until the page is ready
     And I follow "Preferences" in the user menu
     And I follow "Event monitoring"
index 49027fd..d4096b7 100644 (file)
@@ -310,11 +310,10 @@ class profile_manager {
             'CURLOPT_HEADER' => 0,
         ];
         $content = $curl->get($url, null, $options);
-        $errno   = $curl->get_errno();
         $info = $curl->get_info();
 
         // The base cURL seems fine, let's press on.
-        if (!$errno) {
+        if (!$curl->get_errno() && !$curl->error) {
             // WebFinger gave us a 404 back so the user has no droids here.
             if ($info['http_code'] >= 400) {
                 if ($info['http_code'] === 404) {
index a5d6c97..84b0669 100644 (file)
@@ -115,54 +115,69 @@ class issuer extends persistent {
         $mform->addElement('checkbox', 'basicauth', get_string('usebasicauth', 'tool_oauth2'));
         $mform->addHelpButton('basicauth', 'usebasicauth', 'tool_oauth2');
 
+        // Base Url.
+        $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'));
+        $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
+        $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2');
+        if ($this->type && $this->type == 'nextcloud') {
+            $mform->addRule('baseurl', null, 'required', null, 'client');
+        }
+
+        // Image.
+        $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"');
+        $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
+        $mform->addHelpButton('image', 'issuername', 'tool_oauth2');
+
+        // Show on login page.
+        $options = [
+            \core\oauth2\issuer::EVERYWHERE => get_string('issueruseineverywhere', 'tool_oauth2'),
+            \core\oauth2\issuer::LOGINONLY => get_string('issueruseinloginonly', 'tool_oauth2'),
+            \core\oauth2\issuer::SERVICEONLY => get_string('issueruseininternalonly', 'tool_oauth2'),
+        ];
+        $mform->addElement('select', 'showonloginpage', get_string('issuerusein', 'tool_oauth2'), $options);
+        $mform->addHelpButton('showonloginpage', 'issuerusein', 'tool_oauth2');
+
+        // Name on login page.
+        $mform->addElement('text', 'loginpagename', get_string('issuerloginpagename', 'tool_oauth2'));
+        $mform->addRule('loginpagename', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
+        $mform->addHelpButton('loginpagename', 'issuerloginpagename', 'tool_oauth2');
+        $mform->hideIf('loginpagename', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
+
         // Login scopes.
         $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2'));
-        $mform->addRule('loginscopes', null, 'required', null, 'client');
         $mform->addRule('loginscopes', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('loginscopes', 'issuerloginscopes', 'tool_oauth2');
+        $mform->hideIf('loginscopes', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         // Login scopes offline.
         $mform->addElement('text', 'loginscopesoffline', get_string('issuerloginscopesoffline', 'tool_oauth2'));
-        $mform->addRule('loginscopesoffline', null, 'required', null, 'client');
         $mform->addRule('loginscopesoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('loginscopesoffline', 'issuerloginscopesoffline', 'tool_oauth2');
+        $mform->hideIf('loginscopesoffline', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         // Login params.
         $mform->addElement('text', 'loginparams', get_string('issuerloginparams', 'tool_oauth2'));
         $mform->addRule('loginparams', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('loginparams', 'issuerloginparams', 'tool_oauth2');
+        $mform->hideIf('loginparams', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         // Login params offline.
         $mform->addElement('text', 'loginparamsoffline', get_string('issuerloginparamsoffline', 'tool_oauth2'));
         $mform->addRule('loginparamsoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('loginparamsoffline', 'issuerloginparamsoffline', 'tool_oauth2');
-
-        // Base Url.
-        $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'));
-        $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
-        $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2');
-        if ($this->type && $this->type == 'nextcloud') {
-            $mform->addRule('baseurl', null, 'required', null, 'client');
-        }
+        $mform->hideIf('loginparamsoffline', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         // Allowed Domains.
         $mform->addElement('text', 'alloweddomains', get_string('issueralloweddomains', 'tool_oauth2'));
         $mform->addRule('alloweddomains', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
         $mform->addHelpButton('alloweddomains', 'issueralloweddomains', 'tool_oauth2');
-
-        // Image.
-        $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"');
-        $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
-        $mform->addHelpButton('image', 'issuername', 'tool_oauth2');
-
-        // Show on login page.
-        $mform->addElement('checkbox', 'showonloginpage', get_string('issuershowonloginpage', 'tool_oauth2'));
-        $mform->addHelpButton('showonloginpage', 'issuershowonloginpage', 'tool_oauth2');
+        $mform->hideIf('alloweddomains', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         if ($this->showrequireconfirm) {
             // Require confirmation email for new accounts.
             $mform->addElement('advcheckbox', 'requireconfirmation', get_string('issuerrequireconfirmation', 'tool_oauth2'));
             $mform->addHelpButton('requireconfirmation', 'issuerrequireconfirmation', 'tool_oauth2');
+            $mform->hideIf('requireconfirmation', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
         }
 
         if ($this->type == 'imsobv2p1' || $issuer->get('servicetype') == 'imsobv2p1') {
@@ -209,4 +224,36 @@ class issuer extends persistent {
             $mform->getElement('servicetype')->setValue($this->type);
         }
     }
+
+    /**
+     * Define extra validation mechanims.
+     *
+     * The data here:
+     * - does not include {@see self::$fieldstoremove}.
+     * - does include {@see self::$foreignfields}.
+     * - was converted to map persistent-like data, e.g. array $description to string $description + int $descriptionformat.
+     *
+     * You can modify the $errors parameter in order to remove some validation errors should you
+     * need to. However, the best practice is to return new or overriden errors. Only modify the
+     * errors passed by reference when you have no other option.
+     *
+     * Do not add any logic here, it is only intended to be used by child classes.
+     *
+     * @param  stdClass $data Data to validate.
+     * @param  array $files Array of files.
+     * @param  array $errors Currently reported errors.
+     * @return array of additional errors, or overridden errors.
+     */
+    protected function extra_validation($data, $files, array &$errors) {
+        $errors = [];
+        if ($data->showonloginpage != \core\oauth2\issuer::SERVICEONLY) {
+            if (!strlen(trim($data->loginscopes))) {
+                $errors['loginscopes'] = get_string('required');
+            }
+            if (!strlen(trim($data->loginscopesoffline))) {
+                $errors['loginscopesoffline'] = get_string('required');
+            }
+        }
+        return $errors;
+    }
 }
index b838d0a..cb79978 100644 (file)
@@ -53,8 +53,9 @@ class renderer extends plugin_renderer_base {
         $table = new html_table();
         $table->head  = [
             get_string('name'),
-            get_string('configuredstatus', 'tool_oauth2'),
-            get_string('loginissuer', 'tool_oauth2'),
+            get_string('issuerusedforlogin', 'tool_oauth2'),
+            get_string('logindisplay', 'tool_oauth2'),
+            get_string('issuerusedforinternal', 'tool_oauth2'),
             get_string('discoverystatus', 'tool_oauth2') . ' ' . $this->help_icon('discovered', 'tool_oauth2'),
             get_string('systemauthstatus', 'tool_oauth2') . ' ' . $this->help_icon('systemaccountconnected', 'tool_oauth2'),
             get_string('edit'),
@@ -84,21 +85,29 @@ class renderer extends plugin_renderer_base {
             $namecell = new html_table_cell($name);
             $namecell->header = true;
 
-            // Configured.
-            if ($issuer->is_configured()) {
-                $configured = $this->pix_icon('yes', get_string('configured', 'tool_oauth2'), 'tool_oauth2');
+            // Login issuer.
+            if ((int)$issuer->get('showonloginpage') == issuer::SERVICEONLY) {
+                $loginissuer = $this->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2');
+                $logindisplayas = '';
             } else {
-                $configured = $this->pix_icon('no', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2');
+                $logindisplayas = s($issuer->get_display_name());
+                if ($issuer->get('id') && $issuer->is_configured() && !empty($issuer->get_endpoint_url('userinfo'))) {
+                    $loginissuer = $this->pix_icon('yes', get_string('loginissuer', 'tool_oauth2'), 'tool_oauth2');
+                } else {
+                    $loginissuer = $this->pix_icon('notconfigured', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2');
+                }
             }
-            $configuredstatuscell = new html_table_cell($configured);
+            $loginissuerstatuscell = new html_table_cell($loginissuer);
 
-            // Login issuer.
-            if (!empty($issuer->get('showonloginpage'))) {
-                $loginissuer = $this->pix_icon('yes', get_string('loginissuer', 'tool_oauth2'), 'tool_oauth2');
+            // Internal services issuer.
+            if ((int)$issuer->get('showonloginpage') == issuer::LOGINONLY) {
+                $serviceissuer = $this->pix_icon('no', get_string('issuersservicesnotallow', 'tool_oauth2'), 'tool_oauth2');
+            } else if ($issuer->get('id') && $issuer->is_configured()) {
+                $serviceissuer = $this->pix_icon('yes', get_string('issuersservicesallow', 'tool_oauth2'), 'tool_oauth2');
             } else {
-                $loginissuer = $this->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2');
+                $serviceissuer = $this->pix_icon('notconfigured', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2');
             }
-            $loginissuerstatuscell = new html_table_cell($loginissuer);
+            $internalissuerstatuscell = new html_table_cell($serviceissuer);
 
             // Discovered.
             if (!empty($issuer->get('scopessupported'))) {
@@ -186,13 +195,18 @@ class renderer extends plugin_renderer_base {
 
             $row = new html_table_row([
                 $namecell,
-                $configuredstatuscell,
                 $loginissuerstatuscell,
+                $logindisplayas,
+                $internalissuerstatuscell,
                 $discoverystatuscell,
                 $systemauthstatuscell,
                 $editcell,
             ]);
 
+            if (!$issuer->get('enabled')) {
+                $row->attributes['class'] = 'dimmed_text';
+            }
+
             $data[] = $row;
             $index++;
         }
index 6383ef2..ad720c4 100644 (file)
@@ -195,7 +195,7 @@ if ($mform && $mform->is_cancelled()) {
     echo $OUTPUT->header();
     echo $OUTPUT->heading(get_string('pluginname', 'tool_oauth2'));
     echo $OUTPUT->doc_link('OAuth2_Services', get_string('serviceshelp', 'tool_oauth2'));
-    $issuers = core\oauth2\api::get_all_issuers();
+    $issuers = core\oauth2\api::get_all_issuers(true);
     echo $renderer->issuers_table($issuers);
 
     echo $renderer->container_start();
index 9ff13af..b929911 100644 (file)
@@ -70,6 +70,8 @@ $string['issuerdisabled'] = 'Identity issuer disabled';
 $string['issuerenabled'] = 'Identity issuer enabled';
 $string['issuerimage_help'] = 'An image URL used to show a logo for this issuer. May be displayed on login page.';
 $string['issuerimage'] = 'Logo URL';
+$string['issuerloginpagename'] = 'Name displayed on the login page';
+$string['issuerloginpagename_help'] = 'If specified, this name will be used on the login page instead of the service name above';
 $string['issuerloginparams'] = 'Additional parameters included in a login request.';
 $string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the user\'s basic profile.';
 $string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.';
@@ -85,6 +87,16 @@ $string['issuershowonloginpage'] = 'Show on login page';
 $string['issuerrequireconfirmation_help'] = 'Require that all users verify their email address before they can log in with OAuth. This applies to newly created accounts as part of the login process, or when an existing Moodle account is connected to an OAuth login via matching email addresses.';
 $string['issuerrequireconfirmation'] = 'Require email verification';
 $string['issuers'] = 'Issuers';
+$string['issuersservicesallow'] = 'Allow services';
+$string['issuersservicesnotallow'] = 'Do not allow services';
+$string['issuerusein'] = 'This service will be used';
+$string['issuerusein_help'] = 'OAuth 2 services can be used in some internal services, on the login page, or both, if needed';
+$string['issueruseineverywhere'] = 'Login page and internal services';
+$string['issueruseininternalonly'] = 'Internal services only';
+$string['issueruseinloginonly'] = 'Login page only';
+$string['issuerusedforlogin'] = 'Login';
+$string['issuerusedforinternal'] = 'Internal services';
+$string['logindisplay'] = 'Display on login page as';
 $string['loginissuer'] = 'Allow login';
 $string['microsoft_service'] = 'Microsoft';
 $string['nextcloud_service'] = 'Nextcloud';
diff --git a/admin/tool/oauth2/pix/notconfigured.svg b/admin/tool/oauth2/pix/notconfigured.svg
new file mode 100644 (file)
index 0000000..de01599
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+        viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve" preserveAspectRatio="xMinYMid meet">
+<style type="text/css">
+       .st0{fill:#FFA500;}
+</style>
+<path class="st0" d="M9.1,12.9v-1.7c0-0.1,0-0.2-0.1-0.2s-0.1-0.1-0.2-0.1H7.2c-0.1,0-0.1,0-0.2,0.1s-0.1,0.1-0.1,0.2v1.7
+       c0,0.1,0,0.2,0.1,0.2s0.1,0.1,0.2,0.1h1.7c0.1,0,0.1,0,0.2-0.1S9.1,13,9.1,12.9z M9.1,9.5l0.2-4.1c0-0.1,0-0.1-0.1-0.2
+       C9.1,5.2,9,5.2,9,5.2H7c-0.1,0-0.1,0-0.2,0.1c-0.1,0-0.1,0.1-0.1,0.2l0.2,4.1c0,0.1,0,0.1,0.1,0.1c0.1,0,0.1,0.1,0.2,0.1h1.6
+       c0.1,0,0.2,0,0.2-0.1C9.1,9.7,9.1,9.6,9.1,9.5z M9,1.3l6.8,12.5c0.2,0.4,0.2,0.7,0,1.1c-0.1,0.2-0.2,0.3-0.4,0.4s-0.4,0.2-0.6,0.2
+       H1.2c-0.2,0-0.4-0.1-0.6-0.2S0.3,15,0.2,14.9c-0.2-0.4-0.2-0.7,0-1.1L7,1.3c0.1-0.2,0.2-0.3,0.4-0.4S7.8,0.7,8,0.7s0.4,0.1,0.6,0.2
+       C8.8,0.9,8.9,1.1,9,1.3z"/>
+</svg>
index 429d371..1a258b3 100644 (file)
@@ -18,8 +18,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Testing service"
-    And "Configured" "icon" should exist in the "Testing service" "table_row"
     And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
     And "Service discovery successful" "icon" should exist in the "Testing service" "table_row"
     And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
     And I should see "https://accounts.google.com/.well-known/openid-configuration" in the "discovery_endpoint" "table_row"
@@ -51,8 +51,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Testing service"
-    And "Configured" "icon" should exist in the "Testing service" "table_row"
     And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
     And I should see "-" in the "Testing service" "table_row"
     And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
     And I should see "authorization_endpoint"
@@ -83,8 +83,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Testing service"
-    And "Configured" "icon" should exist in the "Testing service" "table_row"
     And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
     And I should see "-" in the "Testing service" "table_row"
     And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
     And I should see "authorization_endpoint"
@@ -120,8 +120,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Testing service"
-    And "Configured" "icon" should exist in the "Testing service" "table_row"
     And "Do not allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
     And I should see "-" in the "Testing service" "table_row"
     And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
     And I should see "authorization_endpoint"
@@ -152,7 +152,7 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "OpenBadges"
-    And "Configured" "icon" should exist in the "OpenBadges" "table_row"
+    And "Allow services" "icon" should exist in the "OpenBadges" "table_row"
     And "Do not allow login" "icon" should exist in the "OpenBadges" "table_row"
     And "Service discovery successful" "icon" should exist in the "OpenBadges" "table_row"
     And the "src" attribute of "table.admintable th img" "css_element" should contain "IMS-Global-Logo.png"
@@ -187,8 +187,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Google custom"
-    And "Configured" "icon" should exist in the "Google custom" "table_row"
     And "Do not allow login" "icon" should exist in the "Google custom" "table_row"
+    And "Allow services" "icon" should exist in the "Google custom" "table_row"
     And "Service discovery successful" "icon" should exist in the "Google custom" "table_row"
     And the "src" attribute of "table.admintable th img" "css_element" should contain "favicon.ico"
     And I click on "Configure endpoints" "link" in the "Google custom" "table_row"
@@ -222,7 +222,7 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Could not discover end points for identity issuer: Invalid custom service"
     And I should see "URL: https://dc.imsglobal.org/.well-known/openid-configuration"
-    And "Configured" "icon" should exist in the "Invalid custom service" "table_row"
+    And "Allow services" "icon" should exist in the "Invalid custom service" "table_row"
     And "Do not allow login" "icon" should exist in the "Invalid custom service" "table_row"
     And I should see "-" in the "Invalid custom service" "table_row"
     And I click on "Configure endpoints" "link" in the "Invalid custom service" "table_row"
@@ -237,8 +237,8 @@ Feature: Basic OAuth2 functionality
       | Name                       | Valid custom service                        |
       | Service base URL           | https://accounts.google.com/                |
     And I press "Save changes"
-    And "Configured" "icon" should exist in the "Valid custom" "table_row"
     And "Do not allow login" "icon" should exist in the "Valid custom" "table_row"
+    And "Allow services" "icon" should exist in the "Valid custom" "table_row"
     And "Service discovery successful" "icon" should exist in the "Valid custom" "table_row"
     And I click on "Edit" "link" in the "Valid custom service" "table_row"
     And I set the following fields to these values:
@@ -263,7 +263,7 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     And I should see "Changes saved"
     And I should see "Empty custom service"
-    And "Configured" "icon" should exist in the "Empty custom service" "table_row"
+    And "Allow services" "icon" should exist in the "Empty custom service" "table_row"
     And "Do not allow login" "icon" should exist in the "Empty custom service" "table_row"
     And I should see "-" in the "Empty custom service" "table_row"
     And I click on "Configure endpoints" "link" in the "Empty custom service" "table_row"
@@ -279,8 +279,8 @@ Feature: Basic OAuth2 functionality
       | Name                       | Valid custom service                      |
       | Service base URL           | https://accounts.google.com               |
     And I press "Save changes"
-    And "Configured" "icon" should exist in the "Valid custom" "table_row"
     And "Do not allow login" "icon" should exist in the "Valid custom" "table_row"
+    And "Allow services" "icon" should exist in the "Valid custom" "table_row"
     And "Service discovery successful" "icon" should exist in the "Valid custom" "table_row"
     And I click on "Edit" "link" in the "Valid custom service" "table_row"
     And I set the following fields to these values:
@@ -301,3 +301,59 @@ Feature: Basic OAuth2 functionality
     And I press "Continue"
     And I should see "Identity issuer deleted"
     And I should not see "Empty custom service"
+
+  Scenario: Create a standard service for Google and test form and UI for login only, services only and both
+    Given I press "Google"
+    And I should see "Create new service: Google"
+    # Create using 'Login page only' option.
+    And I set the following fields to these values:
+      | Name                       | Testing service                           |
+      | Client ID                  | thisistheclientid                         |
+      | Client secret              | supersecret                               |
+      | This service will be used  | Login page only                           |
+    When I press "Save changes"
+    Then I should see "Changes saved"
+    And I should see "Testing service"
+    And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Do not allow services" "icon" should exist in the "Testing service" "table_row"
+    And "Service discovery successful" "icon" should exist in the "Testing service" "table_row"
+    # Change to 'Internal services only'.
+    And I click on "Edit" "link" in the "Testing service" "table_row"
+    And I set the following fields to these values:
+      | This service will be used  | Internal services only                     |
+    And I press "Save changes"
+    And I should see "Changes saved"
+    And "Do not allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
+    # Change to 'Login page and internal services' and add a display name.
+    And I click on "Edit" "link" in the "Testing service" "table_row"
+    And I set the following fields to these values:
+      | This service will be used         | Login page and internal services     |
+      | Name displayed on the login page  | Google new display name              |
+    And I press "Save changes"
+    And I should see "Changes saved"
+    And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
+    And I should see "Google new display name" in the "Testing service" "table_row"
+
+  Scenario: Create a login page only custom OIDC service
+    Given I press "Custom"
+    And I should see "Create new service: Custom"
+    And I set the following fields to these values:
+      | Name                              | Empty custom service                      |
+      | Client ID                         | thisistheclientid                         |
+      | Client secret                     | supersecret                               |
+      | This service will be used         | Login page only                           |
+      | Name displayed on the login page  | Custom display name                       |
+    When I press "Save changes"
+    And I should see "Changes saved"
+    And I should see "Empty custom service"
+    And I should see "Custom display name" in the "Empty custom service" "table_row"
+    And "Not configured" "icon" should exist in the "Empty custom service" "table_row"
+    And "Do not allow services" "icon" should exist in the "Empty custom service" "table_row"
+    And I click on "Edit" "link" in the "Empty custom service" "table_row"
+    And I set the following fields to these values:
+      | Service base URL           | https://accounts.google.com               |
+    And I press "Save changes"
+    And "Allow login" "icon" should exist in the "Empty custom service" "table_row"
+    And "Do not allow services" "icon" should exist in the "Empty custom service" "table_row"
index 4204c8b..300f5e7 100644 (file)
@@ -60,7 +60,7 @@ class process {
     protected $standardfields = [];
     /** @var array */
     protected $profilefields = [];
-    /** @var array */
+    /** @var \profile_field_base[] */
     protected $allprofilefields = [];
     /** @var string|\uu_progress_tracker|null  */
     protected $progresstrackerclass = null;
@@ -142,9 +142,8 @@ class process {
         $this->standardfields = array('id', 'username', 'email', 'emailstop',
             'city', 'country', 'lang', 'timezone', 'mailformat',
             'maildisplay', 'maildigest', 'htmleditor', 'autosubscribe',
-            'institution', 'department', 'idnumber', 'skype',
-            'msn', 'aim', 'yahoo', 'icq', 'phone1', 'phone2', 'address',
-            'url', 'description', 'descriptionformat', 'password',
+            'institution', 'department', 'idnumber', 'phone1', 'phone2', 'address',
+            'description', 'descriptionformat', 'password',
             'auth',        // Watch out when changing auth type or using external auth plugins!
             'oldusername', // Use when renaming users - this is the original username.
             'suspended',   // 1 means suspend user account, 0 means activate user account, nothing means keep as is.
@@ -162,12 +161,13 @@ class process {
      * Profile fields
      */
     protected function find_profile_fields(): void {
-        global $DB;
-        $this->allprofilefields = $DB->get_records('user_info_field');
+        global $CFG;
+        require_once($CFG->dirroot . '/user/profile/lib.php');
+        $this->allprofilefields = profile_get_user_fields_with_data(0);
         $this->profilefields = [];
         if ($proffields = $this->allprofilefields) {
             foreach ($proffields as $key => $proffield) {
-                $profilefieldname = 'profile_field_'.$proffield->shortname;
+                $profilefieldname = 'profile_field_'.$proffield->get_shortname();
                 $this->profilefields[] = $profilefieldname;
                 // Re-index $proffields with key as shortname. This will be
                 // used while checking if profile data is key and needs to be converted (eg. menu profile field).
@@ -529,8 +529,7 @@ class process {
                     }
                 }
             }
-            $proffields = $this->allprofilefields;
-            foreach ($this->profilefields as $field) {
+            foreach ($this->allprofilefields as $field => $profilefield) {
                 if (isset($user->$field)) {
                     continue;
                 }
@@ -540,9 +539,6 @@ class process {
 
                     // Form contains key and later code expects value.
                     // Convert key to value for required profile fields.
-                    require_once($CFG->dirroot.'/user/profile/field/'.$proffields[$field]->datatype.'/field.class.php');
-                    $profilefieldclass = 'profile_field_'.$proffields[$field]->datatype;
-                    $profilefield = new $profilefieldclass($proffields[$field]->id);
                     if (method_exists($profilefield, 'convert_external_data')) {
                         $user->$field = $profilefield->edit_save_data_preprocess($user->$field, null);
                     }
index 68481c7..88f020c 100644 (file)
@@ -415,17 +415,17 @@ function uu_allowed_sysroles_cache() {
  * @return stdClass pre-processed custom profile data
  */
 function uu_pre_process_custom_profile_data($data) {
-    global $CFG, $DB;
+    global $CFG;
+    require_once($CFG->dirroot . '/user/profile/lib.php');
+    $fields = profile_get_user_fields_with_data(0);
+
     // find custom profile fields and check if data needs to converted.
     foreach ($data as $key => $value) {
         if (preg_match('/^profile_field_/', $key)) {
             $shortname = str_replace('profile_field_', '', $key);
-            if ($fields = $DB->get_records('user_info_field', array('shortname' => $shortname))) {
-                foreach ($fields as $field) {
-                    require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
-                    $newfield = 'profile_field_'.$field->datatype;
-                    $formfield = new $newfield($field->id, $data->id);
-                    if (method_exists($formfield, 'convert_external_data')) {
+            if ($fields) {
+                foreach ($fields as $formfield) {
+                    if ($formfield->get_shortname() === $shortname && method_exists($formfield, 'convert_external_data')) {
                         $data->$key = $formfield->convert_external_data($value);
                     }
                 }
@@ -443,7 +443,9 @@ function uu_pre_process_custom_profile_data($data) {
  * @return bool true if no error else false
  */
 function uu_check_custom_profile_data(&$data) {
-    global $CFG, $DB;
+    global $CFG;
+    require_once($CFG->dirroot.'/user/profile/lib.php');
+
     $noerror = true;
     $testuserid = null;
 
@@ -452,15 +454,13 @@ function uu_check_custom_profile_data(&$data) {
             $testuserid = $result[1];
         }
     }
+    $profilefields = profile_get_user_fields_with_data(0);
     // Find custom profile fields and check if data needs to converted.
     foreach ($data as $key => $value) {
         if (preg_match('/^profile_field_/', $key)) {
             $shortname = str_replace('profile_field_', '', $key);
-            if ($fields = $DB->get_records('user_info_field', array('shortname' => $shortname))) {
-                foreach ($fields as $field) {
-                    require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
-                    $newfield = 'profile_field_'.$field->datatype;
-                    $formfield = new $newfield($field->id, 0);
+            foreach ($profilefields as $formfield) {
+                if ($formfield->get_shortname() === $shortname) {
                     if (method_exists($formfield, 'convert_external_data') &&
                             is_null($formfield->convert_external_data($value))) {
                         $data['status'][] = get_string('invaliduserfield', 'error', $shortname);
index 4cdfbd2..04fb69b 100644 (file)
@@ -69,7 +69,8 @@ Feature: Upload users
     # Create user profile field.
     Given I log in as "admin"
     And I navigate to "Users > Accounts > User profile fields" in site administration
-    And I set the field "datatype" to "Text area"
+    And I click on "Create a new profile field" "link"
+    And I click on "Text area" "link"
     And I set the following fields to these values:
       | Short name | superfield  |
       | Name       | Super field |
index a13c047..d2af41c 100644 (file)
@@ -126,14 +126,13 @@ class tool_uploaduser_cli_testcase extends advanced_testcase {
      * User upload with user profile fields
      */
     public function test_upload_with_profile_fields() {
-        global $DB, $CFG;
+        global $CFG;
         $this->resetAfterTest();
         set_config('passwordpolicy', 0);
         $this->setAdminUser();
 
-        $categoryid = $DB->insert_record('user_info_category', ['name' => 'Cat 1', 'sortorder' => 1]);
-        $this->field1 = $DB->insert_record('user_info_field', [
-            'shortname' => 'superfield', 'name' => 'Super field', 'categoryid' => $categoryid,
+        $this->field1 = $this->getDataGenerator()->create_custom_profile_field([
+            'shortname' => 'superfield', 'name' => 'Super field',
             'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1]);
 
         $filepath = $CFG->dirroot.'/lib/tests/fixtures/upload_users_profile.csv';
index 9e90d84..8dcf4ce 100644 (file)
@@ -300,10 +300,6 @@ class admin_uploaduser_form2 extends moodleform {
         $mform->addHelpButton('description', 'userdescription');
         $mform->setAdvanced('description');
 
-        $mform->addElement('text', 'url', get_string('webpage'), 'maxlength="255" size="50"');
-        $mform->setType('url', PARAM_URL);
-        $mform->setAdvanced('url');
-
         $mform->addElement('text', 'idnumber', get_string('idnumber'), 'maxlength="255" size="25"');
         $mform->setType('idnumber', core_user::get_property_type('idnumber'));
         $mform->setForceLtr('idnumber');
index 4e5e8d3..79f3b34 100644 (file)
@@ -48,19 +48,11 @@ if ($dataformat) {
                     'phone1'    => 'phone1',
                     'phone2'    => 'phone2',
                     'city'      => 'city',
-                    'url'       => 'url',
-                    'icq'       => 'icq',
-                    'skype'     => 'skype',
-                    'aim'       => 'aim',
-                    'yahoo'     => 'yahoo',
-                    'msn'       => 'msn',
                     'country'   => 'country');
 
-    if ($extrafields = $DB->get_records('user_info_field')) {
-        foreach ($extrafields as $n => $field) {
-            $fields['profile_field_'.$field->shortname] = 'profile_field_'.$field->shortname;
-            require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
-        }
+    $extrafields = profile_get_user_fields_with_data(0);
+    foreach ($extrafields as $formfield) {
+        $fields['profile_field_'.$formfield->get_shortname()] = 'profile_field_'.$formfield->get_shortname();
     }
 
     $filename = clean_filename(get_string('users'));
@@ -74,11 +66,7 @@ if ($dataformat) {
         if (!$user = $DB->get_record('user', array('id' => $userid))) {
             return null;
         }
-        foreach ($extrafields as $field) {
-            $newfield = 'profile_field_'.$field->datatype;
-            $formfield = new $newfield($field->id, $user->id);
-            $formfield->edit_load_user_data($user);
-        }
+        profile_load_data($user);
         $userprofiledata = array();
         foreach ($fields as $field => $unused) {
             // Custom user profile textarea fields come in an array
index 6b0929e..3b0aced 100644 (file)
@@ -178,6 +178,33 @@ class prediction {
         \core\event\prediction_action_started::create($eventdata)->trigger();
     }
 
+    /**
+     * Get the executed actions.
+     *
+     * Actions could be filtered by actionname.
+     *
+     * @param array $actionnamefilter Limit the results obtained to this list of action names.
+     * @param int $userid the user id. Current user by default.
+     * @return array of actions.
+     */
+    public function get_executed_actions(array $actionnamefilter = null, int $userid = 0): array {
+        global $USER, $DB;
+
+        $conditions[] = "predictionid = :predictionid";
+        $params['predictionid'] = $this->get_prediction_data()->id;
+        if (!$userid) {
+            $userid = $USER->id;
+        }
+        $conditions[] = "userid = :userid";
+        $params['userid'] = $userid;
+        if ($actionnamefilter) {
+            list($actionsql, $actionparams) = $DB->get_in_or_equal($actionnamefilter, SQL_PARAMS_NAMED);
+            $conditions[] = "actionname $actionsql";
+            $params = $params + $actionparams;
+        }
+        return $DB->get_records_select('analytics_prediction_actions', implode(' AND ', $conditions), $params);
+    }
+
     /**
      * format_calculations
      *
index f8f2866..8aa6c24 100644 (file)
@@ -112,6 +112,86 @@ class analytics_prediction_actions_testcase extends advanced_testcase {
         $this->assertEquals(2, $DB->count_records('analytics_prediction_actions'));
     }
 
+    /**
+     * Data provider for test_get_executed_actions.
+     *
+     * @return  array
+     */
+    public function execute_actions_provider(): array {
+        return [
+            'Empty actions with no filter' => [
+                [],
+                [],
+                0
+            ],
+            'Empty actions with filter' => [
+                [],
+                [\core_analytics\prediction::ACTION_FIXED],
+                0
+            ],
+            'Multiple actions with no filter' => [
+                [
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
+                ],
+                [],
+                3
+            ],
+            'Multiple actions applying filter' => [
+                [
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
+                ],
+                [\core_analytics\prediction::ACTION_FIXED],
+                2
+            ],
+            'Multiple actions not applying filter' => [
+                [
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
+                ],
+                [\core_analytics\prediction::ACTION_NOT_APPLICABLE],
+                0
+            ],
+            'Multiple actions with multiple filter' => [
+                [
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
+                ],
+                [\core_analytics\prediction::ACTION_FIXED, \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED],
+                3
+            ],
+        ];
+    }
+
+    /**
+     * Tests for get_executed_actions() function.
+     *
+     * @dataProvider    execute_actions_provider
+     * @param   array   $actionstoexecute    An array of actions to execute
+     * @param   array   $actionnamefilter   Actions to filter
+     * @param   int     $returned             Number of actions returned
+     *
+     * @covers \core_analytics\prediction::get_executed_actions
+     */
+    public function test_get_executed_actions(array $actionstoexecute, array $actionnamefilter, int $returned) {
+
+        $this->setUser($this->teacher2);
+        list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
+        $prediction = reset($predictions);
+        $target = $this->model->get_target();
+        foreach($actionstoexecute as $action) {
+            $prediction->action_executed($action, $target);
+        }
+
+        $filteredactions = $prediction->get_executed_actions($actionnamefilter);
+        $this->assertCount($returned, $filteredactions);
+    }
+
     /**
      * test_get_predictions
      */
index b23200c..0798670 100644 (file)
@@ -11,6 +11,8 @@ information provided here is intended especially for developers.
   by updating the lib/db/analytics.php file and bumping the core version.
 * Final deprecation - get_analysables(). Please see get_analysables_interator() instead.
   get_analysables_iterator() needs to be overridden by the child class.
+* A new function get_executed_actions() has been added to \core_analytics\prediction class
+  to get all (or filtered by action name) executed actions of a prediction
 
 === 3.8 ===
 
index 1de432c..ce16358 100644 (file)
@@ -139,8 +139,7 @@ class auth_db_testcase extends advanced_testcase {
         set_config('field_lock_email', 'unlocked', 'auth_db');
 
         // Create a user profile field and add mapping to it.
-        $DB->insert_record('user_info_field', ['shortname' => 'pet', 'name' => 'Pet', 'required' => 0,
-            'visible' => 1, 'locked' => 0, 'categoryid' => 1, 'datatype' => 'text']);
+        $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'pet', 'name' => 'Pet', 'datatype' => 'text']);
 
         set_config('field_map_profile_field_pet', 'animal', 'auth_db');
         set_config('field_updatelocal_profile_field_pet', 'oncreate', 'auth_db');
index ed1900a..c04a76a 100644 (file)
@@ -44,18 +44,17 @@ class auth_email_external_testcase extends externallib_advanced_testcase {
      * Set up for every test
      */
     public function setUp(): void {
-        global $CFG, $DB;
+        global $CFG;
 
         $this->resetAfterTest(true);
         $CFG->registerauth = 'email';
 
-        $categoryid = $DB->insert_record('user_info_category', array('name' => 'Cat 1', 'sortorder' => 1));
-        $this->field1 = $DB->insert_record('user_info_field', array(
-                'shortname' => 'frogname', 'name' => 'Name of frog', 'categoryid' => $categoryid,
-                'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1));
-        $this->field2 = $DB->insert_record('user_info_field', array(
-                'shortname' => 'sometext', 'name' => 'Some text in textarea', 'categoryid' => $categoryid,
-                'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2));
+        $this->field1 = $this->getDataGenerator()->create_custom_profile_field(array(
+                'shortname' => 'frogname', 'name' => 'Name of frog',
+                'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1))->id;
+        $this->field2 = $this->getDataGenerator()->create_custom_profile_field(array(
+                'shortname' => 'sometext', 'name' => 'Some text in textarea',
+                'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2))->id;
     }
 
     public function test_get_signup_settings() {
@@ -109,8 +108,8 @@ class auth_email_external_testcase extends externallib_advanced_testcase {
         // Create category with MathJax and a new field with MathJax.
         $categoryname = 'Cat $$(a+b)=2$$';
         $fieldname = 'Some text $$(a+b)=2$$';
-        $categoryid = $DB->insert_record('user_info_category', array('name' => $categoryname, 'sortorder' => 1));
-        $field3 = $DB->insert_record('user_info_field', array(
+        $categoryid = $this->getDataGenerator()->create_custom_profile_field_category(['name' => $categoryname])->id;
+        $this->getDataGenerator()->create_custom_profile_field(array(
                 'shortname' => 'mathjaxname', 'name' => $fieldname, 'categoryid' => $categoryid,
                 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2));
 
index c774de1..c1a95c6 100644 (file)
@@ -81,7 +81,6 @@ class provider implements
 
         $externalfields = [
                 'address' => 'privacy:metadata:mnet_external:address',
-                'aim' => 'privacy:metadata:mnet_external:aim',
                 'alternatename' => 'privacy:metadata:mnet_external:alternatename',
                 'autosubscribe' => 'privacy:metadata:mnet_external:autosubscribe',
                 'calendartype' => 'privacy:metadata:mnet_external:calendartype',
@@ -95,7 +94,6 @@ class provider implements
                 'firstaccess' => 'privacy:metadata:mnet_external:firstaccess',
                 'firstname' => 'privacy:metadata:mnet_external:firstname',
                 'firstnamephonetic' => 'privacy:metadata:mnet_external:firstnamephonetic',
-                'icq' => 'privacy:metadata:mnet_external:icq',
                 'id' => 'privacy:metadata:mnet_external:id',
                 'idnumber' => 'privacy:metadata:mnet_external:idnumber',
                 'imagealt' => 'privacy:metadata:mnet_external:imagealt',
@@ -108,19 +106,15 @@ class provider implements
                 'maildigest' => 'privacy:metadata:mnet_external:maildigest',
                 'maildisplay' => 'privacy:metadata:mnet_external:maildisplay',
                 'middlename' => 'privacy:metadata:mnet_external:middlename',
-                'msn' => 'privacy:metadata:mnet_external:msn',
                 'phone1' => 'privacy:metadata:mnet_external:phone1',
                 'pnone2' => 'privacy:metadata:mnet_external:phone2',
                 'picture' => 'privacy:metadata:mnet_external:picture',
                 'policyagreed' => 'privacy:metadata:mnet_external:policyagreed',
-                'skype' => 'privacy:metadata:mnet_external:skype',
                 'suspended' => 'privacy:metadata:mnet_external:suspended',
                 'timezone' => 'privacy:metadata:mnet_external:timezone',
                 'trackforums' => 'privacy:metadata:mnet_external:trackforums',
                 'trustbitmask' => 'privacy:metadata:mnet_external:trustbitmask',
-                'url' => 'privacy:metadata:mnet_external:url',
                 'username' => 'privacy:metadata:mnet_external:username',
-                'yahoo' => 'privacy:metadata:mnet_external:yahoo',
         ];
 
         $collection->add_external_location_link('moodle', $externalfields, 'privacy:metadata:external:moodle');
index 4836214..d07f3a6 100644 (file)
@@ -39,7 +39,6 @@ $string['pluginname'] = 'MNet authentication';
 $string['privacy:metadata:external:mahara'] = 'This plugin can send data externally to a linked Mahara application.';
 $string['privacy:metadata:external:moodle'] = 'This plugin can send data externally to a linked Moodle application.';
 $string['privacy:metadata:mnet_external:address'] = 'The address of the user.';
-$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user';
 $string['privacy:metadata:mnet_external:alternatename'] = 'An alternative name for the user.';
 $string['privacy:metadata:mnet_external:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.';
 $string['privacy:metadata:mnet_external:calendartype'] = 'A user preference for the type of calendar to use.';
@@ -53,7 +52,6 @@ $string['privacy:metadata:mnet_external:emailstop'] = 'A preference to stop emai
 $string['privacy:metadata:mnet_external:firstaccess'] = 'The time that this user first accessed the site.';
 $string['privacy:metadata:mnet_external:firstname'] = 'The first name of the user.';
 $string['privacy:metadata:mnet_external:firstnamephonetic'] = 'The phonetic details about the user\'s first name.';
-$string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.';
 $string['privacy:metadata:mnet_external:id'] = 'The user ID';
 $string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution';
 $string['privacy:metadata:mnet_external:imagealt'] = 'Alternative text for the user\'s image.';
@@ -66,19 +64,15 @@ $string['privacy:metadata:mnet_external:lastnamephonetic'] = 'The phonetic detai
 $string['privacy:metadata:mnet_external:maildigest'] = 'A setting for the mail digest for this user.';
 $string['privacy:metadata:mnet_external:maildisplay'] = 'A preference for the user about displaying their email address to other users.';
 $string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user';
-$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user';
 $string['privacy:metadata:mnet_external:phone1'] = 'A phone number for the user.';
 $string['privacy:metadata:mnet_external:phone2'] = 'An additional phone number for the user.';
 $string['privacy:metadata:mnet_external:picture'] = 'The picture details associated with this user.';
 $string['privacy:metadata:mnet_external:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.';
-$string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user';
 $string['privacy:metadata:mnet_external:suspended'] = 'A flag to show if the user has been suspended on this system.';
 $string['privacy:metadata:mnet_external:timezone'] = 'The timezone of the user';
 $string['privacy:metadata:mnet_external:trackforums'] = 'A preference for forums and tracking them.';
 $string['privacy:metadata:mnet_external:trustbitmask'] = 'The trust bit mask';
-$string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.';
 $string['privacy:metadata:mnet_external:username'] = 'The username for this user.';
-$string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user';
 $string['privacy:metadata:mnet_log'] = 'Details of remote actions carried out by a local user logged in a remote system.';
 $string['privacy:metadata:mnet_log:action'] = 'Action carried out by the user.';
 $string['privacy:metadata:mnet_log:cmid'] = 'ID of the course module.';
@@ -99,4 +93,12 @@ $string['privacy:metadata:mnet_session:token'] = 'Unique session identifier';
 $string['privacy:metadata:mnet_session:useragent'] = 'User agent used to access the remote system';
 $string['privacy:metadata:mnet_session:userid'] = 'ID of the user jumping to remote system.';
 $string['privacy:metadata:mnet_session:username'] = 'Username of the user jumping to remote system.';
-$string['unknownhost'] = 'Unknown host';
\ No newline at end of file
+$string['unknownhost'] = 'Unknown host';
+
+// Deprecated since Moodle 4.0.
+$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user';
+$string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.';
+$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user';
+$string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user';
+$string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.';
+$string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user';
index e5b0789..5c3545e 100644 (file)
@@ -258,7 +258,6 @@ class api {
         $user->mnethostid = $CFG->mnet_localhost_id;
         $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : '';
         $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : '';
-        $user->url = isset($userinfo['url']) ? $userinfo['url'] : '';
         $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : '';
         $user->secret = random_string(15);
 
@@ -307,7 +306,6 @@ class api {
         $user->mnethostid = $CFG->mnet_localhost_id;
         $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : '';
         $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : '';
-        $user->url = isset($userinfo['url']) ? $userinfo['url'] : '';
         $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : '';
         $user->secret = random_string(15);
 
index 4152729..251adbf 100644 (file)
@@ -166,17 +166,6 @@ class auth extends \auth_plugin_base {
         return false;
     }
 
-    /**
-     * Do some checks on the identity provider before showing it on the login page.
-     * @param core\oauth2\issuer $issuer
-     * @return boolean
-     */
-    private function is_ready_for_login_page(\core\oauth2\issuer $issuer) {
-        return $issuer->get('enabled') &&
-                $issuer->is_configured() &&
-                !empty($issuer->get('showonloginpage'));
-    }
-
     /**
      * Return a list of identity providers to display on the login page.
      *
@@ -184,17 +173,17 @@ class auth extends \auth_plugin_base {
      * @return array List of arrays with keys url, iconurl and name.
      */
     public function loginpage_idp_list($wantsurl) {
-        $providers = \core\oauth2\api::get_all_issuers();
+        $providers = \core\oauth2\api::get_all_issuers(true);
         $result = [];
         if (empty($wantsurl)) {
             $wantsurl = '/';
         }
         foreach ($providers as $idp) {
-            if ($this->is_ready_for_login_page($idp)) {
+            if ($idp->is_available_for_login()) {
                 $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()];
                 $url = new moodle_url('/auth/oauth2/login.php', $params);
                 $icon = $idp->get('image');
-                $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get('name')];
+                $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get_display_name()];
             }
         }
         return $result;
index 1243560..07a0428 100644 (file)
@@ -28,7 +28,7 @@ use html_table;
 use html_table_cell;
 use html_table_row;
 use html_writer;
-use auth\oauth2\linked_login;
+use auth_oauth2\linked_login;
 use moodle_url;
 
 defined('MOODLE_INTERNAL') || die();
@@ -43,7 +43,7 @@ class renderer extends plugin_renderer_base {
     /**
      * This function will render one beautiful table with all the linked_logins.
      *
-     * @param \auth\oauth2\linked_login[] $linkedlogins - list of all linked logins.
+     * @param linked_login[] $linkedlogins - list of all linked logins.
      * @return string HTML to output.
      */
     public function linked_logins_table($linkedlogins) {
index 146e261..30df5d4 100644 (file)
@@ -45,7 +45,7 @@ if ($action == 'new') {
     $issuerid = required_param('issuerid', PARAM_INT);
     $issuer = \core\oauth2\api::get_issuer($issuerid);
 
-    if (!$issuer->is_authentication_supported() || !$issuer->get('showonloginpage') || !$issuer->get('enabled')) {
+    if (!$issuer->is_available_for_login()) {
         throw new \moodle_exception('issuernologin', 'auth_oauth2');
     }
 
@@ -89,19 +89,20 @@ $linkedlogin = null;
 
 auth_oauth2\api::clean_orphaned_linked_logins();
 
-$issuers = \core\oauth2\api::get_all_issuers();
+$issuers = \core\oauth2\api::get_all_issuers(true);
 
 $anyshowinloginpage = false;
 $issuerbuttons = array();
 foreach ($issuers as $issuer) {
-    if (!$issuer->is_authentication_supported() || !$issuer->get('showonloginpage') || !$issuer->get('enabled')) {
+    if (!$issuer->is_available_for_login()) {
         continue;
     }
     $anyshowinloginpage = true;
 
     $addparams = ['action' => 'new', 'issuerid' => $issuer->get('id'), 'sesskey' => sesskey(), 'logout' => true];
     $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams);
-    $issuerbuttons[$issuer->get('id')] = $renderer->single_button($addurl, get_string('createnewlinkedlogin', 'auth_oauth2', s($issuer->get('name'))));
+    $issuerbuttons[$issuer->get('id')] = $renderer->single_button($addurl, get_string('createnewlinkedlogin', 'auth_oauth2',
+        s($issuer->get_display_name())));
 }
 
 if (!$anyshowinloginpage) {
index 9abb2a3..949b5a1 100644 (file)
@@ -37,6 +37,9 @@ if (!\auth_oauth2\api::is_enabled()) {
 }
 
 $issuer = new \core\oauth2\issuer($issuerid);
+if (!$issuer->is_available_for_login()) {
+    throw new \moodle_exception('issuernologin', 'auth_oauth2');
+}
 
 $returnparams = ['wantsurl' => $wantsurl, 'sesskey' => sesskey(), 'id' => $issuerid];
 $returnurl = new moodle_url('/auth/oauth2/login.php', $returnparams);
index 37fda53..d6a311d 100644 (file)
@@ -49,12 +49,6 @@ class frontend extends \core_availability\frontend {
             'email' => \core_user\fields::get_display_name('email'),
             'city' => \core_user\fields::get_display_name('city'),
             'country' => \core_user\fields::get_display_name('country'),
-            'url' => \core_user\fields::get_display_name('url'),
-            'icq' => \core_user\fields::get_display_name('icq'),
-            'skype' => \core_user\fields::get_display_name('skype'),
-            'aim' => \core_user\fields::get_display_name('aim'),
-            'yahoo' => \core_user\fields::get_display_name('yahoo'),
-            'msn' => \core_user\fields::get_display_name('msn'),
             'idnumber' => \core_user\fields::get_display_name('idnumber'),
             'institution' => \core_user\fields::get_display_name('institution'),
             'department' => \core_user\fields::get_display_name('department'),
index 816e1fe..690bcd8 100644 (file)
@@ -65,7 +65,8 @@ Feature: availability_profile
     # Add custom field.
     Given I log in as "admin"
     And I navigate to "Users > Accounts > User profile fields" in site administration
-    And I set the field "datatype" to "Text input"
+    And I click on "Create a new profile field" "link"
+    And I click on "Text input" "link"
     And I set the following fields to these values:
       | Short name | superfield  |
       | Name       | Super field |
index 7e3074c..12d4477 100644 (file)
@@ -50,14 +50,10 @@ class availability_profile_condition_testcase extends advanced_testcase {
 
         $this->resetAfterTest();
 
-        // Add a custom profile field type. The API for doing this is indescribably
-        // horrid and tightly intertwined with the form UI, so it's best to add
-        // it directly in database.
-        $DB->insert_record('user_info_field', array(
-                'shortname' => 'frogtype', 'name' => 'Type of frog', 'categoryid' => 1,
+        // Add a custom profile field type.
+        $this->profilefield = $this->getDataGenerator()->create_custom_profile_field(array(
+                'shortname' => 'frogtype', 'name' => 'Type of frog',
                 'datatype' => 'text'));
-        $this->profilefield = $DB->get_record('user_info_field',
-                array('shortname' => 'frogtype'));
 
         // Clear static cache.
         \availability_profile\condition::wipe_static_cache();
@@ -333,11 +329,9 @@ class availability_profile_condition_testcase extends advanced_testcase {
         $info = new \core_availability\mock_info();
 
         // Add custom textarea type.
-        $DB->insert_record('user_info_field', array(
-                'shortname' => 'longtext', 'name' => 'Long text', 'categoryid' => 1,
+        $customfield = $this->getDataGenerator()->create_custom_profile_field(array(
+                'shortname' => 'longtext', 'name' => 'Long text',
                 'datatype' => 'textarea'));
-        $customfield = $DB->get_record('user_info_field',
-                array('shortname' => 'longtext'));
 
         // The list of fields should include the text field added in setUp(),
         // but should not include the textarea field added just now.
@@ -465,11 +459,9 @@ class availability_profile_condition_testcase extends advanced_testcase {
         condition::wipe_static_cache();
 
         // For testing, make another info field with default value.
-        $DB->insert_record('user_info_field', array(
-                'shortname' => 'tonguestyle', 'name' => 'Tongue style', 'categoryid' => 1,
+        $otherprofilefield = $this->getDataGenerator()->create_custom_profile_field(array(
+                'shortname' => 'tonguestyle', 'name' => 'Tongue style',
                 'datatype' => 'text', 'defaultdata' => 'Slimy'));
-        $otherprofilefield = $DB->get_record('user_info_field',
-                array('shortname' => 'tonguestyle'));
 
         // Make a test course and some users.
         $generator = $this->getDataGenerator();
index d2ac8c5..2804661 100644 (file)
@@ -125,6 +125,8 @@ class backup_course_task extends backup_task {
             $this->add_step(new backup_course_logs_structure_step('course_logs', 'logs.xml'));
             // New log stores.
             $this->add_step(new backup_course_logstores_structure_step('course_logstores', 'logstores.xml'));
+            // Last access to course logs.
+            $this->add_step(new backup_course_loglastaccess_structure_step('course_loglastaccess', 'loglastaccess.xml'));
         }
 
         // Generate the course competencies.
index 478ae2a..673b58b 100644 (file)
@@ -99,6 +99,13 @@ class backup_root_task extends backup_task {
         $this->add_setting($roleassignments);
         $users->add_dependency($roleassignments);
 
+        // Define permission.
+        if ($this->plan->get_mode() == backup::MODE_IMPORT) {
+            $permissions = new backup_permissions_setting('permissions', base_setting::IS_BOOLEAN, false);
+            $permissions->set_ui(new backup_setting_ui_checkbox($permissions, get_string('rootsettingpermissions', 'backup')));
+            $this->add_setting($permissions);
+        }
+
         // Define activities
         $activities = new backup_activities_setting('activities', base_setting::IS_BOOLEAN, true);
         $activities->set_ui(new backup_setting_ui_checkbox($activities, get_string('rootsettingactivities', 'backup')));
index 59ea252..ca9224f 100644 (file)
@@ -65,6 +65,12 @@ class backup_filename_setting extends backup_generic_setting {
  */
 class backup_users_setting extends backup_generic_setting {}
 
+/**
+ * root setting to control if backup will include permission information by roles
+ */
+class backup_permissions_setting extends backup_generic_setting {
+}
+
 /**
  * root setting to control if backup will include group information depends on @backup_users_setting
  *
index 4339b0c..9f87ba4 100644 (file)
@@ -1360,11 +1360,10 @@ class backup_users_structure_step extends backup_structure_step {
 
         // Then, the fields potentially needing anonymization
         $anonfields = array(
-            'username', 'idnumber', 'email', 'icq', 'skype',
-            'yahoo', 'aim', 'msn', 'phone1',
+            'username', 'idnumber', 'email', 'phone1',
             'phone2', 'institution', 'department', 'address',
             'city', 'country', 'lastip', 'picture',
-            'url', 'description', 'descriptionformat', 'imagealt', 'auth');
+            'description', 'descriptionformat', 'imagealt', 'auth');
         $anonfields = array_merge($anonfields, \core_user\fields::get_name_fields());
 
         // Add anonymized fields to $userfields with custom final element
@@ -1639,6 +1638,55 @@ class backup_course_logstores_structure_step extends backup_structure_step {
     }
 }
 
+/**
+ * Structure step in charge of constructing the loglastaccess.xml file for the course logs.
+ *
+ * This backup step will backup the logs of the user_lastaccess table.
+ */
+class backup_course_loglastaccess_structure_step extends backup_structure_step {
+
+    /**
+     *  This function creates the structures for the loglastaccess.xml file.
+     *  Expected structure would look like this.
+     *  <loglastaccesses>
+     *      <loglastaccess id=2>
+     *          <userid>5</userid>
+     *          <timeaccess>1616887341</timeaccess>
+     *      </loglastaccess>
+     *  </loglastaccesses>
+     *
+     * @return backup_nested_element
+     */
+    protected function define_structure() {
+
+        // To know if we are including userinfo.
+        $userinfo = $this->get_setting_value('users');
+
+        // Define the structure of logstores container.
+        $lastaccesses = new backup_nested_element('lastaccesses');
+        $lastaccess = new backup_nested_element('lastaccess', array('id'), array('userid', 'timeaccess'));
+
+        // Define build tree.
+        $lastaccesses->add_child($lastaccess);
+
+        // This element should only happen if we are including user info.
+        if ($userinfo) {
+            // Define sources.
+            $lastaccess->set_source_sql('
+                SELECT id, userid, timeaccess
+                  FROM {user_lastaccess}
+                 WHERE courseid = ?',
+                array(backup::VAR_COURSEID));
+
+            // Define userid annotation to user.
+            $lastaccess->annotate_ids('user', 'userid');
+        }
+
+        // Return the root element (lastaccessess).
+        return $lastaccesses;
+    }
+}
+
 /**
  * Structure step in charge of constructing the logstores.xml file for the activity logs.
  *
index b0b6b76..83da7e1 100644 (file)
@@ -93,6 +93,8 @@ class restore_final_task extends restore_task {
             $this->add_step(new restore_course_logs_structure_step('course_logs', 'course/logs.xml'));
             // New log stores.
             $this->add_step(new restore_course_logstores_structure_step('course_logstores', 'course/logstores.xml'));
+            // Last access to course logs.
+            $this->add_step(new restore_course_loglastaccess_structure_step('course_loglastaccess', 'course/loglastaccess.xml'));
         }
 
         // Review all the executed tasks having one after_restore method
index 3b37acc..40345c1 100644 (file)
@@ -146,6 +146,19 @@ class restore_root_task extends restore_task {
         $this->add_setting($roleassignments);
         $users->add_dependency($roleassignments);
 
+        // Define permissions.
+        $defaultvalue = false;                      // Safer default.
+        $changeable = false;
+        // Enable when available, or key doesn't exist (backward compatibility).
+        if (!array_key_exists('permissions', $rootsettings) || !empty($rootsettings['permissions'])) {
+            $defaultvalue = true;
+            $changeable = true;
+        }
+        $permissions = new restore_permissions_setting('permissions', base_setting::IS_BOOLEAN, $defaultvalue);
+        $permissions->set_ui(new backup_setting_ui_checkbox($permissions, get_string('rootsettingpermissions', 'backup')));
+        $permissions->get_ui()->set_changeable($changeable);
+        $this->add_setting($permissions);
+
         // Define activitites
         $defaultvalue = false;                      // Safer default
         $changeable = false;
index 61124de..fb9e064 100644 (file)
@@ -43,6 +43,12 @@ class restore_generic_setting extends root_backup_setting {}
  */
 class restore_users_setting extends restore_generic_setting {}
 
+/**
+ * root setting to control if restore will create override permission information by roles
+ */
+class restore_permissions_setting extends restore_generic_setting {
+}
+
 /**
  * root setting to control if restore will create groups/grouping information. Depends on @restore_users_setting
  *
index 1397c74..abbc179 100644 (file)
@@ -1691,7 +1691,9 @@ class restore_section_structure_step extends restore_structure_step {
      * @param stdClass $data Record data
      */
     public function process_availability_field($data) {
-        global $DB;
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/user/profile/lib.php');
+
         $data = (object)$data;
         // Mark it is as passed by default
         $passed = true;
@@ -1704,9 +1706,8 @@ class restore_section_structure_step extends restore_structure_step {
             // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
             $passed = false;
         } else if (!is_null($data->customfield)) {
-            $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
-            $customfieldid = $DB->get_field('user_info_field', 'id', $params);
-            $passed = ($customfieldid !== false);
+            $field = profile_get_custom_field_data_by_shortname($data->customfield);
+            $passed = $field && $field->datatype == $data->customfieldtype;
         }
 
         if ($passed) {
@@ -2058,7 +2059,9 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
         if ($this->get_setting_value('role_assignments')) {
             $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
         }
-        $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
+        if ($this->get_setting_value('permissions')) {
+            $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
+        }
 
         return $paths;
     }
@@ -3428,6 +3431,81 @@ class restore_course_logstores_structure_step extends restore_structure_step {
     }
 }
 
+/**
+ * Structure step in charge of restoring the loglastaccess.xml file for the course logs.
+ *
+ * This restore step will rebuild the table for user_lastaccess table.
+ */
+class restore_course_loglastaccess_structure_step extends restore_structure_step {
+
+    /**
+     * Conditionally decide if this step should be executed.
+     *
+     * This function checks the following parameter:
+     *
+     *   1. the loglastaccess.xml file exists
+     *
+     * @return bool true is safe to execute, false otherwise
+     */
+    protected function execute_condition() {
+        // Check it is included in the backup.
+        $fullpath = $this->task->get_taskbasepath();
+        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
+        if (!file_exists($fullpath)) {
+            // Not found, can't restore loglastaccess.xml information.
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return the elements to be processed on restore of loglastaccess.
+     *
+     * @return restore_path_element[] array of elements to be processed on restore.
+     */
+    protected function define_structure() {
+
+        $paths = array();
+        // To know if we are including userinfo.
+        $userinfo = $this->get_setting_value('users');
+
+        if ($userinfo) {
+            $paths[] = new restore_path_element('lastaccess', '/lastaccesses/lastaccess');
+        }
+        // Return the paths wrapped.
+        return $paths;
+    }
+
+    /**
+     * Process the 'lastaccess' elements.
+     *
+     * @param array $data element data
+     */
+    protected function process_lastaccess($data) {
+        global $DB;
+
+        $data = (object)$data;
+
+        $data->courseid = $this->get_courseid();
+        if (!$data->userid = $this->get_mappingid('user', $data->userid)) {
+            return; // Nothing to do, not able to find the user to set the lastaccess time.
+        }
+
+        // Check if record does exist.
+        $exists = $DB->get_record('user_lastaccess', array('courseid' => $data->courseid, 'userid' => $data->userid));
+        if ($exists) {
+            // If the time of last access of the restore is newer, then replace and update.
+            if ($exists->timeaccess < $data->timeaccess) {
+                $exists->timeaccess = $data->timeaccess;
+                $DB->update_record('user_lastaccess', $exists);
+            }
+        } else {
+            $DB->insert_record('user_lastaccess', $data);
+        }
+    }
+}
+
 /**
  * Structure step in charge of restoring the logstores.xml file for the activity logs.
  *
@@ -4457,7 +4535,9 @@ class restore_module_structure_step extends restore_structure_step {
      * @param stdClass $data Record data
      */
     protected function process_availability_field($data) {
-        global $DB;
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/user/profile/lib.php');
+
         $data = (object)$data;
         // Mark it is as passed by default
         $passed = true;
@@ -4470,9 +4550,8 @@ class restore_module_structure_step extends restore_structure_step {
             // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
             $passed = false;
         } else if (!empty($data->customfield)) {
-            $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
-            $customfieldid = $DB->get_field('user_info_field', 'id', $params);
-            $passed = ($customfieldid !== false);
+            $field = profile_get_custom_field_data_by_shortname($data->customfield);
+            $passed = $field && $field->datatype == $data->customfieldtype;
         }
 
         if ($passed) {
index c76d952..c2fb61e 100644 (file)
@@ -278,6 +278,73 @@ class core_backup_automated_backup_testcase extends advanced_testcase {
         $this->assertTrue($skipped);
         $this->expectOutputRegex("/Skipping $course->fullname \(Not modified since previous backup\)/");
     }
+
+    /**
+     * Test the task completes when coureid is missing.
+     */
+    public function test_task_complete_when_courseid_is_missing() {
+        global $DB;
+        $admin = get_admin();
+        $classobject = $this->backupcronautomatedhelper->return_this();
+
+        // Create this backup course.
+        $backupcourse = new stdClass;
+        $backupcourse->courseid = $this->course->id;
+        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
+        $DB->insert_record('backup_courses', $backupcourse);
+        $backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]);
+
+        // Create a backup task.
+        $method = new ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task');
+        $method->setAccessible(true); // Allow accessing of private method.
+        $method->invokeArgs($classobject, [$backupcourse, $admin]);
+
+        // Delete course for this test.
+        delete_course($this->course->id, false);
+
+        $task = core\task\manager::get_next_adhoc_task(time());
+
+        ob_start();
+        $task->execute();
+        $output = ob_get_clean();
+
+        $this->assertStringContainsString('Invalid course id: ' . $this->course->id . ', task aborted.', $output);
+        core\task\manager::adhoc_task_complete($task);
+    }
+
+    /**
+     * Test the task completes when backup course is missing.
+     */
+    public function test_task_complete_when_backup_course_is_missing() {
+        global $DB;
+        $admin = get_admin();
+        $classobject = $this->backupcronautomatedhelper->return_this();
+
+        // Create this backup course.
+        $backupcourse = new stdClass;
+        $backupcourse->courseid = $this->course->id;
+        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN;
+        $DB->insert_record('backup_courses', $backupcourse);
+        $backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]);
+
+        // Create a backup task.
+        $method = new ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task');
+        $method->setAccessible(true); // Allow accessing of private method.
+        $method->invokeArgs($classobject, [$backupcourse, $admin]);
+
+        // Delete backup course for this test.
+        $DB->delete_records('backup_courses', ['courseid' => $this->course->id]);
+
+        $task = core\task\manager::get_next_adhoc_task(time());
+
+        ob_start();
+        $task->execute();
+        $output = ob_get_clean();
+
+        $this->assertStringContainsString('Automated backup for course: ' . $this->course->fullname . ' encounters an error.',
+            $output);
+        core\task\manager::adhoc_task_complete($task);
+    }
 }
 
 /**
index e4b71c8..98fdbb3 100644 (file)
@@ -60,6 +60,7 @@ class core_backup_cleanup_task_testcase extends advanced_testcase {
             $user->id
         );
         $controller->execute_plan();
+        $controller->destroy(); // Unset all structures, close files...
         return $controller->get_backupid();
     }
 
diff --git a/backup/tests/backup_restore_base_testcase.php b/backup/tests/backup_restore_base_testcase.php
new file mode 100644 (file)
index 0000000..8b12296
--- /dev/null
@@ -0,0 +1,123 @@
+<?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/>.
+
+/**
+ * Backup restore base tests.
+ *
+ * @package   core_backup
+ * @copyright Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+/**
+ * Basic testcase class for backup / restore functionality.
+ */
+abstract class core_backup_backup_restore_base_testcase extends advanced_testcase {
+
+    /**
+     * Setup test data.
+     */
+    protected function setUp(): void {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+    }
+
+    /**
+     * Backup the course by general mode.
+     *
+     * @param  stdClass $course Course for backup.
+     * @return string Hash string ID from the backup.
+     * @throws coding_exception
+     * @throws moodle_exception
+     */
+    protected function perform_backup($course): string {
+        global $CFG, $USER;
+
+        $coursecontext = context_course::instance($course->id);
+
+        // Start backup process.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id);
+        $bc->execute_plan();
+        $backupid = $bc->get_backupid();
+        $bc->destroy();
+
+        // Get the backup file.
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($coursecontext->id, 'backup', 'course', false, 'id ASC');
+        $backupfile = reset($files);
+
+        // Extract backup file.
+        $path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupid;
+
+        $fp = get_file_packer('application/vnd.moodle.backup');
+        $fp->extract_to_pathname($backupfile, $path);
+
+        return $backupid;
+    }
+
+    /**
+     * Restore from backupid to course.
+     *
+     * @param  string   $backupid Hash string ID from backup.
+     * @param  stdClass $course Course which is restored for.
+     * @throws restore_controller_exception
+     */
+    protected function perform_restore($backupid, $course): void {
+        global $USER;
+
+        // Set up restore.
+        $rc = new restore_controller($backupid, $course->id,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_EXISTING_ADDING);
+        // Execute restore.
+        $rc->execute_precheck();
+        $rc->execute_plan();
+        $rc->destroy();
+    }
+
+    /**
+     * Import course from course1 to course2.
+     *
+     * @param stdClass $course1 Course to be backuped up.
+     * @param stdClass $course2 Course to be restored.
+     * @throws restore_controller_exception
+     */
+    protected function perform_import($course1, $course2): void {
+        global $USER;
+
+        // Start backup process.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
+                backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
+        $backupid = $bc->get_backupid();
+        $bc->execute_plan();
+        $bc->destroy();
+
+        // Set up restore.
+        $rc = new restore_controller($backupid, $course2->id,
+                backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id, backup::TARGET_EXISTING_ADDING);
+        // Execute restore.
+        $rc->execute_precheck();
+        $rc->execute_plan();
+        $rc->destroy();
+    }
+
+}
diff --git a/backup/tests/backup_restore_permission_test.php b/backup/tests/backup_restore_permission_test.php
new file mode 100644 (file)
index 0000000..42929a4
--- /dev/null
@@ -0,0 +1,156 @@
+<?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/>.
+
+/**
+ * Backup restore permission tests.
+ *
+ * @package   core_backup
+ * @copyright Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once('backup_restore_base_testcase.php');
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+/**
+ * Testcase class for permission backup / restore functionality.
+ */
+class core_backup_backup_restore_permission_testcase extends core_backup_backup_restore_base_testcase {
+
+    /** @var stdClass A test course which is restored/imported from. */
+    protected $course1;
+
+    /** @var stdClass A test course which is restored/imported to. */
+    protected $course2;
+
+    /** @var stdClass A user for using in this test. */
+    protected $user;
+
+    /** @var string Capability name for using in this test. */
+    protected $capabilityname;
+
+    /** @var context_course Context instance for course1. */
+    protected $course1context;
+
+    /** @var context_course Context instance for course2. */
+    protected $course2context;
+
+    /**
+     * Setup test data.
+     */
+    protected function setUp(): void {
+        global $DB;
+
+        parent::setUp();
+        // Create a course with some availability data set.
+        $generator = $this->getDataGenerator();
+        $this->course1 = $generator->create_course();
+        $this->course1context = context_course::instance($this->course1->id);
+        $this->course2 = $generator->create_course();
+        $this->course2context = context_course::instance($this->course2->id);
+        $this->capabilityname = 'enrol/manual:enrol';
+        $this->user = $generator->create_user();
+
+        // Set additional permission for course 1.
+        $teacherrole = $DB->get_record('role', ['shortname' => 'teacher'], '*', MUST_EXIST);
+        role_change_permission($teacherrole->id, $this->course1context, $this->capabilityname, CAP_ALLOW);
+
+        // Enrol to the courses.
+        $generator->enrol_user($this->user->id, $this->course1->id, $teacherrole->id);
+        $generator->enrol_user($this->user->id, $this->course2->id, $teacherrole->id);
+    }
+
+    /**
+     * Test having settings.
+     */
+    public function test_having_settings(): void {
+        $this->assertEquals(0, get_config('backup', 'backup_import_permissions'));
+        $this->assertEquals(1, get_config('restore', 'restore_general_permissions'));
+    }
+
+    /**
+     * Test for restore with permission.
+     */
+    public function test_backup_restore_with_permission(): void {
+
+        // Set default setting to restore with permission.
+        set_config('restore_general_permissions', 1, 'restore');
+
+        // Confirm course1 has the capability for the user.
+        $this->assertTrue(has_capability($this->capabilityname, $this->course1context, $this->user));
+
+        // Confirm course2 does not have the capability for the user.
+        $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user));
+
+        // Perform backup and restore.
+        $backupid = $this->perform_backup($this->course1);
+        $this->perform_restore($backupid, $this->course2);
+
+        // Confirm course2 has the capability for the user.
+        $this->assertTrue(has_capability($this->capabilityname, $this->course2context, $this->user));
+    }
+
+    /**
+     * Test for backup / restore without restore permission.
+     */
+    public function test_backup_restore_without_permission(): void {
+
+        // Set default setting to restore without permission.
+        set_config('restore_general_permissions', 0, 'restore');
+
+        // Perform backup and restore.
+        $backupid = $this->perform_backup($this->course1);
+        $this->perform_restore($backupid, $this->course2);
+
+        // Confirm course2 does not have the capability for the user.
+        $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user));
+    }
+
+    /**
+     * Test for import with permission.
+     */
+    public function test_backup_import_with_permission(): void {
+
+        // Set default setting to restore with permission.
+        set_config('backup_import_permissions', 1, 'backup');
+
+        // Perform import.
+        $this->perform_import($this->course1, $this->course2);
+
+        // Confirm course2 does not have the capability for the user.
+        $this->assertTrue(has_capability($this->capabilityname, $this->course2context, $this->user));
+    }
+
+    /**
+     * Test for import without permission.
+     */
+    public function test_backup_import_without_permission(): void {
+
+        // Set default setting to restore without permission.
+        set_config('backup_import_permissions', 0, 'backup');
+
+        // Perform import.
+        $this->perform_import($this->course1, $this->course2);
+
+        // Confirm course2 does not have the capability for the user.
+        $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user));
+    }
+
+}
index a07e0b4..825f55b 100644 (file)
@@ -1,7 +1,13 @@
 This files describes API changes in /backup/*,
 information provided here is intended especially for developers.
 
+=== 3.11 ===
+
+ * New setting called "Include override permissions" has been implemented. The default
+   settings is OFF for import, and ON for restore.
+
 === 3.10 ===
+
  * Local plugins can now hook into a backup and restore process of grade items by
    using define_grade_item_plugin_structure method (See MDL-69418).
 
index e5c0163..7b5cbd4 100644 (file)
@@ -577,6 +577,7 @@ abstract class backup_controller_dbops extends backup_dbops {
                         'backup_import_blocks'             => 'blocks',
                         'backup_import_filters'            => 'filters',
                         'backup_import_calendarevents'     => 'calendarevents',
+                        'backup_import_permissions'        => 'permissions',
                         'backup_import_questionbank'       => 'questionbank',
                         'backup_import_groups'             => 'groups',
                         'backup_import_competencies'       => 'competencies',
index 216da03..4cc7ee2 100644 (file)
@@ -146,6 +146,7 @@ abstract class restore_controller_dbops extends restore_dbops {
             'restore_general_users'              => 'users',
             'restore_general_enrolments'         => 'enrolments',
             'restore_general_role_assignments'   => 'role_assignments',
+            'restore_general_permissions'        => 'permissions',
             'restore_general_activities'         => 'activities',
             'restore_general_blocks'             => 'blocks',
             'restore_general_filters'            => 'filters',
index 3da5cfe..91be3c1 100644 (file)
@@ -1151,6 +1151,7 @@ abstract class restore_dbops {
     public static function create_included_users($basepath, $restoreid, $userid,
             \core\progress\base $progress) {
         global $CFG, $DB;
+        require_once($CFG->dirroot.'/user/profile/lib.php');
         $progress->start_progress('Creating included users');
 
         $authcache = array(); // Cache to get some bits from authentication plugins
@@ -1257,8 +1258,9 @@ abstract class restore_dbops {
                         $udata = (object)$udata;
                         // If the profile field has data and the profile shortname-datatype is defined in server
                         if ($udata->field_data) {
-                            if ($field = $DB->get_record('user_info_field', array('shortname'=>$udata->field_name, 'datatype'=>$udata->field_type))) {
-                            /// Insert the user_custom_profile_field
+                            $field = profile_get_custom_field_data_by_shortname($udata->field_name);
+                            if ($field && $field->datatype === $udata->field_type) {
+                                // Insert the user_custom_profile_field.
                                 $rec = new stdClass();
                                 $rec->userid  = $newuserid;
                                 $rec->fieldid = $field->id;
index 9ccd887..74b7396 100644 (file)
@@ -98,26 +98,6 @@ class backup_anonymizer_helper {
         return 'anon' . $counter . '@doesntexist.invalid'; // Just a counter.
     }
 
-    public static function process_user_icq($value) {
-        return ''; // Clean icq
-    }
-
-    public static function process_user_skype($value) {
-        return ''; // Clean skype
-    }
-
-    public static function process_user_yahoo($value) {
-        return ''; // Clean yahoo
-    }
-
-    public static function process_user_aim($value) {
-        return ''; // Clean aim
-    }
-
-    public static function process_user_msn($value) {
-        return ''; // Clean msn
-    }
-
     public static function process_user_phone1($value) {
         return ''; // Clean phone1
     }
@@ -154,10 +134,6 @@ class backup_anonymizer_helper {
         return 0; // No picture
     }
 
-    public static function process_user_url($value) {
-        return ''; // No url
-    }
-
     public static function process_user_description($value) {
         return ''; // No user description
     }
index 289f30f..1f512d5 100644 (file)
@@ -128,7 +128,7 @@ class backp_settings_testcase extends basic_testcase {
             $this->assertEquals($e->errorcode, 'incorrect_object_passed');
         } catch (TypeError $e) {
             // On PHP7+ we get a TypeError raised, lets check we've the right error.
-            $this->assertMatchesRegularExpression('/must be an instance of backup_setting_ui/', $e->getMessage());
+            $this->assertMatchesRegularExpression('/must be (of type|an instance of) backup_setting_ui/', $e->getMessage());
         }
         restore_error_handler();
 
@@ -145,7 +145,7 @@ class backp_settings_testcase extends basic_testcase {
             $this->assertEquals($e->errorcode, 'incorrect_object_passed');
         } catch (TypeError $e) {
             // On PHP7+ we get a TypeError raised, lets check we've the right error.
-            $this->assertMatchesRegularExpression('/must be an instance of backup_setting_ui/', $e->getMessage());
+            $this->assertMatchesRegularExpression('/must be (of type|an instance of) backup_setting_ui/', $e->getMessage());
         }
         restore_error_handler();
 
@@ -354,7 +354,7 @@ class backp_settings_testcase extends basic_testcase {
             $this->assertEquals($e->errorcode, 'incorrect_object_passed');
         } catch (TypeError $e) {
             // On PHP7+ we get a TypeError raised, lets check we've the right error.
-            $this->assertMatchesRegularExpression('/must be an instance of base_setting/', $e->getMessage());
+            $this->assertMatchesRegularExpression('/must be (an instance of|of type) base_setting/', $e->getMessage());
         }
         restore_error_handler();
 
index 5adde7f..52c1452 100644 (file)
@@ -4,19 +4,21 @@ Feature: Import course's contents into another course
   As a teacher
   I need to import a course contents into another course selecting what I want to import
 
-  Scenario: Import course's contents to another course
+  Background:
     Given the following "courses" exist:
       | fullname | shortname | category |
-      | Course 1 | C1 | 0 |
-      | Course 2 | C2 | 0 |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | 0        |
     And the following "users" exist:
-      | username | firstname | lastname | email |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
     And the following "course enrolments" exist:
-      | user | course | role |
-      | teacher1 | C1 | editingteacher |
-      | teacher1 | C2 | editingteacher |
-    And I log in as "teacher1"
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | teacher1 | C2     | editingteacher |
+
+  Scenario: Import course's contents to another course
+    Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add a "Database" to section "1" and I fill the form with:
       | Name | Test database name |
@@ -32,3 +34,26 @@ Feature: Import course's contents into another course
     And I should see "Test forum name"
     And I should see "Comments" in the "Comments" "block"
     And I should see "Recent blog entries"
+
+  Scenario: Import process with permission option
+    Given the following "permission overrides" exist:
+      | capability         | permission | role    | contextlevel | reference |
+      | enrol/manual:enrol | Allow      | teacher | Course       | C1        |
+    And I log in as "teacher1"
+    When I import "Course 1" course into "Course 2" course using this options:
+      | Initial | Include override permissions | 1 |
+    And I navigate to "Users > Permissions" in current page administration
+    Then I should see "Non-editing teacher (1)"
+    And I set the field "Advanced role override" to "Non-editing teacher (1)"
+    And I press "Go"
+    And "enrol/manual:enrol" capability has "Allow" permission
+
+  Scenario: Import process without permission option
+    Given the following "permission overrides" exist:
+      | capability         | permission | role    | contextlevel | reference |
+      | enrol/manual:enrol | Allow      | teacher | Course       | C1        |
+    And I log in as "teacher1"
+    When I import "Course 1" course into "Course 2" course using this options:
+      | Initial | Include override permissions | 0 |
+    And I navigate to "Users > Permissions" in current page administration
+    Then I should see "Non-editing teacher (0)"
index d1bae45..fb32d18 100644 (file)
@@ -244,3 +244,29 @@ Feature: Restore Moodle 2 course backups
     And I should not see "Topic 16"
     And I should see "Test URL name" in the "Topic 3" "section"
     And I should see "Test forum name" in the "Topic 1" "section"
+
+  @javascript
+  Scenario: Restore a backup with override permission
+    Given the following "permission overrides" exist:
+      | capability         | permission | role           | contextlevel | reference |
+      | enrol/manual:enrol | Allow      | teacher        | Course       | C1        |
+    And I backup "Course 1" course using this options:
+      | Confirmation | Filename | test_backup.mbz |
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings | Include override permissions | 1 |
+    Then I navigate to "Users > Permissions" in current page administration
+    And I should see "Non-editing teacher (1)"
+    And I set the field "Advanced role override" to "Non-editing teacher (1)"
+    And "enrol/manual:enrol" capability has "Allow" permission
+
+  @javascript
+  Scenario: Restore a backup without override permission
+    Given the following "permission overrides" exist:
+      | capability         | permission | role           | contextlevel | reference |
+      | enrol/manual:enrol | Allow      | teacher        | Course       | C1        |
+    And I backup "Course 1" course using this options:
+      | Confirmation | Filename | test_backup.mbz |
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings | Include override permissions | 0 |
+    Then I navigate to "Users > Permissions" in current page administration
+    And I should see "Non-editing teacher (0)"
index 6091b62..b014cf5 100644 (file)
@@ -44,27 +44,22 @@ class award_criteria_profile extends award_criteria {
      *
      */
     public function get_options(&$mform) {
-        global $DB;
+        global $CFG, $DB;
+        require_once($CFG->dirroot . '/user/profile/lib.php');
 
         $none = true;
         $existing = array();
         $missing = array();
 
         // Note: cannot use user_get_default_fields() here because it is not possible to decide which fields user can modify.
-        $dfields = array('firstname', 'lastname', 'email', 'address', 'phone1', 'phone2', 'icq', 'skype', 'yahoo',
-                         'aim', 'msn', 'department', 'institution', 'description', 'picture', 'city', 'url', 'country');
-
-        $sql = "SELECT uf.id as fieldid, uf.name as name, ic.id as categoryid, ic.name as categoryname, uf.datatype
-                FROM {user_info_field} uf
-                JOIN {user_info_category} ic
-                ON uf.categoryid = ic.id AND uf.visible <> 0
-                ORDER BY ic.sortorder ASC, uf.sortorder ASC";
+        $dfields = array('firstname', 'lastname', 'email', 'address', 'phone1', 'phone2',
+                         'department', 'institution', 'description', 'picture', 'city', 'country');
 
         // Get custom fields.
-        $cfields = $DB->get_records_sql($sql);
-        $cfids = array_map(function($o) {
-            return $o->fieldid;
-        }, $cfields);
+        $cfields = array_filter(profile_get_custom_fields(), function($field) {
+            return $field->visible <> 0;
+        });
+        $cfids = array_keys($cfields);
 
         if ($this->id !== 0) {
             $existing = array_keys($this->params);
@@ -98,13 +93,14 @@ class award_criteria_profile extends award_criteria {
             foreach ($cfields as $field) {
                 if (!isset($currentcat) || $currentcat != $field->categoryid) {
                     $currentcat = $field->categoryid;
-                    $mform->addElement('header', 'category_' . $currentcat, format_string($field->categoryname));
+                    $categoryname = $DB->get_field('user_info_category', 'name', ['id' => $field->categoryid]);
+                    $mform->addElement('header', 'category_' . $currentcat, format_string($categoryname));
                 }
                 $checked = false;
-                if (in_array($field->fieldid, $existing)) {
+                if (in_array($field->id, $existing)) {
                     $checked = true;
                 }
-                $this->config_options($mform, array('id' => $field->fieldid, 'checked' => $checked, 'name' => $field->name, 'error' => false));
+                $this->config_options($mform, array('id' => $field->id, 'checked' => $checked, 'name' => $field->name, 'error' => false));
                 $none = false;
             }
         }
@@ -133,11 +129,16 @@ class award_criteria_profile extends award_criteria {
      * @return string
      */
     public function get_details($short = '') {
-        global $DB, $OUTPUT;
+        global $OUTPUT, $CFG;
+        require_once($CFG->dirroot.'/user/profile/lib.php');
+
         $output = array();
         foreach ($this->params as $p) {
             if (is_numeric($p['field'])) {
-                $str = $DB->get_field('user_info_field', 'name', array('id' => $p['field']));
+                $fields = profile_get_custom_fields();
+                // Get formatted field name if such field exists.
+                $str = isset($fields[$p['field']]->name) ?
+                    format_string($fields[$p['field']]->name) : null;
             } else {
                 $str = \core_user\fields::get_display_name($p['field']);
             }
index 3231db1..2be62c0 100644 (file)
@@ -592,9 +592,9 @@ class badgeslib_test extends advanced_testcase {
         require_once($CFG->dirroot.'/user/profile/lib.php');
 
         // Add a custom field of textarea type.
-        $customprofileid = $DB->insert_record('user_info_field', array(
-            'shortname' => 'newfield', 'name' => 'Description of new field', 'categoryid' => 1,
-            'datatype' => 'textarea'));
+        $customprofileid = $this->getDataGenerator()->create_custom_profile_field(array(
+            'shortname' => 'newfield', 'name' => 'Description of new field',
+            'datatype' => 'textarea'))->id;
 
         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
         $badge = new badge($this->coursebadge);
@@ -602,8 +602,8 @@ class badgeslib_test extends advanced_testcase {
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
         $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
-        $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'field_aim' => 'aim',
-            'field_' . $customprofileid => $customprofileid));
+        $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address',
+            'field_department' => 'department', 'field_' . $customprofileid => $customprofileid));
 
         // Assert the badge will not be issued to the user as is.
         $badge = new badge($this->coursebadge);
@@ -612,7 +612,7 @@ class badgeslib_test extends advanced_testcase {
 
         // Set the required fields and make sure the badge got issued.
         $this->user->address = 'Test address';
-        $this->user->aim = '999999999';
+        $this->user->department = 'sillywalks';
         $sink = $this->redirectEmails();
         profile_save_data((object)array('id' => $this->user->id, 'profile_field_newfield' => 'X'));
         user_update_user($this->user, false);
index 714c9d0..c604a37 100644 (file)
@@ -84,26 +84,6 @@ class myprofile implements renderable, templatable {
             $data->useremail = obfuscate_mailto($USER->email, '');
         }
 
-        if (!empty($this->config->display_icq) && !empty($USER->icq)) {
-            $data->usericq = s($USER->icq);
-        }
-
-        if (!empty($this->config->display_skype) && !empty($USER->skype)) {
-            $data->userskype = s($USER->skype);
-        }
-
-        if (!empty($this->config->display_yahoo) && !empty($USER->yahoo)) {
-            $data->useryahoo = s($USER->yahoo);
-        }
-
-        if (!empty($this->config->display_aim) && !empty($USER->aim)) {
-            $data->useraim = s($USER->aim);
-        }
-
-        if (!empty($this->config->display_msn) && !empty($USER->msn)) {
-            $data->usermsn = s($USER->msn);
-        }
-
         if (!empty($this->config->display_phone1) && !empty($USER->phone1)) {
             $data->userphone1 = s($USER->phone1);
         }
index 0302647..6de49b2 100644 (file)
@@ -57,41 +57,6 @@ class block_myprofile_edit_form extends block_edit_form {
             $mform->setDefault('config_display_email', '1');
         }
 
-        $mform->addElement('selectyesno', 'config_display_icq', get_string('display_icq', 'block_myprofile'));
-        if (isset($this->block->config->display_icq)) {
-            $mform->setDefault('config_display_icq', $this->block->config->display_icq);
-        } else {
-            $mform->setDefault('config_display_icq', '0');
-        }
-
-        $mform->addElement('selectyesno', 'config_display_skype', get_string('display_skype', 'block_myprofile'));
-        if (isset($this->block->config->display_skype)) {
-            $mform->setDefault('config_display_skype', $this->block->config->display_skype);
-        } else {
-            $mform->setDefault('config_display_skype', '0');
-        }
-
-        $mform->addElement('selectyesno', 'config_display_yahoo', get_string('display_yahoo', 'block_myprofile'));
-        if (isset($this->block->config->display_yahoo)) {
-            $mform->setDefault('config_display_yahoo', $this->block->config->display_yahoo);
-        } else {
-            $mform->setDefault('config_display_yahoo', '0');
-        }
-
-        $mform->addElement('selectyesno', 'config_display_aim', get_string('display_aim', 'block_myprofile'));
-        if (isset($this->block->config->display_aim)) {
-            $mform->setDefault('config_display_aim', $this->block->config->display_aim);
-        } else {
-            $mform->setDefault('config_display_aim', '0');
-        }
-
-        $mform->addElement('selectyesno', 'config_display_msn', get_string('display_msn', 'block_myprofile'));
-        if (isset($this->block->config->display_msn)) {
-            $mform->setDefault('config_display_msn', $this->block->config->display_msn);
-        } else {
-            $mform->setDefault('config_display_msn', '0');
-        }
-
         $mform->addElement('selectyesno', 'config_display_phone1', get_string('display_phone1', 'block_myprofile'));
         if (isset($this->block->config->display_phone1)) {
             $mform->setDefault('config_display_phone1', $this->block->config->display_phone1);
index 5d886c2..e263d2c 100644 (file)
@@ -28,11 +28,6 @@ $string['display_picture'] = 'Display picture';
 $string['display_country'] = 'Display country';
 $string['display_city'] = 'Display city';
 $string['display_email'] = 'Display email';
-$string['display_icq'] = 'Display ICQ';
-$string['display_skype'] = 'Display Skype';
-$string['display_yahoo'] = 'Display Yahoo';
-$string['display_aim'] = 'Display AIM';
-$string['display_msn'] = 'Display MSN';
 $string['display_phone1'] = 'Display phone';
 $string['display_phone2'] = 'Display mobile phone';
 $string['display_institution'] = 'Display institution';
@@ -46,3 +41,10 @@ $string['myprofile:myaddinstance'] = 'Add a new logged in user block to Dashboar
 $string['myprofile_settings'] = 'Visible user information';
 $string['pluginname'] = 'Logged in user';
 $string['privacy:metadata'] = 'The Logged in user block only shows information about the logged in user and does not store data itself.';
+
+// Deprecated since Moodle 4.0.
+$string['display_icq'] = 'Display ICQ';
+$string['display_skype'] = 'Display Skype';
+$string['display_yahoo'] = 'Display Yahoo';
+$string['display_aim'] = 'Display AIM';
+$string['display_msn'] = 'Display MSN';
\ No newline at end of file
index e132b0d..d9505e7 100644 (file)
         * usercountry
         * usercity
         * useremail
-        * usericq
-        * userskype
-        * useryahoo
-        * useraim
-        * usermsn
         * userphone1
         * userphone2
         * userinstitution
         "usercountry": "Australia",
         "usercity": "Perth",
         "useremail": "<a href=''>john.doe@example.com</a>",
-        "usericq": "12345",
-        "userskype": "john.doe",
-        "useryahoo": "12345",
-        "useraim": "12345",
-        "usermsn": "12345",
         "userphone1": "123456789",
         "userphone2": "123456789",
         "userinstitution": "Institution",
              {{{ useremail }}}
         </div>
     {{/useremail}}
-    {{#usericq}}
-    <div class="myprofileitem icq">
-         <span>ICQ:</span>
-         {{ usericq }}
-    </div>
-    {{/usericq}}
-    {{#userskype}}
-    <div class="myprofileitem skype">
-         <span>Skype:</span>
-         {{ userskype }}
-    </div>
-    {{/userskype}}
-    {{#useryahoo}}
-    <div class="myprofileitem yahoo">
-         <span>Yahoo:</span>
-         {{ useryahoo }}
-    </div>
-    {{/useryahoo}}
-    {{#useraim}}
-    <div class="myprofileitem aim">
-         <span>AIM:</span>
-         {{ useraim }}
-    </div>
-    {{/useraim}}
-    {{#usermsn}}
-    <div class="myprofileitem msn">
-         <span>MSN:</span>
-         {{ usermsn }}
-    </div>
-    {{/usermsn}}
     {{#userphone1}}
     <div class="myprofileitem phone1">
          <span>{{#str}} phone1 {{/str}}:</span>
index 8bd076c..8801de3 100644 (file)
@@ -61,101 +61,6 @@ Feature: The logged in user block allows users to view their profile information
     And I press "Save changes"
     And I should see "teacher1@example.com" in the "Logged in user" "block"
 
-  Scenario: Configure the logged in user block to show / hide the users ICQ
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | icq   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | myicq |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display ICQ           | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "myicq" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display ICQ | Yes |
-    And I press "Save changes"
-    And I should see "myicq" in the "Logged in user" "block"
-
-  Scenario: Configure the logged in user block to show / hide the users Skype
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | skype   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | myskype |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display Skype         | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "myskype" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display Skype | Yes |
-    And I press "Save changes"
-    And I should see "myskype" in the "Logged in user" "block"
-
-  Scenario: Configure the logged in user block to show / hide the users Yahoo
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | yahoo   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | myyahoo |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display Yahoo         | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "myyahoo" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display Yahoo | Yes |
-    And I press "Save changes"
-    And I should see "myyahoo" in the "Logged in user" "block"
-
-  Scenario: Configure the logged in user block to show / hide the users AIM
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | aim   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | myaim |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display AIM           | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "myaim" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display AIM | Yes |
-    And I press "Save changes"
-    And I should see "myaim" in the "Logged in user" "block"
-
-  Scenario: Configure the logged in user block to show / hide the users MSN
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | msn   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | mymsn |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display MSN           | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "mymsn" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display MSN | Yes |
-    And I press "Save changes"
-    And I should see "mymsn" in the "Logged in user" "block"
-
   Scenario: Configure the logged in user block to show / hide the users phone
     Given the following "users" exist:
       | username | firstname | lastname | email                | phone1   |
index 7060c93..e11d693 100644 (file)
@@ -63,7 +63,7 @@ class blog_edit_form extends moodleform {
 
         $mform->addElement('select', 'publishstate', get_string('publishto', 'blog'), $publishstates);
         $mform->addHelpButton('publishstate', 'publishto', 'blog');
-        $mform->setDefault('publishstate', 0);
+        $mform->setDefault('publishstate', 'site');
 
         if (core_tag_tag::is_enabled('core', 'post')) {
             $mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
index 487010b..4e6e5df 100644 (file)
@@ -36,7 +36,6 @@ require_once($CFG->libdir . '/form/datetimeselector.php');
 // Used to test the user datetime profile field.
 require_once($CFG->dirroot . '/user/profile/lib.php');
 require_once($CFG->dirroot . '/user/profile/definelib.php');
-require_once($CFG->dirroot . '/user/profile/index_field_form.php');
 
 /**
  * Unit tests for the calendar type system.
@@ -273,12 +272,13 @@ class core_calendar_type_testcase extends advanced_testcase {
         $formdata['name'] = 'Name';
         $formdata['param1'] = $date['inputminyear'];
         $formdata['param2'] = $date['inputmaxyear'];
+        $formdata['datatype'] = 'datetime';
 
         // Mock submitting this.
-        field_form::mock_submit($formdata);
+        \core_user\form\profile_field_form::mock_submit($formdata);
 
         // Create the user datetime form.
-        $form = new field_form(null, 'datetime');
+        $form = new \core_user\form\profile_field_form();
 
         // Get the data from the submission.
         $submissiondata = $form->get_data();
index a1e2004..5c373dc 100644 (file)
@@ -66,6 +66,102 @@ function create_event($properties) {
     return $event->create($record);
 }
 
+/**
+ * Helper function to create a x number of events for each event type.
+ *
+ * @param int $quantity The quantity of events to be created.
+ * @return array List of created events.
+ */
+function create_standard_events(int $quantity): array {
+    $types = ['site', 'category', 'course', 'group', 'user'];
+
+    $events = [];
+    foreach ($types as $eventtype) {
+        // Create five events of each event type.
+        for ($i = 0; $i < $quantity; $i++) {
+            $events[] = create_event(['eventtype' => $eventtype]);
+        }
+    }
+
+    return $events;
+}
+
+/**
+ * Helper function to create an action event.
+ *
+ * @param array $data The event data.
+ * @return bool|calendar_event
+ */
+function create_action_event(array $data) {
+    global $CFG;
+
+    require_once($CFG->dirroot . '/calendar/lib.php');
+
+    if (!isset($data['modulename']) || !isset($data['instance'])) {
+        throw new coding_exception('Module and instance should be specified when creating an action event.');
+    }
+
+    $isuseroverride = isset($data->priority) && $data->priority == CALENDAR_EVENT_USER_OVERRIDE_PRIORITY;
+    if ($isuseroverride) {
+        if (!in_array($data['modulename'], ['assign', 'lesson', 'quiz'])) {
+            throw new coding_exception('Only assign, lesson and quiz modules supports overrides');
+        }
+    }
+
+    $event = array_merge($data, [
+        'eventtype' => isset($data['eventtype']) ? $data['eventtype'] : 'open',
+        'courseid' => isset($data['courseid']) ? $data['courseid'] : 0,
+        'instance' => $data['instance'],
+        'modulename' => $data['modulename'],
+        'type' => CALENDAR_EVENT_TYPE_ACTION,
+    ]);
+
+    return create_event($event);
+}
+
+/**
+ * Helper function to create an user override calendar event.
+ *
+ * @param string $modulename The modulename.
+ * @param int $instanceid The instance id.
+ * @param int $userid The user id.
+ * @return calendar_event|false
+ */
+function create_user_override_event(string $modulename, int $instanceid, int $userid) {
+    if (!isset($userid)) {
+        throw new coding_exception('Must specify userid when creating a user override.');
+    }
+
+    return create_action_event([
+        'modulename' => $modulename,
+        'instance' => $instanceid,
+        'userid' => $userid,
+        'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY,
+    ]);
+}
+
+/**
+ * Helper function to create an group override calendar event.
+ *
+ * @param string $modulename The modulename.
+ * @param int $instanceid The instance id.
+ * @param int $courseid The course id.
+ * @param int $groupid The group id.
+ * @return calendar_event|false
+ */
+function create_group_override_event(string $modulename, int $instanceid, int $courseid, int $groupid) {
+    if (!isset($groupid)) {
+        throw new coding_exception('Must specify groupid when creating a group override.');
+    }
+
+    return create_action_event([
+        'groupid' => $groupid,
+        'courseid' => $courseid,
+        'modulename' => $modulename,
+        'instance' => $instanceid,
+    ]);
+}
+
 /**
  * A test factory that will create action events.
  *
index 2ac83dd..f401ceb 100644 (file)
@@ -138,6 +138,17 @@ abstract class activity_custom_completion {
         return $descriptions[$rule];
     }
 
+    /**
+     * Show the manual completion or not regardless of the course's showcompletionconditions setting.
+     * Returns false by default for plugins that don't need to override the course's showcompletionconditions setting.
+     * Activity plugins that need to always show manual completion need to override this function.
+     *
+     * @return bool
+     */
+    public function manual_completion_always_shown(): bool {
+        return false;
+    }
+
     /**
      * Fetches the module's custom completion class implementation if it's available.
      *
index 3d9b828..11b9fb3 100644 (file)
@@ -49,6 +49,9 @@ class cm_completion_details {
     /** @var bool Whether to return automatic completion details. */
     protected $returndetails = true;
 
+    /** @var activity_custom_completion Activity custom completion object. */
+    protected $cmcompletion = null;
+
     /**
      * Constructor.
      *
@@ -62,6 +65,10 @@ class cm_completion_details {
         $this->cminfo = $cminfo;
         $this->userid = $userid;
         $this->returndetails = $returndetails;
+        $cmcompletionclass = activity_custom_completion::get_cm_completion_class($this->cminfo->modname);
+        if ($cmcompletionclass) {
+            $this->cmcompletion = new $cmcompletionclass($this->cminfo, $this->userid);
+        }
     }
 
     /**
@@ -116,21 +123,28 @@ class cm_completion_details {
             ];
         }
 
-        // Custom completion rules.
-        $cmcompletionclass = activity_custom_completion::get_cm_completion_class($this->cminfo->modname);
-        if (!isset($completiondata->customcompletion) || !$cmcompletionclass) {
-            // Return early if there are no custom rules to process or the cm completion class implementation is not available.
-            return $details;
+        if ($this->cmcompletion) {
+            if (isset($completiondata->customcompletion)) {
+                foreach ($completiondata->customcompletion as $rule => $status) {
+                    $details[$rule] = (object)[
+                        'status' => !$hasoverride ? $status : $completiondata->completionstate,
+                        'description' => $this->cmcompletion->get_custom_rule_description($rule),
+                    ];
+                }
+            }
+        } else {
+            if (function_exists($this->cminfo->modname . '_get_completion_state')) {
+                // If the plugin does not have the custom completion implementation but implements the get_completion_state() callback,
+                // fallback to displaying the overall completion state of the activity.
+                $details = [
+                    'plugincompletionstate' => (object)[
+                        'status' => $this->get_overall_completion(),
+                        'description' => get_string('completeactivity', 'completion')
+                    ]
+                ];
+            }
         }
 
-        /** @var activity_custom_completion $cmcompletion */
-        $cmcompletion = new $cmcompletionclass($this->cminfo, $this->userid);
-        foreach ($completiondata->customcompletion as $rule => $status) {
-            $details[$rule] = (object)[
-                'status' => !$hasoverride ? $status : $completiondata->completionstate,
-                'description' => $cmcompletion->get_custom_rule_description($rule),
-            ];
-        }
 
         return $details;
     }
@@ -182,6 +196,29 @@ class cm_completion_details {
         return $this->completioninfo->is_tracked_user($this->userid);
     }
 
+    /**
+     * Determine whether to show the manual completion or not.
+     *
+     * @return bool
+     */
+    public function show_manual_completion(): bool {
+        global $PAGE;
+
+        if ($PAGE->context->contextlevel == CONTEXT_MODULE) {
+            // Manual completion should always be shown on the activity page.
+            return true;
+        } else {
+            $course = $this->cminfo->get_course();
+            if ($course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS) {
+                return true;
+            } else if ($this->cmcompletion) {
+                return $this->cmcompletion->manual_completion_always_shown();
+            }
+        }
+
+        return false;
+    }
+
     /**
      * Generates an instance of this class.
      *
index 99ad750..40707e5 100644 (file)
@@ -156,7 +156,7 @@ class completion_criteria_grade extends completion_criteria {
      * @return string
      */
     public function get_type_title() {
-        return get_string('grade');
+        return get_string('gradenoun');
     }
 
     /**
index 7ebe22a..4d78021 100644 (file)
@@ -295,6 +295,32 @@ class behat_completion extends behat_base {
         $this->execute("behat_general::the_element_should_be_disabled", $params);
     }
 
+    /**
+     * Check that the manual completion button for the activity does not exist.
+     *
+     * @Given /^the manual completion button for "(?P<activityname>(?:[^"]|\\")*)" should not exist/
+     * @param string $activityname The activity name.
+     */
+    public function the_manual_completion_button_for_activity_should_not_exist(string $activityname): void {
+        $selector = "div[data-activityname='$activityname'] button";
+
+        $params = [$selector, "css_element"];
+        $this->execute('behat_general::should_not_exist', $params);
+    }
+
+    /**
+     * Check that the manual completion button for the activity exists.
+     *
+     * @Given /^the manual completion button for "(?P<activityname>(?:[^"]|\\")*)" should exist/
+     * @param string $activityname The activity name.
+     */
+    public function the_manual_completion_button_for_activity_should_exist(string $activityname): void {
+        $selector = "div[data-activityname='$activityname'] button";
+
+        $params = [$selector, "css_element"];
+        $this->execute('behat_general::should_exist', $params);
+    }
+
     /**
      * Check that the activity has the given automatic completion condition.
      *
index 783690b..dcc77f6 100644 (file)
@@ -44,26 +44,39 @@ Feature: Allow teachers to edit the visibility of completion conditions in a cou
     # Completion conditions are always shown in the module's view page.
     And I follow "Test choice auto"
     Then "Test choice auto" should have the "Make a choice" completion condition
-    # The manual completion toggle button should be always displayed in both course homepage and activity view.
+    # The manual completion toggle button should not be displayed in the course homepage when completion is disabled.
     And I am on "Course 1" course homepage
-    And the manual completion button for "Test choice manual" should be disabled
+    And the manual completion button for "Test choice manual" should not exist
+    # The manual completion toggle button should always be displayed in the activity view.
     And I follow "Test choice manual"
     And the manual completion button for "Test choice manual" should be disabled
 
-  Scenario: Default show completion conditions value in course form when default show completion conditions admin setting is set to No
+  Scenario Outline: Default showcompletionconditions value in course form on course creation
     Given I log in as "admin"
     And I navigate to "Courses > Course default settings" in site administration
-    When I set the following fields to these values:
-      | Show completion conditions | No |
-    And I click on "Save changes" "button"
-    And I navigate to "Courses > Add a new course" in site administration
-    Then the field "showcompletionconditions" matches value "No"
+    And I set the field "Show completion conditions" to "<siteshowcompletion>"
+    And I press "Save changes"
+    When I navigate to "Courses > Add a new course" in site administration
+    Then the field "showcompletionconditions" matches value "<expected>"
+
+    Examples:
+      | siteshowcompletion | expected |
+      | Yes                | Yes      |
+      | No                 | No       |
 
-  Scenario: Default show completion conditions value in course form when default show completion conditions admin setting is set to Yes
+  Scenario Outline: Default showcompletionconditions displayed when editing a course with disabled completion tracking
     Given I log in as "admin"
     And I navigate to "Courses > Course default settings" in site administration
-    When I set the following fields to these values:
-      | Show completion conditions | Yes |
-    And I click on "Save changes" "button"
-    And I navigate to "Courses > Add a new course" in site administration
-    Then the field "showcompletionconditions" matches value "Yes"
+    And I set the field "Show completion conditions" to "<siteshowcompletion>"
+    And I press "Save changes"
+    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 field "Enable completion tracking" to "No"
+    And I press "Save and display"
+    And I navigate to "Edit settings" in current page administration
+    Then the field "Show completion conditions" matches value "<expected>"
+
+    Examples:
+      | siteshowcompletion  | expected  |
+      | Yes                 | Yes       |
+      | No                  | No        |
index f9657dd..a976cd0 100644 (file)
@@ -19,6 +19,9 @@ information provided here is intended especially for developers.
     - Given the manual completion button of "<Activity name>" overridden by "<User>" is displayed as "<Status>"
   - overridden_activity_completion_condition_displayed_as
     - Given the "<Completion condition>" completion condition of "<Activity name>" overridden by "<User>" is displayed as "<Status>"
+* *_get_completion_state() callback functions have been deprecated and should no longer be used. Plugins that define custom
+  completion rules must implement the mod_[modname]\completion\custom_completion class that extends the
+  \core_completion\activity_custom_completion base class.
 
 === 3.7 ===
  * External function core_completion_external::get_activities_completion_status new returns the following additional field:
index 03e9d42..deaa39c 100644 (file)
@@ -98,6 +98,7 @@ class activity_information implements renderable, templatable {
 
         $data->hascompletion = $this->cmcompletion->has_completion();
         $data->isautomatic = $this->cmcompletion->is_automatic();
+        $data->showmanualcompletion = $this->cmcompletion->show_manual_completion();
 
         // Get the name of the user overriding the completion condition, if available.
         $data->overrideby = null;
index 894c2c2..1825f27 100644 (file)
@@ -89,7 +89,8 @@ class cmitem implements renderable, templatable {
         $data = new stdClass();
         $data->cms = [];
 
-        $showactivityconditions = $course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS;
+        $completionenabled = $course->enablecompletion == COMPLETION_ENABLED;
+        $showactivityconditions = $completionenabled && $course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS;
         $showactivitydates = !empty($course->showactivitydates);
 
         // This will apply styles to the course homepage when the activity information output component is displayed.
index 3233a4d..73b5190 100644 (file)
@@ -322,7 +322,7 @@ class course_edit_form extends moodleform {
             $mform->addElement('selectyesno', 'showcompletionconditions', get_string('showcompletionconditions', 'completion'));
             $mform->addHelpButton('showcompletionconditions', 'showcompletionconditions', 'completion');
             $mform->setDefault('showcompletionconditions', $showcompletionconditions);
-            $mform->hideIf('showcompletionconditions', 'enablecompletion', 'eq', COMPLETION_HIDE_CONDITIONS);
+            $mform->hideIf('showcompletionconditions', 'enablecompletion', 'eq', COMPLETION_DISABLED);
         } else {
             $mform->addElement('hidden', 'enablecompletion');
             $mform->setType('enablecompletion', PARAM_INT);
index 6fdcff4..8e1582c 100644 (file)
@@ -2362,6 +2362,19 @@ function create_course($data, $editoroptions = NULL) {
         $data->summary_format = FORMAT_HTML;
     }
 
+    // Get default completion settings as a fallback in case the enablecompletion field is not set.
+    $courseconfig = get_config('moodlecourse');
+    $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED;
+    $enablecompletion = $data->enablecompletion ?? $defaultcompletion;
+    // Unset showcompletionconditions when completion tracking is not enabled for the course.
+    if ($enablecompletion == COMPLETION_DISABLED) {
+        unset($data->showcompletionconditions);
+    } else if (!isset($data->showcompletionconditions)) {
+        // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults.
+        // This scenario can happen when a course is created through data generators or through a web service.
+        $data->showcompletionconditions = $courseconfig->showcompletionconditions;
+    }
+
     if (!isset($data->visible)) {
         // data not from form, add missing visibility info
         $data->visible = $category->visible;
@@ -2540,6 +2553,11 @@ function update_course($data, $editoroptions = NULL) {
         }
     }
 
+    // Set showcompletionconditions to null when completion tracking has been disabled for the course.
+    if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) {
+        $data->showcompletionconditions = null;
+    }
+
     // Update custom fields if there are any of them in the form.
     $handler = core_course\customfield\course_handler::create();
     $handler->instance_form_save($data);
index d39b892..878aa27 100644 (file)
@@ -995,7 +995,7 @@ abstract class moodleform_mod extends moodleform {
 
         if ($this->_features->hasgrades) {
             if ($this->_features->gradecat) {
-                $mform->addElement('header', 'modstandardgrade', get_string('grade'));
+                $mform->addElement('header', 'modstandardgrade', get_string('gradenoun'));
             }
 
             //if supports grades and grades arent being handled via ratings
@@ -1012,7 +1012,7 @@ abstract class moodleform_mod extends moodleform {
                     $gradeoptions['hasgrades'] = $gradeitem->has_grades();
                 }
             }
-            $mform->addElement('modgrade', $gradefieldname, get_string('grade'), $gradeoptions);
+            $mform->addElement('modgrade', $gradefieldname, get_string('gradenoun'), $gradeoptions);
             $mform->addHelpButton($gradefieldname, 'modgrade', 'grades');
             $mform->setDefault($gradefieldname, $CFG->gradepointdefault);
 
index ac7ef27..f415d2a 100644 (file)
@@ -30,6 +30,7 @@
         "hasdates": true,
         "isautomatic": true,
         "istrackeduser": true,
+        "showmanualcompletion": true,
         "activitydates": [
             {
                 "label": "Opens:",
@@ -67,7 +68,9 @@
                     </div>
                 {{/isautomatic}}
                 {{^isautomatic}}
-                    {{> core_course/completion_manual }}
+                    {{#showmanualcompletion}}
+                        {{> core_course/completion_manual }}
+                    {{/showmanualcompletion}}
                 {{/isautomatic}}
             </div>
         {{/uservisible}}
index 766a0c1..5e93c26 100644 (file)
@@ -1437,8 +1437,9 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $result = core_course_external::get_course_contents($course->id);
         $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
         $this->assertCount(1, $result[0]['modules']);   // One module, first section.
-        $customdata = unserialize(json_decode($result[0]['modules'][0]['customdata']));
-        $this->assertEquals('text/plain', $customdata['filedetails']['mimetype']);
+        $customdata = json_decode($result[0]['modules'][0]['customdata']);
+        $displayoptions = unserialize($customdata->displayoptions);
+        $this->assertEquals('text/plain', $displayoptions['filedetails']['mimetype']);
     }
 
     /**
index ae06260..0a71d05 100644 (file)
@@ -55,12 +55,16 @@ class field_controller extends \core_customfield\field_controller {
 
         $mform->addElement('text', 'configdata[displaysize]', get_string('displaysize', 'customfield_text'), ['size' => 6]);
         $mform->setType('configdata[displaysize]', PARAM_INT);
-        $mform->setDefault('configdata[displaysize]', 50);
+        if (!$this->get_configdata_property('displaysize')) {
+            $mform->setDefault('configdata[displaysize]', 50);
+        }
         $mform->addRule('configdata[displaysize]', null, 'numeric', null, 'client');
 
         $mform->addElement('text', 'configdata[maxlength]', get_string('maxlength', 'customfield_text'), ['size' => 6]);
         $mform->setType('configdata[maxlength]', PARAM_INT);
-        $mform->setDefault('configdata[maxlength]', 1333);
+        if (!$this->get_configdata_property('maxlength')) {
+            $mform->setDefault('configdata[maxlength]', 1333);
+        }
         $mform->addRule('configdata[maxlength]', null, 'numeric', null, 'client');
 
         $mform->addElement('selectyesno', 'configdata[ispassword]', get_string('ispassword', 'customfield_text'));
@@ -113,7 +117,7 @@ class field_controller extends \core_customfield\field_controller {
                     $errors['configdata[link]'] = get_string('errorconfiglinkplaceholder', 'customfield_text');
                 } else if (!validateUrlSyntax(str_replace('$$', 'XYZ', $link), 's+H?S?F-E-u-P-a?I?p?f?q?r?')) {
                     // This validation is more strict than PARAM_URL - it requires the protocol and it must be either http or https.
-                    $errors['configdata[link]'] = get_string('errorconfigdisplaysize', 'customfield_text');
+                    $errors['configdata[link]'] = get_string('errorconfiglinksyntax', 'customfield_text');
                 }
             }
         }
index 116d1ff..0254bde 100644 (file)
@@ -211,11 +211,6 @@ class core_enrol_external extends external_api {
                     'address'     => new external_value(PARAM_MULTILANG, 'Postal address', VALUE_OPTIONAL),
                     'phone1'      => new external_value(PARAM_NOTAGS, 'Phone 1', VALUE_OPTIONAL),
                     'phone2'      => new external_value(PARAM_NOTAGS, 'Phone 2', VALUE_OPTIONAL),
-                    'icq'         => new external_value(PARAM_NOTAGS, 'icq number', VALUE_OPTIONAL),
-                    'skype'       => new external_value(PARAM_NOTAGS, 'skype id', VALUE_OPTIONAL),
-                    'yahoo'       => new external_value(PARAM_NOTAGS, 'yahoo id', VALUE_OPTIONAL),
-                    'aim'         => new external_value(PARAM_NOTAGS, 'aim id', VALUE_OPTIONAL),
-                    'msn'         => new external_value(PARAM_NOTAGS, 'msn number', VALUE_OPTIONAL),
                     'department'  => new external_value(PARAM_TEXT, 'department', VALUE_OPTIONAL),
                     'institution' => new external_value(PARAM_TEXT, 'institution', VALUE_OPTIONAL),
                     'interests'   => new external_value(PARAM_TEXT, 'user interests (separated by commas)', VALUE_OPTIONAL),
@@ -225,7 +220,6 @@ class core_enrol_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'User profile description', VALUE_OPTIONAL),
                     'descriptionformat' => new external_value(PARAM_INT, 'User profile description format', VALUE_OPTIONAL),
                     'city'        => new external_value(PARAM_NOTAGS, 'Home city of the user', VALUE_OPTIONAL),
-                    'url'         => new external_value(PARAM_URL, 'URL of the user', VALUE_OPTIONAL),
                     'country'     => new external_value(PARAM_ALPHA, 'Country code of the user, such as AU or CZ', VALUE_OPTIONAL),
                     'profileimageurlsmall' => new external_value(PARAM_URL, 'User image profile URL - small', VALUE_OPTIONAL),
                     'profileimageurl' => new external_value(PARAM_URL, 'User image profile URL - big', VALUE_OPTIONAL),
@@ -908,11 +902,6 @@ class core_enrol_external extends external_api {
                     'address'     => new external_value(PARAM_TEXT, 'Postal address', VALUE_OPTIONAL),
                     'phone1'      => new external_value(PARAM_NOTAGS, 'Phone 1', VALUE_OPTIONAL),
                     'phone2'      => new external_value(PARAM_NOTAGS, 'Phone 2', VALUE_OPTIONAL),
-                    'icq'         => new external_value(PARAM_NOTAGS, 'icq number', VALUE_OPTIONAL),
-                    'skype'       => new external_value(PARAM_NOTAGS, 'skype id', VALUE_OPTIONAL),
-                    'yahoo'       => new external_value(PARAM_NOTAGS, 'yahoo id', VALUE_OPTIONAL),
-                    'aim'         => new external_value(PARAM_NOTAGS, 'aim id', VALUE_OPTIONAL),
-                    'msn'         => new external_value(PARAM_NOTAGS, 'msn number', VALUE_OPTIONAL),
                     'department'  => new external_value(PARAM_TEXT, 'department', VALUE_OPTIONAL),
                     'institution' => new external_value(PARAM_TEXT, 'institution', VALUE_OPTIONAL),
                     'idnumber'    => new external_value(PARAM_RAW, 'An arbitrary ID code number perhaps from the institution', VALUE_OPTIONAL),
@@ -923,7 +912,6 @@ class core_enrol_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'User profile description', VALUE_OPTIONAL),
                     'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL),
                     'city'        => new external_value(PARAM_NOTAGS, 'Home city of the user', VALUE_OPTIONAL),
-                    'url'         => new external_value(PARAM_URL, 'URL of the user', VALUE_OPTIONAL),
                     'country'     => new external_value(PARAM_ALPHA, 'Home country code of the user, such as AU or CZ', VALUE_OPTIONAL),
                     'profileimageurlsmall' => new external_value(PARAM_URL, 'User image profile URL - small version', VALUE_OPTIONAL),
                     'profileimageurl' => new external_value(PARAM_URL, 'User image profile URL - big version', VALUE_OPTIONAL),
index 0df80cf..20168f6 100644 (file)
@@ -188,7 +188,9 @@ $removeenabled = $canunenrol ? '' : 'disabled="disabled"';
       </td>
       <td id="buttonscell">
           <div id="addcontrols">
-              <input name="add" <?php echo $addenabled; ?> id="add" type="submit" value="<?php echo $OUTPUT->larrow().'&nbsp;'.get_string('add'); ?>" title="<?php print_string('add'); ?>" /><br />
+              <input class="btn btn-secondary" name="add" <?php echo $addenabled; ?> id="add" type="submit"
+                     value="<?php echo $OUTPUT->larrow() . '&nbsp;' . get_string('add'); ?>"
+                     title="<?php print_string('add'); ?>" /><br />
 
               <div class="enroloptions">
 
@@ -205,7 +207,9 @@ $removeenabled = $canunenrol ? '' : 'disabled="disabled"';
           </div>
 
           <div id="removecontrols">
-              <input name="remove" id="remove" <?php echo $removeenabled; ?> type="submit" value="<?php echo get_string('remove').'&nbsp;'.$OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" />
+              <input class="btn btn-secondary" name="remove" id="remove" <?php echo $removeenabled; ?> type="submit"
+                     value="<?php echo get_string('remove') . '&nbsp;' . $OUTPUT->rarrow(); ?>"
+                     title="<?php print_string('remove'); ?>" />
           </div>
       </td>
       <td id="potentialcell">
index 91551f0..c21729f 100644 (file)
@@ -98,6 +98,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe
     And I log in as "student1"
     And I am on "Course 1" course homepage
     And I press "Enrol me"
+    And I should see "You are enrolled in the course"
     And I log out
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
@@ -118,6 +119,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe
     And I log in as "student1"
     And I am on "Course 1" course homepage
     And I press "Enrol me"
+    And I should see "You are enrolled in the course"
     And I log out
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
index acd3e60..05123aa 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_enrollib_testcase extends advanced_testcase {
+class enrollib_test extends advanced_testcase {
 
     public function test_enrol_get_all_users_courses() {
         global $DB, $CFG;
@@ -1036,6 +1036,59 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertEquals([$course1->id, $course2->id, $course3->id, $course4->id], array_keys($courses));
     }
 
+    /**
+     * Data provider for {@see test_enrol_get_my_courses_by_time}
+     *
+     * @return array
+     */
+    public function enrol_get_my_courses_by_time_provider(): array {
+        return [
+            'No start or end time' =>
+                [null, null, true],
+            'Start time now, no end time' =>
+                [0, null, true],
+            'Start time now, end time in the future' =>
+                [0, MINSECS, true],
+            'Start time in the past, no end time' =>
+                [-MINSECS, null, true],
+            'Start time in the past, end time in the future' =>
+                [-MINSECS, MINSECS, true],
+            'Start time in the past, end time in the past' =>
+                [-DAYSECS, -HOURSECS, false],
+            'Start time in the future' =>
+                [MINSECS, null, false],
+        ];
+    }
+
+    /**
+     * Test that expected course enrolments are returned when they have timestart / timeend specified
+     *
+     * @param int|null $timestartoffset Null for 0, otherwise offset from current time
+     * @param int|null $timeendoffset Null for 0, otherwise offset from current time
+     * @param bool $expectreturn
+     *
+     * @dataProvider enrol_get_my_courses_by_time_provider
+     */
+    public function test_enrol_get_my_courses_by_time(?int $timestartoffset, ?int $timeendoffset, bool $expectreturn): void {
+        $this->resetAfterTest();
+
+        $time = time();
+        $timestart = $timestartoffset === null ? 0 : $time + $timestartoffset;
+        $timeend = $timeendoffset === null ? 0 : $time + $timeendoffset;
+
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', $timestart, $timeend);
+        $this->setUser($user);
+
+        $courses = enrol_get_my_courses();
+        if ($expectreturn) {
+            $this->assertCount(1, $courses);
+            $this->assertEquals($course->id, reset($courses)->id);
+        } else {
+            $this->assertEmpty($courses);
+        }
+    }
+
     /**
      * test_course_users
      *
@@ -1362,4 +1415,77 @@ class core_enrollib_testcase extends advanced_testcase {
         $durationinday = $duration / DAYSECS;
         $this->assertEquals(9, $durationinday);
     }
+
+    /**
+     * Test get_enrolled_with_capabilities_join cannotmatchanyrows attribute.
+     *
+     * @dataProvider get_enrolled_with_capabilities_join_cannotmatchanyrows_data()
+     * @param string $capability the tested capability
+     * @param bool $useprohibit if the capability must be assigned to prohibit
+     * @param int $expectedmatch expected cannotmatchanyrows value
+     * @param int $expectedcount expceted count value
+     */
+    public function test_get_enrolled_with_capabilities_join_cannotmatchanyrows(
+        string $capability,
+        bool $useprohibit,
+        int $expectedmatch,
+        int $expectedcount
+    ) {
+        global $DB, $CFG;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        $roleid = $CFG->defaultuserroleid;
+
+        // Override capability if necessary.
+        if ($useprohibit && $capability) {
+            assign_capability($capability, CAP_PROHIBIT, $roleid, $context);
+        }
+
+        // Check if we must enrol or not.
+        $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+
+        $join = get_enrolled_with_capabilities_join($context, '', $capability);
+
+        // Execute query.
+        $sql = "SELECT COUNT(DISTINCT u.id)
+                  FROM {user} u {$join->joins}
+                 WHERE {$join->wheres}";
+        $countrecords = $DB->count_records_sql($sql, $join->params);
+
+        // Validate cannotmatchanyrows.
+        $this->assertEquals($expectedmatch, $join->cannotmatchanyrows);
+        $this->assertEquals($expectedcount, $countrecords);
+    }
+
+    /**
+     * Data provider for test_get_enrolled_with_capabilities_join_cannotmatchanyrows
+     *
+     * @return @array of testing scenarios
+     */
+    public function get_enrolled_with_capabilities_join_cannotmatchanyrows_data() {
+        return [
+            'no prohibits, no capability' => [
+                'capability' => '',
+                'useprohibit' => false,
+                'expectedmatch' => 0,
+                'expectedcount' => 1,
+            ],
+            'no prohibits with capability' => [
+                'capability' => 'moodle/course:manageactivities',
+                'useprohibit' => false,
+                'expectedmatch' => 0,
+                'expectedcount' => 1,
+            ],
+            'prohibits with capability' => [
+                'capability' => 'moodle/course:manageactivities',
+                'useprohibit' => true,
+                'expectedmatch' => 1,
+                'expectedcount' => 0,
+            ],
+        ];
+    }
 }
diff --git a/grade/classes/external/create_gradecategories.php b/grade/classes/external/create_gradecategories.php
new file mode 100644 (file)
index 0000000..924bf4d
--- /dev/null
@@ -0,0 +1,241 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_grades\external;
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use external_single_structure;
+use external_multiple_structure;
+use external_warnings;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once("$CFG->libdir/externallib.php");
+require_once("$CFG->libdir/gradelib.php");
+require_once("$CFG->dirroot/grade/edit/tree/lib.php");
+
+/**
+ * Create gradecategories webservice.
+ *
+ * @package    core_grades
+ * @copyright  2021 Peter Burnett <peterburnett@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.11
+ */
+class create_gradecategories extends external_api {
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.11
+     */
+    public static function execute_parameters(): external_function_parameters {
+        return new external_function_parameters(
+            [
+                'courseid' => new external_value(PARAM_INT, 'id of course', VALUE_REQUIRED),
+                'categories' => new external_multiple_structure(
+                    new external_single_structure([
+                        'fullname' => new external_value(PARAM_TEXT, 'fullname of category', VALUE_REQUIRED),
+                        'options' => new external_single_structure([
+                            'aggregation' => new external_value(PARAM_INT, 'aggregation method', VALUE_OPTIONAL),
+                            'aggregateonlygraded' => new external_value(PARAM_BOOL, 'exclude empty grades', VALUE_OPTIONAL),
+                            'aggregateoutcomes' => new external_value(PARAM_BOOL, 'aggregate outcomes', VALUE_OPTIONAL),
+                            'droplow' => new external_value(PARAM_INT, 'drop low grades', VALUE_OPTIONAL),
+                            'itemname' => new external_value(PARAM_TEXT, 'the category total name', VALUE_OPTIONAL),
+                            'iteminfo' => new external_value(PARAM_TEXT, 'the category iteminfo', VALUE_OPTIONAL),
+                            'idnumber' => new external_value(PARAM_TEXT, 'the category idnumber', VALUE_OPTIONAL),
+                            'gradetype' => new external_value(PARAM_INT, 'the grade type', VALUE_OPTIONAL),
+                            'grademax' => new external_value(PARAM_INT, 'the grade max', VALUE_OPTIONAL),
+                            'grademin' => new external_value(PARAM_INT, 'the grade min', VALUE_OPTIONAL),
+                            'gradepass' => new external_value(PARAM_INT, 'the grade to pass', VALUE_OPTIONAL),
+                            'display' => new external_value(PARAM_INT, 'the display type', VALUE_OPTIONAL),
+                            'decimals' => new external_value(PARAM_INT, 'the decimal count', VALUE_OPTIONAL),
+                            'hiddenuntil' => new external_value(PARAM_INT, 'grades hidden until', VALUE_OPTIONAL),
+                            'locktime' => new external_value(PARAM_INT, 'lock grades after', VALUE_OPTIONAL),
+                            'weightoverride' => new external_value(PARAM_BOOL, 'weight adjusted', VALUE_OPTIONAL),
+                            'aggregationcoef2' => new external_value(PARAM_RAW, 'weight coefficient', VALUE_OPTIONAL),
+                            'parentcategoryid' => new external_value(PARAM_INT, 'The parent category id', VALUE_OPTIONAL),
+                            'parentcategoryidnumber' => new external_value(PARAM_TEXT,
+                                'the parent category idnumber', VALUE_OPTIONAL),
+                        ], 'optional category data', VALUE_DEFAULT, []),
+                    ], 'Category to create', VALUE_REQUIRED)
+                , 'Categories to create', VALUE_REQUIRED)
+            ]
+        );
+    }
+
+    /**
+     * Creates gradecategories inside of the specified course.
+     *
+     * @param int $courseid the courseid to create the gradecategory in.
+     * @param array $categories the categories to create.
+     * @return array array of created categoryids and warnings.
+     * @since Moodle 3.11
+     */
+    public static function execute(int $courseid, array $categories): array {
+        $params = self::validate_parameters(self::execute_parameters(),
+            ['courseid' => $courseid, 'categories' => $categories]);
+
+        // Now params are validated, update the references.
+        $courseid = $params['courseid'];
+        $categories = $params['categories'];
+
+        // Check that the context and permissions are OK.
+        $context = \context_course::instance($courseid);
+        self::validate_context($context);
+        require_capability('moodle/grade:manage', $context);
+
+        return self::create_gradecategories_from_data($courseid, $categories);
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_single_structure
+     * @since Moodle 3.11
+     */
+    public static function execute_returns(): external_single_structure {
+        return new external_single_structure([
+            'categoryids' => new external_multiple_structure(
+                new external_value(PARAM_INT, 'created cateogry ID')
+            ),
+            'warnings' => new external_warnings(),
+        ]);
+    }
+
+    /**
+     * Takes an array of categories and creates the inside the category tree for the supplied courseid.
+     *
+     * @param int $courseid the courseid to create the categories inside of.
+     * @param array $categories the categories to create.
+     * @return array array of results and warnings.
+     */
+    public static function create_gradecategories_from_data(int $courseid, array $categories): array {
+        global $CFG, $DB;
+
+        $defaultparentcat = \grade_category::fetch_course_category($courseid);
+        // Setup default data so WS call needs to contain only data to set.
+        // This is not done in the Parameters, so that the array of options can be optional.
+        $defaultdata = [
+            'aggregation' => grade_get_setting($courseid, 'aggregation', $CFG->grade_aggregation, true),
+            'aggregateonlygraded' => 1,
+            'aggregateoutcomes' => 0,
+            'droplow' => 0,
+            'grade_item_itemname' => '',
+            'grade_item_iteminfo' => '',
+            'grade_item_idnumber' => '',
+            'grade_item_gradetype' => GRADE_TYPE_VALUE,
+            'grade_item_grademax' => 100,
+            'grade_item_grademin' => 1,
+            'grade_item_gradepass' => 1,
+            'grade_item_display' => GRADE_DISPLAY_TYPE_DEFAULT,
+            // Hack. This must be -2 to use the default setting.
+            'grade_item_decimals' => -2,
+            'grade_item_hiddenuntil' => 0,
+            'grade_item_locktime' => 0,
+            'grade_item_weightoverride' => 0,
+            'grade_item_aggregationcoef2' => 0,
+            'parentcategory' => $defaultparentcat->id
+        ];
+
+        // Most of the data items need boilerplate prepended. These are the exceptions.
+        $ignorekeys = [
+            'aggregation',
+            'aggregateonlygraded',
+            'aggregateoutcomes',
+            'droplow',
+            'parentcategoryid',
+            'parentcategoryidnumber'
+        ];
+
+        $createdcats = [];
+        foreach ($categories as $category) {
+            // Setup default data so WS call needs to contain only data to set.
+            // This is not done in the Parameters, so that the array of options can be optional.
+            $data = $defaultdata;
+            $data['fullname'] = $category['fullname'];
+
+            foreach ($category['options'] as $key => $value) {
+                if (!in_array($key, $ignorekeys)) {
+                    $fullkey = 'grade_item_' . $key;
+                    $data[$fullkey] = $value;
+                } else {
+                    $data[$key] = $value;
+                }
+            }
+
+            // Handle parent category special case.
+            // This figures the parent category id from the provided id OR idnumber.
+            if (array_key_exists('parentcategoryid', $category['options']) && $parentcat = $DB->get_record('grade_categories',
+                    ['id' => $category['options']['parentcategoryid'], 'courseid' => $courseid])) {
+                $data['parentcategory'] = $parentcat->id;
+            } else if (array_key_exists('parentcategoryidnumber', $category['options']) &&
+                    $parentcatgradeitem = $DB->get_record('grade_items', [
+                        'itemtype' => 'category',
+                        'courseid' => $courseid,
+                        'idnumber' => $category['options']['parentcategoryidnumber']
+                    ], '*', IGNORE_MULTIPLE)) {
+                if ($parentcat = $DB->get_record('grade_categories',
+                        ['courseid' => $courseid, 'id' => $parentcatgradeitem->iteminstance])) {
+                    $data['parentcategory'] = $parentcat->id;
+                }
+            }
+
+            // Create new gradecategory item.
+            $gradecategory = new \grade_category(['courseid' => $courseid], false);
+            $gradecategory->apply_default_settings();
+            $gradecategory->apply_forced_settings();
+
+            // Data Validation.
+            if (array_key_exists('grade_item_gradetype', $data) and $data['grade_item_gradetype'] == GRADE_TYPE_SCALE) {
+                if (empty($data['grade_item_scaleid'])) {
+                    $warnings[] = ['item' => 'scaleid', 'warningcode' => 'invalidscale',
+                        'message' => get_string('missingscale', 'grades')];
+                }
+            }
+            if (array_key_exists('grade_item_grademin', $data) and array_key_exists('grade_item_grademax', $data)) {
+                if (($data['grade_item_grademax'] != 0 OR $data['grade_item_grademin'] != 0) AND
+                    ($data['grade_item_grademax'] == $data['grade_item_grademin'] OR
+                    $data['grade_item_grademax'] < $data['grade_item_grademin'])) {
+                    $warnings[] = ['item' => 'grademax', 'warningcode' => 'invalidgrade',
+                        'message' => get_string('incorrectminmax', 'grades')];
+                }
+            }
+
+            if (!empty($warnings)) {
+                return ['categoryids' => [], 'warnings' => $warnings];
+            }
+
+            // Now call the update function with data. Transactioned so the gradebook isn't broken on bad data.
+            // This is done per-category so that children can correctly discover the parent categories.
+            try {
+                $transaction = $DB->start_delegated_transaction();
+                \grade_edit_tree::update_gradecategory($gradecategory, (object) $data);
+                $transaction->allow_commit();
+                $createdcats[] = $gradecategory->id;
+            } catch (\Exception $e) {
+                // If the submitted data was broken for any reason.
+                $warnings['database'] = $e->getMessage();
+                $transaction->rollback();
+                return ['warnings' => $warnings];
+            }
+        }
+
+        return['categoryids' => $createdcats, 'warnings' => []];
+    }
+}
index d43220e..1847d07 100644 (file)
@@ -83,7 +83,7 @@ class behat_grading extends behat_base {
     public function i_go_to_activity_advanced_grading_page($userfullname, $activityname) {
 
         // Step to access the user grade page from the grading page.
-        $gradetext = get_string('grade');
+        $gradetext = get_string('gradeverb');
 
         $this->execute('behat_general::click_link', $this->escape($activityname));
 
index df8876a..2167eb2 100644 (file)
@@ -240,6 +240,10 @@ class gradeimport_csv_load_data {
                 $select = "{$field} = :{$field}";
             }
 
+            // Validate if the user id value is numerical.
+            if ($field === 'id' && !is_numeric($value)) {
+                $errorkey = 'usermappingerror';
+            }
             // Make sure the record exists and that there's only one matching record found.
             $user = $DB->get_record_select('user', $select, array($userfields['field'] => $value), '*', MUST_EXIST);
         } catch (dml_missing_record_exception $missingex) {
index 2e8c8d3..0ae85d9 100644 (file)
@@ -3330,14 +3330,10 @@ abstract class grade_helper {
         // Sets the list of custom profile fields
         $customprofilefields = array_map('trim', explode(',', $CFG->grade_export_customprofilefields));
         if ($includecustomfields && !empty($customprofilefields)) {
-            list($wherefields, $whereparams) = $DB->get_in_or_equal($customprofilefields);
-            $customfields = $DB->get_records_sql("SELECT f.*
-                                                    FROM {user_info_field} f
-                                                    JOIN {user_info_category} c ON f.categoryid=c.id
-                                                    WHERE f.shortname $wherefields
-                                                    ORDER BY c.sortorder ASC, f.sortorder ASC", $whereparams);
-
-            foreach ($customfields as $field) {
+            $customfields = profile_get_user_fields_with_data(0);
+
+            foreach ($customfields as $fieldobj) {
+                $field = (object)$fieldobj->get_field_config_for_external();
                 // Make sure we can display this custom field
                 if (!in_array($field->shortname, $customprofilefields)) {
                     continue;
index 167de2d..f7212f7 100644 (file)
@@ -87,7 +87,7 @@ class provider implements
      * @param   int         $userid The userid of the user whose data is to be exported.
      */
     public static function export_user_preferences(int $userid) {
-        $preferences = get_user_preferences();
+        $preferences = get_user_preferences(null, null, $userid);
         foreach ($preferences as $name => $value) {
             $prefname = null;
             $prefdescription = null;
index c411a84..97095a0 100644 (file)
@@ -658,7 +658,6 @@ class grade_report_grader extends grade_report {
         $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
 
         $strfeedback  = $this->get_lang_string("feedback");
-        $strgrade     = $this->get_lang_string('grade');
 
         // TODO Does not support custom user profile fields (MDL-70456).
         $extrafields = \core_user\fields::get_identity_fields($this->context, false);
@@ -813,7 +812,7 @@ class grade_report_grader extends grade_report {
         $numusers = count($this->users);
         $gradetabindex = 1;
         $columnstounset = array();
-        $strgrade = $this->get_lang_string('grade');
+        $strgrade = $this->get_lang_string('gradenoun');
         $strfeedback  = $this->get_lang_string("feedback");
         $arrows = $this->get_sort_arrows();
 
index 5c8b7a5..80ffaae 100644 (file)
@@ -63,14 +63,18 @@ class gradereport_grader_privacy_testcase extends \core_privacy\tests\provider_t
      * These preferences can be set on each course, but the value is shared in the whole site.
      */
     public function test_export_user_preferences_single() {
-        // Add some user preferences.
+        // Create test user, add some preferences.
         $user = $this->getDataGenerator()->create_user();
         $this->setUser($user);
+
         set_user_preference('grade_report_showcalculations', 1, $user);
         set_user_preference('grade_report_meanselection', GRADE_REPORT_MEAN_GRADED, $user);
         set_user_preference('grade_report_studentsperpage', 50, $user);
 
-        // Validate exported data.
+        // Switch to admin user (so we can validate preferences of our test user are still exported).
+        $this->setAdminUser();
+
+        // Validate exported data for our test user.
         provider::export_user_preferences($user->id);
         $context = context_user::instance($user->id);
         $writer = writer::with_context($context);
index e763776..b50c0c6 100644 (file)
@@ -144,12 +144,12 @@ class grade_report_overview extends grade_report {
         if ($this->showrank['any']) {
             $tablecolumns = array('coursename', 'grade', 'rank');
             $tableheaders = array($this->get_lang_string('coursename', 'grades'),
-                                  $this->get_lang_string('grade'),
+                                  $this->get_lang_string('gradenoun'),
                                   $this->get_lang_string('rank', 'grades'));
         } else {
             $tablecolumns = array('coursename', 'grade');
             $tableheaders = array($this->get_lang_string('coursename', 'grades'),
-                                  $this->get_lang_string('grade'));
+                                  $this->get_lang_string('gradenoun'));
         }
         $this->table = new flexible_table('grade-report-overview-'.$this->user->id);