Proyectos de Subversion Moodle

Rev

Rev 1 | | 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
/**
18
 * Base class for representing a column.
19
 *
20
 * @package   core_question
21
 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_question\local\bank;
26
 
27
/**
28
 * Base class for representing a column.
29
 *
30
 * @copyright 2009 Tim Hunt
31
 * @author    2021 Safat Shahin <safatshahin@catalyst-au.net>
32
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33
 */
34
abstract class column_base extends view_component {
35
 
36
    /**
37
     * @var string A separator for joining column attributes together into a unique ID string.
38
     */
39
    const ID_SEPARATOR = '-';
40
 
41
    /**
42
     * @var view $qbank the question bank view we are helping to render.
43
     */
44
    protected $qbank;
45
 
46
    /** @var bool determine whether the column is td or th. */
47
    protected $isheading = false;
48
 
49
    /** @var bool determine whether the column is visible */
50
    public $isvisible = true;
51
 
52
    /**
53
     * Return an instance of this column, based on the column name.
54
     *
55
     * In the case of the base class, we don't actually use the column name since the class represents one specific column.
56
     * However, sub-classes may use the column name as an additional constructor to the parameter.
57
     *
58
     * @param view $view Question bank view
59
     * @param string $columnname The column name for this instance, as returned by {@see get_column_name()}
60
     * @param bool $ingoremissing Whether to ignore if the class does not exist.
61
     * @return column_base|null An instance of this class.
62
     */
63
    public static function from_column_name(view $view, string $columnname, bool $ingoremissing = false): ?column_base {
64
        return new static($view);
65
    }
66
 
67
    /**
68
     * Set the column as heading
69
     */
70
    public function set_as_heading(): void {
71
        $this->isheading = true;
72
    }
73
 
74
    /**
75
     * Check if the column is an extra row of not.
76
     */
77
    public function is_extra_row(): bool {
78
        return false;
79
    }
80
 
81
    /**
82
     * Check if the row has an extra preference to view/hide.
83
     */
84
    public function has_preference(): bool {
85
        return false;
86
    }
87
 
88
    /**
89
     * Get if the preference key of the row.
90
     */
91
    public function get_preference_key(): string {
92
        return '';
93
    }
94
 
95
    /**
96
     * Get if the preference of the row.
97
     */
98
    public function get_preference(): bool {
99
        return false;
100
    }
101
 
102
    /**
103
     * Output the column header cell.
104
     *
105
     * @param column_action_base[] $columnactions A list of column actions to include in the header.
106
     * @param string $width A CSS width property value.
107
     */
108
    public function display_header(array $columnactions = [], string $width = ''): void {
109
        global $PAGE;
110
        $renderer = $PAGE->get_renderer('core_question', 'bank');
111
 
112
        $data = [];
113
        $data['sortable'] = true;
114
        $data['extraclasses'] = $this->get_classes();
115
        $sortable = $this->is_sortable();
116
        $name = str_replace('\\', '__', get_class($this));
117
        $title = $this->get_title();
118
        $tip = $this->get_title_tip();
119
        $links = [];
120
        if (is_array($sortable)) {
121
            if ($title) {
122
                $data['title'] = $title;
123
            }
124
            foreach ($sortable as $subsort => $details) {
125
                $links[] = $this->make_sort_link($name . '-' . $subsort,
126
                        $details['title'], isset($details['tip']) ? $details['tip'] : '', !empty($details['reverse']));
127
            }
128
            $data['sortlinks'] = implode(' / ', $links);
129
        } else if ($sortable) {
130
            $data['sortlinks'] = $this->make_sort_link($name, $title, $tip);
131
        } else {
132
            $data['sortable'] = false;
133
            $data['tiptitle'] = $title;
134
            if ($tip) {
135
                $data['sorttip'] = true;
136
                $data['tip'] = $tip;
137
            }
138
        }
139
        $help = $this->help_icon();
140
        if ($help) {
141
            $data['help'] = $help->export_for_template($renderer);
142
        }
143
 
144
        $data['colname'] = $this->get_column_name();
145
        $data['columnid'] = $this->get_column_id();
146
        $data['name'] = $title;
147
        $data['class'] = $name;
148
        $data['width'] = $width;
149
        if (!empty($columnactions)) {
150
            $actions = array_map(fn($columnaction) => $columnaction->get_action_menu_link($this), $columnactions);
151
            $actionmenu = new \action_menu($actions);
152
            $data['actionmenu'] = $actionmenu->export_for_template($renderer);
153
        }
154
 
155
        echo $renderer->render_column_header($data);
156
    }
157
 
158
    /**
159
     * Title for this column. Not used if is_sortable returns an array.
160
     */
161
    abstract public function get_title();
162
 
163
    /**
164
     * Use this when get_title() returns
165
     * something very short, and you want a longer version as a tool tip.
166
     *
167
     * @return string a fuller version of the name.
168
     */
169
    public function get_title_tip() {
170
        return '';
171
    }
172
 
173
    /**
174
     * If you return a help icon here, it is shown in the column header after the title.
175
     *
176
     * @return \help_icon|null help icon to show, if required.
177
     */
178
    public function help_icon(): ?\help_icon {
179
        return null;
180
    }
181
 
182
    /**
183
     * Get a link that changes the sort order, and indicates the current sort state.
184
     *
185
     * @param string $sortname the column to sort on.
186
     * @param string $title the link text.
187
     * @param string $tip the link tool-tip text. If empty, defaults to title.
188
     * @param bool $defaultreverse whether the default sort order for this column is descending, rather than ascending.
189
     * @return string
190
     */
191
    protected function make_sort_link($sortname, $title, $tip, $defaultreverse = false): string {
192
        global $PAGE;
193
        $sortdata = [];
194
        $currentsort = $this->qbank->get_primary_sort_order($sortname);
195
        $newsortreverse = $defaultreverse;
196
        if ($currentsort) {
197
            $newsortreverse = $currentsort == SORT_ASC;
198
        }
199
        if (!$tip) {
200
            $tip = $title;
201
        }
202
        if ($newsortreverse) {
203
            $tip = get_string('sortbyxreverse', '', $tip);
204
        } else {
205
            $tip = get_string('sortbyx', '', $tip);
206
        }
207
 
208
        $link = $title;
209
        if ($currentsort) {
210
            $link .= $this->get_sort_icon($currentsort == SORT_DESC);
211
        }
212
 
213
        $sortdata['sorturl'] = $this->qbank->new_sort_url($sortname, $newsortreverse);
214
        $sortdata['sortname'] = $sortname;
215
        $sortdata['sortcontent'] = $link;
216
        $sortdata['sorttip'] = $tip;
217
        $sortdata['sortorder'] = $newsortreverse ? SORT_DESC : SORT_ASC;
218
        $renderer = $PAGE->get_renderer('core_question', 'bank');
219
        return $renderer->render_column_sort($sortdata);
220
 
221
    }
222
 
223
    /**
1441 ariadna 224
     * Get an icon representing the current sort state.
225
     *
1 efrain 226
     * @param bool $reverse sort is descending, not ascending.
227
     * @return string HTML image tag.
228
     */
229
    protected function get_sort_icon($reverse): string {
230
        global $OUTPUT;
231
        if ($reverse) {
1441 ariadna 232
            return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'));
1 efrain 233
        } else {
1441 ariadna 234
            return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'));
1 efrain 235
        }
236
    }
237
 
238
    /**
239
     * Output this column.
240
     * @param object $question the row from the $question table, augmented with extra information.
241
     * @param string $rowclasses CSS class names that should be applied to this row of output.
242
     */
243
    public function display($question, $rowclasses): void {
244
        $this->display_start($question, $rowclasses);
245
        $this->display_content($question, $rowclasses);
246
        $this->display_end($question, $rowclasses);
247
    }
248
 
249
    /**
250
     * Output the opening column tag.  If it is set as heading, it will use <th> tag instead of <td>
251
     *
252
     * @param \stdClass $question
253
     * @param string $rowclasses
254
     */
255
    protected function display_start($question, $rowclasses): void {
256
        $tag = 'td';
257
        $attr = [
258
            'class' => $this->get_classes(),
259
            'data-columnid' => $this->get_column_id(),
260
        ];
261
        if ($this->isheading) {
262
            $tag = 'th';
263
            $attr['scope'] = 'row';
264
        }
265
        echo \html_writer::start_tag($tag, $attr);
266
    }
267
 
268
    /**
269
     * The CSS classes to apply to every cell in this column.
270
     *
271
     * @return string
272
     */
273
    protected function get_classes(): string {
274
        $classes = $this->get_extra_classes();
275
        $classes[] = $this->get_name();
276
        return implode(' ', $classes);
277
    }
278
 
279
    /**
280
     * Get the internal name for this column. Used as a CSS class name,
281
     * and to store information about the current sort. Must match PARAM_ALPHA.
282
     *
283
     * @return string column name.
284
     */
285
    abstract public function get_name();
286
 
287
    /**
288
     * Get the name of this column. This must be unique.
289
     * When using the inherited class to make many columns from one parent,
290
     * ensure each instance returns a unique value.
291
     *
292
     * @return string The unique name;
293
     */
294
    public function get_column_name() {
295
        return (new \ReflectionClass($this))->getShortName();
296
    }
297
 
298
    /**
299
     * Return a unique ID for this column object.
300
     *
301
     * This is constructed using the class name and get_column_name(), which must be unique.
302
     *
303
     * The combination of these attributes allows the object to be reconstructed, by splitting the ID into its constituent
304
     * parts then calling {@see from_column_name()}, like this:
305
     * [$class, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2);
306
     * $column = $class::from_column_name($qbank, $columnname);
307
     * Including 2 as the $limit parameter for explode() is a good idea for safely, in case a plugin defines a column with the
308
     * ID_SEPARATOR in the column name.
309
     *
310
     * @return string The column ID.
311
     */
312
    final public function get_column_id(): string {
313
        return implode(self::ID_SEPARATOR, [static::class, $this->get_column_name()]);
314
    }
315
 
316
    /**
317
     * Any extra class names you would like applied to every cell in this column.
318
     *
319
     * @return array
320
     */
321
    public function get_extra_classes(): array {
322
        return [];
323
    }
324
 
325
    /**
326
     * Return the default column width in pixels.
327
     *
328
     * @return int
329
     */
330
    public function get_default_width(): int {
331
        return 120;
332
    }
333
 
334
    /**
335
     * Output the contents of this column.
336
     * @param object $question the row from the $question table, augmented with extra information.
337
     * @param string $rowclasses CSS class names that should be applied to this row of output.
338
     */
339
    abstract protected function display_content($question, $rowclasses);
340
 
341
    /**
342
     * Output the closing column tag
343
     *
344
     * @param object $question
345
     * @param string $rowclasses
346
     */
347
    protected function display_end($question, $rowclasses): void {
348
        $tag = 'td';
349
        if ($this->isheading) {
350
            $tag = 'th';
351
        }
352
        echo \html_writer::end_tag($tag);
353
    }
354
 
355
    public function get_extra_joins(): array {
356
        return [];
357
    }
358
 
359
    public function get_required_fields(): array {
360
        return [];
361
    }
362
 
363
    /**
364
     * If this column requires any aggregated statistics, it should declare that here.
365
     *
366
     * This is those statistics can be efficiently loaded in bulk.
367
     *
368
     * The statistics are all loaded just before load_additional_data is called on each column.
369
     * The values are then available from $this->qbank->get_aggregate_statistic(...);
370
     *
371
     * @return string[] the names of the required statistics fields. E.g. ['facility'].
372
     */
373
    public function get_required_statistics_fields(): array {
374
        return [];
375
    }
376
 
377
    /**
378
     * If this column needs extra data (e.g. tags) then load that here.
379
     *
380
     * The extra data should be added to the question object in the array.
381
     * Probably a good idea to check that another column has not already
382
     * loaded the data you want.
383
     *
384
     * @param \stdClass[] $questions the questions that will be displayed, indexed by question id.
385
     */
386
    public function load_additional_data(array $questions) {
387
    }
388
 
389
    /**
390
     * Load the tags for each question.
391
     *
392
     * Helper that can be used from {@see load_additional_data()};
393
     *
394
     * @param array $questions
395
     */
396
    public function load_question_tags(array $questions): void {
397
        $firstquestion = reset($questions);
398
        if (isset($firstquestion->tags)) {
399
            // Looks like tags are already loaded, so don't do it again.
400
            return;
401
        }
402
 
403
        // Load the tags.
404
        $tagdata = \core_tag_tag::get_items_tags('core_question', 'question',
405
                array_keys($questions));
406
 
407
        // Add them to the question objects.
408
        foreach ($tagdata as $questionid => $tags) {
409
            $questions[$questionid]->tags = $tags;
410
        }
411
    }
412
 
413
    /**
414
     * Can this column be sorted on? You can return either:
415
     *  + false for no (the default),
416
     *  + a field name, if sorting this column corresponds to sorting on that datbase field.
417
     *  + an array of subnames to sort on as follows
418
     *  return [
419
     *      'firstname' => ['field' => 'uc.firstname', 'title' => get_string('firstname')],
420
     *      'lastname' => ['field' => 'uc.lastname', 'title' => get_string('lastname')],
421
     *  ];
422
     * As well as field, and field, you can also add 'revers' => 1 if you want the default sort
423
     * order to be DESC.
424
     * @return mixed as above.
425
     */
426
    public function is_sortable() {
427
        return false;
428
    }
429
 
430
    /**
431
     * Helper method for building sort clauses.
432
     * @param bool $reverse whether the normal direction should be reversed.
433
     * @return string 'ASC' or 'DESC'
434
     */
435
    protected function sortorder($reverse): string {
436
        if ($reverse) {
437
            return ' DESC';
438
        } else {
439
            return ' ASC';
440
        }
441
    }
442
 
443
    /**
444
     * Sorts the expressions.
445
     *
446
     * @param bool $reverse Whether to sort in the reverse of the default sort order.
447
     * @param string $subsort if is_sortable returns an array of subnames, then this will be
448
     *      one of those. Otherwise will be empty.
449
     * @return string some SQL to go in the order by clause.
450
     */
451
    public function sort_expression($reverse, $subsort): string {
452
        $sortable = $this->is_sortable();
453
        if (is_array($sortable)) {
454
            if (array_key_exists($subsort, $sortable)) {
455
                return $sortable[$subsort]['field'] . $this->sortorder($reverse);
456
            } else {
457
                throw new \coding_exception('Unexpected $subsort type: ' . $subsort);
458
            }
459
        } else if ($sortable) {
460
            return $sortable . $this->sortorder($reverse);
461
        } else {
462
            throw new \coding_exception('sort_expression called on a non-sortable column.');
463
        }
464
    }
465
 
466
    /**
467
     * Output the column with an example value.
468
     *
469
     * By default, this will call $this->display() using whatever dummy data is passed in. Columns can override this
470
     * to provide example output without requiring valid data.
471
     *
472
     * @param \stdClass $question the row from the $question table, augmented with extra information.
473
     * @param string $rowclasses CSS class names that should be applied to this row of output.
474
     */
475
    public function display_preview(\stdClass $question, string $rowclasses): void {
476
        $this->display($question, $rowclasses);
477
    }
478
}