Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
declare(strict_types=1);
18
 
19
namespace core_reportbuilder\local\helpers;
20
 
1441 ariadna 21
use core\lang_string;
1 efrain 22
use core_customfield\data_controller;
23
use core_customfield\field_controller;
24
use core_customfield\handler;
1441 ariadna 25
use core_reportbuilder\local\filters\{boolean_select, date, number, select, text};
26
use core_reportbuilder\local\report\{column, filter};
27
use stdClass;
1 efrain 28
 
29
/**
30
 * Helper class for course custom fields.
31
 *
32
 * @package   core_reportbuilder
33
 * @copyright 2021 Sara Arjona <sara@moodle.com> based on David Matamoros <davidmc@moodle.com> code.
34
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class custom_fields {
37
 
1441 ariadna 38
    use join_trait;
1 efrain 39
 
40
    /** @var handler $handler The handler for the customfields */
1441 ariadna 41
    private handler $handler;
1 efrain 42
 
43
    /**
1441 ariadna 44
     * Constructor
1 efrain 45
     *
1441 ariadna 46
     * @param string $tablefieldalias The table/field alias to match the instance ID when adding columns and filters.
47
     * @param string $entityname The entity name used when adding columns and filters.
1 efrain 48
     * @param string $component component name of full frankenstyle plugin name.
49
     * @param string $area name of the area (each component/plugin may define handlers for multiple areas).
50
     * @param int $itemid item id if the area uses them (usually not used).
51
     */
1441 ariadna 52
    public function __construct(
53
        /** @var string The table/field alias to match the instance ID when adding columns and filters */
54
        private readonly string $tablefieldalias,
55
        /** @var string The entity name used when adding columns and filters */
56
        private readonly string $entityname,
57
        string $component,
58
        string $area,
59
        int $itemid = 0,
60
    ) {
1 efrain 61
        $this->handler = handler::get_handler($component, $area, $itemid);
62
    }
63
 
64
    /**
65
     * Get table alias for given custom field
66
     *
67
     * The entity name is used to ensure the alias differs when the entity is used multiple times within the same report, each
68
     * having their own table alias/join
69
     *
70
     * @param field_controller $field
71
     * @return string
72
     */
73
    private function get_table_alias(field_controller $field): string {
74
        static $aliases = [];
75
 
76
        $aliaskey = "{$this->entityname}_{$field->get('id')}";
77
        if (!array_key_exists($aliaskey, $aliases)) {
78
            $aliases[$aliaskey] = database::generate_alias();
79
        }
80
 
81
        return $aliases[$aliaskey];
82
    }
83
 
84
    /**
85
     * Get table join for given custom field
86
     *
87
     * @param field_controller $field
88
     * @return string
89
     */
90
    private function get_table_join(field_controller $field): string {
91
        $customdatatablealias = $this->get_table_alias($field);
92
 
93
        return "LEFT JOIN {customfield_data} {$customdatatablealias}
94
                       ON {$customdatatablealias}.fieldid = {$field->get('id')}
95
                      AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}";
96
    }
97
 
98
    /**
99
     * Gets the custom fields columns for the report.
100
     *
101
     * Column will be named as 'customfield_' + customfield shortname.
102
     *
103
     * @return column[]
104
     */
105
    public function get_columns(): array {
106
        $columns = [];
107
 
108
        $categorieswithfields = $this->handler->get_categories_with_fields();
109
        foreach ($categorieswithfields as $fieldcategory) {
110
            $categoryfields = $fieldcategory->get_fields();
111
            foreach ($categoryfields as $field) {
112
                $datacontroller = data_controller::create(0, null, $field);
113
                $datafield = $datacontroller->datafield();
114
 
1441 ariadna 115
                $customdatatablealias = $this->get_table_alias($field);
116
                $customdatasql = "{$customdatatablealias}.{$datafield}";
117
 
118
                // Numeric column (non-text) should coalesce with default, for aggregation.
1 efrain 119
                $columntype = $this->get_column_type($field, $datafield);
1441 ariadna 120
                if (!in_array($columntype, [column::TYPE_TEXT, column::TYPE_LONGTEXT])) {
121
 
122
                    // See MDL-78783 regarding no bound parameters, and SQL Server limitations of GROUP BY.
123
                    $customdatasql = "
124
                        CASE WHEN {$this->tablefieldalias} IS NOT NULL
125
                             THEN COALESCE({$customdatasql}, " . (float) $datacontroller->get_default_value() . ")
126
                             ELSE NULL
127
                        END";
1 efrain 128
                }
129
 
130
                // Select enough fields to re-create and format each custom field instance value.
1441 ariadna 131
                $customdatasqlextra = "{$customdatatablealias}.id, {$customdatatablealias}.contextid";
1 efrain 132
                if ($datafield === 'value') {
133
                    // We will take the format into account when displaying the individual values.
1441 ariadna 134
                    $customdatasqlextra .= ", {$customdatatablealias}.valueformat, {$customdatatablealias}.valuetrust";
1 efrain 135
                }
136
 
137
                $columns[] = (new column(
138
                    'customfield_' . $field->get('shortname'),
1441 ariadna 139
                    new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name(false)),
1 efrain 140
                    $this->entityname
141
                ))
142
                    ->add_joins($this->get_joins())
143
                    ->add_join($this->get_table_join($field))
1441 ariadna 144
                    ->set_type($columntype)
145
                    ->add_field($customdatasql, $datafield)
146
                    ->add_fields($customdatasqlextra)
1 efrain 147
                    ->add_field($this->tablefieldalias, 'tablefieldalias')
1441 ariadna 148
                    ->set_is_sortable(true)
149
                    ->add_callback(static function($value, stdClass $row, field_controller $field, ?string $aggregation): string {
150
                        if ($row->tablefieldalias === null && $value === null) {
1 efrain 151
                            return '';
152
                        }
1441 ariadna 153
                        // If aggregating numeric column, populate row ID to ensure the controller is created correctly.
154
                        if (in_array((string) $aggregation, ['avg', 'max', 'min', 'sum'])) {
155
                            $row->id ??= -1;
156
                        }
1 efrain 157
                        return (string) data_controller::create(0, $row, $field)->export_value();
158
                    }, $field)
159
                    // Important. If the handler implements can_view() function, it will be called with parameter $instanceid=0.
160
                    // This means that per-instance access validation will be ignored.
161
                    ->set_is_available($this->handler->can_view($field, 0));
162
            }
163
        }
