Merge branch 'master_MDL-66864' of https://github.com/golenkovm/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Sun, 13 Oct 2019 00:08:10 +0000 (02:08 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Sun, 13 Oct 2019 00:08:10 +0000 (02:08 +0200)
207 files changed:
admin/registration/confirmregistration.php
admin/registration/forms.php
admin/registration/index.php
admin/registration/renewregistration.php
admin/renderer.php
admin/settings/analytics.php
admin/settings/appearance.php
admin/settings/subsystems.php
admin/tool/analytics/classes/output/insights_report.php [moved from admin/tool/analytics/classes/output/effectiveness_report.php with 96% similarity]
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/classes/output/renderer.php
admin/tool/analytics/classes/task/predict_models.php
admin/tool/analytics/classes/task/train_models.php
admin/tool/analytics/cli/enable_model.php
admin/tool/analytics/cli/evaluate_model.php
admin/tool/analytics/createmodel.php
admin/tool/analytics/importmodel.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/restoredefault.php
admin/tool/analytics/settings.php
admin/tool/analytics/templates/insights_report.mustache [moved from admin/tool/analytics/templates/effectiveness_report.mustache with 90% similarity]
admin/tool/behat/tests/behat/get_and_set_fields_in_containers.feature [new file with mode: 0644]
admin/tool/lp/classes/output/manage_competency_frameworks_page.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/monitor/lib.php
admin/tool/usertours/classes/manager.php
analytics/classes/manager.php
backup/cc/cc2moodle.php
backup/controller/backup_controller.class.php
backup/moodle2/backup_plugin.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_plugin.class.php
backup/moodle2/restore_stepslib.php
backup/util/plan/restore_structure_step.class.php
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/build/view.min.js.map
blocks/myoverview/amd/build/view_nav.min.js
blocks/myoverview/amd/build/view_nav.min.js.map
blocks/myoverview/amd/src/view.js
blocks/myoverview/amd/src/view_nav.js
blocks/myoverview/block_myoverview.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php
blocks/myoverview/settings.php
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/nav-grouping-selector.mustache
blocks/myoverview/tests/behat/block_myoverview_customfield.feature [new file with mode: 0644]
blocks/myoverview/version.php
blocks/online_users/block_online_users.php
blocks/online_users/classes/fetcher.php
blocks/online_users/lang/en/block_online_users.php
blocks/online_users/settings.php
blocks/online_users/tests/behat/block_online_users_course.feature
blocks/online_users/tests/behat/block_online_users_dashboard.feature
blocks/online_users/tests/behat/block_online_users_frontpage.feature
blocks/online_users/tests/online_users_test.php
blocks/online_users/version.php
blocks/participants/block_participants.php [deleted file]
blocks/participants/tests/behat/block_participants_course.feature [deleted file]
blocks/participants/tests/behat/block_participants_frontpage.feature [deleted file]
blocks/tests/externallib_test.php
blocks/upgrade.txt
blog/classes/privacy/provider.php
blog/tests/privacy_test.php
cache/stores/redis/addinstanceform.php
cache/stores/redis/lang/en/cachestore_redis.php
cache/stores/redis/lib.php
cache/stores/redis/tests/compressor_test.php [new file with mode: 0644]
cache/upgrade.txt
config-dist.php
course/externallib.php
course/lib.php
course/tests/courselib_test.php
customfield/classes/api.php
customfield/classes/field_controller.php
customfield/field/checkbox/classes/field_controller.php
customfield/field/date/classes/field_controller.php
customfield/field/select/classes/field_controller.php
customfield/field/text/classes/field_controller.php
customfield/field/upgrade.txt [new file with mode: 0644]
customfield/tests/api_test.php
customfield/tests/category_controller_test.php
customfield/tests/data_controller_test.php
customfield/tests/field_controller_test.php
customfield/tests/generator_test.php
customfield/tests/privacy_test.php
enrol/tests/enrollib_test.php
index.php
install/lang/fr/install.php
install/lang/nl/install.php
lang/en/admin.php
lang/en/analytics.php
lang/en/deprecated.txt
lang/en/hub.php
lib/amd/build/str.min.js
lib/amd/build/str.min.js.map
lib/amd/src/str.js
lib/classes/hub/api.php
lib/classes/hub/registration.php
lib/classes/hub/site_registration_form.php
lib/classes/hub/site_unregistration_form.php [deleted file]
lib/classes/plugin_manager.php
lib/classes/task/analytics_cleanup_task.php
lib/classes/useragent.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/dml/tests/dml_table_test.php
lib/enrollib.php
lib/filelib.php
lib/filestorage/tests/file_storage_test.php
lib/filestorage/tests/file_system_filedir_test.php
lib/filestorage/tests/file_system_test.php
lib/form/templates/element-select-inline.mustache
lib/form/templates/element-select.mustache
lib/form/templates/element-selectgroups-inline.mustache
lib/form/templates/element-selectgroups.mustache
lib/form/templates/element-selectwithlink.mustache
lib/moodlelib.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/filelib_test.php
lib/tests/upgradelib_test.php
lib/tests/useragent_test.php
message/amd/build/message_send_bulk.min.js [new file with mode: 0644]
message/amd/build/message_send_bulk.min.js.map [new file with mode: 0644]
message/amd/src/message_send_bulk.js [new file with mode: 0644]
message/templates/message_drawer_view_conversation_body.mustache
message/templates/send_bulk_message.mustache [new file with mode: 0644]
mod/forum/classes/plugininfo/forumreport.php [moved from blocks/participants/lang/en/block_participants.php with 56% similarity]
mod/forum/db/subplugins.json [new file with mode: 0644]
mod/forum/lib.php
mod/forum/report/summary/amd/build/filters.min.js [new file with mode: 0644]
mod/forum/report/summary/amd/build/filters.min.js.map [new file with mode: 0644]
mod/forum/report/summary/amd/build/selectors.min.js [new file with mode: 0644]
mod/forum/report/summary/amd/build/selectors.min.js.map [new file with mode: 0644]
mod/forum/report/summary/amd/src/filters.js [new file with mode: 0644]
mod/forum/report/summary/amd/src/selectors.js [new file with mode: 0644]
mod/forum/report/summary/classes/output/filters.php [new file with mode: 0644]
mod/forum/report/summary/classes/privacy/provider.php [moved from blocks/participants/classes/privacy/provider.php with 77% similarity]
mod/forum/report/summary/classes/summary_table.php [new file with mode: 0644]
mod/forum/report/summary/db/access.php [moved from blocks/participants/db/access.php with 57% similarity]
mod/forum/report/summary/index.php [new file with mode: 0644]
mod/forum/report/summary/lang/en/forumreport_summary.php [new file with mode: 0644]
mod/forum/report/summary/renderer.php [new file with mode: 0644]
mod/forum/report/summary/templates/bulk_action_menu.mustache [new file with mode: 0644]
mod/forum/report/summary/templates/filters.mustache [new file with mode: 0644]
mod/forum/report/summary/templates/report.mustache [new file with mode: 0644]
mod/forum/report/summary/tests/behat/bulk_message.feature [new file with mode: 0644]
mod/forum/report/summary/tests/behat/summary_data_attachments.feature [new file with mode: 0644]
mod/forum/report/summary/tests/behat/summary_data_post_dates.feature [new file with mode: 0644]
mod/forum/report/summary/version.php [moved from blocks/participants/version.php with 68% similarity]
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/forum_export.feature [new file with mode: 0644]
mod/forum/tests/managers_capability_test.php
mod/forum/tests/vaults_forum_test.php
mod/forum/tests/vaults_post_test.php
mod/forum/version.php
mod/quiz/attemptlib.php
mod/quiz/renderer.php
mod/quiz/tests/behat/attempt_redo_questions.feature
mod/quiz/tests/behat/backup.feature
mod/quiz/tests/behat/behat_mod_quiz.php
phpunit.xml.dist
privacy/tests/approved_contextlist_test.php
privacy/tests/approved_userlist_test.php
privacy/tests/collection_test.php
privacy/tests/contextlist_base_test.php
privacy/tests/contextlist_collection_test.php
privacy/tests/contextlist_test.php
privacy/tests/manager_test.php
privacy/tests/moodle_content_writer_test.php
privacy/tests/writer_test.php
question/engine/datalib.php
question/engine/questionusage.php
question/engine/states.php
question/engine/tests/datalib_reporting_queries_test.php
question/format.php
question/format/xml/format.php
question/format/xml/tests/fixtures/category_with_description.xml
question/format/xml/tests/fixtures/export_category.xml
question/format/xml/tests/fixtures/nested_categories.xml
question/format/xml/tests/fixtures/nested_categories_with_questions.xml
question/format/xml/tests/qformat_xml_import_export_test.php
question/format/xml/tests/xmlformat_test.php
report/insights/action.php
report/insights/classes/output/renderer.php
report/insights/done.php
report/insights/insights.php
report/insights/lib.php
report/insights/prediction.php
report/insights/settings.php
search/index.php
theme/boost/lang/en/theme_boost.php
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/modules.scss
theme/boost/style/moodle.css
theme/classic/lang/en/theme_classic.php
theme/classic/style/moodle.css
user/classes/privacy/provider.php
user/tests/privacy_test.php
version.php

