Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorIlya Tregubov <ilya@moodle.com>
Thu, 19 May 2022 13:24:23 +0000 (19:24 +0600)
committerIlya Tregubov <ilya@moodle.com>
Thu, 19 May 2022 13:24:23 +0000 (19:24 +0600)
33 files changed:
admin/tool/componentlibrary/content/moodle/components/toggle.md
cohort/classes/reportbuilder/datasource/cohorts.php
course/classes/reportbuilder/datasource/courses.php
enrol/fee/classes/plugin.php
enrol/fee/tests/behat/fee.feature
group/overview.php
lang/en/group.php
lib/moodlelib.php
lib/templates/toggle.mustache
lib/testing/generator/data_generator.php
lib/tests/behat/behat_navigation.php
reportbuilder/amd/build/schedules.min.js
reportbuilder/amd/build/schedules.min.js.map
reportbuilder/amd/src/schedules.js
reportbuilder/classes/datasource.php
reportbuilder/classes/external/custom_report_exporter.php
reportbuilder/classes/local/entities/user.php
reportbuilder/classes/local/filters/date.php
reportbuilder/classes/local/filters/number.php
reportbuilder/classes/local/helpers/database.php
reportbuilder/classes/local/report/base.php
reportbuilder/classes/local/systemreports/report_schedules.php
reportbuilder/classes/local/systemreports/reports_list.php
reportbuilder/classes/output/custom_report.php
reportbuilder/classes/reportbuilder/audience/systemrole.php
reportbuilder/classes/system_report.php
reportbuilder/classes/table/custom_report_table.php
reportbuilder/classes/table/custom_report_table_view.php
reportbuilder/classes/table/custom_report_table_view_filterset.php
reportbuilder/tests/behat/schedules.feature
reportbuilder/tests/local/helpers/database_test.php
reportbuilder/upgrade.txt [new file with mode: 0644]
user/classes/reportbuilder/datasource/users.php

index 15b1c8a..f319431 100644 (file)
@@ -19,6 +19,7 @@ The parameters for the template context are:
 * checked: If the initial status is checked.
 * disabled: If toggle input is disabled.
 * dataattributes: Array of name/value elements added as data-attributes.
+* title: Title text.
 * label: Label text.
 * labelclasses: Any extra classes added to the label container.
 
@@ -35,6 +36,7 @@ Checked toggle and with "sr-only" label.
             "name": "action",
             "value": "toggle-status"
         }],
+        "title": "Toggle Enabled",
         "label": "Enable/disable status",
         "labelclasses": "sr-only"
     }
@@ -52,6 +54,7 @@ Disabled toggle with extra classes.
             "name": "action",
             "value": "toggle-status"
         }],
+        "title": "Toggle Disabled",
         "label": "Enable/disable status"
     }
 {{< /mustache >}}
@@ -68,6 +71,7 @@ The parameters that you can define are:
     * disabled
 * labelmarkup: Label element code block.
   * Should include *class="custom-control-label"*.
+* title: Title text.
 * label: Label text.
 * labelclasses: Any extra classes added to the label container.
 