164
        return $columns;
165
    }
166
 
167
    /**
168
     * Returns the column type
169
     *
170
     * @param field_controller $field
171
     * @param string $datafield
172
     * @return int
173
     */
174
    private function get_column_type(field_controller $field, string $datafield): int {
175
        if ($field->get('type') === 'checkbox') {
176
            return column::TYPE_BOOLEAN;
177
        }
178
 
179
        if ($field->get('type') === 'date') {
180
            return column::TYPE_TIMESTAMP;
181
        }
182
 
183
        if ($field->get('type') === 'select') {
184
            return column::TYPE_TEXT;
185
        }
186
 
187
        if ($datafield === 'intvalue') {
188
            return column::TYPE_INTEGER;
189
        }
190
 
191
        if ($datafield === 'decvalue') {
192
            return column::TYPE_FLOAT;
193
        }
194
 
195
        if ($datafield === 'value') {
196
            return column::TYPE_LONGTEXT;
197
        }
198
 
199
        return column::TYPE_TEXT;
200
    }
201
 
202
    /**
203
     * Returns all available filters on custom fields.
204
     *
205
     * Filter will be named as 'customfield_' + customfield shortname.
206
     *
207
     * @return filter[]
208
     */
209
    public function get_filters(): array {
210
        $filters = [];
211
 
212
        $categorieswithfields = $this->handler->get_categories_with_fields();
213
        foreach ($categorieswithfields as $fieldcategory) {
214
            $categoryfields = $fieldcategory->get_fields();
215
            foreach ($categoryfields as $field) {
1441 ariadna 216
                $datacontroller = data_controller::create(0, null, $field);
217
                $datafield = $datacontroller->datafield();
218
 
1 efrain 219
                $customdatatablealias = $this->get_table_alias($field);
1441 ariadna 220
                $customdatasql = "{$customdatatablealias}.{$datafield}";
221
                $customdataparams = [];
1 efrain 222
 
1441 ariadna 223
                // Account for field default value, when joined to the instance table related to the custom fields.
224
                if (($fielddefault = $datacontroller->get_default_value()) !== null) {
225
                    $paramdefault = database::generate_param_name();
226
                    $customdatasql = "
227
                        CASE WHEN {$this->tablefieldalias} IS NOT NULL
228
                             THEN COALESCE({$customdatasql}, :{$paramdefault})
229
                             ELSE NULL
230
                        END";
231
                    $customdataparams[$paramdefault] = $fielddefault;
1 efrain 232
                }
233
 
234
                $filter = (new filter(
1441 ariadna 235
                    $this->get_filter_class_type($datacontroller),
1 efrain 236
                    'customfield_' . $field->get('shortname'),
1441 ariadna 237
                    new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name(false)),
1 efrain 238
                    $this->entityname,
1441 ariadna 239
                    $customdatasql,
240
                    $customdataparams,
1 efrain 241
                ))
242
                    ->add_joins($this->get_joins())
1441 ariadna 243
                    ->add_join($this->get_table_join($field))
244
                    ->set_is_available($this->handler->can_view($field, 0));
1 efrain 245
 
1441 ariadna 246
                // If using a select filter, then populate the options.
247
                if ($filter->get_filter_class() === select::class) {
248
                    $filter->set_options_callback(fn(): array => $field->get_options());
1 efrain 249
                }
250
 
251
                $filters[] = $filter;
252
            }
253
        }
254
        return $filters;
255
    }
256
 
257
    /**
258
     * Returns class for the filter element that should be used for the field
259
     *
260
     * In some situation we can assume what kind of data is stored in the customfield plugin and we can
261
     * display appropriate filter form element. For all others assume text filter.
262
     *
263
     * @param data_controller $datacontroller
264
     * @return string
265
     */
266
    private function get_filter_class_type(data_controller $datacontroller): string {
267
        $type = $datacontroller->get_field()->get('type');
268
 
269
        switch ($type) {
270
            case 'checkbox':
271
                $classtype = boolean_select::class;
272
                break;
273
            case 'date':
274
                $classtype = date::class;
275
                break;
276
            case 'select':
277
                $classtype = select::class;
278
                break;
279
            default:
280
                // To support third party field type we need to account for stored numbers.
281
                $datafield = $datacontroller->datafield();
282
                if ($datafield === 'intvalue' || $datafield === 'decvalue') {
283
                    $classtype = number::class;
284
                } else {
285
                    $classtype = text::class;
286
                }
287
                break;
288
        }
289
 
290
        return $classtype;
291
    }
292
}