index 7c9d422..ff99680 100644 (file)
@@ -46,7 +46,7 @@ $error = optional_param('error', '', PARAM_ALPHANUM);
 admin_externalpage_setup('registrationmoodleorg');
 
 if ($url !== HUB_MOODLEORGHUBURL) {
-    // Allow other plugins to confirm registration on hubs other than moodle.net . Plugins implementing this
+    // Allow other plugins to confirm registration on custom hubs. Plugins implementing this
     // callback need to redirect or exit. See https://docs.moodle.org/en/Hub_registration .
     $callbacks = get_plugins_with_function('hub_registration');
     foreach ($callbacks as $plugintype => $plugins) {
index 6fe9553..af05a7a 100644 (file)
@@ -32,5 +32,4 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-debugging('Support for alternative hubs has been removed from Moodle in 3.4. For communication with moodle.net ' .
-    'see lib/classes/hub/ .', DEBUG_DEVELOPER);
+debugging('Support for alternative hubs has been removed from Moodle in 3.4.', DEBUG_DEVELOPER);
index 62f251a..ede6a67 100644 (file)
@@ -22,7 +22,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
  * @copyright  (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
  *
- * This page displays the site registration form for Moodle.net.
+ * This page displays the site registration form.
  * It handles redirection to the hub to continue the registration workflow process.
  * It also handles update operation by web service.
  */
@@ -32,24 +32,25 @@ require_once($CFG->libdir . '/adminlib.php');
 
 admin_externalpage_setup('registrationmoodleorg');
 
-$unregistration = optional_param('unregistration', 0, PARAM_INT);
+$unregistration = optional_param('unregistration', false, PARAM_BOOL);
+$confirm = optional_param('confirm', false, PARAM_BOOL);
 
 if ($unregistration && \core\hub\registration::is_registered()) {
-    $siteunregistrationform = new \core\hub\site_unregistration_form();
+    if ($confirm) {
+        require_sesskey();
+        \core\hub\registration::unregister(false, false);
 
-    if ($siteunregistrationform->is_cancelled()) {
-        redirect(new moodle_url('/admin/registration/index.php'));
-    } else if ($data = $siteunregistrationform->get_data()) {
-        \core\hub\registration::unregister($data->unpublishalladvertisedcourses,
-            $data->unpublishalluploadedcourses);
         if (!\core\hub\registration::is_registered()) {
             redirect(new moodle_url('/admin/registration/index.php'));
         }
     }
 
     echo $OUTPUT->header();
-    echo $OUTPUT->heading(get_string('unregisterfrom', 'hub', 'Moodle.net'), 3, 'main');
-    $siteunregistrationform->display();
+    echo $OUTPUT->confirm(
+        get_string('registerwithmoodleorgremove', 'core_hub'),
+        new moodle_url(new moodle_url('/admin/registration/index.php', ['unregistration' => 1, 'confirm' => 1])),
+        new moodle_url(new moodle_url('/admin/registration/index.php'))
+    );
     echo $OUTPUT->footer();
     exit;
 }
@@ -82,7 +83,7 @@ if ($fromform = $siteregistrationform->get_data()) {
 
 echo $OUTPUT->header();
 
-// Current status of registration on Moodle.net.
+// Current status of registration.
 
 $notificationtype = \core\output\notification::NOTIFY_ERROR;
 if (\core\hub\registration::is_registered()) {
@@ -104,11 +105,11 @@ if (\core\hub\registration::is_registered()) {
 
 // Heading.
 if (\core\hub\registration::is_registered()) {
-    echo $OUTPUT->heading(get_string('updatesite', 'hub', 'Moodle.net'));
+    echo $OUTPUT->heading(get_string('registerwithmoodleorgupdate', 'core_hub'));
 } else if ($isinitialregistration) {
-    echo $OUTPUT->heading(get_string('completeregistration', 'hub'));
+    echo $OUTPUT->heading(get_string('registerwithmoodleorgcomplete', 'core_hub'));
 } else {
-    echo $OUTPUT->heading(get_string('registerwithmoodleorg', 'admin'));
+    echo $OUTPUT->heading(get_string('registerwithmoodleorg', 'core_hub'));
 }
 
 $renderer = $PAGE->get_renderer('core', 'admin');
@@ -119,8 +120,9 @@ $siteregistrationform->display();
 if (\core\hub\registration::is_registered()) {
     // Unregister link.
     $unregisterhuburl = new moodle_url("/admin/registration/index.php", ['unregistration' => 1]);
-    echo html_writer::div(html_writer::link($unregisterhuburl, get_string('unregister', 'hub')), 'unregister');
+    echo html_writer::div(html_writer::link($unregisterhuburl, get_string('unregister', 'hub')), 'unregister mt-2');
 } else if ($isinitialregistration) {
-    echo html_writer::div(html_writer::link(new moodle_url($returnurl), get_string('skipregistration', 'hub')), 'skipregistration');
+    echo html_writer::div(html_writer::link(new moodle_url($returnurl), get_string('skipregistration', 'hub')),
+        'skipregistration mt-2');
 }
 echo $OUTPUT->footer();
index 84e727b..bc05da6 100644 (file)
@@ -40,7 +40,7 @@ $token = optional_param('token', '', PARAM_TEXT);
 admin_externalpage_setup('registrationmoodleorg');
 
 if ($url !== HUB_MOODLEORGHUBURL) {
-    // Allow other plugins to renew registration on hubs other than moodle.net . Plugins implementing this
+    // Allow other plugins to renew registration on custom hubs. Plugins implementing this
     // callback need to redirect or exit. See https://docs.moodle.org/en/Hub_registration .
     $callbacks = get_plugins_with_function('hub_registration');
     foreach ($callbacks as $plugintype => $plugins) {
@@ -56,7 +56,7 @@ if ($url !== HUB_MOODLEORGHUBURL) {
 
 echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('renewregistration', 'hub'), 3, 'main');
-$hublink = html_writer::tag('a', 'Moodle.net', array('href' => HUB_MOODLEORGHUBURL));
+$hublink = html_writer::tag('a', HUB_MOODLEORGHUBURL, array('href' => HUB_MOODLEORGHUBURL));
 
 $deletedregmsg = get_string('previousregistrationdeleted', 'hub', $hublink);
 
index 773ba4d..5abd08e 100644 (file)
@@ -2112,6 +2112,27 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string
      */
     public function moodleorg_registration_message() {
-        return format_text(get_string('registermoodlenet', 'admin'), FORMAT_HTML, ['noclean' => true]);
+
+        $out = format_text(get_string('registerwithmoodleorginfo', 'core_hub'), FORMAT_MARKDOWN);
+
+        $out .= html_writer::link(
+            new moodle_url('/admin/settings.php', ['section' => 'moodleservices']),
+            $this->output->pix_icon('i/info', '').' '.get_string('registerwithmoodleorginfoapp', 'core_hub'),
+            ['class' => 'btn btn-link', 'role' => 'opener', 'target' => '_href']
+        );
+
+        $out .= html_writer::link(
+            HUB_MOODLEORGHUBURL,
+            $this->output->pix_icon('i/stats', '').' '.get_string('registerwithmoodleorginfostats', 'core_hub'),
+            ['class' => 'btn btn-link', 'role' => 'opener', 'target' => '_href']
+        );
+
+        $out .= html_writer::link(
+            HUB_MOODLEORGHUBURL.'/sites',
+            $this->output->pix_icon('i/location', '').' '.get_string('registerwithmoodleorginfosites', 'core_hub'),
+            ['class' => 'btn btn-link', 'role' => 'opener', 'target' => '_href']
+        );
+
+        return $this->output->box($out);
     }
 }
index 5270745..a722244 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-if ($hassiteconfig) {
+if ($hassiteconfig && \core_analytics\manager::is_analytics_enabled()) {
 
     $settings = new admin_settingpage('analyticssite', new lang_string('analyticssiteinfo', 'analytics'));
     $ADMIN->add('analytics', $settings);
index dc924f7..c9d406a 100644 (file)
@@ -197,6 +197,10 @@ preferences,moodle|/user/preferences.php|t/preferences',
         'idnumber' => new lang_string('sort_idnumber', 'admin'),
     );
     $temp->add(new admin_setting_configselect('navsortmycoursessort', new lang_string('navsortmycoursessort', 'admin'), new lang_string('navsortmycoursessort_help', 'admin'), 'sortorder', $sortoptions));
+    $temp->add(new admin_setting_configcheckbox('navsortmycourseshiddenlast',
+            new lang_string('navsortmycourseshiddenlast', 'admin'),
+            new lang_string('navsortmycourseshiddenlast_help', 'admin'),
+            1));
     $temp->add(new admin_setting_configtext('navcourselimit', new lang_string('navcourselimit', 'admin'),
         new lang_string('confignavcourselimit', 'admin'), 10, PARAM_INT));
     $temp->add(new admin_setting_configcheckbox('usesitenameforsitepages', new lang_string('usesitenameforsitepages', 'admin'), new lang_string('configusesitenameforsitepages', 'admin'), 0));
index de5f75b..597e36b 100644 (file)
@@ -48,4 +48,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('allowstealth', new lang_string('allowstealthmodules'),
         new lang_string('allowstealthmodules_help'), 0, 1, 0));
+
+    $optionalsubsystems->add(new admin_setting_configcheckbox('enableanalytics', new lang_string('enableanalytics', 'admin'),
+        new lang_string('configenableanalytics', 'admin'), 1, 1, 0));
 }
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Effectiveness report renderable.
+ * Insights report renderable.
  *
  * @package    tool_analytics
  * @copyright  2019 David Monllao {@link http://www.davidmonllao.com}
@@ -27,13 +27,13 @@ namespace tool_analytics\output;
 defined('MOODLE_INTERNAL') || die;
 
 /**
- * Effectiveness report renderable.
+ * Insights report renderable.
  *
  * @package    tool_analytics
  * @copyright  2019 David Monllao {@link http://www.davidmonllao.com}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class effectiveness_report implements \renderable, \templatable {
+class insights_report implements \renderable, \templatable {
 
     /**
      * @var \core_analytics\model
@@ -46,7 +46,7 @@ class effectiveness_report implements \renderable, \templatable {
     private $context = null;
 
     /**
-     * Inits the effectiveness report renderable.
+     * Inits the insights report renderable.
      *
      * @param \core_analytics\model $model
      * @param int|null $contextid
@@ -79,7 +79,7 @@ class effectiveness_report implements \renderable, \templatable {
         $predictioncontexts = $this->model->get_predictions_contexts(false);
         if ($predictioncontexts && count($predictioncontexts) > 1) {
             $url = new \moodle_url('/admin/tool/analytics/model.php', ['id' => $this->model->get_id(),
-                'action' => 'effectivenessreport']);
+                'action' => 'insightsreport']);
 
             if ($this->context) {
                 $selected = $this->context->id;
index be1c7ba..ade6739 100644 (file)
@@ -288,12 +288,12 @@ class models_list implements \renderable, \templatable {
                 }
             }
 
-            // Effectivity report.
+            // Insights report.
             if (!empty($anypredictionobtained) && $model->uses_insights()) {
-                $urlparams['action'] = 'effectivenessreport';
+                $urlparams['action'] = 'insightsreport';
                 $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
-                $pix = new \pix_icon('i/report', get_string('effectivenessreport', 'tool_analytics'));
-                $icon = new \action_menu_link_secondary($url, $pix, get_string('effectivenessreport', 'tool_analytics'));
+                $pix = new \pix_icon('i/report', get_string('insightsreport', 'tool_analytics'));
+                $icon = new \action_menu_link_secondary($url, $pix, get_string('insightsreport', 'tool_analytics'));
                 $actionsmenu->add($icon);
             }
 
index 60bc2a2..edd165d 100644 (file)
@@ -211,12 +211,12 @@ class renderer extends plugin_renderer_base {
     /**
      * Defer to template.
      *
-     * @param \tool_analytics\output\effectiveness_report $effectivenessreport
+     * @param \tool_analytics\output\insights_report $insightsreport
      * @return string HTML
      */
-    protected function render_effectiveness_report(\tool_analytics\output\effectiveness_report $effectivenessreport): string {
-        $data = $effectivenessreport->export_for_template($this);
-        return parent::render_from_template('tool_analytics/effectiveness_report', $data);
+    protected function render_insights_report(\tool_analytics\output\insights_report $insightsreport): string {
+        $data = $insightsreport->export_for_template($this);
+        return parent::render_from_template('tool_analytics/insights_report', $data);
     }
 
     /**
@@ -229,4 +229,26 @@ class renderer extends plugin_renderer_base {
         $data = $invalidanalysables->export_for_template($this);
         return parent::render_from_template('tool_analytics/invalid_analysables', $data);
     }
+
+    /**
+     * Renders an analytics disabled notification.
+     *
+     * @return string HTML
+     */
+    public function render_analytics_disabled() {
+        global $OUTPUT, $PAGE, $FULLME;
+
+        $PAGE->set_url($FULLME);
+        $PAGE->set_title(get_string('pluginname', 'tool_analytics'));
+        $PAGE->set_heading(get_string('pluginname', 'tool_analytics'));
+
+        $output = $OUTPUT->header();
+        $output .= $OUTPUT->notification(get_string('analyticsdisabled', 'analytics'), \core\output\notification::NOTIFY_INFO);
+        $output .= \html_writer::tag('a', get_string('continue'), ['class' => 'btn btn-primary',
+            'href' => (new \moodle_url('/'))->out()]);
+        $output .= $OUTPUT->footer();
+
+        return $output;
+    }
+
 }
index 4551e79..20bf8d3 100644 (file)
@@ -52,6 +52,11 @@ class predict_models extends \core\task\scheduled_task {
     public function execute() {
         global $OUTPUT, $PAGE;
 
+        if (!\core_analytics\manager::is_analytics_enabled()) {
+            mtrace(get_string('analyticsdisabled', 'analytics'));
+            return;
+        }
+
         $models = \core_analytics\manager::get_all_models(true, true);
         if (!$models) {
             mtrace(get_string('errornoenabledandtrainedmodels', 'tool_analytics'));
index c9387d3..f2ce98f 100644 (file)
@@ -52,6 +52,11 @@ class train_models extends \core\task\scheduled_task {
     public function execute() {
         global $OUTPUT, $PAGE;
 
+        if (!\core_analytics\manager::is_analytics_enabled()) {
+            mtrace(get_string('analyticsdisabled', 'analytics'));
+            return;
+        }
+
         $models = \core_analytics\manager::get_all_models(true);
         if (!$models) {
             mtrace(get_string('errornoenabledmodels', 'tool_analytics'));
index ed8e3fc..5da4e1f 100644 (file)
@@ -57,6 +57,11 @@ if ($options['help']) {
     exit(0);
 }
 
+if (!\core_analytics\manager::is_analytics_enabled()) {
+    echo get_string('analyticsdisabled', 'analytics') . PHP_EOL;
+    exit(0);
+}
+
 if ($options['list'] || $options['modelid'] === false) {
     \tool_analytics\clihelper::list_models();
     exit(0);
index 601599e..3094ffa 100644 (file)
@@ -67,6 +67,11 @@ if ($options['help']) {
     exit(0);
 }
 
+if (!\core_analytics\manager::is_analytics_enabled()) {
+    echo get_string('analyticsdisabled', 'analytics') . PHP_EOL;
+    exit(0);
+}
+
 if ($options['list']) {
     \tool_analytics\clihelper::list_models();
     exit(0);
index e5b9b92..ad4fc83 100644 (file)
@@ -27,6 +27,13 @@ require_once(__DIR__ . '/../../../config.php');
 require_login();
 \core_analytics\manager::check_can_manage_models();
 
+if (!\core_analytics\manager::is_analytics_enabled()) {
+    $PAGE->set_context(\context_system::instance());
+    $renderer = $PAGE->get_renderer('tool_analytics');
+    echo $renderer->render_analytics_disabled();
+    exit(0);
+}
+
 $returnurl = new \moodle_url('/admin/tool/analytics/index.php');
 $url = new \moodle_url('/admin/tool/analytics/createmodel.php');
 $title = get_string('createmodel', 'tool_analytics');
index eedeaa0..0d4cd59 100644 (file)
@@ -27,6 +27,13 @@ require_once(__DIR__ . '/../../../config.php');
 require_login();
 \core_analytics\manager::check_can_manage_models();
 
+if (!\core_analytics\manager::is_analytics_enabled()) {
+    $PAGE->set_context(\context_system::instance());
+    $renderer = $PAGE->get_renderer('tool_analytics');
+    echo $renderer->render_analytics_disabled();
+    exit(0);
+}
+
 $returnurl = new \moodle_url('/admin/tool/analytics/index.php');
 $url = new \moodle_url('/admin/tool/analytics/importmodel.php');
 $title = get_string('importmodel', 'tool_analytics');
@@ -56,4 +63,4 @@ if ($form->is_cancelled()) {
 
 echo $OUTPUT->header();
 $form->display();
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
index 7041b83..ca3d1a2 100644 (file)
@@ -25,6 +25,7 @@
 $string['accuracy'] = 'Accuracy';
 $string['actions'] = 'Actions';
 $string['actionsexecutedbyusers'] = 'Actions executed by users';
+$string['actionsexecutedbyusersfor'] = 'Actions executed by users for "{$a}" model';
 $string['actionexecutedgroupedusefulness'] = 'Grouped actions';
 $string['allpredictions'] = 'All predictions';
 $string['alltimesplittingmethods'] = 'All analysis intervals';
@@ -51,8 +52,6 @@ $string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? Th
 $string['disabled'] = 'Disabled';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its analysis interval will delete its previous predictions and start generating new predictions.';
-$string['effectivenessreport'] = 'Effectiveness report';
-$string['effectivenessreportfor'] = 'Model "{$a}" effectiveness';
 $string['enabled'] = 'Enabled';
 $string['errorcantenablenotimesplitting'] = 'You need to select an analysis interval before enabling the model';
 $string['errornoenabledandtrainedmodels'] = 'There are no enabled and trained models to predict.';
@@ -91,6 +90,7 @@ $string['indicators_help'] = 'The indicators are what you think will lead to an
 $string['indicators_link'] = 'Indicators';
 $string['indicatorsnum'] = 'Number of indicators: {$a}';
 $string['info'] = 'Info';
+$string['insightsreport'] = 'Insights report';
 $string['ignoreversionmismatches'] = 'Ignore version mismatches';
 $string['ignoreversionmismatchescheckbox'] = 'Ignore the differences between this site version and the original site version.';
 $string['importedsuccessfully'] = 'The model has been successfully imported.';
index 653ab82..4886ff5 100644 (file)
@@ -33,6 +33,13 @@ require_login();
 $model = new \core_analytics\model($id);
 \core_analytics\manager::check_can_manage_models();
 
+if (!\core_analytics\manager::is_analytics_enabled()) {
+    $PAGE->set_context(\context_system::instance());
+    $renderer = $PAGE->get_renderer('tool_analytics');
+    echo $renderer->render_analytics_disabled();
+    exit(0);
+}
+
 $returnurl = new \moodle_url('/admin/tool/analytics/index.php');
 $params = array('id' => $id, 'action' => $action);
 $url = new \moodle_url('/admin/tool/analytics/model.php', $params);
@@ -69,8 +76,8 @@ switch ($action) {
     case 'clear':
         $title = get_string('clearpredictions', 'tool_analytics');
         break;
-    case 'effectivenessreport':
-        $title = get_string('effectivenessreport', 'tool_analytics');
+    case 'insightsreport':
+        $title = get_string('insightsreport', 'tool_analytics');
         break;
     case 'invalidanalysables':
         $title = get_string('invalidanalysables', 'tool_analytics');
@@ -282,13 +289,13 @@ switch ($action) {
         redirect($returnurl);
         break;
 
-    case 'effectivenessreport':
+    case 'insightsreport':
 
         $contextid = optional_param('contextid', null, PARAM_INT);
 
         echo $OUTPUT->header();
 
-        $renderable = new \tool_analytics\output\effectiveness_report($model, $contextid);
+        $renderable = new \tool_analytics\output\insights_report($model, $contextid);
         $renderer = $PAGE->get_renderer('tool_analytics');
         echo $renderer->render($renderable);
 
index 28b1b02..ce0f8ae 100644 (file)
@@ -27,6 +27,13 @@ require_once(__DIR__ . '/../../../config.php');
 require_login();
 \core_analytics\manager::check_can_manage_models();
 
+if (!\core_analytics\manager::is_analytics_enabled()) {
+    $PAGE->set_context(\context_system::instance());
+    $renderer = $PAGE->get_renderer('tool_analytics');
+    echo $renderer->render_analytics_disabled();
+    exit(0);
+}
+
 $confirmed = optional_param('confirmed', false, PARAM_BOOL);
 $restoreids = optional_param_array('restoreid', [], PARAM_ALPHANUM);
 
index aad459a..1f7adbe 100644 (file)
@@ -24,5 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$ADMIN->add('analytics', new admin_externalpage('analyticmodels', get_string('analyticmodels', 'tool_analytics'),
-    "$CFG->wwwroot/$CFG->admin/tool/analytics/index.php", 'moodle/analytics:managemodels'));
+if (\core_analytics\manager::is_analytics_enabled()) {
+    $ADMIN->add('analytics', new admin_externalpage('analyticmodels', get_string('analyticmodels', 'tool_analytics'),
+        "$CFG->wwwroot/$CFG->admin/tool/analytics/index.php", 'moodle/analytics:managemodels'));
+}
@@ -15,9 +15,9 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template tool_analytics/effectiveness_report
+    @template tool_analytics/insights_report
 
-    Template for the effectiveness report.
+    Template for the insights report.
 
     Classes required for JS:
     * none
@@ -39,7 +39,7 @@
 }}
 
 <div class="box">
-    <h3>{{#str}}effectivenessreportfor, tool_analytics, {{modelname}}{{/str}}</h3>
+    <h3>{{#str}}actionsexecutedbyusersfor, tool_analytics, {{modelname}}{{/str}}</h3>
 
     {{#contextselect}}
         <div class="mt-3">
diff --git a/admin/tool/behat/tests/behat/get_and_set_fields_in_containers.feature b/admin/tool/behat/tests/behat/get_and_set_fields_in_containers.feature
new file mode 100644 (file)
index 0000000..cbc84c3
--- /dev/null
@@ -0,0 +1,27 @@
+@tool_behat
+Feature: Behat steps for interacting with form work
+  In order to test my Moodle code
+  As a developer
+  I need the Behat steps for form elements to work reliably
+
+  @javascript
+  Scenario: Test fields in containers
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    When I log in as "admin"
+    And I am on "Course 1" course homepage
+    # Just get to any form.
+    And I navigate to "Edit settings" in current page administration
+    And I set the field "Course full name" in the "General" "fieldset" to "Frog"
+    And I set the following fields in the "Appearance" "fieldset" to these values:
+      | Show activity reports   | Yes |
+      | Number of announcements | 1   |
+    Then the field "Show activity reports" in the "Appearance" "fieldset" matches value "Yes"
+    And the field "Show activity reports" in the "Appearance" "fieldset" does not match value "No"
+    And the following fields in the "region-main" "region" match these values:
+      | Course full name        | Frog |
+      | Number of announcements | 1    |
+    And the following fields in the "region-main" "region" do not match these values:
+      | Course full name        | Course 1 |
+      | Number of announcements | 5        |
index 7a2181b..07a6dad 100644 (file)
@@ -75,7 +75,7 @@ class manage_competency_frameworks_page implements renderable, templatable {
             );
             $this->navigation[] = $addpage;
             $competenciesrepository = new single_button(
-                new moodle_url('https://moodle.net/competencies'),
+                new moodle_url('https://archive.moodle.net/competencies'),
                 get_string('competencyframeworksrepository', 'tool_lp'),
                 'get'
             );
index 978f950..1b34498 100644 (file)
@@ -177,6 +177,10 @@ class api {
             'langmenu' => $CFG->langmenu,
             'langlist' => $CFG->langlist,
             'locale' => $CFG->locale,
+            'tool_mobile_minimumversion' => get_config('tool_mobile', 'minimumversion'),
+            'tool_mobile_iosappid' => get_config('tool_mobile', 'iosappid'),
+            'tool_mobile_androidappid' => get_config('tool_mobile', 'androidappid'),
+            'tool_mobile_setuplink' => clean_param(get_config('tool_mobile', 'setuplink'), PARAM_URL),
         );
 
         $typeoflogin = get_config('tool_mobile', 'typeoflogin');
index 73c81ff..fe1dad0 100644 (file)
@@ -179,6 +179,13 @@ class external extends external_api {
                 'langmenu' => new external_value(PARAM_INT, 'Whether the language menu should be displayed.', VALUE_OPTIONAL),
                 'langlist' => new external_value(PARAM_RAW, 'Languages on language menu.', VALUE_OPTIONAL),
                 'locale' => new external_value(PARAM_RAW, 'Sitewide locale.', VALUE_OPTIONAL),
+                'tool_mobile_minimumversion' => new external_value(PARAM_NOTAGS, 'Minimum required version to access.',
+                    VALUE_OPTIONAL),
+                'tool_mobile_iosappid' => new external_value(PARAM_ALPHANUM, 'iOS app\'s unique identifier.',
+                    VALUE_OPTIONAL),
+                'tool_mobile_androidappid' => new external_value(PARAM_NOTAGS, 'Android app\'s unique identifier.',
+                    VALUE_OPTIONAL),
+                'tool_mobile_setuplink' => new external_value(PARAM_URL, 'App download page.', VALUE_OPTIONAL),
                 'warnings' => new external_warnings(),
             )
         );
index b02ff9e..5c7686a 100644 (file)
@@ -75,6 +75,8 @@ $string['loginintheapp'] = 'Via the app';
 $string['logininthebrowser'] = 'Via a browser window (for SSO plugins)';
 $string['loginintheembeddedbrowser'] = 'Via an embedded browser (for SSO plugins)';
 $string['mainmenu'] = 'Main menu';
+$string['minimumversion'] = 'Require users to upgrade their apps to the minimum version indicated. Those using previous versions of the app will not be able to access to the site. This works since app version 3.8.0 onward.';
+$string['minimumversion_key'] = 'Minimum app version required';
 $string['mobileapp'] = 'Mobile app';
 $string['mobileappconnected'] = 'Mobile app connected';
 $string['mobileappenabled'] = 'This site has mobile app access enabled.<br /><a href="{$a}">Download the mobile app</a>.';
index b5033d8..2121e84 100644 (file)
@@ -65,6 +65,10 @@ if ($hassiteconfig) {
                     new lang_string('forcedurlscheme_key', 'tool_mobile'),
                     new lang_string('forcedurlscheme', 'tool_mobile'), 'moodlemobile', PARAM_ALPHANUM));
 
+        $temp->add(new admin_setting_configtext('tool_mobile/minimumversion',
+                    new lang_string('minimumversion_key', 'tool_mobile'),
+                    new lang_string('minimumversion', 'tool_mobile'), '', PARAM_NOTAGS));
+
         $ADMIN->add('mobileapp', $temp);
 
         // Appearance related settings.
index 955fbb9..c2118dc 100644 (file)
@@ -95,6 +95,10 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             'langmenu' => $CFG->langmenu,
             'langlist' => $CFG->langlist,
             'locale' => $CFG->locale,
+            'tool_mobile_minimumversion' => '',
+            'tool_mobile_iosappid' => get_config('tool_mobile', 'iosappid'),
+            'tool_mobile_androidappid' => get_config('tool_mobile', 'androidappid'),
+            'tool_mobile_setuplink' => get_config('tool_mobile', 'setuplink'),
             'warnings' => array()
         );
         $this->assertEquals($expected, $result);
@@ -111,6 +115,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         set_config('autolang', 1);
         set_config('lang', 'a_b');  // Set invalid lang.
         set_config('disabledfeatures', 'myoverview', 'tool_mobile');
+        set_config('minimumversion', '3.8.0', 'tool_mobile');
 
         list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
         $expected['registerauth'] = 'email';
@@ -123,6 +128,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $expected['autolang'] = '1';
         $expected['lang'] = ''; // Expect empty because it was set to an invalid lang.
         $expected['tool_mobile_disabledfeatures'] = 'myoverview';
+        $expected['tool_mobile_minimumversion'] = '3.8.0';
 
         if ($logourl = $OUTPUT->get_logo_url()) {
             $expected['logourl'] = $logourl->out(false);
index 58078ec..df11ac9 100644 (file)
@@ -119,7 +119,9 @@ function tool_monitor_can_subscribe() {
  * @return array|bool Returns an array of courses or false if the user has no permission to subscribe to rules.
  */
 function tool_monitor_get_user_courses() {
-    $orderby = 'visible DESC, sortorder ASC';
+    // Get the course sorting according to the admin settings.
+    $sort = enrol_get_courses_sortingsql();
+
     $options = array();
     if (has_capability('tool/monitor:subscribe', context_system::instance())) {
         $options[0] = get_string('site');
@@ -134,7 +136,7 @@ function tool_monitor_get_user_courses() {
         );
 
     $fields = implode(', ', $fieldlist);
-    if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, $fields, $orderby)) {
+    if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, $fields, $sort)) {
         foreach ($courses as $course) {
             context_helper::preload_from_record($course);
             $coursectx = context_course::instance($course->id);
index 33cd752..40d2595 100644 (file)
@@ -266,7 +266,7 @@ class manager {
                 'title' => get_string('importtour', 'tool_usertours'),
             ],
             (object) [
-                'link'  => new \moodle_url('https://moodle.net/tours'),
+                'link'  => new \moodle_url('https://archive.moodle.net/tours'),
                 'linkproperties' => [
                         'target' => '_blank',
                     ],
index 00a3d58..60f1f45 100644 (file)
@@ -99,6 +99,22 @@ class manager {
         }
     }
 
+    /**
+     * Is analytics enabled globally?
+     *
+     * return bool
+     */
+    public static function is_analytics_enabled(): bool {
+        global $CFG;
+
+        if (isset($CFG->enableanalytics)) {
+            return $CFG->enableanalytics;
+        }
+
+        // Enabled by default.
+        return true;
+    }
+
     /**
      * Returns all system models that match the provided filters.
      *
index 6ecf6fe..88895a9 100644 (file)
@@ -384,7 +384,7 @@ class cc2moodle {
             if (isset($CFG->defaultblocks)) {
                 $blocknames = $CFG->defaultblocks;
             } else {
-                $blocknames = 'participants,activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
+                $blocknames = 'activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
             }
         }
 
index cf5fcf9..52793b2 100644 (file)
@@ -55,6 +55,7 @@ class backup_controller extends base_controller {
 
     protected $status; // Current status of the controller (created, planned, configured...)
 
+    /** @var backup_plan */
     protected $plan;   // Backup execution plan
     protected $includefiles; // Whether this backup includes files or not.
 
index 6a060ab..32f6c1d 100644 (file)
@@ -34,13 +34,27 @@ defined('MOODLE_INTERNAL') || die();
  */
 abstract class backup_plugin {
 
+    /** @var string */
     protected $plugintype;
+    /** @var string */
     protected $pluginname;
+    /** @var string */
     protected $connectionpoint;
+    /** @var backup_optigroup_element */
     protected $optigroup; // Optigroup, parent of all optigroup elements
+    /** @var backup_structure_step */
     protected $step;
+    /** @var backup_course_task|backup_activity_task */
     protected $task;
 
+    /**
+     * backup_plugin constructor.
+     *
+     * @param string $plugintype
+     * @param string $pluginname
+     * @param backup_optigroup_element $optigroup
+     * @param backup_structure_step $step
+     */
     public function __construct($plugintype, $pluginname, $optigroup, $step) {
         $this->plugintype = $plugintype;
         $this->pluginname = $pluginname;
index 11dd4ce..ac8c917 100644 (file)
@@ -125,10 +125,9 @@ abstract class backup_activity_structure_step extends backup_structure_step {
 }
 
 /**
- * Abstract structure step, to be used by all the activities using core questions stuff
- * (namely quiz module), supporting question plugins, states and sessions
+ * Helper code for use by any plugin that stores question attempt data that it needs to back up.
  */
-abstract class backup_questions_activity_structure_step extends backup_activity_structure_step {
+trait backup_questions_attempt_data_trait {
 
     /**
      * Attach to $element (usually attempts) the needed backup structures
@@ -200,6 +199,17 @@ abstract class backup_questions_activity_structure_step extends backup_activity_
 }
 
 
+/**
+ * Abstract structure step to help activities that store question attempt data.
+ *
+ * @copyright 2011 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class backup_questions_activity_structure_step extends backup_activity_structure_step {
+    use backup_questions_attempt_data_trait;
+}
+
+
 /**
  * backup structure step in charge of calculating the categories to be
  * included in backup, based in the context being backuped (module/course)
@@ -275,6 +285,9 @@ class backup_module_structure_step extends backup_structure_step {
         // attach format plugin structure to $module element, only one allowed
         $this->add_plugin_structure('format', $module, false);
 
+        // Attach report plugin structure to $module element, multiple allowed.
+        $this->add_plugin_structure('report', $module, true);
+
         // attach plagiarism plugin structure to $module element, there can be potentially
         // many plagiarism plugins storing information about this course
         $this->add_plugin_structure('plagiarism', $module, true);
index b90bcdf..31a94f5 100644 (file)
@@ -34,12 +34,24 @@ defined('MOODLE_INTERNAL') || die();
  */
 abstract class restore_plugin {
 
+    /** @var string */
     protected $plugintype;
+    /** @var string */
     protected $pluginname;
+    /** @var string */
     protected $connectionpoint;
+    /** @var restore_structure_step */
     protected $step;
+    /** @var restore_course_task|restore_activity_task */
     protected $task;
 
+    /**
+     * restore_plugin constructor.
+     *
+     * @param string $plugintype
+     * @param string $pluginname
+     * @param restore_structure_step $step
+     */
     public function __construct($plugintype, $pluginname, $step) {
         $this->plugintype = $plugintype;
         $this->pluginname = $pluginname;
@@ -58,9 +70,11 @@ abstract class restore_plugin {
         $methodname = 'define_' . basename($this->connectionpoint->get_path()) . '_plugin_structure';
 
         if (method_exists($this, $methodname)) {
-            if ($bluginpaths = $this->$methodname()) {
-                foreach ($bluginpaths as $path) {
-                    $path->set_processing_object($this);
+            if ($pluginpaths = $this->$methodname()) {
+                foreach ($pluginpaths as $path) {
+                    if ($path->get_processing_object() === null && !$this->step->grouped_parent_exists($path, $paths)) {
+                        $path->set_processing_object($this);
+                    }
                     $paths[] = $path;
                 }
             }
@@ -258,4 +272,13 @@ abstract class restore_plugin {
                'plugin_' . $this->plugintype . '_' .
                $this->pluginname . '_' . basename($this->connectionpoint->get_path()) . $path;
     }
+
+    /**
+     * Get the task we are part of.
+     *
+     * @return restore_activity_task|restore_course_task the task.
+     */
+    protected function get_task() {
+        return $this->task;
+    }
 }
index 0d6518d..10199e0 100644 (file)
@@ -4180,6 +4180,9 @@ class restore_module_structure_step extends restore_structure_step {
         // Apply for 'format' plugins optional paths at module level
         $this->add_plugin_structure('format', $module);
 
+        // Apply for 'report' plugins optional paths at module level.
+        $this->add_plugin_structure('report', $module);
+
         // Apply for 'plagiarism' plugins optional paths at module level
         $this->add_plugin_structure('plagiarism', $module);
 
@@ -5323,10 +5326,9 @@ class restore_process_file_aliases_queue extends restore_execution_step {
 
 
 /**
- * Abstract structure step, to be used by all the activities using core questions stuff
- * (like the quiz module), to support qtype plugins, states and sessions
+ * Helper code for use by any plugin that stores question attempt data that it needs to back up.
  */
-abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
+trait restore_questions_attempt_data_trait {
     /** @var array question_attempt->id to qtype. */
     protected $qtypes = array();
     /** @var array question_attempt->id to questionid. */
@@ -5378,21 +5380,21 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     /**
      * Process question_usages
      */
-    protected function process_question_usage($data) {
+    public function process_question_usage($data) {
         $this->restore_question_usage_worker($data, '');
     }
 
     /**
      * Process question_attempts
      */
-    protected function process_question_attempt($data) {
+    public function process_question_attempt($data) {
         $this->restore_question_attempt_worker($data, '');
     }
 
     /**
      * Process question_attempt_steps
      */
-    protected function process_question_attempt_step($data) {
+    public function process_question_attempt_step($data) {
         $this->restore_question_attempt_step_worker($data, '');
     }
 
@@ -5412,8 +5414,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
         $data = (object)$data;
         $oldid = $data->id;
 
-        $oldcontextid = $this->get_task()->get_old_contextid();
-        $data->contextid  = $this->get_mappingid('context', $this->task->get_old_contextid());
+        $data->contextid  = $this->task->get_contextid();
 
         // Everything ready, insert (no mapping needed)
         $newitemid = $DB->insert_record('question_usages', $data);
@@ -5569,6 +5570,17 @@ abstract class restore_questions_activity_structure_step extends restore_activit
             $this->add_related_files('question', $filearea, 'question_attempt_step');
         }
     }
+}
+
+
+/**
+ * Abstract structure step to help activities that store question attempt data.
+ *
+ * @copyright 2011 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
+    use restore_questions_attempt_data_trait;
 
     /**
      * Attach below $element (usually attempts) the needed restore_path_elements
@@ -5612,7 +5624,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     /**
      * Process the attempt data defined by {@link add_legacy_question_attempt_data()}.
      * @param object $data contains all the grouped attempt data to process.
-     * @param pbject $quiz data about the activity the attempts belong to. Required
+     * @param object $quiz data about the activity the attempts belong to. Required
      * fields are (basically this only works for the quiz module):
      *      oldquestions => list of question ids in this activity - using old ids.
      *      preferredbehaviour => the behaviour to use for questionattempts.
@@ -5674,7 +5686,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
 
         $data->uniqueid = $usage->id;
         $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas,
-                 $this->questions_recode_layout($quiz->oldquestions));
+                $this->questions_recode_layout($quiz->oldquestions));
     }
 
     protected function find_question_session_and_states($data, $questionid) {
@@ -5734,6 +5746,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     }
 }
 
+
 /**
  * Restore completion defaults for each module type
  *
index bffc7b7..3fe1978 100644 (file)
@@ -496,9 +496,9 @@ abstract class restore_structure_step extends restore_step {
         // Now, for each element not having one processing object, if
         // not child of grouped element, assign $this (the step itself) as processing element
         // Note method must exist or we'll get one @restore_path_element_exception
-        foreach($paths as $key => $pelement) {
+        foreach ($paths as $pelement) {
             if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $paths)) {
-                $paths[$key]->set_processing_object($this);
+                $pelement->set_processing_object($this);
             }
             // Populate $elementsoldid and $elementsoldid based on available pathelements
             $this->elementsoldid[$pelement->get_name()] = null;
@@ -510,18 +510,22 @@ abstract class restore_structure_step extends restore_step {
 
     /**
      * Given one pathelement, return true if grouped parent was found
+     *
+     * @param restore_path_element $pelement the element we are interested in.
+     * @param restore_path_element[] $elements the elements that exist.
+     * @return bool true if this element is inside a grouped parent.
      */
-    protected function grouped_parent_exists($pelement, $elements) {
+    public function grouped_parent_exists($pelement, $elements) {
         foreach ($elements as $element) {
             if ($pelement->get_path() == $element->get_path()) {
-                continue; // Don't compare against itself
+                continue; // Don't compare against itself.
             }
-            // If element is grouped and parent of pelement, return true
+            // If element is grouped and parent of pelement, return true.
             if ($element->is_grouped() and strpos($pelement->get_path() .  '/', $element->get_path()) === 0) {
                 return true;
             }
         }
-        return false; // no grouped parent found
+        return false; // No grouped parent found.
     }
 
     /**
index d9e5ceb..7ee4ae4 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index 0c0cc3c..581a46a 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js.map and b/blocks/myoverview/amd/build/view.min.js.map differ
index ce56253..565b7b9 100644 (file)
Binary files a/blocks/myoverview/amd/build/view_nav.min.js and b/blocks/myoverview/amd/build/view_nav.min.js differ
index dfa381d..815b2ae 100644 (file)
Binary files a/blocks/myoverview/amd/build/view_nav.min.js.map and b/blocks/myoverview/amd/build/view_nav.min.js.map differ
index b439834..9d637b7 100644 (file)
@@ -102,6 +102,8 @@ function(
             grouping: courseRegion.attr('data-grouping'),
             sort: courseRegion.attr('data-sort'),
             displaycategories: courseRegion.attr('data-displaycategories'),
+            customfieldname: courseRegion.attr('data-customfieldname'),
+            customfieldvalue: courseRegion.attr('data-customfieldvalue'),
         };
     };
 
@@ -126,7 +128,9 @@ function(
             offset: courseOffset,
             limit: limit,
             classification: filters.grouping,
-            sort: filters.sort
+            sort: filters.sort,
+            customfieldname: filters.customfieldname,
+            customfieldvalue: filters.customfieldvalue
         });
     };
 
index 29d114e..99ee871 100644 (file)
@@ -55,6 +55,8 @@ function(
             type = 'block_myoverview_user_view_preference';
         } else if (filter == 'sort') {
             type = 'block_myoverview_user_sort_preference';
+        } else if (filter == 'customfieldvalue') {
+            type = 'block_myoverview_user_grouping_customfieldvalue_preference';
         } else {
             type = 'block_myoverview_user_grouping_preference';
         }
@@ -92,10 +94,16 @@ function(
 
                 var filter = option.attr('data-filter');
                 var pref = option.attr('data-pref');
+                var customfieldvalue = option.attr('data-customfieldvalue');
 
                 root.find(Selectors.courseView.region).attr('data-' + filter, option.attr('data-value'));
                 updatePreferences(filter, pref);
 
+                if (customfieldvalue) {
+                    root.find(Selectors.courseView.region).attr('data-customfieldvalue', customfieldvalue);
+                    updatePreferences('customfieldvalue', customfieldvalue);
+                }
+
                 // Reset the views.
                 View.init(root);
 
index 01744b9..3a7dd13 100644 (file)
@@ -53,8 +53,9 @@ class block_myoverview extends block_base {
         $sort = get_user_preferences('block_myoverview_user_sort_preference');
         $view = get_user_preferences('block_myoverview_user_view_preference');
         $paging = get_user_preferences('block_myoverview_user_paging_preference');
+        $customfieldvalue = get_user_preferences('block_myoverview_user_grouping_customfieldvalue_preference');
 
-        $renderable = new \block_myoverview\output\main($group, $sort, $view, $paging);
+        $renderable = new \block_myoverview\output\main($group, $sort, $view, $paging, $customfieldvalue);
         $renderer = $this->page->get_renderer('block_myoverview');
 
         $this->content = new stdClass();
index 5f4a033..5f44bb3 100644 (file)
@@ -130,6 +130,27 @@ class main implements renderable, templatable {
      */
     private $displaygroupinghidden;
 
+    /**
+     * Store a course grouping option setting.
+     *
+     * @var bool
+     */
+    private $displaygroupingcustomfield;
+
+    /**
+     * Store the custom field used by customfield grouping.
+     *
+     * @var string
+     */
+    private $customfiltergrouping;
+
+    /**
+     * Store the selected custom field value to group by.
+     *
+     * @var string
+     */
+    private $customfieldvalue;
+
     /**
      * main constructor.
      * Initialize the user preferences
@@ -137,10 +158,12 @@ class main implements renderable, templatable {
      * @param string $grouping Grouping user preference
      * @param string $sort Sort user preference
      * @param string $view Display user preference
+     * @param int $paging
+     * @param string $customfieldvalue
      *
      * @throws \dml_exception
      */
-    public function __construct($grouping, $sort, $view, $paging) {
+    public function __construct($grouping, $sort, $view, $paging, $customfieldvalue = null) {
         // Get plugin config.
         $config = get_config('block_myoverview');
 
@@ -153,28 +176,14 @@ class main implements renderable, templatable {
             // Otherwise fall back to another grouping in a reasonable order.
             // This is done to prevent one-time UI glitches in the case when a user has chosen a grouping option previously which
             // was then disabled by the admin in the meantime.
-        } else if ($config->displaygroupingall == true) {
-            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALL;
-        } else if ($config->displaygroupingallincludinghidden == true) {
-            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
-        } else if ($config->displaygroupinginprogress == true) {
-            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_INPROGRESS;
-        } else if ($config->displaygroupingfuture == true) {
-            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_FUTURE;
-        } else if ($config->displaygroupingpast == true) {
-            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_PAST;
-        } else if ($config->displaygroupingstarred == true) {
-            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_FAVOURITES;
-        } else if ($config->displaygroupinghidden == true) {
-            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_HIDDEN;
-
-            // In this case, no grouping option is enabled and the grouping is not needed at all.
-            // But it's better not to leave $this->grouping unset for any unexpected case.
         } else {
-            $this->grouping = BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
+            $this->grouping = $this->get_fallback_grouping($config);
         }
         unset ($groupingconfigname);
 
+        // Remember which custom field value we were using, if grouping by custom field.
+        $this->customfieldvalue = $customfieldvalue;
+
         // Check and remember the given sorting.
         $this->sort = $sort ? $sort : BLOCK_MYOVERVIEW_SORTING_TITLE;
 
@@ -207,6 +216,8 @@ class main implements renderable, templatable {
         $this->displaygroupingpast = $config->displaygroupingpast;
         $this->displaygroupingstarred = $config->displaygroupingstarred;
         $this->displaygroupinghidden = $config->displaygroupinghidden;
+        $this->displaygroupingcustomfield = ($config->displaygroupingcustomfield && $config->customfiltergrouping);
+        $this->customfiltergrouping = $config->customfiltergrouping;
 
         // Check and remember if the grouping selector should be shown at all or not.
         // It will be shown if more than 1 grouping option is enabled.
@@ -218,7 +229,7 @@ class main implements renderable, templatable {
                 $this->displaygroupingstarred,
                 $this->displaygroupinghidden);
         $displaygroupingselectorscount = count(array_filter($displaygroupingselectors));
-        if ($displaygroupingselectorscount > 1) {
+        if ($displaygroupingselectorscount > 1 || $this->displaygroupingcustomfield) {
             $this->displaygroupingselector = true;
         } else {
             $this->displaygroupingselector = false;
@@ -226,6 +237,41 @@ class main implements renderable, templatable {
         unset ($displaygroupingselectors, $displaygroupingselectorscount);
     }
 
+    /**
+     * Determine the most sensible fallback grouping to use (in cases where the stored selection
+     * is no longer available).
+     * @param object $config
+     * @return string
+     */
+    private function get_fallback_grouping($config) {
+        if ($config->displaygroupingall == true) {
+            return BLOCK_MYOVERVIEW_GROUPING_ALL;
+        }
+        if ($config->displaygroupingallincludinghidden == true) {
+            return BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
+        }
+        if ($config->displaygroupinginprogress == true) {
+            return BLOCK_MYOVERVIEW_GROUPING_INPROGRESS;
+        }
+        if ($config->displaygroupingfuture == true) {
+            return BLOCK_MYOVERVIEW_GROUPING_FUTURE;
+        }
+        if ($config->displaygroupingpast == true) {
+            return BLOCK_MYOVERVIEW_GROUPING_PAST;
+        }
+        if ($config->displaygroupingstarred == true) {
+            return BLOCK_MYOVERVIEW_GROUPING_FAVOURITES;
+        }
+        if ($config->displaygroupinghidden == true) {
+            return BLOCK_MYOVERVIEW_GROUPING_HIDDEN;
+        }
+        if ($config->displaygroupingcustomfield == true) {
+            return BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD;
+        }
+        // In this case, no grouping option is enabled and the grouping is not needed at all.
+        // But it's better not to leave $this->grouping unset for any unexpected case.
+        return BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
+    }
 
     /**
      * Set the available layouts based on the config table settings,
@@ -292,6 +338,50 @@ class main implements renderable, templatable {
 
     }
 
+    /**
+     * Get the list of values to add to the grouping dropdown
+     * @return object[] containing name, value and active fields
+     */
+    public function get_customfield_values_for_export() {
+        global $DB, $USER;
+        if (!$this->displaygroupingcustomfield) {
+            return [];
+        }
+        $fieldid = $DB->get_field('customfield_field', 'id', ['shortname' => $this->customfiltergrouping]);
+        if (!$fieldid) {
+            return [];
+        }
+        $courses = enrol_get_all_users_courses($USER->id, true);
+        if (!$courses) {
+            return [];
+        }
+        list($csql, $params) = $DB->get_in_or_equal(array_keys($courses), SQL_PARAMS_NAMED);
+        $select = "instanceid $csql AND fieldid = :fieldid";
+        $params['fieldid'] = $fieldid;
+        $distinctablevalue = $DB->sql_compare_text('value');
+        $values = $DB->get_records_select_menu('customfield_data', $select, $params, $DB->sql_order_by_text('value'),
+            "DISTINCT $distinctablevalue, $distinctablevalue AS value2");
+        $values = array_filter($values);
+        if (!$values) {
+            return [];
+        }
+        $field = \core_customfield\field_controller::create($fieldid);
+        if (!$field->supports_course_grouping()) {
+            return []; // The field shouldn't have been selectable in the global settings, but just skip it now.
+        }
+        $values = $field->course_grouping_format_values($values);
+        $customfieldactive = ($this->grouping === BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD);
+        $ret = [];
+        foreach ($values as $value => $name) {
+            $ret[] = (object)[
+                'name' => $name,
+                'value' => $value,
+                'active' => ($customfieldactive && ($this->customfieldvalue == $value)),
+            ];
+        }
+        return $ret;
+    }
+
     /**
      * Export this data so it can be used as the context for a mustache template.
      *
@@ -305,6 +395,29 @@ class main implements renderable, templatable {
 
         $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
 
+        $customfieldvalues = $this->get_customfield_values_for_export();
+        $selectedcustomfield = '';
+        if ($this->grouping == BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD) {
+            foreach ($customfieldvalues as $field) {
+                if ($field->value == $this->customfieldvalue) {
+                    $selectedcustomfield = $field->name;
+                    break;
+                }
+            }
+            // If the selected custom field value has not been found (possibly because the field has
+            // been changed in the settings) find a suitable fallback.
+            if (!$selectedcustomfield) {
+                $this->grouping = $this->get_fallback_grouping(get_config('block_myoverview'));
+                if ($this->grouping == BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD) {
+                    // If the fallback grouping is still customfield, then select the first field.
+                    $firstfield = reset($customfieldvalues);
+                    if ($firstfield) {
+                        $selectedcustomfield = $firstfield->name;
+                        $this->customfieldvalue = $firstfield->value;
+                    }
+                }
+            }
+        }
         $preferences = $this->get_preferences_as_booleans();
         $availablelayouts = $this->get_formatted_available_layouts_for_export();
 
@@ -327,8 +440,13 @@ class main implements renderable, templatable {
             'displaygroupingstarred' => $this->displaygroupingstarred,
             'displaygroupinghidden' => $this->displaygroupinghidden,
             'displaygroupingselector' => $this->displaygroupingselector,
+            'displaygroupingcustomfield' => $this->displaygroupingcustomfield && $customfieldvalues,
+            'customfieldname' => $this->customfiltergrouping,
+            'customfieldvalue' => $this->customfieldvalue,
+            'customfieldvalues' => $customfieldvalues,
+            'selectedcustomfield' => $selectedcustomfield,
         ];
         return array_merge($defaultvariables, $preferences);
 
     }
-}
\ No newline at end of file
+}
index e95ce8c..7c5520f 100644 (file)
@@ -33,6 +33,7 @@ $string['aria:controls'] = 'Course overview controls';
 $string['aria:courseactions'] = 'Actions for current course';
 $string['aria:coursesummary'] = 'Course summary text:';
 $string['aria:courseprogress'] = 'Course progress:';
+$string['aria:customfield'] = 'Show {$a} courses';
 $string['aria:displaydropdown'] = 'Display drop-down menu';
 $string['aria:favourites'] = 'Show starred courses';
 $string['aria:future'] = 'Show future courses';
@@ -51,6 +52,9 @@ $string['card'] = 'Card';
 $string['cards'] = 'Cards';
 $string['courseprogress'] = 'Course progress:';
 $string['completepercent'] = '{$a}% complete';
+$string['customfield'] = 'Custom field';
+$string['customfiltergrouping'] = 'Field to use';
+$string['customfiltergrouping_nofields'] = 'You need to add at least one course custom field in order to use this setting';
 $string['displaycategories'] = 'Display categories';
 $string['displaycategories_help'] = 'Display the course category on dashboard course items including cards, list items and summary items.';
 $string['favourites'] = 'Starred';
@@ -61,6 +65,7 @@ $string['layouts'] = 'Available layouts';
 $string['layouts_help'] = 'Course overview layouts which are available for selection by users. If none are selected, the card layout will be used.';
 $string['list'] = 'List';
 $string['myoverview:myaddinstance'] = 'Add a new course overview block to Dashboard';
+$string['nocustomvalue'] = 'No {$a}';
 $string['past'] = 'Past';
 $string['pluginname'] = 'Course overview';
 $string['privacy:metadata:overviewsortpreference'] = 'The Course overview block sort preference.';
index 36fc201..21f9b70 100644 (file)
@@ -34,6 +34,12 @@ define('BLOCK_MYOVERVIEW_GROUPING_FUTURE', 'future');
 define('BLOCK_MYOVERVIEW_GROUPING_PAST', 'past');
 define('BLOCK_MYOVERVIEW_GROUPING_FAVOURITES', 'favourites');
 define('BLOCK_MYOVERVIEW_GROUPING_HIDDEN', 'hidden');
+define('BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD', 'customfield');
+
+/**
+ * Allows selection of all courses without a value for the custom field.
+ */
+define('BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY', -1);
 
 /**
  * Constants for the user preferences sorting options
@@ -81,9 +87,17 @@ function block_myoverview_user_preferences() {
             BLOCK_MYOVERVIEW_GROUPING_FUTURE,
             BLOCK_MYOVERVIEW_GROUPING_PAST,
             BLOCK_MYOVERVIEW_GROUPING_FAVOURITES,
-            BLOCK_MYOVERVIEW_GROUPING_HIDDEN
+            BLOCK_MYOVERVIEW_GROUPING_HIDDEN,
+            BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD,
         )
     );
+
+    $preferences['block_myoverview_user_grouping_customfieldvalue_preference'] = [
+        'null' => NULL_ALLOWED,
+        'default' => null,
+        'type' => PARAM_RAW,
+    ];
+
     $preferences['block_myoverview_user_sort_preference'] = array(
         'null' => NULL_NOT_ALLOWED,
         'default' => BLOCK_MYOVERVIEW_SORTING_TITLE,
index b633f22..0ec6ebe 100644 (file)
@@ -86,6 +86,29 @@ if ($ADMIN->fulltree) {
             '',
             1));
 
+    $settings->add(new admin_setting_configcheckbox(
+            'block_myoverview/displaygroupingcustomfield',
+            get_string('customfield', 'block_myoverview'),
+            '',
+            0));
+
+    $choices = \core_customfield\api::get_fields_supporting_course_grouping();
+    if ($choices) {
+        $choices  = ['' => get_string('choosedots')] + $choices;
+        $settings->add(new admin_setting_configselect(
+                'block_myoverview/customfiltergrouping',
+                get_string('customfiltergrouping', 'block_myoverview'),
+                '',
+                '',
+                $choices));
+    } else {
+        $settings->add(new admin_setting_configempty(
+                'block_myoverview/customfiltergrouping',
+                get_string('customfiltergrouping', 'block_myoverview'),
+                get_string('customfiltergrouping_nofields', 'block_myoverview')));
+    }
+    $settings->hide_if('block_myoverview/customfiltergrouping', 'block_myoverview/displaygroupingcustomfield');
+
     $settings->add(new admin_setting_configcheckbox(
             'block_myoverview/displaygroupingstarred',
             get_string('favourites', 'block_myoverview'),
index 5be3c98..76f73b9 100644 (file)
@@ -31,6 +31,8 @@
     data-region="courses-view"
     data-display="{{view}}"
     data-grouping="{{grouping}}"
+    data-customfieldname="{{customfieldname}}"
+    data-customfieldvalue="{{customfieldvalue}}"
     data-sort="{{sort}}"
     data-prev-display="{{view}}"
     data-paging="{{paging}}"
@@ -40,4 +42,4 @@
     <div data-region="course-view-content">
         {{> block_myoverview/placeholders }}
     </div>
-</div>
\ No newline at end of file
+</div>
index 703482f..5d6b598 100644 (file)
@@ -50,6 +50,7 @@
             {{#past}}{{#str}} past, block_myoverview {{/str}}{{/past}}
             {{#favourites}}{{#str}} favourites, block_myoverview {{/str}}{{/favourites}}
             {{#hidden}}{{#str}} hiddencourses, block_myoverview {{/str}}{{/hidden}}
+            {{selectedcustomfield}}
         </span>
     </button>
     <ul class="dropdown-menu" data-show-active-item data-active-item-text aria-labelledby="groupingdropdown">
             </a>
         </li>
         {{/displaygroupingpast}}
+        {{#displaygroupingcustomfield}}
+            <li class="dropdown-divider" role="presentation">
+                <span class="filler">&nbsp;</span>
+            </li>
+            {{#customfieldvalues}}
+                <li>
+                    <a class="dropdown-item {{#active}}active{{/active}}" href="#" data-filter="grouping"
+                       data-value="customfield" data-pref="customfield" data-customfieldvalue="{{value}}"
+                       aria-label="{{#str}}aria:customfield, block_myoverview, {{name}}{{/str}}"
+                       aria-controls="courses-view-{{uniqid}}">
+                        {{name}}
+                    </a>
+                </li>
+            {{/customfieldvalues}}
+        {{/displaygroupingcustomfield}}
         {{#displaygroupingstarred}}
         <li class="dropdown-divider" role="presentation">
             <span class="filler">&nbsp;</span>
diff --git a/blocks/myoverview/tests/behat/block_myoverview_customfield.feature b/blocks/myoverview/tests/behat/block_myoverview_customfield.feature
new file mode 100644 (file)
index 0000000..57d0684
--- /dev/null
@@ -0,0 +1,174 @@
+@block @block_myoverview @javascript
+Feature: The my overview block allows users to group courses by custom fields
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | student1 | Student   | X        | student1@example.com | S1       |
+    And the following "custom field categories" exist:
+      | name          | component   | area   | itemid |
+      | Course fields | core_course | course | 0      |
+    And the following "custom fields" exist:
+      | name           | category      | type     | shortname     | configdata                                           |
+      | Checkbox field | Course fields | checkbox | checkboxfield |                                                      |
+      | Date field     | Course fields | date     | datefield     | {"mindate":0, "maxdate":0}                           |
+      | Select field   | Course fields | select   | selectfield   | {"options":"Option 1\nOption 2\nOption 3\nOption 4"} |
+      | Text field     | Course fields | text     | textfield     |                                                      |
+    And the following "courses" exist:
+      | fullname | shortname | category | customfield_checkboxfield | customfield_datefield | customfield_selectfield | customfield_textfield |
+      | Course 1 | C1        | 0        | 1                         | 981028800             | 1                       | fish                  |
+      | Course 2 | C2        | 0        | 0                         | 334324800             |                         |                       |
+      | Course 3 | C3        | 0        | 0                         | 981028800             | 2                       | dog                   |
+      | Course 4 | C4        | 0        | 1                         |                       | 3                       | cat                   |
+      | Course 5 | C5        | 0        |                           | 334411200             | 2                       | fish                  |
+    And the following "course enrolments" exist:
+      | user     | course | role    |
+      | student1 | C1     | student |
+      | student1 | C2     | student |
+      | student1 | C3     | student |
+      | student1 | C4     | student |
+      | student1 | C5     | student |
+
+  Scenario: Group courses by checkbox: Yes
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1             | block_myoverview |
+      | customfiltergrouping       | checkboxfield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "Checkbox field: Yes" "link" in the "Course overview" "block"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by checkbox: No
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1             | block_myoverview |
+      | customfiltergrouping       | checkboxfield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "Checkbox field: No" "link" in the "Course overview" "block"
+    Then I should not see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by date: 1 February 2001
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1         | block_myoverview |
+      | customfiltergrouping       | datefield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "1 February 2001" "link" in the "Course overview" "block"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by date: 6 August 1980
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1         | block_myoverview |
+      | customfiltergrouping       | datefield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "6 August 1980" "link" in the "Course overview" "block"
+    Then I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by date: No Date field
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1         | block_myoverview |
+      | customfiltergrouping       | datefield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "No Date field" "link" in the "Course overview" "block"
+    Then I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by select: Option 1
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1           | block_myoverview |
+      | customfiltergrouping       | selectfield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    And I should not see "Option 4" in the "Course overview" "block"
+    When I click on "Option 1" "link" in the "Course overview" "block"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by select: Option 2
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1           | block_myoverview |
+      | customfiltergrouping       | selectfield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "Option 2" "link" in the "Course overview" "block"
+    Then I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by select: No Select field
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1           | block_myoverview |
+      | customfiltergrouping       | selectfield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "No Select field" "link" in the "Course overview" "block"
+    Then I should not see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by text: fish
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1         | block_myoverview |
+      | customfiltergrouping       | textfield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "fish" "link" in the "Course overview" "block"
+    Then I should see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by text: dog
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1         | block_myoverview |
+      | customfiltergrouping       | textfield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "dog" "link" in the "Course overview" "block"
+    Then I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+
+  Scenario: Group courses by text: No Text field
+    Given the following config values are set as admin:
+      | displaygroupingcustomfield | 1         | block_myoverview |
+      | customfiltergrouping       | textfield | block_myoverview |
+    And I log in as "student1"
+    And I click on "All (except removed from view)" "button" in the "Course overview" "block"
+    When I click on "No Text field" "link" in the "Course overview" "block"
+    Then I should not see "Course 1" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
index 974bebf..bb97960 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019091800;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019100900;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019051100;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index 01ada95..9d1fc90 100644 (file)
@@ -129,14 +129,16 @@ class block_online_users extends block_base {
                     $this->content->text .= $OUTPUT->user_picture($user, array('size'=>16, 'alttext'=>false, 'link'=>false)) .$user->fullname.'</a></div>';
 
                     if ($USER->id == $user->id) {
-                        $action = ($user->uservisibility != null && $user->uservisibility == 0) ? 'show' : 'hide';
-                        $anchortagcontents = $OUTPUT->pix_icon('t/' . $action,
-                            get_string('online_status:' . $action, 'block_online_users'));
-                        $anchortag = html_writer::link("", $anchortagcontents,
-                            array('title' => get_string('online_status:' . $action, 'block_online_users'),
-                                'data-action' => $action, 'data-userid' => $user->id, 'id' => 'change-user-visibility'));
-
-                        $this->content->text .= '<div class="uservisibility">' . $anchortag . '</div>';
+                        if ($CFG->block_online_users_onlinestatushiding) {
+                            $action = ($user->uservisibility != null && $user->uservisibility == 0) ? 'show' : 'hide';
+                            $anchortagcontents = $OUTPUT->pix_icon('t/' . $action,
+                                get_string('online_status:' . $action, 'block_online_users'));
+                            $anchortag = html_writer::link("", $anchortagcontents,
+                                array('title' => get_string('online_status:' . $action, 'block_online_users'),
+                                    'data-action' => $action, 'data-userid' => $user->id, 'id' => 'change-user-visibility'));
+
+                            $this->content->text .= '<div class="uservisibility">' . $anchortag . '</div>';
+                        }
                     } else {
                         if ($canshowicon) {  // Only when logged in and messaging active etc.
                             $anchortagcontents = $OUTPUT->pix_icon('t/message', get_string('messageselectadd'));
index 20f706f..a18e625 100644 (file)
@@ -67,7 +67,7 @@ class fetcher {
      * @param int $courseid The course id to check
      */
     protected function set_sql($currentgroup, $now, $timetoshowusers, $context, $sitelevel, $courseid) {
-        global $USER, $DB;
+        global $USER, $DB, $CFG;
 
         $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache.
 
@@ -76,7 +76,14 @@ class fetcher {
         $groupby       = "";
         $lastaccess    = ", lastaccess";
         $timeaccess    = ", ul.timeaccess AS lastaccess";
-        $uservisibility = ", up.value AS uservisibility";
+        $uservisibility = "";
+        $uservisibilityselect = "";
+        if ($CFG->block_online_users_onlinestatushiding) {
+            $uservisibility = ", up.value AS uservisibility";
+            $uservisibilityselect = "AND (" . $DB->sql_cast_char2int('up.value') . " = 1
+                                    OR up.value IS NULL
+                                    OR u.id = :userid)";
+        }
         $params = array();
 
         $userfields = \user_picture::fields('u', array('username'));
@@ -88,7 +95,9 @@ class fetcher {
             $groupby = "GROUP BY $userfields";
             $lastaccess = ", MAX(u.lastaccess) AS lastaccess";
             $timeaccess = ", MAX(ul.timeaccess) AS lastaccess";
-            $uservisibility = ", MAX(up.value) AS uservisibility";
+            if ($CFG->block_online_users_onlinestatushiding) {
+                $uservisibility = ", MAX(up.value) AS uservisibility";
+            }
             $params['currentgroup'] = $currentgroup;
         }
 
@@ -105,9 +114,7 @@ class fetcher {
                      WHERE u.lastaccess > :timefrom
                            AND u.lastaccess <= :now
                            AND u.deleted = 0
-                           AND (" . $DB->sql_cast_char2int('up.value') . " = 1
-                               OR up.value IS NULL
-                               OR u.id = :userid)
+                           $uservisibilityselect
                            $groupselect $groupby
                   ORDER BY lastaccess DESC ";
 
@@ -118,9 +125,7 @@ class fetcher {
                       WHERE u.lastaccess > :timefrom
                             AND u.lastaccess <= :now
                             AND u.deleted = 0
-                            AND (" . $DB->sql_cast_char2int('up.value') . " = 1
-                                OR up.value IS NULL
-                                OR u.id = :userid)
+                            $uservisibilityselect
                             $groupselect";
         } else {
             // Course level - show only enrolled users for now.
@@ -138,9 +143,7 @@ class fetcher {
                            AND ul.courseid = :courseid
                            AND ul.timeaccess <= :now
                            AND u.deleted = 0
-                           AND (" . $DB->sql_cast_char2int('up.value') . " = 1
-                               OR up.value IS NULL
-                               OR u.id = :userid)
+                           $uservisibilityselect
                            $groupselect $groupby
                   ORDER BY lastaccess DESC";
 
@@ -154,9 +157,7 @@ class fetcher {
                            AND ul.courseid = :courseid
                            AND ul.timeaccess <= :now
                            AND u.deleted = 0
-                           AND (" . $DB->sql_cast_char2int('up.value') . " = 1
-                               OR up.value IS NULL
-                               OR u.id = :userid)
+                           $uservisibilityselect
                            $groupselect";
 
             $params['courseid'] = $courseid;
index c6bc022..f34f5d6 100644 (file)
@@ -23,7 +23,9 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['onlinestatushiding_desc'] = 'If enabled, users have the option to hide their online status from other users.';
 $string['configtimetosee'] = 'Number of minutes determining the period of inactivity after which a user is no longer considered to be online.';
+$string['onlinestatushiding'] = 'Online status hiding';
 $string['nouser'] = 'No online users';
 $string['numuser'] = '{$a} online user';
 $string['numusers'] = '{$a} online users';
index b9d7d81..39d3bf0 100644 (file)
@@ -27,5 +27,9 @@ defined('MOODLE_INTERNAL') || die;
 if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configtext('block_online_users_timetosee', get_string('timetosee', 'block_online_users'),
                    get_string('configtimetosee', 'block_online_users'), 5, PARAM_INT));
+
+    $settings->add(new admin_setting_configcheckbox('block_online_users_onlinestatushiding',
+            get_string('onlinestatushiding', 'block_online_users'),
+            get_string('onlinestatushiding_desc', 'block_online_users'), 1));
 }
 
index 20b0d95..097cee1 100644 (file)
@@ -42,7 +42,9 @@ Feature: The online users block allow you to see who is currently online
 
   @javascript
   Scenario: Hide/show user's online status from/to other users in the online users block on course page
-    Given I log in as "teacher1"
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Online users" block
     And I log out
@@ -71,3 +73,45 @@ Feature: The online users block allow you to see who is currently online
     Then I should see "2 online users" in the "Online users" "block"
     And I should see "Teacher 1" in the "Online users" "block"
     And I should see "Student 1" in the "Online users" "block"
+
+  @javascript
+  Scenario: Hide/show icon is not visible in the online users block on course page when the setting is disabled
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Online users" block
+    And I log out
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And "Hide" "icon" should exist in the ".block.block_online_users" "css_element"
+    And I log out
+    And the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 0 |
+    When I log in as "student1"
+    Then I should see "Student 1" in the "Online users" "block"
+    And "Hide" "icon" should not exist in the ".block.block_online_users" "css_element"
+
+  @javascript
+  Scenario: User is displayed in the online users block on course page when visibility setting is disabled,
+            ignoring the previously set visibility state
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Online users" block
+    And I log out
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And "Hide" "icon" should exist in the "#change-user-visibility" "css_element"
+    And I click on "#change-user-visibility" "css_element"
+    And I wait "1" seconds
+    And "Show" "icon" should exist in the "#change-user-visibility" "css_element"
+    And I log out
+    And the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 0 |
+    And I log in as "teacher1"
+    When I am on "Course 1" course homepage
+    Then I should see "2 online users" in the "Online users" "block"
+    And I should see "Teacher 1" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
index 0d01207..2d32fc6 100644 (file)
@@ -29,7 +29,9 @@ Feature: The online users block allow you to see who is currently online on dash
 
   @javascript
   Scenario: Hide/show user's online status from/to other users in the online users block on dashboard
-    Given I log in as "student1"
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "student1"
     And I should see "1 online user" in the "Online users" "block"
     And I should see "Student 1" in the "Online users" "block"
     And "Hide" "icon" should exist in the "#change-user-visibility" "css_element"
@@ -52,3 +54,44 @@ Feature: The online users block allow you to see who is currently online on dash
     Then I should see "2 online users" in the "Online users" "block"
     And I should see "Student 2" in the "Online users" "block"
     And I should see "Student 1" in the "Online users" "block"
+
+  @javascript
+  Scenario: Hide/show icon is not visible in the online users block when the setting is disabled
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "student1"
+    And I should see "1 online user" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
+    And "Hide" "icon" should exist in the ".block.block_online_users" "css_element"
+    And I log out
+    And the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 0 |
+    When I log in as "student1"
+    Then I should see "1 online user" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
+    And "Hide" "icon" should not exist in the ".block.block_online_users" "css_element"
+
+  @javascript
+  Scenario: User is displayed in the online users block when visibility setting is disabled,
+            ignoring the previously set visibility state
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "student1"
+    And I should see "1 online user" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
+    And "Hide" "icon" should exist in the "#change-user-visibility" "css_element"
+    And I click on "#change-user-visibility" "css_element"
+    And I wait "1" seconds
+    And "Show" "icon" should exist in the "#change-user-visibility" "css_element"
+    And I log out
+    And I log in as "student2"
+    And I should see "1 online user" in the "Online users" "block"
+    And I should see "Student 2" in the "Online users" "block"
+    And I should not see "Student 1" in the "Online users" "block"
+    And I log out
+    And the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 0 |
+    When I log in as "student2"
+    Then I should see "2 online users" in the "Online users" "block"
+    And I should see "Student 2" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
index 097c9e7..ca817e8 100644 (file)
@@ -52,7 +52,9 @@ Feature: The online users block allow you to see who is currently online on fron
 
   @javascript
   Scenario: Hide/show user's online status from/to other users in the online users block on front page
-    Given I log in as "admin"
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "admin"
     And I am on site homepage
     And I navigate to "Turn editing on" in current page administration
     And I add the "Online users" block
@@ -84,3 +86,55 @@ Feature: The online users block allow you to see who is currently online on fron
     And I should see "Admin" in the "Online users" "block"
     And I should see "Student 2" in the "Online users" "block"
     And I should see "Student 1" in the "Online users" "block"
+
+  @javascript
+  Scenario: Hide/show icon is not visible in the online users block on front page when the setting is disabled
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" in current page administration
+    And I add the "Online users" block
+    And I log out
+    And I log in as "student1"
+    And I am on site homepage
+    And "Hide" "icon" should exist in the ".block.block_online_users" "css_element"
+    And I log out
+    And the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 0 |
+    When I log in as "student1"
+    Then I should see "Student 1" in the "Online users" "block"
+    And "Hide" "icon" should not exist in the ".block.block_online_users" "css_element"
+
+  @javascript
+  Scenario: User is displayed in the online users block on front page when visibility setting is disabled,
+            ignoring the previously set visibility state
+    Given the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" in current page administration
+    And I add the "Online users" block
+    And I log out
+    And I log in as "student1"
+    And I am on site homepage
+    And "Hide" "icon" should exist in the "#change-user-visibility" "css_element"
+    And I click on "#change-user-visibility" "css_element"
+    And I wait "1" seconds
+    And "Show" "icon" should exist in the "#change-user-visibility" "css_element"
+    And I log out
+    And I log in as "student2"
+    And I am on site homepage
+    And I should see "2 online user" in the "Online users" "block"
+    And I should see "Admin" in the "Online users" "block"
+    And I should see "Student 2" in the "Online users" "block"
+    And I should not see "Student 1" in the "Online users" "block"
+    And I log out
+    And the following config values are set as admin:
+      | block_online_users_onlinestatushiding | 0 |
+    And I log in as "student2"
+    When I am on site homepage
+    Then I should see "3 online users" in the "Online users" "block"
+    And I should see "Admin" in the "Online users" "block"
+    And I should see "Student 2" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
index 946e456..da436ac 100644 (file)
@@ -155,6 +155,8 @@ class block_online_users_testcase extends advanced_testcase {
     public function test_user_visibility_course1_group1_members() {
         global $CFG;
 
+        // Enable users to set their visibility to others in the online users block.
+        $CFG->block_online_users_onlinestatushiding = true;
         $groupid = $this->data['group1']->id;
         $now = time();
         $timetoshowusers = $CFG->block_online_users_timetosee * 60;
@@ -190,6 +192,18 @@ class block_online_users_testcase extends advanced_testcase {
         // User1 should not be displayed in the online users block.
         $this->assertEquals(2, $usercount);
         $this->assertFalse(array_key_exists($user1->id, $users));
+
+        // Disable users to set their visibility to others in the online users block.
+        // All users should be displayed now and the visibility status of a users should be ignored,
+        // as the capability of setting the visibility to other user has been disabled.
+        $CFG->block_online_users_onlinestatushiding = false;
+        // Test if the fetcher gets all the users including user1.
+        $onlineusers = new fetcher($groupid, $now, $timetoshowusers, $context, false, $courseid);
+        $users = $onlineusers->get_users();
+        $usercount = $onlineusers->count_users();
+        // User1 should be displayed in the online users block.
+        $this->assertEquals(3, $usercount);
+        $this->assertTrue(array_key_exists($user1->id, $users));
     }
 
     /**
@@ -234,6 +248,18 @@ class block_online_users_testcase extends advanced_testcase {
         // User1 should not be displayed in the online users block.
         $this->assertEquals(8, $usercount);
         $this->assertFalse(array_key_exists($user1->id, $users));
+
+        // Disable users to set their visibility to others in the online users block.
+        // All users should be displayed now and the visibility status of a users should be ignored,
+        // as the capability of setting the visibility to other user has been disabled.
+        $CFG->block_online_users_onlinestatushiding = false;
+        // Test if the fetcher gets all the users including user1.
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, false, $courseid);
+        $users = $onlineusers->get_users();
+        $usercount = $onlineusers->count_users();
+        // User1 should be displayed in the online users block.
+        $this->assertEquals(9, $usercount);
+        $this->assertTrue(array_key_exists($user1->id, $users));
     }
 
     /**
@@ -277,5 +303,17 @@ class block_online_users_testcase extends advanced_testcase {
         // User1 should not be displayed in the online users block.
         $this->assertEquals(11, $usercount);
         $this->assertFalse(array_key_exists($user1->id, $users));
+
+        // Disable users to set their visibility to others in the online users block.
+        // All users should be displayed now and the visibility status of a users should be ignored,
+        // as the capability of setting the visibility to other user has been disabled.
+        $CFG->block_online_users_onlinestatushiding = false;
+        // Test if the fetcher gets all the users including user1.
+        $onlineusers = new fetcher($currentgroup, $now, $timetoshowusers, $context, true);
+        $users = $onlineusers->get_users();
+        $usercount = $onlineusers->count_users();
+        // User1 should be displayed in the online users block.
+        $this->assertEquals(12, $usercount);
+        $this->assertTrue(array_key_exists($user1->id, $users));
     }
 }
index eaf8a55..c5806d3 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019052000;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2019052001;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2019051100;        // Requires this Moodle version
 $plugin->component = 'block_online_users'; // Full name of the plugin (used for diagnostics)
diff --git a/blocks/participants/block_participants.php b/blocks/participants/block_participants.php
deleted file mode 100644 (file)
index b1b433b..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Participants block
- *
- * @package    block_participants
- * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once($CFG->dirroot . '/course/lib.php');
-
-/**
- * Participants block
- *
- * @package    block_participants
- * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class block_participants extends block_list {
-    function init() {
-        $this->title = get_string('pluginname', 'block_participants');
-    }
-
-    function get_content() {
-
-        global $CFG, $OUTPUT;
-
-        if (empty($this->instance)) {
-            $this->content = '';
-            return $this->content;
-        }
-
-        $this->content = new stdClass();
-        $this->content->items = array();
-        $this->content->icons = array();
-        $this->content->footer = '';
-
-        // user/index.php expect course context, so get one if page has module context.
-        $currentcontext = $this->page->context->get_course_context(false);
-
-        if (empty($currentcontext)) {
-            $this->content = '';
-            return $this->content;
-        } else if ($this->page->course->id == SITEID) {
-            if (!course_can_view_participants(context_system::instance())) {
-                $this->content = '';
-                return $this->content;
-            }
-        } else {
-            if (!course_can_view_participants($currentcontext)) {
-                $this->content = '';
-                return $this->content;
-            }
-        }
-
-        $icon = $OUTPUT->pix_icon('i/users', '');
-        $this->content->items[] = '<a title="'.get_string('listofallpeople').'" href="'.
-                                  $CFG->wwwroot.'/user/index.php?contextid='.$currentcontext->id.'">'.$icon.get_string('participants').'</a>';
-
-        return $this->content;
-    }
-
-    // my moodle can only have SITEID and it's redundant here, so take it away
-    function applicable_formats() {
-        return array('all' => true, 'my' => false, 'tag' => false);
-    }
-
-}
diff --git a/blocks/participants/tests/behat/block_participants_course.feature b/blocks/participants/tests/behat/block_participants_course.feature
deleted file mode 100644 (file)
index 5fa4a0b..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-@block @block_participants
-Feature: People Block used in a course
-  In order to view participants in a course
-  As a teacher
-  I can add the people block to a course
-
-  Background:
-    Given the following "courses" exist:
-      | fullname | shortname | category |
-      | Course 1 | C101      | 0        |
-    And the following "users" exist:
-      | username    | firstname | lastname | email            |
-      | student1    | Sam       | Student  | student1@example.com |
-    And the following "course enrolments" exist:
-      | user        | course | role           |
-      | student1    | C101   | student        |
-    And I log in as "admin"
-    And I am on "Course 1" course homepage with editing mode on
-    And I add the "People" block
-    And I log out
-
-  Scenario: Student can view participants link
-    When I log in as "student1"
-    And I am on "Course 1" course homepage
-    Then "People" "block" should exist
-    And I should see "Participants" in the "People" "block"
-
-  Scenario: Student can follow participants link and be directed to the correct page
-    When I log in as "student1"
-    And I am on "Course 1" course homepage
-    And I click on "Participants" "link" in the "People" "block"
-    Then I should see "Participants" in the "#page-content" "css_element"
-
-  Scenario: Student without permission can not view participants link
-    Given the following "permission overrides" exist:
-         | capability | permission | role | contextlevel | reference |
-         | moodle/course:viewparticipants | Prevent | student | Course | C101 |
-    When I log in as "student1"
-    And I am on "Course 1" course homepage
-    Then "People" "block" should not exist
diff --git a/blocks/participants/tests/behat/block_participants_frontpage.feature b/blocks/participants/tests/behat/block_participants_frontpage.feature
deleted file mode 100644 (file)
index 3e47bd2..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-@block @block_participants
-Feature: People Block used on frontpage
-  In order to view participants in a site
-  As a admin
-  I can add the people block to the front page
-
-  Background:
-    Given the following "users" exist:
-      | username    | firstname | lastname | email            |
-      | student1    | Sam       | Student  | student1@example.com |
-    And I log in as "admin"
-    And I am on site homepage
-    And I navigate to "Turn editing on" in current page administration
-    And I add the "People" block
-    And I log out
-
-  Scenario: Admin can view site participants link
-    When I log in as "admin"
-    And I am on site homepage
-    Then "People" "block" should exist
-    And I should see "Participants" in the "People" "block"
-
-  Scenario: Student can not follow participants link on frontpage
-    When I log in as "student1"
-    And I am on site homepage
-    Then "People" "block" should not exist
index 647e992..1694636 100644 (file)
@@ -112,7 +112,7 @@ class core_block_externallib_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
-        $CFG->defaultblocks_override = 'participants,search_forums,course_list:calendar_upcoming,recent_activity';
+        $CFG->defaultblocks_override = 'search_forums,course_list:calendar_upcoming,recent_activity';
 
         $user = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course();
@@ -126,10 +126,10 @@ class core_block_externallib_testcase extends externallib_advanced_testcase {
         // We need to execute the return values cleaning process to simulate the web service server.
         $result = external_api::clean_returnvalue(core_block_external::get_course_blocks_returns(), $result);
 
-        // Expect 5 default blocks.
-        $this->assertCount(5, $result['blocks']);
+        // Expect 4 default blocks.
+        $this->assertCount(4, $result['blocks']);
 
-        $expectedblocks = array('navigation', 'settings', 'participants', 'search_forums', 'course_list',
+        $expectedblocks = array('navigation', 'settings', 'search_forums', 'course_list',
                                 'calendar_upcoming', 'recent_activity');
         foreach ($result['blocks'] as $block) {
             if (!in_array($block['name'], $expectedblocks)) {
index 0f370ee..5894b64 100644 (file)
@@ -3,6 +3,7 @@ information provided here is intended especially for developers.
 
 === 3.8 ===
 * Block block_community is no longer a part of core.
+* Block block_participants is no longer a part of core.
 
 === 3.7 ===
 * The block:addinstance capability is no longer required if the block can only be added to a dashboard.
index 4bbc744..8ff19c3 100644 (file)
@@ -460,8 +460,7 @@ class provider implements
             $params = array_merge($inparams, ['userid' => $userid]);
             $associds = $DB->get_fieldset_sql($sql, $params);
 
-            list($insql, $inparams) = $DB->get_in_or_equal($associds, SQL_PARAMS_NAMED, 'param', true);
-            $DB->delete_records_select('blog_association', "id $insql", $inparams);
+            $DB->delete_records_list('blog_association', 'id', $associds);
         }
     }
 
index 8db5bdd..7002552 100644 (file)
@@ -370,6 +370,37 @@ class core_blog_privacy_testcase extends provider_testcase {
         $this->assertTrue($DB->record_exists('post', ['courseid' => $c1->id, 'userid' => $u1->id, 'module' => 'notes']));
     }
 
+    /**
+     * Test provider delete_data_for_user with a context that contains no entries
+     *
+     * @return void
+     */
+    public function test_delete_data_for_user_empty_context() {
+        global $DB;
+
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        // Create a blog entry for user, associated with course.
+        $entry = new blog_entry($this->create_post(['userid' => $user->id, 'courseid' => $course->id])->id);
+        $entry->add_association($context->id);
+
+        // Generate list of contexts for user.
+        $contexts = provider::get_contexts_for_userid($user->id);
+        $this->assertContains($context->id, $contexts->get_contextids());
+
+        // Now delete the blog entry.
+        $entry->delete();
+
+        // Try to delete user data using contexts obtained prior to entry deletion.
+        $contextlist = new approved_contextlist($user, 'core_blog', $contexts->get_contextids());
+        provider::delete_data_for_user($contextlist);
+
+        // Sanity check to ensure blog_associations is really empty.
+        $this->assertEmpty($DB->get_records('blog_association', ['contextid' => $context->id]));
+    }
+
     public function test_delete_data_for_all_users_in_context() {
         global $DB;
 
index ad9bc00..1a3faa1 100644 (file)
@@ -58,5 +58,11 @@ class cachestore_redis_addinstance_form extends cachestore_addinstance_form {
         $form->addHelpButton('serializer', 'useserializer', 'cachestore_redis');
         $form->setDefault('serializer', Redis::SERIALIZER_PHP);
         $form->setType('serializer', PARAM_INT);
+
+        $compressoroptions = cachestore_redis::config_get_compressor_options();
+        $form->addElement('select', 'compressor', get_string('usecompressor', 'cachestore_redis'), $compressoroptions);
+        $form->addHelpButton('compressor', 'usecompressor', 'cachestore_redis');
+        $form->setDefault('compressor', cachestore_redis::COMPRESSOR_NONE);
+        $form->setType('compressor', PARAM_INT);
     }
 }
index 0d155be..a52b93f 100644 (file)
@@ -24,6 +24,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['compressor_none'] = 'No compression.';
+$string['compressor_php_gzip'] = 'Use gzip compression.';
+$string['compressor_php_zstd'] = 'Use Zstandard compression.';
 $string['pluginname'] = 'Redis';
 $string['prefix'] = 'Key prefix';
 $string['prefix_help'] = 'This prefix is used for all key names on the Redis server.
@@ -48,3 +51,5 @@ $string['useserializer'] = 'Use serializer';
 $string['useserializer_help'] = 'Specifies the serializer to use for serializing.
 The valid serializers are Redis::SERIALIZER_PHP or Redis::SERIALIZER_IGBINARY.
 The latter is supported only when phpredis is configured with --enable-redis-igbinary option and the igbinary extension is loaded.';
+$string['usecompressor'] = 'Use compressor';
+$string['usecompressor_help'] = 'Specifies the compressor to use after serializing. It is done at Moodle Cache API level, not at php-redis level.';
index 3596200..bde6b9e 100644 (file)
@@ -38,6 +38,21 @@ defined('MOODLE_INTERNAL') || die();
  */
 class cachestore_redis extends cache_store implements cache_is_key_aware, cache_is_lockable,
         cache_is_configurable, cache_is_searchable {
+    /**
+     * Compressor: none.
+     */
+    const COMPRESSOR_NONE = 0;
+
+    /**
+     * Compressor: PHP GZip.
+     */
+    const COMPRESSOR_PHP_GZIP = 1;
+
+    /**
+     * Compressor: PHP Zstandard.
+     */
+    const COMPRESSOR_PHP_ZSTD = 2;
+
     /**
      * Name of this store.
      *
@@ -80,6 +95,13 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
      */
     protected $serializer = Redis::SERIALIZER_PHP;
 
+    /**
+     * Compressor for this store.
+     *
+     * @var int
+     */
+    protected $compressor = self::COMPRESSOR_NONE;
+
     /**
      * Determines if the requirements for this type of store are met.
      *
@@ -134,6 +156,9 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
         if (array_key_exists('serializer', $configuration)) {
             $this->serializer = (int)$configuration['serializer'];
         }
+        if (array_key_exists('compressor', $configuration)) {
+            $this->compressor = (int)$configuration['compressor'];
+        }
         $password = !empty($configuration['password']) ? $configuration['password'] : '';
         $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
         $this->redis = $this->new_redis($configuration['server'], $prefix, $password);
@@ -161,7 +186,10 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
             if (!empty($password)) {
                 $redis->auth($password);
             }
-            $redis->setOption(Redis::OPT_SERIALIZER, $this->serializer);
+            // If using compressor, serialisation will be done at cachestore level, not php-redis.
+            if ($this->compressor == self::COMPRESSOR_NONE) {
+                $redis->setOption(Redis::OPT_SERIALIZER, $this->serializer);
+            }
             if (!empty($prefix)) {
                 $redis->setOption(Redis::OPT_PREFIX, $prefix);
             }
@@ -236,7 +264,13 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
      * @return mixed The value of the key, or false if there is no value associated with the key.
      */
     public function get($key) {
-        return $this->redis->hGet($this->hash, $key);
+        $value = $this->redis->hGet($this->hash, $key);
+
+        if ($this->compressor == self::COMPRESSOR_NONE) {
+            return $value;
+        }
+
+        return $this->uncompress($value);
     }
 
     /**
@@ -246,7 +280,17 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
      * @return array An array of the values of the given keys.
      */
     public function get_many($keys) {
-        return $this->redis->hMGet($this->hash, $keys);
+        $values = $this->redis->hMGet($this->hash, $keys);
+
+        if ($this->compressor == self::COMPRESSOR_NONE) {
+            return $values;
+        }
+
+        foreach ($values as &$value) {
+            $value = $this->uncompress($value);
+        }
+
+        return $values;
     }
 
     /**
@@ -257,6 +301,10 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
      * @return bool True if the operation succeeded, false otherwise.
      */
     public function set($key, $value) {
+        if ($this->compressor != self::COMPRESSOR_NONE) {
+            $value = $this->compress($value);
+        }
+
         return ($this->redis->hSet($this->hash, $key, $value) !== false);
     }
 
@@ -270,7 +318,12 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
     public function set_many(array $keyvaluearray) {
         $pairs = [];
         foreach ($keyvaluearray as $pair) {
-            $pairs[$pair['key']] = $pair['value'];
+            $key = $pair['key'];
+            if ($this->compressor != self::COMPRESSOR_NONE) {
+                $pairs[$key] = $this->compress($pair['value']);
+            } else {
+                $pairs[$key] = $pair['value'];
+            }
         }
         if ($this->redis->hMSet($this->hash, $pairs)) {
             return count($pairs);
@@ -446,7 +499,8 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
             'server' => $data->server,
             'prefix' => $data->prefix,
             'password' => $data->password,
-            'serializer' => $data->serializer
+            'serializer' => $data->serializer,
+            'compressor' => $data->compressor,
         );
     }
 
@@ -465,6 +519,9 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
         if (!empty($config['serializer'])) {
             $data['serializer'] = $config['serializer'];
         }
+        if (!empty($config['compressor'])) {
+            $data['compressor'] = $config['compressor'];
+        }
         $editform->set_data($data);
     }
 
@@ -538,4 +595,115 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
         }
         return $options;
     }
+
+    /**
+     * Gets an array of options to use as the compressor.
+     *
+     * @return array
+     */
+    public static function config_get_compressor_options() {
+        $arr = [
+            self::COMPRESSOR_NONE     => get_string('compressor_none', 'cachestore_redis'),
+            self::COMPRESSOR_PHP_GZIP => get_string('compressor_php_gzip', 'cachestore_redis'),
+        ];
+
+        // Check if the Zstandard PHP extension is installed.
+        if (extension_loaded('zstd')) {
+            $arr[self::COMPRESSOR_PHP_ZSTD] = get_string('compressor_php_zstd', 'cachestore_redis');
+        }
+
+        return $arr;
+    }
+
+    /**
+     * Compress the given value, serializing it first.
+     *
+     * @param mixed $value
+     * @return string
+     */
+    private function compress($value) {
+        $value = $this->serialize($value);
+
+        switch ($this->compressor) {
+            case self::COMPRESSOR_NONE:
+                return $value;
+
+            case self::COMPRESSOR_PHP_GZIP:
+                return gzencode($value);
+
+            case self::COMPRESSOR_PHP_ZSTD:
+                return zstd_compress($value);
+
+            default:
+                debugging("Invalid compressor: {$this->compressor}");
+                return $value;
+        }
+    }
+
+    /**
+     * Uncompresses (deflates) the data, unserialising it afterwards.
+     *
+     * @param string $value
+     * @return mixed
+     */
+    private function uncompress($value) {
+        if ($value === false) {
+            return false;
+        }
+
+        switch ($this->compressor) {
+            case self::COMPRESSOR_NONE:
+                break;
+            case self::COMPRESSOR_PHP_GZIP:
+                $value = gzdecode($value);
+                break;
+            case self::COMPRESSOR_PHP_ZSTD:
+                $value = zstd_uncompress($value);
+                break;
+            default:
+                debugging("Invalid compressor: {$this->compressor}");
+        }
+
+        return $this->unserialize($value);
+    }
+
+    /**
+     * Serializes the data according to the configured serializer.
+     *
+     * @param mixed $value
+     * @return string
+     */
+    private function serialize($value) {
+        switch ($this->serializer) {
+            case Redis::SERIALIZER_NONE:
+                return $value;
+            case Redis::SERIALIZER_PHP:
+                return serialize($value);
+            case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY:
+                return igbinary_serialize($value);
+            default:
+                debugging("Invalid serializer: {$this->serializer}");
+                return $value;
+        }
+    }
+
+    /**
+     * Unserializes the data according to the configured serializer
+     *
+     * @param string $value
+     * @return mixed
+     */
+    private function unserialize($value) {
+        switch ($this->serializer) {
+            case Redis::SERIALIZER_NONE:
+                return $value;
+            case Redis::SERIALIZER_PHP:
+                return unserialize($value);
+            case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY:
+                return igbinary_unserialize($value);
+            default:
+                debugging("Invalid serializer: {$this->serializer}");
+                return $value;
+        }
+    }
 }
diff --git a/cache/stores/redis/tests/compressor_test.php b/cache/stores/redis/tests/compressor_test.php
new file mode 100644 (file)
index 0000000..0d9a583
--- /dev/null
@@ -0,0 +1,287 @@
+<?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/>.
+
+/**
+ * Redis cache test.
+ *
+ * If you wish to use these unit tests all you need to do is add the following definition to
+ * your config.php file.
+ *
+ * define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');
+ *
+ * @package   cachestore_redis
+ * @copyright 2018 Catalyst IT Australia {@link http://www.catalyst-au.net}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/../../../tests/fixtures/stores.php');
+require_once(__DIR__.'/../lib.php');
+
+/**
+ * Redis cache test - compressor settings.
+ *
+ * @package   cachestore_redis
+ * @author    Daniel Thee Roperto <daniel.roperto@catalyst-au.net>
+ * @copyright 2018 Catalyst IT Australia {@link http://www.catalyst-au.net}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_redis_compressor_test extends advanced_testcase {
+
+    /**
+     * Test set up
+     */
+    public function setUp() {
+        if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) {
+            $this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.');
+        }
+
+        parent::setUp();
+    }
+
+    /**
+     * Create a cachestore.
+     *
+     * @param int $compressor
+     * @param int $serializer
+     * @return cachestore_redis
+     */
+    public function create_store($compressor, $serializer) {
+        /** @var cache_definition $definition */
+        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test');
+        $config = cachestore_redis::unit_test_configuration();
+        $config['compressor'] = $compressor;
+        $config['serializer'] = $serializer;
+        $store = new cachestore_redis('Test', $config);
+        $store->initialise($definition);
+
+        return $store;
+    }
+
+    /**
+     * It misses a value.
+     */
+    public function test_it_can_miss_one() {
+        $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, Redis::SERIALIZER_PHP);
+
+        self::assertFalse($store->get('missme'));
+    }
+
+    /**
+     * It misses many values.
+     */
+    public function test_it_can_miss_many() {
+        $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, Redis::SERIALIZER_PHP);
+
+        $expected = ['missme' => false, 'missmetoo' => false];
+        $actual = $store->get_many(array_keys($expected));
+        self::assertSame($expected, $actual);
+    }
+
+    /**
+     * It misses some values.
+     */
+    public function test_it_can_miss_some() {
+        $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, Redis::SERIALIZER_PHP);
+        $store->set('iamhere', 'youfoundme');
+
+        $expected = ['missme' => false, 'missmetoo' => false, 'iamhere' => 'youfoundme'];
+        $actual = $store->get_many(array_keys($expected));
+        self::assertSame($expected, $actual);
+    }
+
+    /**
+     * A provider for test_works_with_different_types
+     *
+     * @return array
+     */
+    public function provider_for_test_it_works_with_different_types() {
+        $object = new stdClass();
+        $object->field = 'value';
+
+        return [
+            ['string', 'Abc Def'],
+            ['string_empty', ''],
+            ['string_binary', gzencode('some binary data')],
+            ['int', 123],
+            ['int_zero', 0],
+            ['int_negative', -100],
+            ['int_huge', PHP_INT_MAX],
+            ['float', 3.14],
+            ['boolean_true', true],
+            // Boolean 'false' is not tested as it is not allowed in Moodle.
+            ['array', [1, 'b', 3.4]],
+            ['array_map', ['a' => 'b', 'c' => 'd']],
+            ['object_stdClass', $object],
+            ['null', null],
+        ];
+    }
+
+    /**
+     * It works with different types.
+     *
+     * @dataProvider provider_for_test_it_works_with_different_types
+     * @param string $key
+     * @param mixed $value
+     */
+    public function test_it_works_with_different_types($key, $value) {
+        $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, Redis::SERIALIZER_PHP);
+        $store->set($key, $value);
+
+        self::assertEquals($value, $store->get($key), "Failed set/get for: {$key}");
+    }
+
+    /**
+     * Test it works with different types for many.
+     */
+    public function test_it_works_with_different_types_for_many() {
+        $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, Redis::SERIALIZER_PHP);
+
+        $provider = $this->provider_for_test_it_works_with_different_types();
+        $keys = [];
+        $values = [];
+        $expected = [];
+        foreach ($provider as $item) {
+            $keys[] = $item[0];
+            $values[] = ['key' => $item[0], 'value' => $item[1]];
+            $expected[$item[0]] = $item[1];
+        }
+        $store->set_many($values);
+        $actual = $store->get_many($keys);
+        self::assertEquals($expected, $actual);
+    }
+
+    /**
+     * Provider for set/get combination tests.
+     *
+     * @return array
+     */
+    public function provider_for_tests_setget() {
+        $data = [
+            ['none, none',
+                Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_NONE,
+                'value1', 'value2'],
+            ['none, gzip',
+                Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_PHP_GZIP,
+                gzencode('value1'), gzencode('value2')],
+            ['php, none',
+                Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_NONE,
+                serialize('value1'), serialize('value2')],
+            ['php, gzip',
+                Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_PHP_GZIP,
+                gzencode(serialize('value1')), gzencode(serialize('value2'))],
+        ];
+
+        if (defined('Redis::SERIALIZER_IGBINARY')) {
+            $data[] = [
+                'igbinary, none',
+                    Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_NONE,
+                    igbinary_serialize('value1'), igbinary_serialize('value2'),
+            ];
+            $data[] = [
+                'igbinary, gzip',
+                    Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_PHP_GZIP,
+                    gzencode(igbinary_serialize('value1')), gzencode(igbinary_serialize('value2')),
+            ];
+        }
+
+        if (extension_loaded('zstd')) {
+            $data[] = [
+                'none, zstd',
+                Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_PHP_ZSTD,
+                zstd_compress('value1'), zstd_compress('value2'),
+            ];
+            $data[] = [
+                'php, zstd',
+                Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_PHP_ZSTD,
+                zstd_compress(serialize('value1')), zstd_compress(serialize('value2')),
+            ];
+
+            if (defined('Redis::SERIALIZER_IGBINARY')) {
+                $data[] = [
+                    'igbinary, zstd',
+                    Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_PHP_ZSTD,
+                    zstd_compress(igbinary_serialize('value1')), zstd_compress(igbinary_serialize('value2')),
+                ];
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Test we can use get and set with all combinations.
+     *
+     * @dataProvider provider_for_tests_setget
+     * @param string $name
+     * @param int $serializer
+     * @param int $compressor
+     * @param string $rawexpected1
+     * @param string $rawexpected2
+     */
+    public function test_it_can_use_getset($name, $serializer, $compressor, $rawexpected1, $rawexpected2) {
+        // Create a connection with the desired serialisation.
+        $store = $this->create_store($compressor, $serializer);
+        $store->set('key', 'value1');
+
+        // Disable compressor and serializer to check the actual stored value.
+        $rawstore = $this->create_store(cachestore_redis::COMPRESSOR_NONE, Redis::SERIALIZER_NONE);
+
+        $data = $store->get('key');
+        $rawdata = $rawstore->get('key');
+        self::assertSame('value1', $data, "Invalid serialisation/unserialisation for: {$name}");
+        self::assertSame($rawexpected1, $rawdata, "Invalid rawdata for: {$name}");
+    }
+
+    /**
+     * Test we can use get and set many with all combinations.
+     *
+     * @dataProvider provider_for_tests_setget
+     * @param string $name
+     * @param int $serializer
+     * @param int $compressor
+     * @param string $rawexpected1
+     * @param string $rawexpected2
+     */
+    public function test_it_can_use_getsetmany($name, $serializer, $compressor, $rawexpected1, $rawexpected2) {
+        $many = [
+            ['key' => 'key1', 'value' => 'value1'],
+            ['key' => 'key2', 'value' => 'value2'],
+        ];
+        $keys = ['key1', 'key2'];
+        $expectations = ['key1' => 'value1', 'key2' => 'value2'];
+        $rawexpectations = ['key1' => $rawexpected1, 'key2' => $rawexpected2];
+
+        // Create a connection with the desired serialisation.
+        $store = $this->create_store($compressor, $serializer);
+        $store->set_many($many);
+
+        // Disable compressor and serializer to check the actual stored value.
+        $rawstore = $this->create_store(cachestore_redis::COMPRESSOR_NONE, Redis::SERIALIZER_NONE);
+
+        $data = $store->get_many($keys);
+        $rawdata = $rawstore->get_many($keys);
+        foreach ($keys as $key) {
+            self::assertSame($expectations[$key],
+                             $data[$key],
+                             "Invalid serialisation/unserialisation for {$key} with serializer {$name}");
+            self::assertSame($rawexpectations[$key],
+                             $rawdata[$key],
+                             "Invalid rawdata for {$key} with serializer {$name}");
+        }
+    }
+}
index 4a801da..eaf6344 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
+=== 3.8 ===
+* The Redis cache store can now make use of the Zstandard compression algorithm (see MDL-66428).
+
 === 3.7 ===
 * Upgraded MongoDB cache store to use the new lower level PHP-driver and MongoDB PHP Library.
 * The mongodb extension has replaced the old mongo extension. The mongodb pecl extension >= 1.5 must be installed to use MongoDB
index 56b3803..5227584 100644 (file)
@@ -206,17 +206,17 @@ $CFG->admin = 'admin';
 //
 // These variables define DEFAULT block variables for new courses
 // If this one is set it overrides all others and is the only one used.
-//      $CFG->defaultblocks_override = 'participants,activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
+//      $CFG->defaultblocks_override = 'activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
 //
 // These variables define the specific settings for defined course formats.
 // They override any settings defined in the formats own config file.
 //      $CFG->defaultblocks_site = 'site_main_menu,course_list:course_summary,calendar_month';
-//      $CFG->defaultblocks_social = 'participants,search_forums,calendar_month,calendar_upcoming,social_activities,recent_activity,course_list';
-//      $CFG->defaultblocks_topics = 'participants,activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
-//      $CFG->defaultblocks_weeks = 'participants,activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
+//      $CFG->defaultblocks_social = 'search_forums,calendar_month,calendar_upcoming,social_activities,recent_activity,course_list';
+//      $CFG->defaultblocks_topics = 'activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
+//      $CFG->defaultblocks_weeks = 'activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
 //
 // These blocks are used when no other default setting is found.
-//      $CFG->defaultblocks = 'participants,activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
+//      $CFG->defaultblocks = 'activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity';
 //
 // You can specify a different class to be created for the $PAGE global, and to
 // compute which blocks appear on each page. However, I cannot think of any good
index 9f666a5..da5d4ee 100644 (file)
@@ -3694,7 +3694,11 @@ class core_course_external extends external_api {
                 'classification' => new external_value(PARAM_ALPHA, 'future, inprogress, or past'),
                 'limit' => new external_value(PARAM_INT, 'Result set limit', VALUE_DEFAULT, 0),
                 'offset' => new external_value(PARAM_INT, 'Result set offset', VALUE_DEFAULT, 0),
-                'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null)
+                'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null),
+                'customfieldname' => new external_value(PARAM_ALPHANUMEXT, 'Used when classification = customfield',
+                    VALUE_DEFAULT, null),
+                'customfieldvalue' => new external_value(PARAM_RAW, 'Used when classification = customfield',
+                    VALUE_DEFAULT, null),
             )
         );
     }
@@ -3717,6 +3721,8 @@ class core_course_external extends external_api {
      * @param  int $limit Result set limit
      * @param  int $offset Offset the full course set before timeline classification is applied
      * @param  string $sort SQL sort string for results
+     * @param  string $customfieldname
+     * @param  string $customfieldvalue
      * @return array list of courses and warnings
      * @throws  invalid_parameter_exception
      */
@@ -3724,7 +3730,9 @@ class core_course_external extends external_api {
         string $classification,
         int $limit = 0,
         int $offset = 0,
-        string $sort = null
+        string $sort = null,
+        string $customfieldname = null,
+        string $customfieldvalue = null
     ) {
         global $CFG, $PAGE, $USER;
         require_once($CFG->dirroot . '/course/lib.php');
@@ -3735,6 +3743,7 @@ class core_course_external extends external_api {
                 'limit' => $limit,
                 'offset' => $offset,
                 'sort' => $sort,
+                'customfieldvalue' => $customfieldvalue,
             )
         );
 
@@ -3742,6 +3751,7 @@ class core_course_external extends external_api {
         $limit = $params['limit'];
         $offset = $params['offset'];
         $sort = $params['sort'];
+        $customfieldvalue = $params['customfieldvalue'];
 
         switch($classification) {
             case COURSE_TIMELINE_ALLINCLUDINGHIDDEN:
@@ -3758,6 +3768,8 @@ class core_course_external extends external_api {
                 break;
             case COURSE_TIMELINE_HIDDEN:
                 break;
+            case COURSE_CUSTOMFIELD:
+                break;
             default:
                 throw new invalid_parameter_exception('Invalid classification');
         }
@@ -3801,6 +3813,13 @@ class core_course_external extends external_api {
                 $favouritecourseids,
                 $limit
             );
+        } else if ($classification == COURSE_CUSTOMFIELD) {
+            list($filteredcourses, $processedcount) = course_filter_courses_by_customfield(
+                $courses,
+                $customfieldname,
+                $customfieldvalue,
+                $limit
+            );
         } else {
             list($filteredcourses, $processedcount) = course_filter_courses_by_timeline_classification(
                 $courses,
index 8e70e79..fbeea7a 100644 (file)
@@ -63,7 +63,10 @@ define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
 define('COURSE_TIMELINE_FUTURE', 'future');
 define('COURSE_FAVOURITES', 'favourites');
 define('COURSE_TIMELINE_HIDDEN', 'hidden');
+define('COURSE_CUSTOMFIELD', 'customfield');
 define('COURSE_DB_QUERY_LIMIT', 1000);
+/** Searching for all courses that have no value for the specified custom field. */
+define('COURSE_CUSTOMFIELD_EMPTY', -1);
 
 function make_log_url($module, $url) {
     switch ($module) {
@@ -4397,6 +4400,103 @@ function course_filter_courses_by_favourites(
     return [$filteredcourses, $numberofcoursesprocessed];
 }
 
+/**
+ * Search the given $courses for any that have a $customfieldname value that matches the given
+ * $customfieldvalue, up to the specified $limit.
+ *
+ * This function will return the subset of courses that matches the value as well as the
+ * number of courses it had to process to build that subset.
+ *
+ * It is recommended that for larger sets of courses this function is given a Generator that loads
+ * the courses from the database in chunks.
+ *
+ * @param array|Traversable $courses List of courses to process
+ * @param string $customfieldname the shortname of the custom field to match against
+ * @param string $customfieldvalue the value this custom field needs to match
+ * @param int $limit Limit the number of results to this amount
+ * @return array First value is the filtered courses, second value is the number of courses processed
+ */
+function course_filter_courses_by_customfield(
+    $courses,
+    $customfieldname,
+    $customfieldvalue,
+    int $limit = 0
+) : array {
+    global $DB;
+
+    if (!$courses) {
+        return [[], 0];
+    }
+
+    // Prepare the list of courses to search through.
+    $coursesbyid = [];
+    foreach ($courses as $course) {
+        $coursesbyid[$course->id] = $course;
+    }
+    if (!$coursesbyid) {
+        return [[], 0];
+    }
+    list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED);
+
+    // Get the id of the custom field.
+    $sql = "
+       SELECT f.id
+         FROM {customfield_field} f
+         JOIN {customfield_category} cat ON cat.id = f.categoryid
+        WHERE f.shortname = ?
+          AND cat.component = 'core_course'
+          AND cat.area = 'course'
+    ";
+    $fieldid = $DB->get_field_sql($sql, [$customfieldname]);
+    if (!$fieldid) {
+        return [[], 0];
+    }
+
+    // Get a list of courseids that match that custom field value.
+    if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) {
+        $comparevalue = $DB->sql_compare_text('cd.value');
+        $sql = "
+           SELECT c.id
+             FROM {course} c
+        LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid
+            WHERE c.id $csql
+              AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0')
+        ";
+        $params['fieldid'] = $fieldid;
+        $matchcourseids = $DB->get_fieldset_sql($sql, $params);
+    } else {
+        $comparevalue = $DB->sql_compare_text('value');
+        $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql";
+        $params['fieldid'] = $fieldid;
+        $params['customfieldvalue'] = $customfieldvalue;
+        $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params);
+    }
+
+    // Prepare the list of courses to return.
+    $filteredcourses = [];
+    $numberofcoursesprocessed = 0;
+    $filtermatches = 0;
+
+    foreach ($coursesbyid as $course) {
+        $numberofcoursesprocessed++;
+
+        if (in_array($course->id, $matchcourseids)) {
+            $filteredcourses[] = $course;
+            $filtermatches++;
+        }
+
+        if ($limit && $filtermatches >= $limit) {
+            // We've found the number of requested courses. No need to continue searching.
+            break;
+        }
+    }
+
+    // Return the number of filtered courses as well as the number of courses that were searched
+    // in order to find the matching courses. This allows the calling code to do some kind of
+    // pagination.
+    return [$filteredcourses, $numberofcoursesprocessed];
+}
+
 /**
  * Check module updates since a given time.
  * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
index 1b0a8ac..6779405 100644 (file)
@@ -4866,6 +4866,262 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEquals($expectedprocessedcount, $processedcount);
     }
 
+    /**
+     * Test cases for the course_filter_courses_by_timeline_classification tests.
+     */
+    public function get_course_filter_courses_by_customfield_test_cases() {
+        global $CFG;
+        require_once($CFG->dirroot.'/blocks/myoverview/lib.php');
+        $coursedata = [
+            [
+                'shortname' => 'C1',
+                'customfield_checkboxfield' => 1,
+                'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
+                'customfield_selectfield' => 1,
+                'customfield_textfield' => 'fish',
+            ],
+            [
+                'shortname' => 'C2',
+                'customfield_checkboxfield' => 0,
+                'customfield_datefield' => strtotime('1980-08-05T13:00:00Z'),
+            ],
+            [
+                'shortname' => 'C3',
+                'customfield_checkboxfield' => 0,
+                'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
+                'customfield_selectfield' => 2,
+                'customfield_textfield' => 'dog',
+            ],
+            [
+                'shortname' => 'C4',
+                'customfield_checkboxfield' => 1,
+                'customfield_selectfield' => 3,
+                'customfield_textfield' => 'cat',
+            ],
+            [
+                'shortname' => 'C5',
+                'customfield_datefield' => strtotime('1980-08-06T13:00:00Z'),
+                'customfield_selectfield' => 2,
+                'customfield_textfield' => 'fish',
+            ],
+        ];
+
+        return [
+            'empty set' => [
+                'coursedata' => [],
+                'customfield' => 'checkboxfield',
+                'customfieldvalue' => 1,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => [],
+                'expectedprocessedcount' => 0
+            ],
+            'checkbox yes' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'checkboxfield',
+                'customfieldvalue' => 1,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C1', 'C4'],
+                'expectedprocessedcount' => 5
+            ],
+            'checkbox no' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'checkboxfield',
+                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C2', 'C3', 'C5'],
+                'expectedprocessedcount' => 5
+            ],
+            'date 1 Feb 2001' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'datefield',
+                'customfieldvalue' => strtotime('2001-02-01T12:00:00Z'),
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C1', 'C3'],
+                'expectedprocessedcount' => 5
+            ],
+            'date 6 Aug 1980' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'datefield',
+                'customfieldvalue' => strtotime('1980-08-06T13:00:00Z'),
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C5'],
+                'expectedprocessedcount' => 5
+            ],
+            'date no date' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'datefield',
+                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C4'],
+                'expectedprocessedcount' => 5
+            ],
+            'select Option 1' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'selectfield',
+                'customfieldvalue' => 1,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C1'],
+                'expectedprocessedcount' => 5
+            ],
+            'select Option 2' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'selectfield',
+                'customfieldvalue' => 2,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C3', 'C5'],
+                'expectedprocessedcount' => 5
+            ],
+            'select no select' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'selectfield',
+                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C2'],
+                'expectedprocessedcount' => 5
+            ],
+            'text fish' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'textfield',
+                'customfieldvalue' => 'fish',
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C1', 'C5'],
+                'expectedprocessedcount' => 5
+            ],
+            'text dog' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'textfield',
+                'customfieldvalue' => 'dog',
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C3'],
+                'expectedprocessedcount' => 5
+            ],
+            'text no text' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'textfield',
+                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['C2'],
+                'expectedprocessedcount' => 5
+            ],
+            'checkbox limit no' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'checkboxfield',
+                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
+                'limit' => 2,
+                'offset' => 0,
+                'expectedcourses' => ['C2', 'C3'],
+                'expectedprocessedcount' => 3
+            ],
+            'checkbox limit offset no' => [
+                'coursedata' => $coursedata,
+                'customfield' => 'checkboxfield',
+                'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
+                'limit' => 2,
+                'offset' => 3,
+                'expectedcourses' => ['C5'],
+                'expectedprocessedcount' => 2
+            ],
+        ];
+    }
+
+    /**
+     * Test the course_filter_courses_by_customfield function.
+     *
+     * @dataProvider get_course_filter_courses_by_customfield_test_cases()
+     * @param array $coursedata Course test data to create.
+     * @param string $customfield Shortname of the customfield.
+     * @param string $customfieldvalue the value to filter by.
+     * @param int $limit Maximum number of results to return.
+     * @param int $offset Results to skip at the start of the result set.
+     * @param string[] $expectedcourses Expected courses in results.
+     * @param int $expectedprocessedcount Expected number of course records to be processed.
+     */
+    public function test_course_filter_courses_by_customfield(
+        $coursedata,
+        $customfield,
+        $customfieldvalue,
+        $limit,
+        $offset,
+        $expectedcourses,
+        $expectedprocessedcount
+    ) {
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+
+        // Create the custom fields.
+        $generator->create_custom_field_category([
+            'name' => 'Course fields',
+            'component' => 'core_course',
+            'area' => 'course',
+            'itemid' => 0,
+        ]);
+        $generator->create_custom_field([
+            'name' => 'Checkbox field',
+            'category' => 'Course fields',
+            'type' => 'checkbox',
+            'shortname' => 'checkboxfield',
+        ]);
+        $generator->create_custom_field([
+            'name' => 'Date field',
+            'category' => 'Course fields',
+            'type' => 'date',
+            'shortname' => 'datefield',
+            'configdata' => '{"mindate":0, "maxdate":0}',
+        ]);
+        $generator->create_custom_field([
+            'name' => 'Select field',
+            'category' => 'Course fields',
+            'type' => 'select',
+            'shortname' => 'selectfield',
+            'configdata' => '{"options":"Option 1\nOption 2\nOption 3\nOption 4"}',
+        ]);
+        $generator->create_custom_field([
+            'name' => 'Text field',
+            'category' => 'Course fields',
+            'type' => 'text',
+            'shortname' => 'textfield',
+        ]);
+
+        $courses = array_map(function($coursedata) use ($generator) {
+            return $generator->create_course($coursedata);
+        }, $coursedata);
+
+        $student = $generator->create_user();
+
+        foreach ($courses as $course) {
+            $generator->enrol_user($student->id, $course->id, 'student');
+        }
+
+        $this->setUser($student);
+
+        $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
+        list($result, $processedcount) = course_filter_courses_by_customfield(
+            $coursesgenerator,
+            $customfield,
+            $customfieldvalue,
+            $limit
+        );
+
+        $actual = array_map(function($course) {
+            return $course->shortname;
+        }, $result);
+
+        $this->assertEquals($expectedcourses, $actual);
+        $this->assertEquals($expectedprocessedcount, $processedcount);
+    }
+
     /**
      * Test cases for the course_filter_courses_by_timeline_classification w/ hidden courses tests.
      */
index 440ba61..087a0d3 100644 (file)
@@ -412,4 +412,29 @@ class api {
         $field->prepare_for_config_form($formdata);
         return $formdata;
     }
+
+    /**
+     * Get a list of the course custom fields that support course grouping in
+     * block_myoverview
+     * @return array $shortname => $name
+     */
+    public static function get_fields_supporting_course_grouping() {
+        global $DB;
+        $sql = "
+            SELECT f.*
+              FROM {customfield_field} f
+              JOIN {customfield_category} cat ON cat.id = f.categoryid
+             WHERE cat.component = 'core_course' AND cat.area = 'course'
+             ORDER BY f.name
+        ";
+        $ret = [];
+        $fields = $DB->get_records_sql($sql);
+        foreach ($fields as $field) {
+            $inst = field_controller::create(0, $field);
+            if ($inst->supports_course_grouping()) {
+                $ret[$inst->get('shortname')] = $inst->get('name');
+            }
+        }
+        return $ret;
+    }
 }
index b9da5b9..07e22b7 100644 (file)
@@ -249,4 +249,23 @@ abstract class field_controller {
         $context = $this->get_handler()->get_configuration_context();
         return format_string($this->get('name'), true, ['context' => $context]);
     }
+
+    /**
+     * Does this custom field type support being used as part of the block_myoverview
+     * custom field grouping?
+     * @return bool
+     */
+    public function supports_course_grouping(): bool {
+        return false;
+    }
+
+    /**
+     * If this field supports course filtering, then this function needs overriding to
+     * return the formatted values for this.
+     * @param array $values the used values that need grouping
+     * @return array
+     */
+    public function course_grouping_format_values($values): array {
+        return [];
+    }
 }
index b25736f..6e4f647 100644 (file)
@@ -68,4 +68,27 @@ class field_controller  extends \core_customfield\field_controller {
 
         return $errors;
     }
+
+    /**
+     * Does this custom field type support being used as part of the block_myoverview
+     * custom field grouping?
+     * @return bool
+     */
+    public function supports_course_grouping(): bool {
+        return true;
+    }
+
+    /**
+     * If this field supports course grouping, then this function needs overriding to
+     * return the formatted values for this.
+     * @param array $values the used values that need formatting
+     * @return array
+     */
+    public function course_grouping_format_values($values): array {
+        $name = $this->get_formatted_name();
+        return [
+            1 => $name.': '.get_string('yes'),
+            BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY => $name.': '.get_string('no'),
+        ];
+    }
 }
index 213fe8d..bcdf196 100644 (file)
@@ -83,4 +83,35 @@ class field_controller extends \core_customfield\field_controller {
         $mform->hideIf('configdata[mindate][hour]', 'configdata[includetime]');
         $mform->hideIf('configdata[mindate][minute]', 'configdata[includetime]');
     }
+
+    /**
+     * Does this custom field type support being used as part of the block_myoverview
+     * custom field grouping?
+     * @return bool
+     */
+    public function supports_course_grouping(): bool {
+        return true;
+    }
+
+    /**
+     * If this field supports course grouping, then this function needs overriding to
+     * return the formatted values for this.
+     * @param array $values the used values that need formatting
+     * @return array
+     */
+    public function course_grouping_format_values($values): array {
+        $format = get_string('strftimedate', 'langconfig');
+        $ret = [];
+        foreach ($values as $value) {
+            if ($value) {
+                $ret[$value] = userdate($value, $format);
+            }
+        }
+        if (!$ret) {
+            return []; // If the only dates found are 0, then do not show any options.
+        }
+        $ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
+            $this->get_formatted_name());
+        return $ret;
+    }
 }
index cfce18c..79fa408 100644 (file)
@@ -91,4 +91,32 @@ class field_controller extends \core_customfield\field_controller {
         }
         return $errors;
     }
+
+    /**
+     * Does this custom field type support being used as part of the block_myoverview
+     * custom field grouping?
+     * @return bool
+     */
+    public function supports_course_grouping(): bool {
+        return true;
+    }
+
+    /**
+     * If this field supports course grouping, then this function needs overriding to
+     * return the formatted values for this.
+     * @param array $values the used values that need formatting
+     * @return array
+     */
+    public function course_grouping_format_values($values): array {
+        $options = self::get_options_array($this);
+        $ret = [];
+        foreach ($values as $value) {
+            if (isset($options[$value])) {
+                $ret[$value] = format_string($options[$value]);
+            }
+        }
+        $ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
+            $this->get_formatted_name());
+        return $ret;
+    }
 }
index 06b040a..ae06260 100644 (file)
@@ -120,4 +120,29 @@ class field_controller extends \core_customfield\field_controller {
 
         return $errors;
     }
+
+    /**
+     * Does this custom field type support being used as part of the block_myoverview
+     * custom field grouping?
+     * @return bool
+     */
+    public function supports_course_grouping(): bool {
+        return true;
+    }
+
+    /**
+     * If this field supports course grouping, then this function needs overriding to
+     * return the formatted values for this.
+     * @param array $values the used values that need formatting
+     * @return array
+     */
+    public function course_grouping_format_values($values): array {
+        $ret = [];
+        foreach ($values as $value) {
+            $ret[$value] = format_string($value);
+        }
+        $ret[BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY] = get_string('nocustomvalue', 'block_myoverview',
+            $this->get_formatted_name());
+        return $ret;
+    }
 }
diff --git a/customfield/field/upgrade.txt b/customfield/field/upgrade.txt
new file mode 100644 (file)
index 0000000..2dab74b
--- /dev/null
@@ -0,0 +1,5 @@
+This files describes API changes in /customfield/field/* - customfield field types,
+information provided here is intended especially for developers.
+
+=== 3.8 ===
+* supports_course_grouping() and course_grouping_format_values() functions added to support use of custom fields in block_myoverview
index 45d50d1..2aa96e1 100644 (file)
@@ -37,6 +37,14 @@ use \core_customfield\category_controller;
  */
 class core_customfield_api_testcase extends advanced_testcase {
 
+    /**
+     * This method is called after the last test of this test class is run.
+     */
+    public static function tearDownAfterClass() {
+        $handler = core_course\customfield\course_handler::create();
+        $handler->delete_all();
+    }
+
     /**
      * Tests set up.
      */
index 656bc5b..56aa2d0 100644 (file)
@@ -36,6 +36,14 @@ use \core_customfield\field_controller;
  */
 class core_customfield_category_controller_testcase extends advanced_testcase {
 
+    /**
+     * This method is called after the last test of this test class is run.
+     */
+    public static function tearDownAfterClass() {
+        $handler = core_course\customfield\course_handler::create();
+        $handler->delete_all();
+    }
+
     /**
      * Tests set up.
      */
index ac5b405..7cf0b7e 100644 (file)
@@ -34,6 +34,14 @@ use core_customfield\data_controller;
  */
 class core_customfield_data_controller_testcase extends advanced_testcase {
 
+    /**
+     * This method is called after the last test of this test class is run.
+     */
+    public static function tearDownAfterClass() {
+        $handler = core_course\customfield\course_handler::create();
+        $handler->delete_all();
+    }
+
     /**
      * Tests set up.
      */
index b704469..e490ef1 100644 (file)
@@ -38,6 +38,14 @@ use \core_customfield\field_controller;
  */
 class core_customfield_field_controller_testcase extends advanced_testcase {
 
+    /**
+     * This method is called after the last test of this test class is run.
+     */
+    public static function tearDownAfterClass() {
+        $handler = core_course\customfield\course_handler::create();
+        $handler->delete_all();
+    }
+
     /**
      * Tests set up.
      */
index 4788d9c..059c868 100644 (file)
@@ -35,6 +35,14 @@ defined('MOODLE_INTERNAL') || die();
  */
 class core_customfield_generator_testcase extends advanced_testcase {
 
+    /**
+     * This method is called after the last test of this test class is run.
+     */
+    public static function tearDownAfterClass() {
+        $handler = core_course\customfield\course_handler::create();
+        $handler->delete_all();
+    }
+
     /**
      * Get generator
      * @return core_customfield_generator
index 9a15ced..69ea102 100644 (file)
@@ -45,6 +45,14 @@ class core_customfield_privacy_testcase extends provider_testcase {
     /** @var \core_customfield\field_controller[] */
     private $cffields = [];
 
+    /**
+     * This method is called after the last test of this test class is run.
+     */
+    public static function tearDownAfterClass() {
+        $handler = core_course\customfield\course_handler::create();
+        $handler->delete_all();
+    }
+
     /**
      * Set up
      */
index e44cef6..9604b19 100644 (file)
@@ -58,14 +58,17 @@ class core_enrollib_testcase extends advanced_testcase {
 
         $course1 = $this->getDataGenerator()->create_course(array(
             'shortname' => 'Z',
+            'idnumber' => '123',
             'category' => $category1->id,
         ));
         $course2 = $this->getDataGenerator()->create_course(array(
             'shortname' => 'X',
+            'idnumber' => '789',
             'category' => $category2->id,
         ));
         $course3 = $this->getDataGenerator()->create_course(array(
             'shortname' => 'Y',
+            'idnumber' => '456',
             'category' => $category2->id,
             'visible' => 0,
         ));
@@ -163,7 +166,7 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertTrue(property_exists($course, 'timecreated'));
 
         $courses = enrol_get_all_users_courses($user2->id, false, null, 'id DESC');
-        $this->assertEquals(array($course3->id, $course2->id, $course1->id), array_keys($courses));
+        $this->assertEquals(array($course2->id, $course3->id, $course1->id), array_keys($courses));
 
         // Make sure that implicit sorting defined in navsortmycoursessort is respected.
 
@@ -175,7 +178,54 @@ class core_enrollib_testcase extends advanced_testcase {
         // But still the explicit sorting takes precedence over the implicit one.
 
         $courses = enrol_get_all_users_courses($user1->id, false, null, 'shortname DESC');
+        $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
+
+        // Make sure that implicit visibility sorting defined in navsortmycourseshiddenlast is respected for all course sortings.
+
+        $CFG->navsortmycoursessort = 'sortorder';
+        $CFG->navsortmycourseshiddenlast = true;
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
+
+        $CFG->navsortmycoursessort = 'sortorder';
+        $CFG->navsortmycourseshiddenlast = false;
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course1->id, $course3->id, $course2->id), array_keys($courses));
+
+        $CFG->navsortmycoursessort = 'fullname';
+        $CFG->navsortmycourseshiddenlast = true;
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
+
+        $CFG->navsortmycoursessort = 'fullname';
+        $CFG->navsortmycourseshiddenlast = false;
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
+
+        $CFG->navsortmycoursessort = 'shortname';
+        $CFG->navsortmycourseshiddenlast = true;
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course2->id, $course3->id, $course1->id), array_keys($courses));
+
+        $CFG->navsortmycoursessort = 'shortname';
+        $CFG->navsortmycourseshiddenlast = false;
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course2->id, $course3->id, $course1->id), array_keys($courses));
+
+        $CFG->navsortmycoursessort = 'idnumber';
+        $CFG->navsortmycourseshiddenlast = true;
+        $courses = enrol_get_all_users_courses($user1->id);
+        $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
+
+        $CFG->navsortmycoursessort = 'idnumber';
+        $CFG->navsortmycourseshiddenlast = false;
+        $courses = enrol_get_all_users_courses($user1->id);
         $this->assertEquals(array($course1->id, $course3->id, $course2->id), array_keys($courses));
+
+        // But still the explicit visibility sorting takes precedence over the implicit one.
+
+        $courses = enrol_get_all_users_courses($user1->id, false, null, 'visible DESC, shortname DESC');
+        $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
     }
 
     public function test_enrol_user_sees_own_courses() {
index 01b2b5f..2346d39 100644 (file)
--- a/index.php
+++ b/index.php
@@ -91,18 +91,6 @@ if (get_home_page() != HOMEPAGE_SITE) {
 // Trigger event.
 course_view(context_course::instance(SITEID));
 
-// If the hub plugin is installed then we let it take over the homepage here.
-if (file_exists($CFG->dirroot.'/local/hub/lib.php') and get_config('local_hub', 'hubenabled')) {
-    require_once($CFG->dirroot.'/local/hub/lib.php');
-    $hub = new local_hub();
-    $continue = $hub->display_homepage();
-    // Function display_homepage() returns true if the hub home page is not displayed
-    // ...mostly when search form is not displayed for not logged users.
-    if (empty($continue)) {
-        exit;
-    }
-}
-
 $PAGE->set_pagetype('site-index');
 $PAGE->set_docs_path('');
 $editing = $PAGE->user_is_editing();
index 1efd078..d05ce4a 100644 (file)
@@ -48,7 +48,7 @@ $string['environmenthead'] = 'Vérification de l\'environnement...';
 $string['environmentsub2'] = 'Chaque version de Moodle nécessite une version minimale de certains composants PHP et des extensions de PHP obligatoires. Une vérification complète de l\'environnement est effectuée avec chaque installation et chaque mise à jour. Veuillez contacter l\'administrateur du serveur si vous ne savez pas comment installer une nouvelle version ou activer des extensions de PHP.';
 $string['errorsinenvironment'] = 'Échec de la vérification de l\'environnement !';
 $string['installation'] = 'Installation';
-$string['langdownloaderror'] = 'La langue {$a} n\'a pas pu être téléchargée. La suite de l\'installation se déroulera en anglais. Vous pourrez télécharger et installer d\'autres langues à la fin de l\'installation';
+$string['langdownloaderror'] = 'La langue « {$a} » n\'a pas pu être téléchargée. La suite de l\'installation se déroulera en anglais. Vous pourrez télécharger et installer d\'autres langues à la fin de l\'installation';
 $string['memorylimithelp'] = '<p>La limite de mémoire de PHP sur votre serveur est actuellement de {$a}.</p>
 <p>Cette valeur trop basse risque de générer des problèmes de manque de mémoire pour Moodle, notamment si vous utilisez beaucoup de modules et/ou si vous avez un grand nombre d\'utilisateurs.</p>
 <p>Il est recommandé de configurer PHP avec une limite de mémoire aussi élevée que possible, par exemple 40 Mo. Vous pouvez obtenir cela de différentes façons :</p>
index 623e935..27bc411 100644 (file)
@@ -88,7 +88,7 @@ $string['welcomep20'] = 'Je krijgt deze pagina te zien omdat je met succes het <
 $string['welcomep30'] = 'Deze uitgave van <strong>{$a->installername}</strong> bevat de software die nodig is om een omgeving te creëren waarin <strong>Moodle</strong> zal werken, namelijk:';
 $string['welcomep40'] = 'Dit pakket bevat ook <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'Het gebruik van alle programma\'s in dit pakket wordt geregeld door hun respectievelijke licenties. Het complete <strong>{$a->installername}</strong> pakket is
-<a href="http://www.opensource.org/docs/definition_plain.html">open source</a> en wordt verdeeld onder de <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a> licentie.';
+<a href="https://www.opensource.org/docs/definition_plain.html">open source</a> en wordt verdeeld onder de <a href="https://www.gnu.org/copyleft/gpl.html">GPL</a> licentie.';
 $string['welcomep60'] = 'De volgende pagina\'s leiden je door een aantal makkelijk te volgen stappen om <strong>Moodle</strong> te installeren op je computer. Je kunt de standaardinstellingen overnemen of, optioneel, ze aanpassen aan je noden.';
 $string['welcomep70'] = 'Klik op de "volgende"-knop om verder te gaan met de installatie van <strong>Moodle</strong>';
 $string['wwwroot'] = 'Web adres';
index c31faad..3a51249 100644 (file)
@@ -209,6 +209,7 @@ $string['configdefaultuserroleid'] = 'All logged in users will be given the capa
 $string['configdeleteincompleteusers'] = 'After this period, any account without the first name, last name or email field filled in is deleted.';
 $string['configdeleteunconfirmed'] = 'For certain authentication methods, such as email-based self-registration, users must confirm their account within a certain time. After this period, any old unconfirmed accounts are deleted.';
 $string['configdenyemailaddresses'] = 'To deny email addresses from particular domains list them here in the same way.  All other domains will be accepted. To deny subdomains add the domain with a preceding \'.\'. eg <strong>hotmail.com yahoo.co.uk .live.com</strong>';
+$string['configenableanalytics'] = 'Analytics models, such as \'Students at risk of dropping out\' or \'Upcoming activities due\', can generate predictions, send insight notifications and offer further actions such as messaging users.';
 $string['configenableblogs'] = 'This switch provides all site users with their own blog.';
 $string['configenabledevicedetection'] = 'Enables detection of mobiles, smartphones, tablets or default devices (desktop PCs, laptops, etc) for the application of themes and other features.';
 $string['configdisableuserimages'] = 'Disable the ability for users to change user profile images.';
@@ -514,6 +515,7 @@ $string['emoticons_desc'] = 'This form defines the emoticons (or smileys) used a
 $string['emoticonsreset'] = 'Reset emoticons setting to default values';
 $string['emptysettingvalue'] = 'Empty';
 $string['enableactivitychooser'] = 'Enable activity chooser';
+$string['enableanalytics'] = 'Analytics';
 $string['enableblogs'] = 'Enable blogs';
 $string['enablecalendarexport'] = 'Enable calendar export';
 $string['enablecomments'] = 'Enable comments';
@@ -822,6 +824,8 @@ $string['navshowallcourses'] = 'Show all courses';
 $string['navshowcategories'] = 'Show course categories';
 $string['navshowmycoursecategories'] = 'Show my course categories';
 $string['navshowmycoursecategories_help'] = 'If enabled courses in the users my courses branch will be shown in categories.';
+$string['navsortmycourseshiddenlast'] = 'Sort my hidden courses last';
+$string['navsortmycourseshiddenlast_help'] = 'If enabled, any hidden courses will be listed after visible courses (for users who can view hidden courses). Otherwise, all courses, regardless of their visibility, will be listed according to the \'Sort my courses\' setting.';
 $string['navsortmycoursessort'] = 'Sort my courses';
 $string['navsortmycoursessort_help'] = 'This determines whether courses are listed under My courses according to the sort order (i.e. the order set in Site administration > Courses > Manage courses and categories) or alphabetically by course setting.';
 $string['never'] = 'Never';
@@ -1011,11 +1015,6 @@ $string['quizattemptsupgradedmessage'] = 'In Moodle 2.1 there was a major upgrad
 $string['recaptchaprivatekey'] = 'ReCAPTCHA secret key';
 $string['recaptchapublickey'] = 'ReCAPTCHA site key';
 $string['register'] = 'Register your site';
-$string['registermoodlenet'] = '<p>We\'d love to stay in touch and provide you with important things for your Moodle site!</p><p>By registering:</p><ul><li>You can subscribe to receive notifications of new Moodle releases, security alerts and other important news.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle app</a>.</li><li>You are contributing to our <a href="https://moodle.net/stats/">Moodle statistics</a> of the worldwide community, which help us improve Moodle and our community sites.</li><li>If you wish, your site can be included in the <a href="https://moodle.net/sites/">list of registered Moodle sites</a> in your country.</li></ul>';
-$string['registermoodleorg'] = 'When you register your site';
-$string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
-$string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
-$string['registerwithmoodleorg'] = 'Register your site';
 $string['registration'] = 'Registration';
 $string['registration_help'] = 'By registering:
 
@@ -1432,3 +1431,8 @@ $string['allowblockstodock'] = 'Allow blocks to use the dock';
 $string['configallowblockstodock'] = 'If enabled and supported by the selected theme users can choose to move blocks to a special dock.';
 // Deprecated since Moodle 3.8.
 $string['configuserquota'] = 'The maximum number of bytes that a user can store in their own private file area. {$a->bytes} bytes == {$a->displaysize}';
+$string['registermoodlenet'] = '<p>We\'d love to stay in touch and provide you with important things for your Moodle site!</p><p>By registering:</p><ul><li>You can subscribe to receive notifications of new Moodle releases, security alerts and other important news.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle app</a>.</li><li>You are contributing to our <a href="https://stats.moodle.org">Moodle statistics</a> of the worldwide community, which help us improve Moodle and our community sites.</li><li>If you wish, your site can be included in the <a href="https://stats.moodle.org/sites/">list of registered Moodle sites</a> in your country.</li></ul>';
+$string['registermoodleorg'] = 'When you register your site';
+$string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
+$string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
+$string['registerwithmoodleorg'] = 'Register your site';
index af65a28..1c72cb1 100644 (file)
@@ -26,6 +26,7 @@ $string['analysablenotused'] = 'Analysable {$a->analysableid} not used: {$a->err
 $string['analysablenotvalidfortarget'] = 'Analysable {$a->analysableid} is not valid for this target: {$a->result}';
 $string['analysisinprogress'] = 'Still being analysed by a previous execution';
 $string['analytics'] = 'Analytics';
+$string['analyticsdisabled'] = 'Analytics is disabled. You can enable it in "Site administration > Advanced features".';
 $string['analyticslogstore'] = 'Log store used for analytics';
 $string['analyticslogstore_help'] = 'The log store that will be used by the analytics API to read users\' activity.';
 $string['analyticssettings'] = 'Analytics settings';
index 91d0a4f..0595e0c 100644 (file)
@@ -106,3 +106,12 @@ unpublishalluploadedcourses,core_hub
 unpublishconfirmation,core_hub
 unpublishcourse,core_hub
 updatestatus,core_hub
+registermoodlenet,core_admin
+registermoodleorg,core_admin
+registermoodleorgli1,core_admin
+registermoodleorgli2,core_admin
+registerwithmoodleorg,core_admin
+completeregistration,core_hub
+registersite,core_hub
+updatesite,core_hub
+unregisterexplained,core_hub
index ebe023f..3df03ba 100644 (file)
@@ -33,7 +33,6 @@ $string['audiencestudents'] = 'Students';
 $string['audienceadmins'] = 'Moodle administrators';
 $string['badgesnumber'] = 'Number of badges ({$a})';
 $string['communityremoved'] = 'That course link has been removed from your list';
-$string['completeregistration'] = 'Complete registration with Moodle.net';
 $string['confirmregistration'] = 'Confirm registration';
 $string['coursename'] = 'Name';
 $string['coursepublished'] = 'This course has been shared successfully on \'{$a}\'.';
@@ -55,7 +54,7 @@ $string['eduleveltertiary'] = 'Tertiary';
 $string['emailalert'] = 'Email notifications';
 $string['emailalert_help'] = 'If this is enabled the hub administrator will send you emails about security issues and other important news.';
 $string['enrollable'] = 'Enrollable';
-$string['errorotherhubsnotsupported'] = 'This page can no longer be used for registration with sites other than Moodle.net';
+$string['errorotherhubsnotsupported'] = 'This page can no longer be used for registration with custom sites directories.';
 $string['errorws'] = '{$a}';
 $string['errorwstokenreset'] = '{$a}. Registration token on this site has been reset. You can now register your site again.';
 $string['errorregistrationupdate'] = 'An error occurred during registration update ({$a})';
@@ -86,7 +85,7 @@ $string['none'] = 'None';
 $string['operation'] = 'Actions';
 $string['participantnumberaverage'] = 'Average number of participants ({$a})';
 $string['policyagreed'] = 'Privacy notice and data processing agreement';
-$string['policyagreeddesc'] = 'I agree to the <a href="{$a}" target="_blank">Privacy notice and data processing agreement</a> for Moodle.net';
+$string['policyagreeddesc'] = 'I agree to the <a href="{$a}" target="_blank">Privacy notice and data processing agreement</a>';
 $string['postaladdress'] = 'Postal address';
 $string['postaladdress_help'] = 'Postal address of this site, or of the entity represented by this site.';
 $string['postsnumber'] = 'Number of posts ({$a})';
@@ -97,7 +96,19 @@ $string['registeredsites'] = 'Registered sites';
 $string['registrationinfo'] = 'Registration information';
 $string['registereduserdevices'] = 'Number of users with registered mobile devices ({$a})';
 $string['registeredactiveuserdevices'] = 'Number of active users with registered mobile devices which are receiving notifications ({$a})';
-$string['registersite'] = 'Register with {$a}';
+$string['registerwithmoodleorg'] = 'Register your site';
+$string['registerwithmoodleorgupdate'] = 'Update your site registration';
+$string['registerwithmoodleorgcomplete'] = 'Complete your site registration';
+$string['registerwithmoodleorginfo'] = 'We\'d love to stay in touch and provide you with important things for your Moodle site! By registering:
+
+* You can subscribe to receive notifications of new Moodle releases, security alerts and other important news.
+* You can access and activate mobile push notifications from your Moodle site through our free Moodle app.
+* You are contributing to our Moodle statistics of the worldwide community, which help us improve Moodle and our community sites.
+* If you wish, your site can be included in the list of registered Moodle sites in your country.';
+$string['registerwithmoodleorginfoapp'] = 'About the Moodle app';
+$string['registerwithmoodleorginfostats'] = 'Moodle statistics';
+$string['registerwithmoodleorginfosites'] = 'Other sites in my country';
+$string['registerwithmoodleorgremove'] = 'You are going to unregister your site. If you continue, you will no longer have access to important notifications and security alerts. Your users will not be able to receive push notifications from your site to their Moodle mobile app. Are you sure you want to unregister your site?';
 $string['registrationconfirmed'] = 'Site registration confirmed';
 $string['registrationconfirmedon'] = 'Thank you for registering your site. Registration information will be kept up to date by the \'Site registration\' scheduled task.';
 $string['renewregistration'] = 'Renew registration';
@@ -151,13 +162,11 @@ $string['subject'] = 'Subject';
 $string['subject_help'] = 'Select the main subject area which the course covers.';
 $string['type'] = 'Shared';
 $string['unregister'] = 'Unregister';
-$string['unregisterfrom'] = 'Unregister from {$a}';
-$string['unregistrationerror'] = 'An error occurred when the site tried to unregister from Moodle.net: {$a}';
+$string['unregistrationerror'] = 'An error occurred while attempting to unregister the site: {$a}';
 $string['update'] = 'Update';
-$string['updatesite'] = 'Update registration on {$a}';
+$string['updatesiteregistration'] = 'Update registration';
 $string['usedifferentemail'] = 'Use different email';
-$string['unregisterexplained'] = 'If the site with URL {$a} is registered on Moodle.net its registration will be removed.';
-$string['urlalreadyregistered'] = 'Your site seems to be already registered on Moodle.net, which means something has gone wrong. Please contact the Moodle.net administrator to reset your registration so you can try again.';
+$string['urlalreadyregistered'] = 'Your site seems to be already registered, which means something has gone wrong. Please contact the sites directory administrator to reset your registration so you can try again.';
 $string['usersnumber'] = 'Number of users ({$a})';
 $string['wrongtoken'] = 'The registration failed for some unknown reason (network?). Please try again.';
 
@@ -168,6 +177,7 @@ $string['advertised'] = 'For people to join';
 $string['advertiseon'] = 'Share this course on {$a}';
 $string['readvertiseon'] = 'Update advertising information on {$a}';
 $string['advertisepublication_help'] = 'This course will be listed on Moodle.net as a course that people can enrol in and participate. Email-based self-registration should be enabled on the site and you need to enable self enrolment in this course.';
+$string['completeregistration'] = 'Complete registration with Moodle.net';
 $string['courseunpublished'] = 'The course {$a->courseshortname} is no longer shared on {$a->hubname}.';
 $string['courseurl'] = 'Course URL';
 $string['courseurl_help'] = 'It is the URL of your course. This URL is displayed as a link in a search result.';
@@ -201,6 +211,7 @@ $string['publisheremail'] = 'Publisher email';
 $string['publisheremail_help'] = 'The publisher email address allows the hub administrator to alert the publisher about any changes to the status of the published course.';
 $string['publishername'] = 'Publisher';
 $string['publishername_help'] = 'The publisher is the person or organisation that is the official publisher of the course.  Unless you are publishing it on behalf of someone else, it will usually be you.';
+$string['registersite'] = 'Register with {$a}';
 $string['removefromhub'] = 'Remove from Moodle.net';
 $string['screenshots'] = 'Screenshots';
 $string['screenshots_help'] = 'Any screenshots of the course will be displayed in search results.';
@@ -219,4 +230,7 @@ $string['unpublishalladvertisedcourses'] = 'Remove all courses that were shared
 $string['unpublishalluploadedcourses'] = 'Remove all courses that were shared on Moodle.net for people to download';
 $string['unpublishconfirmation'] = 'Do you really want to remove the course "{$a->courseshortname}" from "{$a->hubname}"';
 $string['unpublishcourse'] = 'Stop sharing {$a}';
-$string['updatestatus'] = 'Check it now.';
\ No newline at end of file
+$string['updatesite'] = 'Update registration on {$a}';
+$string['updatestatus'] = 'Check it now.';
+$string['unregisterfrom'] = 'Unregister from {$a}';
+$string['unregisterexplained'] = 'If the site with URL {$a} is registered, then its registration will be removed.';
\ No newline at end of file
index fc1e1a0..099ec88 100644 (file)
Binary files a/lib/amd/build/str.min.js and b/lib/amd/build/str.min.js differ
index 40e3262..1bfa1a9 100644 (file)
Binary files a/lib/amd/build/str.min.js.map and b/lib/amd/build/str.min.js.map differ
index 7f46e2c..246fa3a 100644 (file)
@@ -63,7 +63,7 @@ export const get_strings = (requests) => {
 
     const stringPromises = requests.map((request) => {
         const cacheKey = getCacheKey(request);
-        const {component, key, param, lang} = request;
+        const {component, key, param, lang = pageLang} = request;
         // Helper function to add the promise to cache.
         const buildReturn = (promise) => {
             // Make sure the promise cache contains our promise.
index 2a1434d..7f8979d 100644 (file)
@@ -33,7 +33,7 @@ use coding_exception;
 use moodle_url;
 
 /**
- * Methods to communicate with moodle.net web services
+ * Provides methods to communicate with the hub (sites directory) web services.
  *
  * @package    core
  * @copyright  2017 Marina Glancy
@@ -51,11 +51,11 @@ class api {
     const HUB_BACKUP_FILE_TYPE = 'backup';
 
     /**
-     * Calls moodle.net WS
+     * Calls a remote function exposed via web services on the hub.
      *
      * @param string $function name of WS function
      * @param array $data parameters of WS function
-     * @param bool $allowpublic allow request without moodle.net registration
+     * @param bool $allowpublic allow request without registration on the hub
      * @return mixed depends on the function
      * @throws moodle_exception
      */
@@ -71,7 +71,7 @@ class api {
     }
 
     /**
-     * Performs REST request to moodle.net (using GET method)
+     * Performs a REST request to the hub site (using the GET method).
      *
      * @param string $token
      * @param string $function
@@ -88,13 +88,14 @@ class api {
 
         $curl = new curl();
         $serverurl = HUB_MOODLEORGHUBURL . "/local/hub/webservice/webservices.php";
-        $curloutput = @json_decode($curl->get($serverurl, $params), true);
+        $query = http_build_query($params, '', '&');
+        $curloutput = @json_decode($curl->post($serverurl, $query), true);
         $info = $curl->get_info();
         if ($curl->get_errno()) {
             // Connection error.
             throw new moodle_exception('errorconnect', 'hub', '', $curl->error);
         } else if (isset($curloutput['exception'])) {
-            // Exception occurred on moodle.net .
+            // Exception occurred on the remote side.
             self::process_curl_exception($token, $curloutput);
         } else if ($info['http_code'] != 200) {
             throw new moodle_exception('errorconnect', 'hub', '', $info['http_code']);
@@ -104,7 +105,7 @@ class api {
     }
 
     /**
-     * Analyses exception received from moodle.net
+     * Analyses exception received from the hub server.
      *
      * @param string $token token used for CURL request
      * @param array $curloutput output from CURL request
@@ -127,7 +128,7 @@ class api {
     }
 
     /**
-     * Update site registration on moodle.net
+     * Update site registration on the hub.
      *
      * @param array $siteinfo
      * @throws moodle_exception
@@ -138,20 +139,20 @@ class api {
     }
 
     /**
-     * Returns information about moodle.net
+     * Returns information about the hub.
      *
      * Example of the return array:
      * {
      *     "courses": 384,
-     *     "description": "Moodle.net connects you with free content and courses shared by Moodle ...",
-     *     "downloadablecourses": 190,
-     *     "enrollablecourses": 194,
+     *     "description": "Official Moodle sites directory.",
+     *     "downloadablecourses": 0,
+     *     "enrollablecourses": 0,
      *     "hublogo": 1,
      *     "language": "en",
-     *     "name": "Moodle.net",
+     *     "name": "moodle",
      *     "sites": 274175,
-     *     "url": "https://moodle.net",
-     *     "imgurl": "https://moodle.net/local/hub/webservice/download.php?filetype=hubscreenshot"
+     *     "url": "https://stats.moodle.org",
+     *     "imgurl": "https://stats.moodle.org/local/hub/webservice/download.php?filetype=hubscreenshot"
      * }
      *
      * @return array
@@ -326,7 +327,7 @@ class api {
      *
      * @param array|\stdClass $courseinfo
      * @return int id of the published course on the hub
-     * @throws moodle_exception if communication to moodle.net failed or course could not be published
+     * @throws moodle_exception if the communication with the hub failed or the course could not be published
      */
     public static function register_course($courseinfo) {
         debugging("This function has been deprecated as part of the Moodle.net sunsetting process.");
@@ -338,7 +339,7 @@ class api {
      *
      * @deprecated since Moodle 3.8. Moodle.net has been sunsetted making this function useless.
      *
-     * @param int $hubcourseid id of the published course on moodle.net, it must be published from this site
+     * @param int $hubcourseid id of the published course on the hub, it must be published from this site
      * @param \stored_file $file
      * @param int $screenshotnumber ordinal number of the screenshot
      */
@@ -351,7 +352,7 @@ class api {
      *
      * @deprecated since Moodle 3.8. Moodle.net has been sunsetted making this function useless.
      *
-     * @param int $hubcourseid id of the course on moodle.net
+     * @param int $hubcourseid id of the course on the hub
      * @param string $path local path (in tempdir) to save the downloaded backup to.
      */
     public static function download_course_backup($hubcourseid, $path) {
@@ -363,7 +364,7 @@ class api {
      *
      * @deprecated since Moodle 3.8. Moodle.net has been sunsetted making this function useless.
      *
-     * @param int $hubcourseid id of the published course on moodle.net, it must be published from this site
+     * @param int $hubcourseid id of the published course on the hub, it must be published from this site
      * @param \stored_file $backupfile
      */
     public static function upload_course_backup($hubcourseid, \stored_file $backupfile) {
index 2586791..cd716ef 100644 (file)
@@ -32,7 +32,7 @@ use stdClass;
 use html_writer;
 
 /**
- * Methods to use when publishing and searching courses on moodle.net
+ * Methods to use when registering the site at the moodle sites directory.
  *
  * @package    core
  * @copyright  2017 Marina Glancy
@@ -40,11 +40,11 @@ use html_writer;
  */
 class registration {
 
-    /** @var Fields used in a site registration form.
+    /** @var array Fields used in a site registration form.
      * IMPORTANT: any new fields with non-empty defaults have to be added to CONFIRM_NEW_FIELDS */
-    const FORM_FIELDS = ['name', 'description', 'contactname', 'contactemail', 'contactphone', 'imageurl', 'privacy', 'street',
-        'regioncode', 'countrycode', 'geolocation', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail',
-        'language', 'policyagreed'];
+    const FORM_FIELDS = ['policyagreed', 'language', 'countrycode', 'privacy',
+        'contactemail', 'contactable', 'emailalert', 'emailalertemail', 'commnews', 'commnewsemail',
+        'contactname', 'name', 'description', 'imageurl', 'contactphone', 'regioncode', 'geolocation', 'street'];
 
     /** @var List of new FORM_FIELDS or siteinfo fields added indexed by the version when they were added.
      * If site was already registered, admin will be promted to confirm new registration data manually. Until registration is manually confirmed,
@@ -147,7 +147,7 @@ class registration {
     }
 
     /**
-     * Calculates and prepares site information to send to moodle.net as part of registration or update
+     * Calculates and prepares site information to send to the sites directory as a part of registration.
      *
      * @param array $defaults default values for inputs in the registration form (if site was never registered before)
      * @return array site info
@@ -158,9 +158,8 @@ class registration {
         require_once($CFG->dirroot . "/course/lib.php");
 
         $siteinfo = array();
-        $cleanhuburl = clean_param(HUB_MOODLEORGHUBURL, PARAM_ALPHANUMEXT);
         foreach (self::FORM_FIELDS as $field) {
-            $siteinfo[$field] = get_config('hub', 'site_'.$field.'_' . $cleanhuburl);
+            $siteinfo[$field] = get_config('hub', 'site_'.$field);
             if ($siteinfo[$field] === false) {
                 $siteinfo[$field] = array_key_exists($field, $defaults) ? $defaults[$field] : null;
             }
@@ -210,7 +209,7 @@ class registration {
     }
 
     /**
-     * Human-readable summary of data that will be sent to moodle.net
+     * Human-readable summary of data that will be sent to the sites directory.
      *
      * @param array $siteinfo result of get_site_info()
      * @return string
@@ -264,13 +263,12 @@ class registration {
      * @param stdClass $formdata data from {@link site_registration_form}
      */
     public static function save_site_info($formdata) {
-        $cleanhuburl = clean_param(HUB_MOODLEORGHUBURL, PARAM_ALPHANUMEXT);
         foreach (self::FORM_FIELDS as $field) {
-            set_config('site_' . $field . '_' . $cleanhuburl, $formdata->$field, 'hub');
+            set_config('site_' . $field, $formdata->$field, 'hub');
         }
-        // Even if the the connection with moodle.net fails, admin has manually submitted the form which means they don't need
+        // Even if the connection with the sites directory fails, admin has manually submitted the form which means they don't need
         // to be redirected to the site registration page any more.
-        set_config('site_regupdateversion_' . $cleanhuburl, max(array_keys(self::CONFIRM_NEW_FIELDS)), 'hub');
+        set_config('site_regupdateversion', max(array_keys(self::CONFIRM_NEW_FIELDS)), 'hub');
     }
 
     /**
@@ -329,7 +327,7 @@ class registration {
     }
 
     /**
-     * Confirms registration by moodle.net
+     * Confirms registration by the sites directory.
      *
      * @param string $token
      * @param string $newtoken
@@ -350,6 +348,13 @@ class registration {
         $record['timemodified'] = time();
         $DB->update_record('registration_hubs', $record);
         self::$registration = null;
+
+        $siteinfo = self::get_site_info();
+        if (strlen(http_build_query($siteinfo)) > 1800) {
+            // Update registration again because the initial request was too long and could have been truncated.
+            api::update_registration($siteinfo);
+            self::$registration = null;
+        }
     }
 
     /**
@@ -368,8 +373,8 @@ class registration {
      * Registers a site
      *
      * This method will make sure that unconfirmed registration record is created and then redirect to
-     * registration script on https://moodle.net
-     * Moodle.net will check that the site is accessible, register it and redirect back
+     * registration script on the sites directory.
+     * The sites directory will check that the site is accessible, register it and redirect back
      * to /admin/registration/confirmregistration.php
      *
      * @param string $returnurl
@@ -390,7 +395,7 @@ class registration {
             $hub->token = get_site_identifier();
             $hub->secret = $hub->token;
             $hub->huburl = HUB_MOODLEORGHUBURL;
-            $hub->hubname = 'Moodle.net';
+            $hub->hubname = 'moodle';
             $hub->confirmed = 0;
             $hub->timemodified = time();
             $hub->id = $DB->insert_record('registration_hubs', $hub);
@@ -398,10 +403,21 @@ class registration {
         }
 
         $params = self::get_site_info();
-        $params['token'] = $hub->token;
+
+        // The most conservative limit for the redirect URL length is 2000 characters. Only pass parameters before
+        // we reach this limit. The next registration update will update all fields.
+        // We will also update registration after we receive confirmation from moodle.net.
+        $url = new moodle_url(HUB_MOODLEORGHUBURL . '/local/hub/siteregistration.php',
+            ['token' => $hub->token, 'url' => $params['url']]);
+        foreach ($params as $key => $value) {
+            if (strlen($url->out(false, [$key => $value])) > 2000) {
+                break;
+            }
+            $url->param($key, $value);
+        }
 
         $SESSION->registrationredirect = $returnurl;
-        redirect(new moodle_url(HUB_MOODLEORGHUBURL . '/local/hub/siteregistration.php', $params));
+        redirect($url);
     }
 
     /**
@@ -469,20 +485,20 @@ class registration {
     }
 
     /**
-     * Returns information about moodle.net
+     * Returns information about the sites directory.
      *
      * Example of the return array:
      * {
      *     "courses": 384,
-     *     "description": "Moodle.net connects you with free content and courses shared by Moodle ...",
-     *     "downloadablecourses": 190,
-     *     "enrollablecourses": 194,
+     *     "description": "Official moodle sites directory",
+     *     "downloadablecourses": 0,
+     *     "enrollablecourses": 0,
      *     "hublogo": 1,
      *     "language": "en",
-     *     "name": "Moodle.net",
+     *     "name": "moodle",
      *     "sites": 274175,
-     *     "url": "https://moodle.net",
-     *     "imgurl": moodle_url : "https://moodle.net/local/hub/webservice/download.php?filetype=hubscreenshot"
+     *     "url": "https://stats.moodle.org",
+     *     "imgurl": "https://stats.moodle.org/local/hub/webservice/download.php?filetype=hubscreenshot"
      * }
      *
      * @return array|null
@@ -491,8 +507,8 @@ class registration {
         try {
             return api::get_hub_info();
         } catch (moodle_exception $e) {
-            // Ignore error, we only need it for displaying information about moodle.net, if this request
-            // fails, it's not a big deal.
+            // Ignore error, we only need it for displaying information about the sites directory.
+            // If this request fails, it's not a big deal.
             return null;
         }
     }
@@ -537,8 +553,7 @@ class registration {
             return $fieldsneedconfirm;
         }
 
-        $cleanhuburl = clean_param(HUB_MOODLEORGHUBURL, PARAM_ALPHANUMEXT);
-        $lastupdated = (int)get_config('hub', 'site_regupdateversion_' . $cleanhuburl);
+        $lastupdated = (int)get_config('hub', 'site_regupdateversion');
         foreach (self::CONFIRM_NEW_FIELDS as $version => $fields) {
             if ($version > $lastupdated) {
                 $fieldsneedconfirm = array_merge($fieldsneedconfirm, $fields);
index 510a2da..f58f9ef 100644 (file)
@@ -32,7 +32,7 @@ global $CFG;
 require_once($CFG->libdir . '/formslib.php');
 
 /**
- * The site registration form. Information will be sent to moodle.net
+ * The site registration form. Information will be sent to the sites directory.
  *
  * @author     Jerome Mouneyrac <jerome@mouneyrac.com>
  * @package    core
@@ -75,7 +75,7 @@ class site_registration_form extends \moodleform {
         $mform->addElement('header', 'moodle', get_string('registrationinfo', 'hub'));
 
         $mform->addElement('text', 'name', get_string('sitename', 'hub'),
-            array('class' => 'registration_textfield'));
+            array('class' => 'registration_textfield', 'maxlength' => 255));
         $mform->setType('name', PARAM_TEXT);
         $mform->addHelpButton('name', 'sitename', 'hub');
 
@@ -161,16 +161,16 @@ class site_registration_form extends \moodleform {
         $mform->addElement('static', 'urlstring', get_string('siteurl', 'hub'), $siteinfo['url']);
         $mform->addHelpButton('urlstring', 'siteurl', 'hub');
 
-        // Display statistic that are going to be retrieve by moodle.net.
+        // Display statistic that are going to be retrieve by the sites directory.
         $mform->addElement('static', 'siteinfosummary', get_string('sendfollowinginfo', 'hub'), registration::get_stats_summary($siteinfo));
 
         // Check if it's a first registration or update.
         if (registration::is_registered()) {
-            $buttonlabel = get_string('updatesite', 'hub', 'Moodle.net');
+            $buttonlabel = get_string('updatesiteregistration', 'core_hub');
             $mform->addElement('hidden', 'update', true);
             $mform->setType('update', PARAM_BOOL);
         } else {
-            $buttonlabel = get_string('registersite', 'hub', 'Moodle.net');
+            $buttonlabel = get_string('register', 'core_admin');
         }
 
         $this->add_action_buttons(false, $buttonlabel);
diff --git a/lib/classes/hub/site_unregistration_form.php b/lib/classes/hub/site_unregistration_form.php
deleted file mode 100644 (file)
index 70fc070..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Class site_unregistration_form
- *
- * @package    core
- * @copyright  2017 Marina Glancy
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core\hub;
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/formslib.php');
-
-/**
- * This form display a unregistration form.
- *
- * @author     Jerome Mouneyrac <jerome@mouneyrac.com>
- * @package    core
- * @copyright  2017 Marina Glancy
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class site_unregistration_form extends \moodleform {
-
-    /**
-     * Form definition
-     */
-    public function definition() {
-        global $CFG;
-        $mform = & $this->_form;
-        $mform->addElement('header', 'site', get_string('unregister', 'hub'));
-
-        $unregisterlabel = get_string('unregister', 'hub');
-        $mform->addElement('advcheckbox', 'unpublishalladvertisedcourses', '',
-            ' ' . get_string('unpublishalladvertisedcourses', 'hub'));
-        $mform->setType('unpublishalladvertisedcourses', PARAM_INT);
-        $mform->addElement('advcheckbox', 'unpublishalluploadedcourses', '',
-            ' ' . get_string('unpublishalluploadedcourses', 'hub'));
-        $mform->setType('unpublishalluploadedcourses', PARAM_INT);
-
-        $mform->addElement('hidden', 'unregistration', 1);
-        $mform->setType('unregistration', PARAM_INT);
-
-        $mform->addElement('static', 'explanation', '', get_string('unregisterexplained', 'hub', $CFG->wwwroot));
-
-        $this->add_action_buttons(true, $unregisterlabel);
-    }
-}
index 45ed163..32a29d7 100644 (file)
@@ -1647,7 +1647,7 @@ class core_plugin_manager {
         $plugins = array(
             'qformat' => array('blackboard', 'learnwise'),
             'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'),
-            'block' => array('course_overview', 'messages', 'community'),
+            'block' => array('course_overview', 'messages', 'community', 'participants'),
             'cachestore' => array('memcache'),
             'enrol' => array('authorize'),
             'report' => array('search'),
@@ -1718,7 +1718,7 @@ class core_plugin_manager {
                 'completionstatus', 'course_list', 'course_summary',
                 'feedback', 'globalsearch', 'glossary_random', 'html',
                 'login', 'lp', 'mentees', 'mnet_hosts', 'myoverview', 'myprofile',
-                'navigation', 'news_items', 'online_users', 'participants',
+                'navigation', 'news_items', 'online_users',
                 'private_files', 'quiz_results', 'recent_activity', 'recentlyaccesseditems',
                 'recentlyaccessedcourses', 'rss_client', 'search_forums', 'section_links',
                 'selfcompletion', 'settings', 'site_main_menu',
index 0824d93..90e6e13 100644 (file)
@@ -50,6 +50,11 @@ class analytics_cleanup_task extends \core\task\scheduled_task {
      * @return void
      */
     public function execute() {
+
+        if (!\core_analytics\manager::is_analytics_enabled()) {
+            mtrace(get_string('analyticsdisabled', 'analytics'));
+            return;
+        }
         $models = \core_analytics\manager::cleanup();
     }
 }
index 4626e17..bcf2cc9 100644 (file)
@@ -137,6 +137,18 @@ class core_useragent {
         }
     }
 
+    /**
+     * Get the MoodleBot UserAgent for this site.
+     *
+     * @return string UserAgent
+     */
+    public static function get_moodlebot_useragent() {
+        global $CFG;
+
+        $version = moodle_major_version(); // Only major version for security.
+        return "MoodleBot/$version (+{$CFG->wwwroot})";
+    }
+
     /**
      * Returns the user agent string.
      * @return bool|string The user agent string or false if one isn't available.
@@ -215,7 +227,8 @@ class core_useragent {
      * @return bool
      */
     protected function is_useragent_web_crawler() {
-        $regex = '/Googlebot|google\.com|Yahoo! Slurp|\[ZSEBOT\]|msnbot|bingbot|BingPreview|Yandex|AltaVista|Baiduspider|Teoma/i';
+        $regex = '/MoodleBot|Googlebot|google\.com|Yahoo! Slurp|\[ZSEBOT\]|msnbot|bingbot|BingPreview|Yandex|AltaVista'
+                .'|Baiduspider|Teoma/i';
         return (preg_match($regex, $this->useragent));
     }
 
index 8bbc93b..f6022fd 100644 (file)
@@ -3558,5 +3558,58 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2019092700.01);
     }
 
+    if ($oldversion < 2019100800.02) {
+        // Rename the official moodle sites directory the site is registered with.
+        $DB->execute("UPDATE {registration_hubs}
+                         SET hubname = ?, huburl = ?
+                       WHERE huburl = ?", ['moodle', 'https://stats.moodle.org', 'https://moodle.net']);
+
+        // Convert the hub site specific settings to the new naming format without the hub URL in the name.
+        $hubconfig = get_config('hub');
+
+        if (!empty($hubconfig)) {
+            foreach (upgrade_convert_hub_config_site_param_names($hubconfig, 'https://moodle.net') as $name => $value) {
+                set_config($name, $value, 'hub');
+            }
+        }
+
+        upgrade_main_savepoint(true, 2019100800.02);
+    }
+
+    if ($oldversion < 2019100900.00) {
+        // If block_participants is no longer present, remove it.
+        if (!file_exists($CFG->dirroot . '/blocks/participants/block_participants.php')) {
+            // Delete instances.
+            $instances = $DB->get_records_list('block_instances', 'blockname', ['participants']);
+            $instanceids = array_keys($instances);
+
+            if (!empty($instanceids)) {
+                $DB->delete_records_list('block_positions', 'blockinstanceid', $instanceids);
+                $DB->delete_records_list('block_instances', 'id', $instanceids);
+                list($sql, $params) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED);
+                $params['contextlevel'] = CONTEXT_BLOCK;
+                $DB->delete_records_select('context', "contextlevel=:contextlevel AND instanceid " . $sql, $params);
+
+                $preferences = array();
+                foreach ($instances as $instanceid => $instance) {
+                    $preferences[] = 'block' . $instanceid . 'hidden';
+                    $preferences[] = 'docked_block_instance_' . $instanceid;
+                }
+                $DB->delete_records_list('user_preferences', 'name', $preferences);
+            }
+
+            // Delete the block from the block table.
+            $DB->delete_records('block', array('name' => 'participants'));
+
+            // Remove capabilities.
+            capabilities_cleanup('block_participants');
+
+            // Clean config.
+            unset_all_config_for_plugin('block_participants');
+        }
+
+        upgrade_main_savepoint(true, 2019100900.00);
+    }
+
     return true;
 }
index 4c47550..802417a 100644 (file)
@@ -350,10 +350,10 @@ function upgrade_course_letter_boundary($courseid = null) {
     }
     $lettercolumnsql = '';
     if ($usergradelettercolumnsetting) {
-        // the system default is to show a column with letters (and the course uses the defaults).
+        // The system default is to show a column with letters (and the course uses the defaults).
         $lettercolumnsql = '(gss.value is NULL OR ' . $DB->sql_compare_text('gss.value') .  ' <> \'0\')';
     } else {
-        // the course displays a column with letters.
+        // The course displays a column with letters.
         $lettercolumnsql = $DB->sql_compare_text('gss.value') .  ' = \'1\'';
     }
 
@@ -608,4 +608,48 @@ function upgrade_rename_prediction_actions_useful_incorrectly_flagged() {
 
         $DB->execute($updatesql, $params + ['modelid' => $model->id]);
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Convert the site settings for the 'hub' component in the config_plugins table.
+ *
+ * @param stdClass $hubconfig Settings loaded for the 'hub' component.
+ * @param string $huburl The URL of the hub to use as the valid one in case of conflict.
+ * @return stdClass List of new settings to be applied (including null values to be unset).
+ */
+function upgrade_convert_hub_config_site_param_names(stdClass $hubconfig, string $huburl): stdClass {
+
+    $cleanhuburl = clean_param($huburl, PARAM_ALPHANUMEXT);
+    $converted = [];
+
+    foreach ($hubconfig as $oldname => $value) {
+        if (preg_match('/^site_([a-z]+)([A-Za-z0-9_-]*)/', $oldname, $matches)) {
+            $newname = 'site_'.$matches[1];
+
+            if ($oldname === $newname) {
+                // There is an existing value with the new naming convention already.
+                $converted[$newname] = $value;
+
+            } else if (!array_key_exists($newname, $converted)) {
+                // Add the value under a new name and mark the original to be unset.
+                $converted[$newname] = $value;
+                $converted[$oldname] = null;
+
+            } else if ($matches[2] === '_'.$cleanhuburl) {
+                // The new name already exists, overwrite only if coming from the valid hub.
+                $converted[$newname] = $value;
+                $converted[$oldname] = null;
+
+            } else {
+                // Just unset the old value.
+                $converted[$oldname] = null;
+            }
+
+        } else {
+            // Not a hub-specific site setting, just keep it.
+            $converted[$oldname] = $value;
+        }
+    }
+
+    return (object) $converted;
+}
index 4d3cd85..7ce85d3 100644 (file)
@@ -33,7 +33,6 @@ use \core\dml\table;
  * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @coversDefaultClass \core\dml\table
- * @covers ::<!public>
  */
 class core_dml_table_testcase extends database_driver_testcase {
 
index aac1cac..fbb5c39 100644 (file)
@@ -567,13 +567,8 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei
     $offset = 0, $excludecourses = []) {
     global $DB, $USER, $CFG;
 
-    if ($sort === null) {
-        if (empty($CFG->navsortmycoursessort)) {
-            $sort = 'visible DESC, sortorder ASC';
-        } else {
-            $sort = 'visible DESC, '.$CFG->navsortmycoursessort.' ASC';
-        }
-    }
+    // Re-Arrange the course sorting according to the admin settings.
+    $sort = enrol_get_courses_sortingsql($sort);
 
     // Guest account does not have any enrolled courses.
     if (!$allaccessible && (isguestuser() or !isloggedin())) {
@@ -804,6 +799,41 @@ function enrol_get_course_info_icons($course, array $instances = NULL) {
     return $icons;
 }
 
+/**
+ * Returns SQL ORDER arguments which reflect the admin settings to sort my courses.
+ *
+ * @param string|null $sort SQL ORDER arguments which were originally requested (optionally).
+ * @return string SQL ORDER arguments.
+ */
+function enrol_get_courses_sortingsql($sort = null) {
+    global $CFG;
+
+    // Prepare the visible SQL fragment as empty.
+    $visible = '';
+    // Only create a visible SQL fragment if the caller didn't already pass a sort order which contains the visible field.
+    if ($sort === null || strpos($sort, 'visible') === false) {
+        // If the admin did not explicitly want to have shown and hidden courses sorted as one list, we will sort hidden
+        // courses to the end of the course list.
+        if (!isset($CFG->navsortmycourseshiddenlast) || $CFG->navsortmycourseshiddenlast == true) {
+            $visible = 'visible DESC, ';
+        }
+    }
+
+    // Only create a sortorder SQL fragment if the caller didn't already pass one.
+    if ($sort === null) {
+        // If the admin has configured a course sort order, we will use this.
+        if (!empty($CFG->navsortmycoursessort)) {
+            $sort = $CFG->navsortmycoursessort . ' ASC';
+
+            // Otherwise we will fall back to the sortorder sorting.
+        } else {
+            $sort = 'sortorder ASC';
+        }
+    }
+
+    return $visible . $sort;
+}
+
 /**
  * Returns course enrolment detailed information.
  *
@@ -955,15 +985,10 @@ function enrol_user_sees_own_courses($user = null) {
  * @return array
  */
 function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
-    global $CFG, $DB;
+    global $DB;
 
-    if ($sort === null) {
-        if (empty($CFG->navsortmycoursessort)) {
-            $sort = 'visible DESC, sortorder ASC';
-        } else {
-            $sort = 'visible DESC, '.$CFG->navsortmycoursessort.' ASC';
-        }
-    }
+    // Re-Arrange the course sorting according to the admin settings.
+    $sort = enrol_get_courses_sortingsql($sort);
 
     // Guest account does not have any courses
     if (isguestuser($userid) or empty($userid)) {
index c541731..cb20d06 100644 (file)
@@ -3103,7 +3103,7 @@ class curl {
      */
     public function resetopt() {
         $this->options = array();
-        $this->options['CURLOPT_USERAGENT']         = 'MoodleBot/1.0';
+        $this->options['CURLOPT_USERAGENT']         = \core_useragent::get_moodlebot_useragent();
         // True to include the header in the output
         $this->options['CURLOPT_HEADER']            = 0;
         // True to Exclude the body from the output
@@ -3346,7 +3346,7 @@ class curl {
         } else if (!empty($this->options['CURLOPT_USERAGENT'])) {
             $useragent = $this->options['CURLOPT_USERAGENT'];
         } else {
-            $useragent = 'MoodleBot/1.0';
+            $useragent = \core_useragent::get_moodlebot_useragent();
         }
 
         // Set headers.
index 30c15ad..20db7b7 100644 (file)
@@ -43,7 +43,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Files can be created from strings.
      *
      * @covers ::create_file_from_string
-     * @covers ::<!public>
      */
     public function test_create_file_from_string() {
         global $DB;
@@ -117,7 +116,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Local files can be added to the filepool
      *
      * @covers ::create_file_from_pathname
-     * @covers ::<!public>
      */
     public function test_create_file_from_pathname() {
         global $CFG, $DB;
@@ -200,7 +198,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Tests get get file.
      *
      * @covers ::get_file
-     * @covers ::<!public>
      */
     public function test_get_file() {
         global $CFG;
@@ -241,7 +238,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * @param stored_file $file
      * @depends test_get_file
      * @covers ::get_file_preview
-     * @covers ::<!public>
      */
     public function test_get_file_preview(stored_file $file) {
         global $CFG;
@@ -265,7 +261,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Tests for get_file_preview without an image.
      *
      * @covers ::get_file_preview
-     * @covers ::<!public>
      */
     public function test_get_file_preview_nonimage() {
         $this->resetAfterTest(true);
@@ -293,7 +288,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      *
      * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
      * @covers stored_file::rename
-     * @covers ::<!public>
      */
     public function test_file_renaming() {
         global $CFG;
@@ -340,7 +334,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      *
      * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
      * @covers ::create_file_from_reference
-     * @covers ::<!public>
      */
     public function test_create_file_from_reference() {
         global $CFG, $DB;
@@ -427,7 +420,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      *
      * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
      * @covers ::create_file_from_reference
-     * @covers ::<!public>
      */
     public function test_create_file_from_reference_with_content_hash() {
         global $CFG, $DB;
@@ -537,7 +529,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Tests for get_area_files
      *
      * @covers ::get_area_files
-     * @covers ::<!public>
      */
     public function test_get_area_files() {
         $user = $this->setup_three_private_files();
@@ -598,7 +589,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Tests for get_area_tree
      *
      * @covers ::get_area_tree
-     * @covers ::<!public>
      */
     public function test_get_area_tree() {
         $user = $this->setup_three_private_files();
@@ -658,7 +648,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Tests for get_file_by_id
      *
      * @covers ::get_file_by_id
-     * @covers ::<!public>
      */
     public function test_get_file_by_id() {
         $user = $this->setup_three_private_files();
@@ -680,7 +669,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Tests for get_file_by_hash
      *
      * @covers ::get_file_by_hash
-     * @covers ::<!public>
      */
     public function test_get_file_by_hash() {
         $user = $this->setup_three_private_files();
@@ -701,7 +689,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Tests for get_external_files
      *
      * @covers ::get_external_files
-     * @covers ::<!public>
      */
     public function test_get_external_files() {
         $user = $this->setup_three_private_files();
@@ -768,7 +755,6 @@ class core_files_file_storage_testcase extends advanced_testcase {
      * Tests for create_directory with a negative contextid.
      *
      * @covers ::create_directory
-     * @covers&nb