index 4f63e88..ef96f08 100644 (file)
@@ -70,20 +70,8 @@ class cohorts extends datasource {
 
         $this->add_entity($userentity->add_joins([$cohortmemberjoin, $userjoin]));
 
-        // Add all columns from entities to be available in custom reports.
-        $this->add_columns_from_entity($cohortentity->get_entity_name());
-        $this->add_columns_from_entity($cohortmemberentity->get_entity_name());
-        $this->add_columns_from_entity($userentity->get_entity_name());
-
-        // Add all filters from entities to be available in custom reports.
-        $this->add_filters_from_entity($cohortentity->get_entity_name());
-        $this->add_filters_from_entity($cohortmemberentity->get_entity_name());
-        $this->add_filters_from_entity($userentity->get_entity_name());
-
-        // Add all conditions from entities to be available in custom reports.
-        $this->add_conditions_from_entity($cohortentity->get_entity_name());
-        $this->add_conditions_from_entity($cohortmemberentity->get_entity_name());
-        $this->add_conditions_from_entity($userentity->get_entity_name());
+        // Add all columns/filters/conditions from entities to be available in custom reports.
+        $this->add_all_from_entities();
     }
 
     /**
index ebc56af..13dd4ed 100644 (file)
@@ -63,17 +63,9 @@ class courses extends datasource {
             ->add_join("JOIN {course_categories} {$coursecattablealias}
                 ON {$coursecattablealias}.id = {$coursetablealias}.category"));
 
-        // Add all columns from entities to be available in custom reports.
-        $this->add_columns_from_entity($coursecatentity->get_entity_name());
-        $this->add_columns_from_entity($courseentity->get_entity_name());
-
-        // Add all filters from entities to be available in custom reports.
-        $this->add_filters_from_entity($coursecatentity->get_entity_name());
-        $this->add_filters_from_entity($courseentity->get_entity_name());
-
-        // Add all conditions from entities to be available in custom reports.
-        $this->add_conditions_from_entity($coursecatentity->get_entity_name());
-        $this->add_conditions_from_entity($courseentity->get_entity_name());
+        // Add all columns/filters/conditions from entities to be available in custom reports.
+        $this->add_all_from_entity($coursecatentity->get_entity_name());
+        $this->add_all_from_entity($courseentity->get_entity_name());
     }
 
     /**
index 68170c7..bd37fd5 100644 (file)
@@ -164,7 +164,32 @@ class enrol_fee_plugin extends enrol_plugin {
      * @return string html text, usually a form in a text box
      */
     public function enrol_page_hook(stdClass $instance) {
-        global $CFG, $USER, $OUTPUT, $PAGE, $DB;
+        return $this->show_payment_info($instance);
+    }
+
+    /**
+     * Returns optional enrolment instance description text.
+     *
+     * This is used in detailed course information.
+     *
+     *
+     * @param object $instance
+     * @return string short html text
+     */
+    public function get_description_text($instance) {
+        return $this->show_payment_info($instance);
+    }
+
+    /**
+     * Generates payment information to display on enrol/info page.
+     *
+     * @param stdClass $instance
+     * @return false|string
+     * @throws coding_exception
+     * @throws dml_exception
+     */
+    private function show_payment_info(stdClass $instance) {
+        global $USER, $OUTPUT, $DB;
 
         ob_start();
 
@@ -183,19 +208,6 @@ class enrol_fee_plugin extends enrol_plugin {
         $course = $DB->get_record('course', array('id' => $instance->courseid));
         $context = context_course::instance($course->id);
 
-        $shortname = format_string($course->shortname, true, array('context' => $context));
-        $strloginto = get_string("loginto", "", $shortname);
-        $strcourses = get_string("courses");
-
-        // Pass $view=true to filter hidden caps if the user cannot see them.
-        if ($users = get_users_by_capability($context, 'moodle/course:update', 'u.*', 'u.id ASC',
-                                             '', '', '', '', false, true)) {
-            $users = sort_by_roleassignment_authority($users, $context);
-            $teacher = array_shift($users);
-        } else {
-            $teacher = false;
-        }
-
         if ( (float) $instance->cost <= 0 ) {
             $cost = (float) $this->get_config('cost');
         } else {
@@ -207,7 +219,7 @@ class enrol_fee_plugin extends enrol_plugin {
         } else {
 
             $data = [
-                'isguestuser' => isguestuser(),
+                'isguestuser' => isguestuser() || !isloggedin(),
                 'cost' => \core_payment\helper::get_cost_as_string($cost, $instance->currency),
                 'instanceid' => $instance->id,
                 'description' => get_string('purchasedescription', 'enrol_fee',
index 397d83f..7bd5e59 100644 (file)
@@ -21,10 +21,7 @@ Feature: Signing up for a course with a fee enrolment method
     And I navigate to "Plugins > Enrolments > Manage enrol plugins" in site administration
     And I click on "Enable" "link" in the "Enrolment on payment" "table_row"
     And I log out
-
-  @javascript
-  Scenario: Student can see the payment prompt on the course enrolment page
-    When I log in as "manager1"
+    And I log in as "manager1"
     And I am on the "Course 1" "enrolment methods" page
     And I select "Enrolment on payment" from the "Add method" singleselect
     And I set the following fields to these values:
@@ -33,7 +30,10 @@ Feature: Signing up for a course with a fee enrolment method
       | Currency        | Euro     |
     And I press "Add method"
     And I log out
-    And I log in as "student1"
+
+  @javascript
+  Scenario: Student can see the payment prompt on the course enrolment page
+    When I log in as "student1"
     And I am on course index
     And I follow "Course 1"
     Then I should see "This course requires a payment for entry."
@@ -41,3 +41,11 @@ Feature: Signing up for a course with a fee enrolment method
     And I press "Select payment type"
     And I should see "PayPal" in the "Select payment type" "dialogue"
     And I click on "Cancel" "button" in the "Select payment type" "dialogue"
+
+  Scenario: Guest can see the login prompt on the course enrolment page
+    When I log in as "guest"
+    And I am on course index
+    And I follow "Course 1"
+    Then I should see "This course requires a payment for entry."
+    And I should see "123.45"
+    And I should see "Log in to the site"
index 8a99ba3..392bcdf 100644 (file)
@@ -33,6 +33,7 @@ define('OVERVIEW_GROUPING_NO_GROUP', -2); // The fake grouping for users with no
 $courseid   = required_param('id', PARAM_INT);
 $groupid    = optional_param('group', 0, PARAM_INT);
 $groupingid = optional_param('grouping', 0, PARAM_INT);
+$dataformat = optional_param('dataformat', '', PARAM_ALPHA);
 
 $returnurl = $CFG->wwwroot.'/group/index.php?id='.$courseid;
 $rooturl   = $CFG->wwwroot.'/group/overview.php?id='.$courseid;
@@ -176,7 +177,8 @@ if ($groupid <= 0 && $groupingid <= 0) {
                    WHERE g.courseid = :courseid
                    ) grouped ON grouped.userid = u.id
                   $userfieldsjoin
-             WHERE grouped.userid IS NULL";
+             WHERE grouped.userid IS NULL
+             ORDER BY $sort";
     $params['courseid'] = $courseid;
 
     $nogroupusers = $DB->get_records_sql($sql, array_merge($params, $userfieldsparams));
@@ -186,6 +188,84 @@ if ($groupid <= 0 && $groupingid <= 0) {
     }
 }
 
+// Export groups if requested.
+if ($dataformat !== '') {
+    $columnnames = array(
+        'grouping' => $strgrouping,
+        'group' => $strgroup,
+        'firstname' => get_string('firstname'),
+        'lastname' => get_string('lastname'),
+    );
+    $extrafields = \core_user\fields::get_identity_fields($context, false);
+    foreach ($extrafields as $field) {
+        $columnnames[$field] = \core_user\fields::get_display_name($field);
+    }
+    $alldata = array();
+    // Generate file name.
+    $shortname = format_string($course->shortname, true, array('context' => $context))."_groups";
+    $i = 0;
+    foreach ($members as $gpgid => $groupdata) {
+        if ($groupingid and $groupingid != $gpgid) {
+            if ($groupingid > 0 || $gpgid > 0) {
+                // Still show 'not in group' when 'no grouping' selected.
+                continue; // Do not export.
+            }
+        }
+        if ($gpgid < 0) {
+            // Display 'not in group' for grouping id == OVERVIEW_GROUPING_NO_GROUP.
+            if ($gpgid == OVERVIEW_GROUPING_NO_GROUP) {
+                $groupingname = $strnotingroup;
+            } else {
+                $groupingname = $strnotingrouping;
+            }
+        } else {
+            $groupingname = $groupings[$gpgid]->formattedname;
+        }
+        if (empty($groupdata)) {
+            $alldata[$i] = array_fill_keys(array_keys($columnnames), '');
+            $alldata[$i]['grouping'] = $groupingname;
+            $i++;
+        }
+        foreach ($groupdata as $gpid => $users) {
+            if ($groupid and $groupid != $gpid) {
+                continue;
+            }
+            if (empty($users)) {
+                $alldata[$i] = array_fill_keys(array_keys($columnnames), '');
+                $alldata[$i]['grouping'] = $groupingname;
+                $alldata[$i]['group'] = $groups[$gpid]->name;
+                $i++;
+            }
+            foreach ($users as $option => $user) {
+                $alldata[$i]['grouping'] = $groupingname;
+                $alldata[$i]['group'] = $groups[$gpid]->name;
+                $alldata[$i]['firstname'] = $user->firstname;
+                $alldata[$i]['lastname'] = $user->lastname;
+                foreach ($extrafields as $field) {
+                    $alldata[$i][$field] = $user->$field;
+                }
+                $i++;
+            }
+        }
+    }
+
+    \core\dataformat::download_data(
+        $shortname,
+        $dataformat,
+        $columnnames,
+        $alldata,
+        function($record, $supportshtml) use ($extrafields) {
+            if ($supportshtml) {
+                foreach ($extrafields as $extrafield) {
+                    $record[$extrafield] = s($record[$extrafield]);
+                }
+            }
+            return $record;
+        });
+    die;
+}
+
+// Main page content.
 navigation_node::override_active_url(new moodle_url('/group/index.php', array('id'=>$courseid)));
 $PAGE->navbar->add(get_string('overview', 'group'));
 
@@ -288,4 +368,11 @@ foreach ($members as $gpgid=>$groupdata) {
     $printed = true;
 }
 
+// Add buttons for exporting groups/groupings.
+echo $OUTPUT->download_dataformat_selector(get_string('exportgroupsgroupings', 'group'), 'overview.php', 'dataformat', [
+    'id' => $courseid,
+    'group' => $groupid,
+    'grouping' => $groupingid,
+]);
+
 echo $OUTPUT->footer();
index 5dcaaa5..0e2577a 100644 (file)
@@ -85,6 +85,7 @@ $string['eventgroupinggroupassigned'] = 'Group assigned to grouping';
 $string['eventgroupinggroupunassigned'] = 'Group unassigned from grouping';
 $string['eventgroupingupdated'] = 'Grouping updated';
 $string['existingmembers'] = 'Existing members: {$a}';
+$string['exportgroupsgroupings'] = 'Export groups and groupings as';
 $string['filtergroups'] = 'Filter groups by:';
 $string['group'] = 'Group';
 $string['groupaddedsuccesfully'] = 'Group {$a} added successfully';
index ecaff41..12d8a39 100644 (file)
@@ -1408,64 +1408,67 @@ function html_is_blank($string) {
  * @param string $plugin (optional) the plugin scope, default null
  * @return bool true or exception
  */
-function set_config($name, $value, $plugin=null) {
+function set_config($name, $value, $plugin = null) {
     global $CFG, $DB;
 
-    if (empty($plugin)) {
-        if (!array_key_exists($name, $CFG->config_php_settings)) {
-            // So it's defined for this invocation at least.
-            if (is_null($value)) {
-                unset($CFG->$name);
-            } else {
-                // Settings from db are always strings.
-                $CFG->$name = (string)$value;
-            }
-        }
+    // Redirect to appropriate handler when value is null.
+    if ($value === null) {
+        return unset_config($name, $plugin);
+    }
 
-        if ($DB->get_field('config', 'name', array('name' => $name))) {
-            if ($value === null) {
-                $DB->delete_records('config', array('name' => $name));
-            } else {
-                $DB->set_field('config', 'value', $value, array('name' => $name));
-            }
-        } else {
-            if ($value !== null) {
-                $config = new stdClass();
-                $config->name  = $name;
-                $config->value = $value;
-                $DB->insert_record('config', $config, false);
-            }
-            // When setting config during a Behat test (in the CLI script, not in the web browser
-            // requests), remember which ones are set so that we can clear them later.
-            if (defined('BEHAT_TEST')) {
-                if (!property_exists($CFG, 'behat_cli_added_config')) {
-                    $CFG->behat_cli_added_config = [];
-                }
-                $CFG->behat_cli_added_config[$name] = true;
-            }
-        }
-        if ($name === 'siteidentifier') {
-            cache_helper::update_site_identifier($value);
-        }
-        cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
+    // Set variables determining conditions and where to store the new config.
+    // Plugin config goes to {config_plugins}, core config goes to {config}.
+    $iscore = empty($plugin);
+    if ($iscore) {
+        // If it's for core config.
+        $table = 'config';
+        $conditions = ['name' => $name];
+        $invalidatecachekey = 'core';
     } else {
-        // Plugin scope.
-        if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
-            if ($value===null) {
-                $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
-            } else {
-                $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
-            }
-        } else {
-            if ($value !== null) {
-                $config = new stdClass();
-                $config->plugin = $plugin;
-                $config->name   = $name;
-                $config->value  = $value;
-                $DB->insert_record('config_plugins', $config, false);
-            }
+        // If it's a plugin.
+        $table = 'config_plugins';
+        $conditions = ['name' => $name, 'plugin' => $plugin];
+        $invalidatecachekey = $plugin;
+    }
+
+    // DB handling - checks for existing config, updating or inserting only if necessary.
+    $invalidatecache = true;
+    $inserted = false;
+    $record = $DB->get_record($table, $conditions, 'id, value');
+    if ($record === false) {
+        // Inserts a new config record.
+        $config = new stdClass();
+        $config->name  = $name;
+        $config->value = $value;
+        if (!$iscore) {
+            $config->plugin = $plugin;
         }
-        cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
+        $inserted = $DB->insert_record($table, $config, false);
+    } else if ($invalidatecache = ($record->value !== $value)) {
+        // Record exists - Check and only set new value if it has changed.
+        $DB->set_field($table, 'value', $value, ['id' => $record->id]);
+    }
+
+    if ($iscore && !isset($CFG->config_php_settings[$name])) {
+        // So it's defined for this invocation at least.
+        // Settings from db are always strings.
+        $CFG->$name = (string) $value;
+    }
+
+    // When setting config during a Behat test (in the CLI script, not in the web browser
+    // requests), remember which ones are set so that we can clear them later.
+    if ($iscore && $inserted && defined('BEHAT_TEST')) {
+        $CFG->behat_cli_added_config[$name] = true;
+    }
+
+    // Update siteidentifier cache, if required.
+    if ($iscore && $name === 'siteidentifier') {
+        cache_helper::update_site_identifier($value);
+    }
+
+    // Invalidate cache, if required.
+    if ($invalidatecache) {
+        cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
     }
 
     return true;
index 5bac5ae..c1f19fa 100644 (file)
@@ -28,6 +28,7 @@
             "name": "action",
             "value": "toggle-reality"
         }],
+        "title": "Title example",
         "label": "Enable/disable reality",
         "labelclasses": "sr-only"
     }
@@ -41,7 +42,7 @@
             {{#disabled}}disabled{{/disabled}}
         {{/attributes}}>
     {{$labelmarkup}}
-        <label class="custom-control-label" for="{{$id}}{{id}}{{/id}}">
+        <label class="custom-control-label" for="{{$id}}{{id}}{{/id}}" {{#title}}data-toggle="tooltip" data-placement="top" title="{{title}}"{{/title}}>
             <span class="{{$labelclasses}}{{labelclasses}}{{/labelclasses}}">{{$label}}{{label}}{{/label}}</span>
         </label>
     {{/labelmarkup}}
index a32a83b..ca66664 100644 (file)
@@ -87,10 +87,18 @@ EOD;
      * @return void
      */
     public function reset() {
+        $this->gradecategorycounter = 0;
+        $this->gradeitemcounter = 0;
+        $this->gradeoutcomecounter = 0;
         $this->usercounter = 0;
         $this->categorycount = 0;
+        $this->cohortcount = 0;
         $this->coursecount = 0;
         $this->scalecount = 0;
+        $this->groupcount = 0;
+        $this->groupingcount = 0;
+        $this->rolecount = 0;
+        $this->tagcount = 0;
 
         foreach ($this->generators as $generator) {
             $generator->reset();
index 40042da..a093952 100644 (file)
@@ -627,7 +627,7 @@ class behat_navigation extends behat_base {
             }
             return [$component, $name];
         } else {
-            throw new coding_exception('The page name most be in the form ' .
+            throw new coding_exception('The page name must be in the form ' .
                     '"{page-name}" for core pages, or "{component} > {page-name}" ' .
                     'for pages belonging to other components. ' .
                     'For example "Admin notifications" or "mod_quiz > View".');
index 1f6a65a..94cd374 100644 (file)
Binary files a/reportbuilder/amd/build/schedules.min.js and b/reportbuilder/amd/build/schedules.min.js differ
index 54677ab..0fe6728 100644 (file)
Binary files a/reportbuilder/amd/build/schedules.min.js.map and b/reportbuilder/amd/build/schedules.min.js.map differ
index a5fb027..28815d8 100644 (file)
@@ -99,7 +99,7 @@ export const init = reportId => {
             toggleSchedule(reportId, scheduleToggle.dataset.id, scheduleStateToggle)
                 .then(() => {
                     const tableRow = scheduleToggle.closest('tr');
-                    tableRow.classList.toggle('dimmed_text');
+                    tableRow.classList.toggle('text-muted');
 
                     scheduleToggle.dataset.state = scheduleStateToggle;
 
index d852ebb..cddbcf4 100644 (file)
@@ -266,4 +266,24 @@ abstract class datasource extends base {
 
         return $conditions;
     }
+
+    /**
+     * Adds all columns/filters/conditions from the given entity to the report at once
+     *
+     * @param string $entityname
+     */
+    final protected function add_all_from_entity(string $entityname): void {
+        $this->add_columns_from_entity($entityname);
+        $this->add_filters_from_entity($entityname);
+        $this->add_conditions_from_entity($entityname);
+    }
+
+    /**
+     * Adds all columns/filters/conditions from all the entities added to the report at once
+     */
+    final protected function add_all_from_entities(): void {
+        foreach ($this->get_entities() as $entity) {
+            $this->add_all_from_entity($entity->get_entity_name());
+        }
+    }
 }
index 8fb9026..635f32e 100644 (file)
@@ -30,6 +30,7 @@ use core_reportbuilder\table\custom_report_table_filterset;
 use core_reportbuilder\table\custom_report_table_view;
 use core_reportbuilder\table\custom_report_table_view_filterset;
 use core_reportbuilder\local\helpers\report as report_helper;
+use core_table\local\filter\integer_filter;
 
 /**
  * Custom report exporter class
@@ -78,6 +79,7 @@ class custom_report_exporter extends persistent_exporter {
      */
     protected static function define_related(): array {
         return [
+            'pagesize' => 'int?',
         ];
     }
 
@@ -119,8 +121,12 @@ class custom_report_exporter extends persistent_exporter {
             $table = custom_report_table::create($this->persistent->get('id'));
             $table->set_filterset(new custom_report_table_filterset());
         } else {
+            // We store the pagesize within the table filterset so that it's available between AJAX requests.
+            $filterset = new custom_report_table_view_filterset();
+            $filterset->add_filter(new integer_filter('pagesize', null, [$this->related['pagesize']]));
+
             $table = custom_report_table_view::create($this->persistent->get('id'), $this->download);
-            $table->set_filterset(new custom_report_table_view_filterset());
+            $table->set_filterset($filterset);
 
             // Generate filters form if report contains any filters.
             $source = $this->persistent->get('source');
index f690cc2..868fbce 100644 (file)
@@ -18,7 +18,9 @@ declare(strict_types=1);
 
 namespace core_reportbuilder\local\entities;
 
+use context_helper;
 use context_system;
+use context_user;
 use html_writer;
 use lang_string;
 use moodle_url;
@@ -51,7 +53,10 @@ class user extends base {
      * @return array
      */
     protected function get_default_table_aliases(): array {
-        return ['user' => 'u'];
+        return [
+            'user' => 'u',
+            'context' => 'uctx',
+        ];
     }
 
     /**
@@ -109,6 +114,7 @@ class user extends base {
      */
     protected function get_all_columns(): array {
         $usertablealias = $this->get_table_alias('user');
+        $contexttablealias = $this->get_table_alias('context');
 
         $fullnameselect = self::get_name_fields_select($usertablealias);
         $fullnamesort = explode(', ', $fullnameselect);
@@ -235,6 +241,14 @@ class user extends base {
                     $countries = get_string_manager()->get_list_of_countries(true);
                     return $countries[$country] ?? '';
                 });
+            } else if ($userfield === 'description') {
+                // Select enough fields in order to format the column.
+                $column
+                    ->add_join("LEFT JOIN {context} {$contexttablealias}
+                           ON {$contexttablealias}.contextlevel = " . CONTEXT_USER . "
+                          AND {$contexttablealias}.instanceid = {$usertablealias}.id")
+                    ->add_fields("{$usertablealias}.descriptionformat, {$usertablealias}.id")
+                    ->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias));
             }
 
             $columns[] = $column;
@@ -252,6 +266,7 @@ class user extends base {
     protected function is_sortable(string $fieldname): bool {
         // Some columns can't be sorted, like longtext or images.
         $nonsortable = [
+            'description',
             'picture',
         ];
 
@@ -267,6 +282,8 @@ class user extends base {
      * @return string
      */
     public function format($value, stdClass $row, string $fieldname): string {
+        global $CFG;
+
         if ($this->get_user_field_type($fieldname) === column::TYPE_BOOLEAN) {
             return format::boolean_as_text($value);
         }
@@ -275,6 +292,20 @@ class user extends base {
             return format::userdate($value, $row);
         }
 
+        if ($fieldname === 'description') {
+            if (empty($row->id)) {
+                return '';
+            }
+
+            require_once("{$CFG->libdir}/filelib.php");
+
+            context_helper::preload_from_record($row);
+            $context = context_user::instance($row->id);
+
+            $description = file_rewrite_pluginfile_urls($value, 'pluginfile.php', $context->id, 'user', 'profile', null);
+            return format_text($description, $row->descriptionformat, ['context' => $context->id]);
+        }
+
         return s($value);
     }
 
@@ -320,6 +351,7 @@ class user extends base {
             'email' => new lang_string('email'),
             'city' => new lang_string('city'),
             'country' => new lang_string('country'),
+            'description' => new lang_string('description'),
             'firstnamephonetic' => new lang_string('firstnamephonetic'),
             'lastnamephonetic' => new lang_string('lastnamephonetic'),
             'middlename' => new lang_string('middlename'),
@@ -346,6 +378,9 @@ class user extends base {
      */
     protected function get_user_field_type(string $userfield): int {
         switch ($userfield) {
+            case 'description':
+                $fieldtype = column::TYPE_LONGTEXT;
+                break;
             case 'confirmed':
             case 'suspended':
                 $fieldtype = column::TYPE_BOOLEAN;
@@ -367,6 +402,8 @@ class user extends base {
      * @return filter[]
      */
     protected function get_all_filters(): array {
+        global $DB;
+
         $filters = [];
         $tablealias = $this->get_table_alias('user');
 
@@ -386,6 +423,13 @@ class user extends base {
         // User fields filters.
         $fields = $this->get_user_fields();
         foreach ($fields as $field => $name) {
+            // Filtering isn't supported for LONGTEXT fields on Oracle.
+            if ($this->get_user_field_type($field) === column::TYPE_LONGTEXT &&
+                    $DB->get_dbfamily() === 'oracle') {
+
+                continue;
+            }
+
             $optionscallback = [static::class, 'get_options_for_' . $field];
             if (is_callable($optionscallback)) {
                 $classname = select::class;
index 93771ea..4d29229 100644 (file)
@@ -199,10 +199,10 @@ class date extends base {
                     return ['', []];
                 }
 
-                $paramdatefrom = database::generate_param_name();
-                $paramdateto = database::generate_param_name();
-
+                // Generate parameters and SQL clause for the relative date comparison.
+                [$paramdatefrom, $paramdateto] = database::generate_param_names(2);
                 $sql = "{$fieldsql} >= :{$paramdatefrom} AND {$fieldsql} <= :{$paramdateto}";
+
                 [
                     $params[$paramdatefrom],
                     $params[$paramdateto],
index 90d0425..fa1245e 100644 (file)
@@ -123,8 +123,8 @@ class number extends base {
             return ['', []];
         }
 
-        $param = database::generate_param_name();
-        $param2 = database::generate_param_name();
+        [$param, $param2] = database::generate_param_names(2);
+
         $fieldsql = $this->filter->get_field_sql();
         $params = $this->filter->get_field_params();
 
index 9bb3d08..3199180 100644 (file)
@@ -46,6 +46,19 @@ class database {
 
         return static::GENERATE_ALIAS_PREFIX . ($aliascount++);
     }
+
+    /**
+     * Generate multiple unique table/column aliases, see {@see generate_alias} for info
+     *
+     * @param int $count
+     * @return string[]
+     */
+    public static function generate_aliases(int $count): array {
+        return array_map([
+            static::class, 'generate_alias'
+        ], array_fill(0, $count, null));
+    }
+
     /**
      * Generates unique parameter name that must be used in generated SQL
      *
@@ -57,6 +70,18 @@ class database {
         return static::GENERATE_PARAM_PREFIX . ($paramcount++);
     }
 
+    /**
+     * Generate multiple unique parameter names, see {@see generate_param_name} for info
+     *
+     * @param int $count
+     * @return string[]
+     */
+    public static function generate_param_names(int $count): array {
+        return array_map([
+            static::class, 'generate_param_name'
+        ], array_fill(0, $count, null));
+    }
+
     /**
      * Validate that parameter names were generated using {@see generate_param_name}.
      *
index 5366ba6..5abe25d 100644 (file)
@@ -84,6 +84,9 @@ abstract class base {
     /** @var string $downloadfilename Name of the downloaded file */
     private $downloadfilename = '';
 
+    /** @var int Default paging size */
+    private $defaultperpage = self::DEFAULT_PAGESIZE;
+
     /**
      * Base report constructor
      *
@@ -269,6 +272,15 @@ abstract class base {
         return $this->entities[$name];
     }
 
+    /**
+     * Returns the list of all the entities added to the report
+     *
+     * @return entity_base[]
+     */
+    final protected function get_entities(): array {
+        return $this->entities;
+    }
+
     /**
      * Define a new entity for the report
      *
@@ -714,4 +726,22 @@ abstract class base {
     public function get_context(): context {
         return $this->report->get_context();
     }
+
+    /**
+     * Set the default 'per page' size
+     *
+     * @param int $defaultperpage
+     */
+    public function set_default_per_page(int $defaultperpage): void {
+        $this->defaultperpage = $defaultperpage;
+    }
+
+    /**
+     * Default 'per page' size
+     *
+     * @return int
+     */
+    public function get_default_per_page(): int {
+        return $this->defaultperpage;
+    }
 }
index dc77dde..071850c 100644 (file)
@@ -102,7 +102,7 @@ class report_schedules extends system_report {
      * @return string
      */
     public function get_row_class(stdClass $row): string {
-        return $row->enabled ? '' : 'dimmed_text';
+        return $row->enabled ? '' : 'text-muted';
     }
 
     /**
index 44d2fc6..58c3272 100644 (file)
@@ -110,7 +110,7 @@ class reports_list extends system_report {
      * @return string
      */
     public function get_row_class(stdClass $row): string {
-        return $this->report_source_valid($row->source) ? '' : 'dimmed_text';
+        return $this->report_source_valid($row->source) ? '' : 'text-muted';
     }
 
     /**
index f907ff8..0e4cdfb 100644 (file)
@@ -18,6 +18,7 @@ declare(strict_types=1);
 
 namespace core_reportbuilder\output;
 
+use core_reportbuilder\manager;
 use core_reportbuilder\external\custom_report_exporter;
 use core_reportbuilder\local\models\report;
 use renderable;
@@ -63,7 +64,11 @@ class custom_report implements renderable, templatable {
      * @return stdClass
      */
     public function export_for_template(renderer_base $output): stdClass {
-        $exporter = new custom_report_exporter($this->persistent, [], $this->editmode, $this->download);
+        $report = manager::get_report_from_persistent($this->persistent);
+
+        $exporter = new custom_report_exporter($this->persistent, [
+            'pagesize' => $report->get_default_per_page(),
+        ], $this->editmode, $this->download);
 
         return $exporter->export($output);
     }
index 315aa2d..7dff0bf 100644 (file)
@@ -57,17 +57,17 @@ class systemrole extends base {
         $prefix = database::generate_param_name() . '_';
         [$insql, $inparams] = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, $prefix);
 
-        $contextid = database::generate_param_name();
-        $ra = database::generate_alias();
-        $ctx = database::generate_alias();
+        // Ensure parameter names and aliases are unique, as the same audience type can be added multiple times to a report.
+        $paramcontextid = database::generate_param_name();
+        [$roleassignments, $context] = database::generate_aliases(2);
 
         $join = "
-            JOIN {role_assignments} {$ra} ON {$ra}.userid = {$usertablealias}.id
-            JOIN {context} {$ctx} ON {$ctx}.id = {$ra}.contextid";
+            JOIN {role_assignments} {$roleassignments} ON {$roleassignments}.userid = {$usertablealias}.id
+            JOIN {context} {$context} ON {$context}.id = {$roleassignments}.contextid";
 
-        $where = "{$ra}.contextid = :{$contextid} AND {$ra}.roleid {$insql}";
+        $where = "{$roleassignments}.contextid = :{$paramcontextid} AND {$roleassignments}.roleid {$insql}";
 
-        return [$join, $where, $inparams + [$contextid => context_system::instance()->id]];
+        return [$join, $where, $inparams + [$paramcontextid => context_system::instance()->id]];
     }
 
     /**
index 3c6c67c..d8de149 100644 (file)
@@ -208,15 +208,6 @@ abstract class system_report extends base {
         return '';
     }
 
-    /**
-     * Default 'per page' size. Can be overridden by system reports to define a different paging value
-     *
-     * @return int
-     */
-    public function get_default_per_page(): int {
-        return self::DEFAULT_PAGESIZE;
-    }
-
     /**
      * Called before rendering each row. Can be overridden to pre-fetch/create objects and store them in the class, which can
      * later be used in column and action callbacks
index d393460..6bdb7e2 100644 (file)
@@ -140,6 +140,7 @@ class custom_report_table extends base_report_table {
         $this->initialbars(false);
         $this->collapsible(false);
         $this->pageable(true);
+        $this->set_default_per_page($this->report->get_default_per_page());
 
         // Initialise table SQL properties.
         $this->set_report_editing(static::REPORT_EDITING);
index c257298..2ed750d 100644 (file)
@@ -44,6 +44,17 @@ class custom_report_table_view extends custom_report_table {
         base_report_table::print_headers();
     }
 
+    /**
+     * Override base implementation, return pagesize as defined in table filterset
+     *
+     * @return int
+     */
+    public function get_default_per_page(): int {
+        $filterset = $this->get_filterset();
+
+        return $filterset->get_filter('pagesize')->current();
+    }
+
     /**
      * Get the html for the download buttons
      *
index 4521686..2b34585 100644 (file)
@@ -18,6 +18,9 @@ declare(strict_types=1);
 
 namespace core_reportbuilder\table;
 
+use core_table\local\filter\filterset;
+use core_table\local\filter\integer_filter;
+
 /**
  * Custom report dynamic table filterset class
  *
@@ -25,5 +28,16 @@ namespace core_reportbuilder\table;
  * @copyright   2021 Paul Holden <paulh@moodle.com>
  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class custom_report_table_view_filterset extends custom_report_table_filterset {
+class custom_report_table_view_filterset extends filterset {
+
+    /**
+     * Get the required filters
+     *
+     * @return array.
+     */
+    public function get_required_filters(): array {
+        return [
+            'pagesize' => integer_filter::class,
+        ];
+    }
 }
index 5cee037..d415312 100644 (file)
@@ -93,7 +93,7 @@ Feature: Manage custom report schedules
     And I am on the "My report" "reportbuilder > Editor" page logged in as "admin"
     And I click on the "Schedules" dynamic tab
     When I click on "Disable schedule" "field" in the "My schedule" "table_row"
-    Then the "class" attribute of "My schedule" "table_row" should contain "dimmed_text"
+    Then the "class" attribute of "My schedule" "table_row" should contain "text-muted"
     And I click on "Enable schedule" "field" in the "My schedule" "table_row"
 
   Scenario: Edit report schedule
index 286c3be..5dc496b 100644 (file)
@@ -33,41 +33,67 @@ use core_user;
 class database_test extends advanced_testcase {
 
     /**
-     * Test generating table alias and parameter names
+     * Test generating alias
      */
-    public function test_generate_alias_params(): void {
-        global $DB;
-
-        $admin = core_user::get_user_by_username('admin');
+    public function test_generate_alias(): void {
+        $this->assertMatchesRegularExpression('/^rbalias(\d+)$/', database::generate_alias());
+    }
 
-        $usertablealias = database::generate_alias();
-        $usertablealiasjoin = database::generate_alias();
-        $useridalias = database::generate_alias();
+    /**
+     * Test generating multiple aliases
+     */
+    public function test_generate_aliases(): void {
+        $aliases = database::generate_aliases(3);
 
-        $paramuserid = database::generate_param_name();
-        $paramuserdeleted = database::generate_param_name();
+        $this->assertCount(3, $aliases);
+        [$aliasone, $aliastwo, $aliasthree] = $aliases;
 
         // Ensure they are different.
-        $this->assertNotEquals($usertablealias, $usertablealiasjoin);
-        $this->assertNotEquals($paramuserid, $paramuserdeleted);
+        $this->assertNotEquals($aliasone, $aliastwo);
+        $this->assertNotEquals($aliasone, $aliasthree);
+        $this->assertNotEquals($aliastwo, $aliasthree);
+    }
 
-        $sql = "SELECT {$usertablealias}.id AS {$useridalias}
-                  FROM {user} {$usertablealias}
-                  JOIN {user} {$usertablealiasjoin} ON {$usertablealiasjoin}.id = {$usertablealias}.id
-                 WHERE {$usertablealias}.id = :{$paramuserid} AND {$usertablealias}.deleted = :{$paramuserdeleted}";
-        $params = [$paramuserid => $admin->id, $paramuserdeleted => 0];
+    /**
+     * Test generating parameter name
+     */
+    public function test_generate_param_name(): void {
+        $this->assertMatchesRegularExpression('/^rbparam(\d+)$/', database::generate_param_name());
+    }
 
-        $validated = database::validate_params($params);
-        $this->assertTrue($validated);
+    /**
+     * Test generating multiple parameter names
+     */
+    public function test_generate_param_names(): void {
+        $params = database::generate_param_names(3);
 
-        $record = $DB->get_record_sql($sql, $params);
-        $this->assertEquals($admin->id, $record->{$useridalias});
+        $this->assertCount(3, $params);
+        [$paramone, $paramtwo, $paramthree] = $params;
+
+        // Ensure they are different.
+        $this->assertNotEquals($paramone, $paramtwo);
+        $this->assertNotEquals($paramone, $paramthree);
+        $this->assertNotEquals($paramtwo, $paramthree);
     }
 
     /**
      * Test parameter validation
      */
     public function test_validate_params(): void {
+        [$paramone, $paramtwo] = database::generate_param_names(2);
+
+        $params = [
+            $paramone => 1,
+            $paramtwo => 2,
+        ];
+
+        $this->assertTrue(database::validate_params($params));
+    }
+
+    /**
+     * Test parameter validation for invalid parameters
+     */
+    public function test_validate_params_invalid(): void {
         $params = [
             database::generate_param_name() => 1,
             'invalidfoo' => 2,
@@ -78,4 +104,39 @@ class database_test extends advanced_testcase {
         $this->expectExceptionMessage('Invalid parameter names (invalidfoo, invalidbar)');
         database::validate_params($params);
     }
+
+    /**
+     * Generate aliases and parameters and confirm they can be used within a query
+     */
+    public function test_generated_data_in_query(): void {
+        global $DB;
+
+        // Unique aliases.
+        [
+            $usertablealias,
+            $userfieldalias,
+        ] = database::generate_aliases(2);
+
+        // Unique parameters.
+        [
+            $paramuserid,
+            $paramuserdeleted,
+        ] = database::generate_param_names(2);
+
+        // Simple query to retrieve the admin user.
+        $sql = "SELECT {$usertablealias}.id AS {$userfieldalias}
+                  FROM {user} {$usertablealias}
+                 WHERE {$usertablealias}.id = :{$paramuserid}
+                   AND {$usertablealias}.deleted = :{$paramuserdeleted}";
+
+        $admin = core_user::get_user_by_username('admin');
+
+        $params = [
+            $paramuserid => $admin->id,
+            $paramuserdeleted => 0,
+        ];
+
+        $record = $DB->get_record_sql($sql, $params);
+        $this->assertEquals($admin->id, $record->{$userfieldalias});
+    }
 }
diff --git a/reportbuilder/upgrade.txt b/reportbuilder/upgrade.txt
new file mode 100644 (file)
index 0000000..098f510
--- /dev/null
@@ -0,0 +1,11 @@
+This file describes API changes in /reportbuilder/*
+Information provided here is intended especially for developers.
+
+=== 4.1 ===
+* 'set_default_per_page' and 'get_default_per_page' methods have been added to \local\report\base class
+   to manage the default displayed rows per page.
+* Added two new methods in the datasource class:
+    - add_all_from_entity() to add all columns/filters/conditions from the given entity to the report at once
+    - add_all_from_entities() to add all columns/filters/conditions from all the entities added to the report at once
+=======
+* New database helper methods for generating multiple unique values: `generate_aliases` and `generate_param_names`
index c37eb9f..e8c599d 100644 (file)
@@ -56,13 +56,11 @@ class users extends datasource {
             $userparamguest => $CFG->siteguest,
         ]);
 
-        // Add all columns from entities to be available in custom reports.
         $this->add_entity($userentity);
 
+        // Add all columns/filters/conditions from entities to be available in custom reports.
         $userentityname = $userentity->get_entity_name();
-        $this->add_columns_from_entity($userentityname);
-        $this->add_filters_from_entity($userentityname);
-        $this->add_conditions_from_entity($userentityname);
+        $this->add_all_from_entity($userentityname);
     }
 
     /**